Skip to content

Commit e6a2c1d

Browse files
committed
feat: show auto-generated session names in resume picker
- Pino's resume picker now prefers pi session_info name (if set by session-namer) over the preview text. Last session_info wins on rename. - Fallback: preview (first message) or timestamp if no name is set. - Verified with 6-test suite (new test covers name precedence).
1 parent a29d90f commit e6a2c1d

2 files changed

Lines changed: 60 additions & 3 deletions

File tree

server/src/pi-sessions.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,55 @@ test("listPiSessions parses headers, filters by cwd, sorts by mtime desc", () =>
6767
}
6868
});
6969

70+
test("listPiSessions prefers the session_info name (last wins), else preview", () => {
71+
const agentDir = mkdtempSync(join(tmpdir(), "pino-agent-"));
72+
try {
73+
const cwd = "/work/named";
74+
const dir = piSessionsDir(cwd, agentDir);
75+
mkdirSync(dir, { recursive: true });
76+
77+
// Named session: two session_info entries → the LAST one wins for the label,
78+
// while `preview` still reflects the first user message.
79+
const named = join(dir, "2026-01-01T00-00-00-000Z_named.jsonl");
80+
writeFileSync(
81+
named,
82+
[
83+
JSON.stringify({ type: "session", version: 3, id: "named", timestamp: "2026-01-01T00:00:00.000Z", cwd }),
84+
JSON.stringify({ type: "message", message: { role: "user", content: [{ type: "text", text: "add rate limiting to my express api" }] } }),
85+
JSON.stringify({ type: "session_info", id: "si1", timestamp: "2026-01-01T00:01:00.000Z", name: "Rough First Name" }),
86+
JSON.stringify({ type: "message", message: { role: "assistant", content: [{ type: "text", text: "sure" }] } }),
87+
JSON.stringify({ type: "session_info", id: "si2", timestamp: "2026-01-01T00:02:00.000Z", name: "Add Rate Limiting to Express API" }),
88+
].join("\n") + "\n",
89+
);
90+
91+
// Unnamed session: no session_info → falls back to the first user message.
92+
const unnamed = join(dir, "2026-01-01T00-00-00-000Z_unnamed.jsonl");
93+
writeFileSync(
94+
unnamed,
95+
[
96+
JSON.stringify({ type: "session", version: 3, id: "unnamed", timestamp: "2026-01-01T00:00:00.000Z", cwd }),
97+
JSON.stringify({ type: "message", message: { role: "user", content: [{ type: "text", text: "just the first message" }] } }),
98+
].join("\n") + "\n",
99+
);
100+
101+
utimesSync(named, new Date(2000), new Date(2000));
102+
utimesSync(unnamed, new Date(1000), new Date(1000));
103+
104+
const list = listPiSessions(cwd, agentDir);
105+
const byId = Object.fromEntries(list.map((m) => [m.piSessionId, m]));
106+
107+
// Named: label is the latest session_info name; preview keeps the 1st message.
108+
assert.equal(byId.named.name, "Add Rate Limiting to Express API");
109+
assert.equal(byId.named.preview, "add rate limiting to my express api");
110+
111+
// Unnamed: label falls back to the first user message.
112+
assert.equal(byId.unnamed.name, "just the first message");
113+
assert.equal(byId.unnamed.preview, "just the first message");
114+
} finally {
115+
rmSync(agentDir, { recursive: true, force: true });
116+
}
117+
});
118+
70119
test("listPiSessions returns [] when the slug dir is absent", () => {
71120
const agentDir = mkdtempSync(join(tmpdir(), "pino-agent-"));
72121
try {

server/src/pi-sessions.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ export interface PiSessionMeta {
2525
piSessionId: string;
2626
/** Full path to the .jsonl transcript (server-internal; harmless to app). */
2727
path: string;
28-
/** Human label — first user message, else the timestamp. */
28+
/** Human label — auto-generated name (if any), else first user message, else timestamp. */
2929
name: string;
3030
/** First user message, truncated. */
3131
preview: string;
@@ -102,9 +102,10 @@ export function listPiSessions(cwd: string, agentDir: string = realAgentDir()):
102102
}
103103

104104
/** Build metadata (preview / count / mtime) for a single transcript. */
105-
function summarize(path: string, header: Header): PiSessionMeta {
105+
export function summarize(path: string, header: Header): PiSessionMeta {
106106
let preview = "";
107107
let messageCount = 0;
108+
let autoGeneratedName = ""; // pi session-namer auto-generated name
108109
try {
109110
for (const line of readFileSync(path, "utf8").split("\n")) {
110111
if (!line.trim()) continue;
@@ -121,6 +122,10 @@ function summarize(path: string, header: Header): PiSessionMeta {
121122
preview = extractText(o.message?.content).slice(0, PREVIEW_MAX);
122123
}
123124
}
125+
// Capture the most recent auto-generated name from session_info entries.
126+
if (o?.type === "session_info" && typeof o?.name === "string" && o.name) {
127+
autoGeneratedName = o.name;
128+
}
124129
}
125130
} catch {
126131
// Best-effort; a partially-read file still yields a usable meta.
@@ -133,10 +138,13 @@ function summarize(path: string, header: Header): PiSessionMeta {
133138
lastActivityAt = Date.parse(header.timestamp) || 0;
134139
}
135140

141+
// Prefer auto-generated name, then user's first message, then timestamp.
142+
const name = autoGeneratedName || preview || header.timestamp;
143+
136144
return {
137145
piSessionId: header.id,
138146
path,
139-
name: preview || header.timestamp,
147+
name,
140148
preview,
141149
messageCount,
142150
lastActivityAt,

0 commit comments

Comments
 (0)