pi-sessions 0.2.2 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -1
- package/extensions/session-auto-title/command.ts +4 -3
- package/extensions/session-auto-title/controller.ts +2 -15
- package/extensions/session-auto-title/generate.ts +256 -0
- package/extensions/session-auto-title/retitle.ts +39 -375
- package/extensions/session-auto-title/wizard.ts +8 -12
- package/extensions/session-auto-title.ts +23 -10
- package/extensions/session-handoff/extract.ts +29 -12
- package/extensions/session-handoff/metadata.ts +5 -0
- package/extensions/session-handoff/spawn.ts +26 -5
- package/extensions/session-handoff.ts +54 -16
- package/extensions/session-index.ts +2 -1
- package/extensions/shared/pi-mode.ts +5 -0
- package/extensions/shared/settings.ts +9 -0
- package/package.json +9 -9
- package/extensions/session-auto-title/prompt.ts +0 -54
|
@@ -18,6 +18,7 @@ import { parseTypeBoxValue } from "../shared/typebox.js";
|
|
|
18
18
|
|
|
19
19
|
const MAX_RELEVANT_FILES = 12;
|
|
20
20
|
const MAX_OPEN_QUESTIONS = 8;
|
|
21
|
+
const MAX_HANDOFF_TITLE_LENGTH = 64;
|
|
21
22
|
|
|
22
23
|
const HANDOFF_SYSTEM_PROMPT = `You extract context for a deliberate session handoff.
|
|
23
24
|
|
|
@@ -27,12 +28,16 @@ Rules:
|
|
|
27
28
|
- Extract only context that is relevant to the next task.
|
|
28
29
|
- Keep the summary compact and concrete.
|
|
29
30
|
- Prefer workspace-relative file paths when possible.
|
|
31
|
+
- title must be a short session title for the new handoff thread, 64 characters or less, without prefixes like "Handoff:" or otherwise referencing the current thread.
|
|
30
32
|
- nextTask must be the concrete next action for the new session.
|
|
31
33
|
- openQuestions should contain only unresolved items that materially affect the next task.
|
|
32
34
|
- If there are no meaningful open questions, omit openQuestions entirely.
|
|
33
35
|
- Do not write the final handoff prompt yourself.`;
|
|
34
36
|
|
|
35
37
|
const HANDOFF_EXTRACTION_PARAMETERS = Type.Object({
|
|
38
|
+
title: Type.String({
|
|
39
|
+
description: "Short display title for the new handoff session.",
|
|
40
|
+
}),
|
|
36
41
|
summary: Type.String({
|
|
37
42
|
description: "Only the context relevant to the next task.",
|
|
38
43
|
}),
|
|
@@ -58,11 +63,13 @@ const HANDOFF_EXTRACTION_TOOL: Tool<typeof HANDOFF_EXTRACTION_PARAMETERS> = {
|
|
|
58
63
|
type RequiredHandoffExtractionArgs = Static<typeof REQUIRED_HANDOFF_EXTRACTION_PARAMETERS>;
|
|
59
64
|
|
|
60
65
|
const REQUIRED_HANDOFF_EXTRACTION_PARAMETERS = Type.Object({
|
|
66
|
+
title: HANDOFF_EXTRACTION_PARAMETERS.properties.title,
|
|
61
67
|
summary: HANDOFF_EXTRACTION_PARAMETERS.properties.summary,
|
|
62
68
|
nextTask: HANDOFF_EXTRACTION_PARAMETERS.properties.nextTask,
|
|
63
69
|
});
|
|
64
70
|
|
|
65
71
|
export interface HandoffContext {
|
|
72
|
+
title: string;
|
|
66
73
|
summary: string;
|
|
67
74
|
relevantFiles: string[];
|
|
68
75
|
nextTask: string;
|
|
@@ -139,11 +146,13 @@ export async function generateHandoffDraft(
|
|
|
139
146
|
throw new Error(response.errorMessage ?? "Handoff generation failed.");
|
|
140
147
|
}
|
|
141
148
|
|
|
142
|
-
const
|
|
143
|
-
if (!
|
|
144
|
-
throw new Error(
|
|
149
|
+
const extraction = extractHandoffContext(response, goal);
|
|
150
|
+
if (!extraction.context) {
|
|
151
|
+
throw new Error(extraction.error);
|
|
145
152
|
}
|
|
146
153
|
|
|
154
|
+
const handoffContext = extraction.context;
|
|
155
|
+
|
|
147
156
|
const sessionId = ctx.sessionManager.getSessionId();
|
|
148
157
|
const sessionPath = ctx.sessionManager.getSessionFile();
|
|
149
158
|
|
|
@@ -208,10 +217,10 @@ export function assembleHandoffDraft(
|
|
|
208
217
|
export function extractHandoffContext(
|
|
209
218
|
response: AssistantMessage,
|
|
210
219
|
goal: string,
|
|
211
|
-
): HandoffContext | undefined {
|
|
220
|
+
): { context: HandoffContext; error?: undefined } | { context?: undefined; error: string } {
|
|
212
221
|
const toolCall = response.content.find(isCreateHandoffContextToolCall);
|
|
213
222
|
if (!toolCall) {
|
|
214
|
-
return
|
|
223
|
+
return { error: "Handoff extraction did not return structured context." };
|
|
215
224
|
}
|
|
216
225
|
|
|
217
226
|
let requiredArguments: RequiredHandoffExtractionArgs;
|
|
@@ -222,7 +231,12 @@ export function extractHandoffContext(
|
|
|
222
231
|
"Invalid create_handoff_context arguments",
|
|
223
232
|
);
|
|
224
233
|
} catch {
|
|
225
|
-
return
|
|
234
|
+
return { error: "Handoff extraction did not return structured context." };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const title = normalizeText(requiredArguments.title);
|
|
238
|
+
if (title.length > MAX_HANDOFF_TITLE_LENGTH) {
|
|
239
|
+
return { error: "Handoff title must be 64 characters or less." };
|
|
226
240
|
}
|
|
227
241
|
|
|
228
242
|
const summary = normalizeText(requiredArguments.summary);
|
|
@@ -230,15 +244,18 @@ export function extractHandoffContext(
|
|
|
230
244
|
const nextTask = normalizeText(requiredArguments.nextTask) || goal.trim();
|
|
231
245
|
const openQuestions = normalizeStringArray(toolCall.arguments.openQuestions, MAX_OPEN_QUESTIONS);
|
|
232
246
|
|
|
233
|
-
if (!summary || !nextTask) {
|
|
234
|
-
return
|
|
247
|
+
if (!summary || !nextTask || !title) {
|
|
248
|
+
return { error: "Handoff extraction did not return structured context." };
|
|
235
249
|
}
|
|
236
250
|
|
|
237
251
|
return {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
252
|
+
context: {
|
|
253
|
+
title,
|
|
254
|
+
summary,
|
|
255
|
+
relevantFiles,
|
|
256
|
+
nextTask,
|
|
257
|
+
openQuestions,
|
|
258
|
+
},
|
|
242
259
|
};
|
|
243
260
|
}
|
|
244
261
|
|
|
@@ -12,6 +12,7 @@ export const HANDOFF_SESSION_METADATA_SCHEMA = Type.Object({
|
|
|
12
12
|
origin: Type.Literal("handoff"),
|
|
13
13
|
goal: Type.String(),
|
|
14
14
|
nextTask: Type.String(),
|
|
15
|
+
title: Type.String(),
|
|
15
16
|
initial_prompt: Type.String(),
|
|
16
17
|
});
|
|
17
18
|
|
|
@@ -19,6 +20,7 @@ export const HANDOFF_BOOTSTRAP_SCHEMA = Type.Object({
|
|
|
19
20
|
sessionId: Type.String(),
|
|
20
21
|
goal: Type.String(),
|
|
21
22
|
nextTask: Type.String(),
|
|
23
|
+
title: Type.String(),
|
|
22
24
|
initialPrompt: Type.String(),
|
|
23
25
|
});
|
|
24
26
|
|
|
@@ -29,6 +31,7 @@ export function createHandoffSessionMetadata(
|
|
|
29
31
|
goal: string,
|
|
30
32
|
nextTask: string,
|
|
31
33
|
initialPrompt: string,
|
|
34
|
+
title: string,
|
|
32
35
|
): HandoffSessionMetadata {
|
|
33
36
|
const normalizedGoal = goal.trim();
|
|
34
37
|
const normalizedNextTask = nextTask.trim() || normalizedGoal;
|
|
@@ -37,6 +40,7 @@ export function createHandoffSessionMetadata(
|
|
|
37
40
|
origin: "handoff",
|
|
38
41
|
goal: normalizedGoal,
|
|
39
42
|
nextTask: normalizedNextTask,
|
|
43
|
+
title,
|
|
40
44
|
initial_prompt: initialPrompt.trim(),
|
|
41
45
|
};
|
|
42
46
|
}
|
|
@@ -49,6 +53,7 @@ export function createHandoffBootstrap(
|
|
|
49
53
|
sessionId,
|
|
50
54
|
goal: metadata.goal,
|
|
51
55
|
nextTask: metadata.nextTask,
|
|
56
|
+
title: metadata.title,
|
|
52
57
|
initialPrompt: metadata.initial_prompt,
|
|
53
58
|
};
|
|
54
59
|
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
type ExtensionAPI,
|
|
7
7
|
type ExtensionCommandContext,
|
|
8
8
|
type SessionHeader,
|
|
9
|
+
type SessionInfoEntry,
|
|
9
10
|
} from "@earendil-works/pi-coding-agent";
|
|
10
11
|
import { HANDOFF_BOOTSTRAP_ENV } from "./metadata.js";
|
|
11
12
|
|
|
@@ -44,6 +45,7 @@ export function createHandoffSession(options: {
|
|
|
44
45
|
cwd: string;
|
|
45
46
|
sessionDir: string;
|
|
46
47
|
parentSessionFile: string;
|
|
48
|
+
title: string;
|
|
47
49
|
}): CreatedHandoffSession {
|
|
48
50
|
const sessionId = randomUUID();
|
|
49
51
|
const timestamp = new Date().toISOString();
|
|
@@ -59,7 +61,18 @@ export function createHandoffSession(options: {
|
|
|
59
61
|
parentSession: options.parentSessionFile,
|
|
60
62
|
};
|
|
61
63
|
|
|
62
|
-
|
|
64
|
+
const titleEntry: SessionInfoEntry = {
|
|
65
|
+
type: "session_info",
|
|
66
|
+
id: randomUUID(),
|
|
67
|
+
parentId: null,
|
|
68
|
+
timestamp,
|
|
69
|
+
name: options.title,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
writeFileSync(
|
|
73
|
+
sessionFile,
|
|
74
|
+
`${[JSON.stringify(header), JSON.stringify(titleEntry)].join("\n")}\n`,
|
|
75
|
+
);
|
|
63
76
|
|
|
64
77
|
return { sessionId, sessionFile };
|
|
65
78
|
}
|
|
@@ -72,12 +85,14 @@ export async function launchSplitHandoffSession(
|
|
|
72
85
|
direction: HandoffSplitDirection;
|
|
73
86
|
sessionId: string;
|
|
74
87
|
bootstrapValue: string;
|
|
88
|
+
title: string;
|
|
75
89
|
},
|
|
76
90
|
): Promise<{ success: true } | { success: false; error: string }> {
|
|
77
91
|
const piCommand = buildPiLaunchCommand(
|
|
78
92
|
options.sessionDir,
|
|
79
93
|
options.sessionId,
|
|
80
94
|
options.bootstrapValue,
|
|
95
|
+
options.title,
|
|
81
96
|
);
|
|
82
97
|
const escapedCwd = escapeAppleScriptString(options.cwd);
|
|
83
98
|
const escapedCommand = escapeAppleScriptString(piCommand);
|
|
@@ -113,23 +128,29 @@ export function buildPiResumeCommand(
|
|
|
113
128
|
sessionDir: string,
|
|
114
129
|
sessionId: string,
|
|
115
130
|
bootstrapValue: string,
|
|
131
|
+
title: string,
|
|
116
132
|
): string {
|
|
117
|
-
|
|
133
|
+
const args = [
|
|
118
134
|
`${HANDOFF_BOOTSTRAP_ENV}=${shellQuote(bootstrapValue)}`,
|
|
119
135
|
"pi",
|
|
120
136
|
"--session-dir",
|
|
121
137
|
shellQuote(sessionDir),
|
|
122
|
-
"--session",
|
|
138
|
+
"--session-id",
|
|
123
139
|
shellQuote(sessionId),
|
|
124
|
-
|
|
140
|
+
"--name",
|
|
141
|
+
shellQuote(title),
|
|
142
|
+
];
|
|
143
|
+
|
|
144
|
+
return args.join(" ");
|
|
125
145
|
}
|
|
126
146
|
|
|
127
147
|
export function buildPiLaunchCommand(
|
|
128
148
|
sessionDir: string,
|
|
129
149
|
sessionId: string,
|
|
130
150
|
bootstrapValue: string,
|
|
151
|
+
title: string,
|
|
131
152
|
): string {
|
|
132
|
-
const payload = `${buildPiResumeCommand(sessionDir, sessionId, bootstrapValue)}; exec /bin/zsh -il`;
|
|
153
|
+
const payload = `${buildPiResumeCommand(sessionDir, sessionId, bootstrapValue, title)}; exec /bin/zsh -il`;
|
|
133
154
|
return `/bin/zsh -ilc ${shellQuote(payload)}`;
|
|
134
155
|
}
|
|
135
156
|
|
|
@@ -23,17 +23,23 @@ import {
|
|
|
23
23
|
launchSplitHandoffSession,
|
|
24
24
|
validateSplitHandoffPrerequisites,
|
|
25
25
|
} from "./session-handoff/spawn.js";
|
|
26
|
+
import { isTuiMode } from "./shared/pi-mode.js";
|
|
26
27
|
import { loadSettings } from "./shared/settings.js";
|
|
27
28
|
|
|
28
29
|
const HANDOFF_USAGE = "Usage: /handoff [--left|--right|--up|--down] <goal for new thread>";
|
|
29
30
|
|
|
31
|
+
interface HandoffPromptContext {
|
|
32
|
+
ui: ExtensionCommandContext["ui"];
|
|
33
|
+
sendUserMessage(content: string): Promise<void>;
|
|
34
|
+
}
|
|
35
|
+
|
|
30
36
|
export default function sessionHandoffExtension(pi: ExtensionAPI): void {
|
|
31
37
|
const settings = loadSettings();
|
|
32
38
|
|
|
33
39
|
pi.registerCommand("handoff", {
|
|
34
40
|
description: "Transfer context to a new focused session",
|
|
35
41
|
handler: async (args: string, ctx: ExtensionCommandContext): Promise<void> => {
|
|
36
|
-
if (!ctx
|
|
42
|
+
if (!isTuiMode(ctx)) {
|
|
37
43
|
ctx.ui.notify("handoff requires interactive mode", "error");
|
|
38
44
|
return;
|
|
39
45
|
}
|
|
@@ -99,28 +105,30 @@ export default function sessionHandoffExtension(pi: ExtensionAPI): void {
|
|
|
99
105
|
parsedArgs.goal,
|
|
100
106
|
generatedDraft.context.nextTask,
|
|
101
107
|
approvedDraft,
|
|
108
|
+
generatedDraft.context.title,
|
|
102
109
|
);
|
|
103
|
-
const createdSession = createHandoffSession({
|
|
104
|
-
cwd: ctx.cwd,
|
|
105
|
-
sessionDir: ctx.sessionManager.getSessionDir(),
|
|
106
|
-
parentSessionFile,
|
|
107
|
-
});
|
|
108
|
-
const bootstrapValue = encodeHandoffBootstrap(
|
|
109
|
-
createHandoffBootstrap(createdSession.sessionId, handoffMetadata),
|
|
110
|
-
);
|
|
111
|
-
|
|
112
110
|
if (parsedArgs.splitDirection) {
|
|
111
|
+
const createdSession = createHandoffSession({
|
|
112
|
+
cwd: ctx.cwd,
|
|
113
|
+
sessionDir: ctx.sessionManager.getSessionDir(),
|
|
114
|
+
parentSessionFile,
|
|
115
|
+
title: handoffMetadata.title,
|
|
116
|
+
});
|
|
117
|
+
const bootstrapValue = encodeHandoffBootstrap(
|
|
118
|
+
createHandoffBootstrap(createdSession.sessionId, handoffMetadata),
|
|
119
|
+
);
|
|
113
120
|
const launchResult = await launchSplitHandoffSession(pi, {
|
|
114
121
|
cwd: ctx.cwd,
|
|
115
122
|
sessionDir: ctx.sessionManager.getSessionDir(),
|
|
116
123
|
direction: parsedArgs.splitDirection,
|
|
117
124
|
sessionId: createdSession.sessionId,
|
|
118
125
|
bootstrapValue,
|
|
126
|
+
title: handoffMetadata.title,
|
|
119
127
|
});
|
|
120
128
|
|
|
121
129
|
if (!launchResult.success) {
|
|
122
130
|
ctx.ui.notify(
|
|
123
|
-
`${launchResult.error} Created handoff session ${createdSession.sessionId}; start it manually with: ${buildPiResumeCommand(ctx.sessionManager.getSessionDir(), createdSession.sessionId, bootstrapValue)}`,
|
|
131
|
+
`${launchResult.error} Created handoff session ${createdSession.sessionId}; start it manually with: ${buildPiResumeCommand(ctx.sessionManager.getSessionDir(), createdSession.sessionId, bootstrapValue, handoffMetadata.title)}`,
|
|
124
132
|
"error",
|
|
125
133
|
);
|
|
126
134
|
return;
|
|
@@ -130,10 +138,14 @@ export default function sessionHandoffExtension(pi: ExtensionAPI): void {
|
|
|
130
138
|
return;
|
|
131
139
|
}
|
|
132
140
|
|
|
133
|
-
const switchResult = await ctx.
|
|
141
|
+
const switchResult = await ctx.newSession({
|
|
142
|
+
parentSession: parentSessionFile,
|
|
143
|
+
setup: async (sessionManager) => {
|
|
144
|
+
sessionManager.appendSessionInfo(handoffMetadata.title);
|
|
145
|
+
sessionManager.appendCustomEntry(HANDOFF_METADATA_CUSTOM_TYPE, handoffMetadata);
|
|
146
|
+
},
|
|
134
147
|
withSession: async (nextCtx) => {
|
|
135
|
-
|
|
136
|
-
nextCtx.ui.notify("Handoff started in a new session.", "info");
|
|
148
|
+
startHandoffPromptAfterSessionRender(nextCtx, approvedDraft);
|
|
137
149
|
},
|
|
138
150
|
});
|
|
139
151
|
|
|
@@ -146,7 +158,7 @@ export default function sessionHandoffExtension(pi: ExtensionAPI): void {
|
|
|
146
158
|
pi.registerShortcut(settings.handoff.pickerShortcut, {
|
|
147
159
|
description: "Open the session reference picker",
|
|
148
160
|
handler: async (ctx) => {
|
|
149
|
-
if (!ctx
|
|
161
|
+
if (!isTuiMode(ctx)) {
|
|
150
162
|
return;
|
|
151
163
|
}
|
|
152
164
|
|
|
@@ -191,10 +203,19 @@ export default function sessionHandoffExtension(pi: ExtensionAPI): void {
|
|
|
191
203
|
if (!getHandoffMetadataFromEntries(entries)) {
|
|
192
204
|
pi.appendEntry(
|
|
193
205
|
HANDOFF_METADATA_CUSTOM_TYPE,
|
|
194
|
-
createHandoffSessionMetadata(
|
|
206
|
+
createHandoffSessionMetadata(
|
|
207
|
+
bootstrap.goal,
|
|
208
|
+
bootstrap.nextTask,
|
|
209
|
+
bootstrap.initialPrompt,
|
|
210
|
+
bootstrap.title,
|
|
211
|
+
),
|
|
195
212
|
);
|
|
196
213
|
}
|
|
197
214
|
|
|
215
|
+
if (!ctx.sessionManager.getSessionName()) {
|
|
216
|
+
pi.setSessionName(bootstrap.title);
|
|
217
|
+
}
|
|
218
|
+
|
|
198
219
|
pi.sendUserMessage(bootstrap.initialPrompt);
|
|
199
220
|
} finally {
|
|
200
221
|
delete process.env[HANDOFF_BOOTSTRAP_ENV];
|
|
@@ -266,6 +287,23 @@ async function runWithLoader<T>(
|
|
|
266
287
|
return result;
|
|
267
288
|
}
|
|
268
289
|
|
|
290
|
+
function startHandoffPromptAfterSessionRender(
|
|
291
|
+
ctx: HandoffPromptContext,
|
|
292
|
+
approvedDraft: string,
|
|
293
|
+
): void {
|
|
294
|
+
// ctx.newSession() renders the replacement session only after withSession returns.
|
|
295
|
+
setImmediate(() => {
|
|
296
|
+
void (async () => {
|
|
297
|
+
try {
|
|
298
|
+
await ctx.sendUserMessage(approvedDraft);
|
|
299
|
+
ctx.ui.notify("Handoff started in a new session.", "info");
|
|
300
|
+
} catch (error) {
|
|
301
|
+
ctx.ui.notify(formatHandoffError(error), "error");
|
|
302
|
+
}
|
|
303
|
+
})();
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
|
|
269
307
|
function formatHandoffError(error: unknown): string {
|
|
270
308
|
if (error instanceof Error && error.message.trim()) {
|
|
271
309
|
return error.message;
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "@earendil-works/pi-coding-agent";
|
|
7
7
|
import { type Focusable, matchesKey, visibleWidth } from "@earendil-works/pi-tui";
|
|
8
8
|
import { type ReindexResult, rebuildSessionIndex } from "./session-search/reindex.js";
|
|
9
|
+
import { isTuiMode } from "./shared/pi-mode.js";
|
|
9
10
|
import { getIndexStatus, type SessionIndexStatus } from "./shared/session-index/index.js";
|
|
10
11
|
import { loadSettings } from "./shared/settings.js";
|
|
11
12
|
|
|
@@ -17,7 +18,7 @@ export default function sessionIndexExtension(pi: ExtensionAPI): void {
|
|
|
17
18
|
pi.registerCommand("session-index", {
|
|
18
19
|
description: "Open the session index control panel",
|
|
19
20
|
handler: async (_args, ctx) => {
|
|
20
|
-
if (!ctx
|
|
21
|
+
if (!isTuiMode(ctx)) {
|
|
21
22
|
ctx.ui.notify("/session-index requires interactive mode.", "warning");
|
|
22
23
|
return;
|
|
23
24
|
}
|
|
@@ -6,6 +6,7 @@ import { type Static, Type } from "typebox";
|
|
|
6
6
|
import { parseTypeBoxValue } from "./typebox.js";
|
|
7
7
|
|
|
8
8
|
export const DEFAULT_AUTO_TITLE_REFRESH_TURNS = 4;
|
|
9
|
+
export const DEFAULT_AUTO_TITLE_PROMPT = `Name this coding session (under 80 chars). Be specific to what is being discussed. Your exact output will be displayed to the user, so make sure that it contains ONLY the title itself and nothing else.`;
|
|
9
10
|
const SESSION_FILE_SETTINGS_SCHEMA = Type.Object({
|
|
10
11
|
handoff: Type.Optional(
|
|
11
12
|
Type.Object({
|
|
@@ -21,6 +22,7 @@ const SESSION_FILE_SETTINGS_SCHEMA = Type.Object({
|
|
|
21
22
|
Type.Object({
|
|
22
23
|
refreshTurns: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
23
24
|
model: Type.Optional(Type.String()),
|
|
25
|
+
prompt: Type.Optional(Type.String()),
|
|
24
26
|
}),
|
|
25
27
|
),
|
|
26
28
|
});
|
|
@@ -42,6 +44,7 @@ export class ModelReference {
|
|
|
42
44
|
export interface AutoTitleSettings {
|
|
43
45
|
refreshTurns: number;
|
|
44
46
|
model: ModelReference | undefined;
|
|
47
|
+
prompt: string;
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
export interface SessionSettings {
|
|
@@ -113,6 +116,11 @@ function parseModelReference(value: string | undefined): ModelReference | undefi
|
|
|
113
116
|
return new ModelReference(trimmed.slice(0, slashIndex), trimmed.slice(slashIndex + 1));
|
|
114
117
|
}
|
|
115
118
|
|
|
119
|
+
function normalizeAutoTitlePrompt(value: string | undefined): string {
|
|
120
|
+
const trimmed = value?.trim();
|
|
121
|
+
return trimmed ? trimmed : DEFAULT_AUTO_TITLE_PROMPT;
|
|
122
|
+
}
|
|
123
|
+
|
|
116
124
|
function loadSessionFileSettings(): SessionFileSettings {
|
|
117
125
|
const globalSettings = SettingsManager.create(process.cwd()).getGlobalSettings();
|
|
118
126
|
const parsed = parseTypeBoxValue(ROOT_SETTINGS_SCHEMA, globalSettings, "Invalid settings");
|
|
@@ -132,6 +140,7 @@ function resolveSessionSettings(fileSettings: SessionFileSettings): SessionSetti
|
|
|
132
140
|
autoTitle: {
|
|
133
141
|
refreshTurns: fileSettings.autoTitle?.refreshTurns ?? DEFAULT_AUTO_TITLE_REFRESH_TURNS,
|
|
134
142
|
model: parseModelReference(fileSettings.autoTitle?.model),
|
|
143
|
+
prompt: normalizeAutoTitlePrompt(fileSettings.autoTitle?.prompt),
|
|
135
144
|
},
|
|
136
145
|
};
|
|
137
146
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-sessions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pi session search, ask, handoff, auto-titling, and indexing tools",
|
|
6
6
|
"license": "MIT",
|
|
@@ -42,10 +42,10 @@
|
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@biomejs/biome": "^2.4.14",
|
|
45
|
-
"@earendil-works/pi-agent-core": "^0.
|
|
46
|
-
"@earendil-works/pi-ai": "^0.
|
|
47
|
-
"@earendil-works/pi-coding-agent": "^0.
|
|
48
|
-
"@earendil-works/pi-tui": "^0.
|
|
45
|
+
"@earendil-works/pi-agent-core": "^0.78.1",
|
|
46
|
+
"@earendil-works/pi-ai": "^0.78.1",
|
|
47
|
+
"@earendil-works/pi-coding-agent": "^0.78.1",
|
|
48
|
+
"@earendil-works/pi-tui": "^0.78.1",
|
|
49
49
|
"@types/better-sqlite3": "^7.6.13",
|
|
50
50
|
"@types/node": "^25.6.2",
|
|
51
51
|
"husky": "^9.1.7",
|
|
@@ -55,10 +55,10 @@
|
|
|
55
55
|
"vitest": "^4.1.5"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@earendil-works/pi-agent-core": ">=0.
|
|
59
|
-
"@earendil-works/pi-ai": ">=0.
|
|
60
|
-
"@earendil-works/pi-coding-agent": ">=0.
|
|
61
|
-
"@earendil-works/pi-tui": ">=0.
|
|
58
|
+
"@earendil-works/pi-agent-core": ">=0.78.1",
|
|
59
|
+
"@earendil-works/pi-ai": ">=0.78.1",
|
|
60
|
+
"@earendil-works/pi-coding-agent": ">=0.78.1",
|
|
61
|
+
"@earendil-works/pi-tui": ">=0.78.1",
|
|
62
62
|
"typebox": ">=1.1.24"
|
|
63
63
|
},
|
|
64
64
|
"lint-staged": {
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import type { AutoTitleContext } from "./context.js";
|
|
2
|
-
import type { AutoTitleTrigger } from "./state.js";
|
|
3
|
-
|
|
4
|
-
const AUTO_TITLE_CHAR_MAX = 120;
|
|
5
|
-
|
|
6
|
-
export const AUTO_TITLE_SYSTEM_PROMPT = `You generate concrete coding session titles from the conversation provided.
|
|
7
|
-
|
|
8
|
-
Return title text only.
|
|
9
|
-
|
|
10
|
-
Rules:
|
|
11
|
-
- Prefer 3-15 words.
|
|
12
|
-
- Maximum 120 characters.
|
|
13
|
-
- Use the full conversation, while letting the most recent work refine the title when needed.
|
|
14
|
-
- Describe the current task, bug, feature, or investigation on the active branch.
|
|
15
|
-
- Mention specific subsystem or file only when it improves clarity.
|
|
16
|
-
- No quotes, markdown, emojis, prefixes, or explanations.
|
|
17
|
-
- No trailing punctuation.
|
|
18
|
-
- Avoid generic titles like Coding help or Working on project.`;
|
|
19
|
-
|
|
20
|
-
export function buildAutoTitlePrompt(context: AutoTitleContext, trigger: AutoTitleTrigger): string {
|
|
21
|
-
const sections = [
|
|
22
|
-
["## Trigger", trigger],
|
|
23
|
-
["## Current Title", context.currentTitle ?? "(none)"],
|
|
24
|
-
["## Counts", formatCounts(context)],
|
|
25
|
-
["## Conversation", context.conversationText || "(none)"],
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
if (context.cwd) {
|
|
29
|
-
sections.unshift(["## Cwd", context.cwd]);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return sections.map(([heading, body]) => `${heading}\n${body}`).join("\n\n");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export function normalizeGeneratedAutoTitle(value: string): string | undefined {
|
|
36
|
-
const withoutQuotes = value.trim().replace(/^["'`]+|["'`]+$/g, "");
|
|
37
|
-
const collapsed = withoutQuotes
|
|
38
|
-
.replace(/\s+/g, " ")
|
|
39
|
-
.replace(/[.!?]+$/g, "")
|
|
40
|
-
.trim();
|
|
41
|
-
if (!collapsed) {
|
|
42
|
-
return undefined;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
const truncated = collapsed.slice(0, AUTO_TITLE_CHAR_MAX).trim();
|
|
46
|
-
return truncated || undefined;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
function formatCounts(context: AutoTitleContext): string {
|
|
50
|
-
return [
|
|
51
|
-
`user_turns: ${context.userTurnCount}`,
|
|
52
|
-
`assistant_turns: ${context.assistantTurnCount}`,
|
|
53
|
-
].join("\n");
|
|
54
|
-
}
|