First version
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
//
|
||||
// sessions-store.js — reading Claude Code's own session history.
|
||||
//
|
||||
// Claude Code writes one JSONL transcript per session under
|
||||
// ~/.claude/projects/<slug>/<session-id>.jsonl, where the slug is the project
|
||||
// directory with path separators and dots flattened to dashes. Listing that
|
||||
// directory is enough to offer a "resume" picker without shelling out to the
|
||||
// interactive one.
|
||||
//
|
||||
|
||||
const { log, oneLine } = require("./util.js");
|
||||
|
||||
function projectSlug(path) {
|
||||
return String(path).replace(/[/.]/g, "-");
|
||||
}
|
||||
|
||||
function projectDirectory(workspacePath) {
|
||||
const home = nova.environment["HOME"];
|
||||
if (!home || !workspacePath) return null;
|
||||
return nova.path.join(home, ".claude", "projects", projectSlug(workspacePath));
|
||||
}
|
||||
|
||||
/** First user prompt in a transcript, used as the picker label. */
|
||||
function firstPrompt(filePath) {
|
||||
let file = null;
|
||||
try {
|
||||
file = nova.fs.open(filePath, "r");
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const line = file.readline();
|
||||
if (!line) break;
|
||||
|
||||
let record;
|
||||
try {
|
||||
record = JSON.parse(line);
|
||||
} catch (_) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (record.type !== "user" || !record.message) continue;
|
||||
if (record.isMeta || record.isSynthetic) continue;
|
||||
|
||||
const content = record.message.content;
|
||||
if (typeof content === "string") {
|
||||
if (content.trim()) return oneLine(content, 70);
|
||||
} else if (Array.isArray(content)) {
|
||||
for (const block of content) {
|
||||
if (block && block.type === "text" && block.text && block.text.trim()) {
|
||||
return oneLine(block.text, 70);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
log("could not read session transcript:", filePath, err);
|
||||
} finally {
|
||||
if (file) {
|
||||
try {
|
||||
file.close();
|
||||
} catch (_) {
|
||||
/* already closed */
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recent sessions for a workspace, newest first.
|
||||
* Returns [{ id, path, modifiedAt, label }].
|
||||
*/
|
||||
function recentSessions(workspacePath, limit = 15) {
|
||||
const directory = projectDirectory(workspacePath);
|
||||
if (!directory) return [];
|
||||
|
||||
let names;
|
||||
try {
|
||||
if (!nova.fs.access(directory, nova.fs.R_OK)) return [];
|
||||
names = nova.fs.listdir(directory);
|
||||
} catch (err) {
|
||||
log("no session history:", err);
|
||||
return [];
|
||||
}
|
||||
|
||||
const sessions = [];
|
||||
for (const name of names) {
|
||||
if (!name.endsWith(".jsonl")) continue;
|
||||
|
||||
const filePath = nova.path.join(directory, name);
|
||||
const stats = nova.fs.stat(filePath);
|
||||
if (!stats || !stats.isFile() || stats.size === 0) continue;
|
||||
|
||||
sessions.push({
|
||||
id: name.slice(0, -".jsonl".length),
|
||||
path: filePath,
|
||||
modifiedAt: stats.mtime,
|
||||
size: stats.size,
|
||||
});
|
||||
}
|
||||
|
||||
sessions.sort((a, b) => b.modifiedAt.getTime() - a.modifiedAt.getTime());
|
||||
|
||||
const recent = sessions.slice(0, limit);
|
||||
for (const session of recent) {
|
||||
session.label = firstPrompt(session.path) || session.id.slice(0, 8);
|
||||
}
|
||||
|
||||
return recent;
|
||||
}
|
||||
|
||||
module.exports = { recentSessions, projectDirectory, projectSlug };
|
||||
Reference in New Issue
Block a user