pi-sessions 0.2.1 → 0.3.0
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 +2 -1
- package/extensions/session-ask.ts +4 -4
- package/extensions/session-auto-title/command.ts +2 -2
- package/extensions/session-auto-title/context.ts +1 -1
- package/extensions/session-auto-title/controller.ts +3 -16
- package/extensions/session-auto-title/generate.ts +256 -0
- package/extensions/session-auto-title/model.ts +2 -2
- package/extensions/session-auto-title/retitle.ts +41 -377
- package/extensions/session-auto-title/state.ts +2 -2
- package/extensions/session-auto-title/wizard.ts +12 -16
- package/extensions/session-auto-title.ts +22 -10
- package/extensions/session-handoff/extract.ts +33 -16
- package/extensions/session-handoff/metadata.ts +7 -2
- package/extensions/session-handoff/picker.ts +2 -2
- package/extensions/session-handoff/review.ts +3 -3
- package/extensions/session-handoff/spawn.ts +27 -6
- package/extensions/session-handoff.ts +54 -17
- package/extensions/session-hooks.ts +1 -1
- package/extensions/session-index.ts +2 -2
- package/extensions/session-search/extract.ts +4 -4
- package/extensions/session-search/hooks.ts +2 -2
- package/extensions/session-search/reindex.ts +1 -1
- package/extensions/session-search.ts +3 -3
- package/extensions/shared/session-index/common.ts +1 -1
- package/extensions/shared/session-index/lineage.ts +1 -1
- package/extensions/shared/session-index/search.ts +1 -1
- package/extensions/shared/settings.ts +12 -3
- package/extensions/shared/typebox.ts +4 -4
- package/package.json +18 -13
- package/extensions/session-auto-title/prompt.ts +0 -54
|
@@ -6,18 +6,19 @@ import {
|
|
|
6
6
|
type ThinkingContent,
|
|
7
7
|
type Tool,
|
|
8
8
|
type ToolCall,
|
|
9
|
-
} from "@
|
|
10
|
-
import type { ExtensionContext } from "@
|
|
9
|
+
} from "@earendil-works/pi-ai";
|
|
10
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
11
11
|
import {
|
|
12
12
|
buildSessionContext,
|
|
13
13
|
convertToLlm,
|
|
14
14
|
serializeConversation,
|
|
15
|
-
} from "@
|
|
16
|
-
import { type Static, Type } from "
|
|
15
|
+
} from "@earendil-works/pi-coding-agent";
|
|
16
|
+
import { type Static, Type } from "typebox";
|
|
17
17
|
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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
|
-
import type { CustomEntry, SessionEntry } from "@
|
|
3
|
-
import { type Static, Type } from "
|
|
2
|
+
import type { CustomEntry, SessionEntry } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { type Static, Type } from "typebox";
|
|
4
4
|
import { safeParseTypeBoxValue } from "../shared/typebox.js";
|
|
5
5
|
|
|
6
6
|
export const HANDOFF_METADATA_CUSTOM_TYPE = "pi-sessions.handoff";
|
|
@@ -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
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionContext, KeybindingsManager, Theme } from "@
|
|
1
|
+
import type { ExtensionContext, KeybindingsManager, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import {
|
|
3
3
|
type Focusable,
|
|
4
4
|
Input,
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
type TUI,
|
|
8
8
|
truncateToWidth,
|
|
9
9
|
visibleWidth,
|
|
10
|
-
} from "@
|
|
10
|
+
} from "@earendil-works/pi-tui";
|
|
11
11
|
import {
|
|
12
12
|
stripSearchSnippetMarkers,
|
|
13
13
|
transformSearchSnippetMatches,
|
|
@@ -2,15 +2,15 @@ import type {
|
|
|
2
2
|
ExtensionCommandContext,
|
|
3
3
|
ExtensionUIContext,
|
|
4
4
|
Theme,
|
|
5
|
-
} from "@
|
|
6
|
-
import type { Component } from "@
|
|
5
|
+
} from "@earendil-works/pi-coding-agent";
|
|
6
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
7
7
|
import {
|
|
8
8
|
Key,
|
|
9
9
|
matchesKey,
|
|
10
10
|
truncateToWidth,
|
|
11
11
|
visibleWidth,
|
|
12
12
|
wrapTextWithAnsi,
|
|
13
|
-
} from "@
|
|
13
|
+
} from "@earendil-works/pi-tui";
|
|
14
14
|
|
|
15
15
|
const PREVIEW_TIMEOUT_MS = 8_000;
|
|
16
16
|
const PREVIEW_BODY_LINE_LIMIT = 16;
|
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
type ExtensionAPI,
|
|
7
7
|
type ExtensionCommandContext,
|
|
8
8
|
type SessionHeader,
|
|
9
|
-
|
|
9
|
+
type SessionInfoEntry,
|
|
10
|
+
} from "@earendil-works/pi-coding-agent";
|
|
10
11
|
import { HANDOFF_BOOTSTRAP_ENV } from "./metadata.js";
|
|
11
12
|
|
|
12
13
|
const GHOSTTY_MACOS_ONLY_MESSAGE = "Split handoff currently supports Ghostty on macOS only.";
|
|
@@ -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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionCommandContext } from "@
|
|
2
|
-
import { buildSessionContext } from "@
|
|
3
|
-
import { Key, matchesKey } from "@
|
|
1
|
+
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { buildSessionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
4
4
|
import { generateHandoffDraft, type HandoffDraftResult } from "./session-handoff/extract.js";
|
|
5
5
|
import {
|
|
6
6
|
createHandoffBootstrap,
|
|
@@ -27,6 +27,11 @@ import { loadSettings } from "./shared/settings.js";
|
|
|
27
27
|
|
|
28
28
|
const HANDOFF_USAGE = "Usage: /handoff [--left|--right|--up|--down] <goal for new thread>";
|
|
29
29
|
|
|
30
|
+
interface HandoffPromptContext {
|
|
31
|
+
ui: ExtensionCommandContext["ui"];
|
|
32
|
+
sendUserMessage(content: string): Promise<void>;
|
|
33
|
+
}
|
|
34
|
+
|
|
30
35
|
export default function sessionHandoffExtension(pi: ExtensionAPI): void {
|
|
31
36
|
const settings = loadSettings();
|
|
32
37
|
|
|
@@ -99,28 +104,30 @@ export default function sessionHandoffExtension(pi: ExtensionAPI): void {
|
|
|
99
104
|
parsedArgs.goal,
|
|
100
105
|
generatedDraft.context.nextTask,
|
|
101
106
|
approvedDraft,
|
|
107
|
+
generatedDraft.context.title,
|
|
102
108
|
);
|
|
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
109
|
if (parsedArgs.splitDirection) {
|
|
110
|
+
const createdSession = createHandoffSession({
|
|
111
|
+
cwd: ctx.cwd,
|
|
112
|
+
sessionDir: ctx.sessionManager.getSessionDir(),
|
|
113
|
+
parentSessionFile,
|
|
114
|
+
title: handoffMetadata.title,
|
|
115
|
+
});
|
|
116
|
+
const bootstrapValue = encodeHandoffBootstrap(
|
|
117
|
+
createHandoffBootstrap(createdSession.sessionId, handoffMetadata),
|
|
118
|
+
);
|
|
113
119
|
const launchResult = await launchSplitHandoffSession(pi, {
|
|
114
120
|
cwd: ctx.cwd,
|
|
115
121
|
sessionDir: ctx.sessionManager.getSessionDir(),
|
|
116
122
|
direction: parsedArgs.splitDirection,
|
|
117
123
|
sessionId: createdSession.sessionId,
|
|
118
124
|
bootstrapValue,
|
|
125
|
+
title: handoffMetadata.title,
|
|
119
126
|
});
|
|
120
127
|
|
|
121
128
|
if (!launchResult.success) {
|
|
122
129
|
ctx.ui.notify(
|
|
123
|
-
`${launchResult.error} Created handoff session ${createdSession.sessionId}; start it manually with: ${buildPiResumeCommand(ctx.sessionManager.getSessionDir(), createdSession.sessionId, bootstrapValue)}`,
|
|
130
|
+
`${launchResult.error} Created handoff session ${createdSession.sessionId}; start it manually with: ${buildPiResumeCommand(ctx.sessionManager.getSessionDir(), createdSession.sessionId, bootstrapValue, handoffMetadata.title)}`,
|
|
124
131
|
"error",
|
|
125
132
|
);
|
|
126
133
|
return;
|
|
@@ -130,10 +137,14 @@ export default function sessionHandoffExtension(pi: ExtensionAPI): void {
|
|
|
130
137
|
return;
|
|
131
138
|
}
|
|
132
139
|
|
|
133
|
-
const switchResult = await ctx.
|
|
140
|
+
const switchResult = await ctx.newSession({
|
|
141
|
+
parentSession: parentSessionFile,
|
|
142
|
+
setup: async (sessionManager) => {
|
|
143
|
+
sessionManager.appendSessionInfo(handoffMetadata.title);
|
|
144
|
+
sessionManager.appendCustomEntry(HANDOFF_METADATA_CUSTOM_TYPE, handoffMetadata);
|
|
145
|
+
},
|
|
134
146
|
withSession: async (nextCtx) => {
|
|
135
|
-
|
|
136
|
-
nextCtx.ui.notify("Handoff started in a new session.", "info");
|
|
147
|
+
startHandoffPromptAfterSessionRender(nextCtx, approvedDraft);
|
|
137
148
|
},
|
|
138
149
|
});
|
|
139
150
|
|
|
@@ -191,10 +202,19 @@ export default function sessionHandoffExtension(pi: ExtensionAPI): void {
|
|
|
191
202
|
if (!getHandoffMetadataFromEntries(entries)) {
|
|
192
203
|
pi.appendEntry(
|
|
193
204
|
HANDOFF_METADATA_CUSTOM_TYPE,
|
|
194
|
-
createHandoffSessionMetadata(
|
|
205
|
+
createHandoffSessionMetadata(
|
|
206
|
+
bootstrap.goal,
|
|
207
|
+
bootstrap.nextTask,
|
|
208
|
+
bootstrap.initialPrompt,
|
|
209
|
+
bootstrap.title,
|
|
210
|
+
),
|
|
195
211
|
);
|
|
196
212
|
}
|
|
197
213
|
|
|
214
|
+
if (!ctx.sessionManager.getSessionName()) {
|
|
215
|
+
pi.setSessionName(bootstrap.title);
|
|
216
|
+
}
|
|
217
|
+
|
|
198
218
|
pi.sendUserMessage(bootstrap.initialPrompt);
|
|
199
219
|
} finally {
|
|
200
220
|
delete process.env[HANDOFF_BOOTSTRAP_ENV];
|
|
@@ -266,6 +286,23 @@ async function runWithLoader<T>(
|
|
|
266
286
|
return result;
|
|
267
287
|
}
|
|
268
288
|
|
|
289
|
+
function startHandoffPromptAfterSessionRender(
|
|
290
|
+
ctx: HandoffPromptContext,
|
|
291
|
+
approvedDraft: string,
|
|
292
|
+
): void {
|
|
293
|
+
// ctx.newSession() renders the replacement session only after withSession returns.
|
|
294
|
+
setImmediate(() => {
|
|
295
|
+
void (async () => {
|
|
296
|
+
try {
|
|
297
|
+
await ctx.sendUserMessage(approvedDraft);
|
|
298
|
+
ctx.ui.notify("Handoff started in a new session.", "info");
|
|
299
|
+
} catch (error) {
|
|
300
|
+
ctx.ui.notify(formatHandoffError(error), "error");
|
|
301
|
+
}
|
|
302
|
+
})();
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
269
306
|
function formatHandoffError(error: unknown): string {
|
|
270
307
|
if (error instanceof Error && error.message.trim()) {
|
|
271
308
|
return error.message;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext } from "@
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { HANDOFF_BOOTSTRAP_ENV, parseHandoffBootstrap } from "./session-handoff/metadata.js";
|
|
3
3
|
import { createSessionHookController } from "./session-search/hooks.js";
|
|
4
4
|
import { loadSettings } from "./shared/settings.js";
|
|
@@ -3,8 +3,8 @@ import {
|
|
|
3
3
|
type ExtensionAPI,
|
|
4
4
|
type ExtensionCommandContext,
|
|
5
5
|
type Theme,
|
|
6
|
-
} from "@
|
|
7
|
-
import { type Focusable, matchesKey, visibleWidth } from "@
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { type Focusable, matchesKey, visibleWidth } from "@earendil-works/pi-tui";
|
|
8
8
|
import { type ReindexResult, rebuildSessionIndex } from "./session-search/reindex.js";
|
|
9
9
|
import { getIndexStatus, type SessionIndexStatus } from "./shared/session-index/index.js";
|
|
10
10
|
import { loadSettings } from "./shared/settings.js";
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import type { AgentMessage } from "@
|
|
4
|
-
import type { AssistantMessage, ToolCall, ToolResultMessage } from "@
|
|
3
|
+
import type { AgentMessage } from "@earendil-works/pi-agent-core";
|
|
4
|
+
import type { AssistantMessage, ToolCall, ToolResultMessage } from "@earendil-works/pi-ai";
|
|
5
5
|
import {
|
|
6
6
|
type CustomEntry,
|
|
7
7
|
parseSessionEntries,
|
|
8
8
|
type SessionEntry,
|
|
9
9
|
type SessionHeader,
|
|
10
10
|
type SessionMessageEntry,
|
|
11
|
-
} from "@
|
|
12
|
-
import { Type } from "
|
|
11
|
+
} from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { Type } from "typebox";
|
|
13
13
|
import {
|
|
14
14
|
HANDOFF_METADATA_CUSTOM_TYPE,
|
|
15
15
|
type HandoffSessionMetadata,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { existsSync } from "node:fs";
|
|
2
|
-
import type { ImageContent, TextContent } from "@
|
|
2
|
+
import type { ImageContent, TextContent } from "@earendil-works/pi-ai";
|
|
3
3
|
import {
|
|
4
4
|
isToolCallEventType,
|
|
5
5
|
type ToolCallEvent,
|
|
6
6
|
type ToolResultEvent,
|
|
7
|
-
} from "@
|
|
7
|
+
} from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import {
|
|
9
9
|
clearSessionIndexedData,
|
|
10
10
|
getIndexStatus,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
|
-
import type { ExtensionAPI, ExtensionContext, Theme } from "@
|
|
3
|
-
import { Text } from "@
|
|
4
|
-
import { Type } from "
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
4
|
+
import { Type } from "typebox";
|
|
5
5
|
import {
|
|
6
6
|
stripSearchSnippetMarkers,
|
|
7
7
|
transformSearchSnippetMatches,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { Type } from "@sinclair/typebox";
|
|
2
1
|
import type Database from "better-sqlite3";
|
|
2
|
+
import { Type } from "typebox";
|
|
3
3
|
import type { FileTouchOp, FileTouchSource, PathScope } from "../../session-search/normalize.js";
|
|
4
4
|
import { safeParseTypeBoxJson } from "../typebox.js";
|
|
5
5
|
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { SettingsManager } from "@
|
|
4
|
-
import type { KeyId } from "@
|
|
5
|
-
import { type Static, Type } from "
|
|
3
|
+
import { SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import type { KeyId } from "@earendil-works/pi-tui";
|
|
5
|
+
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
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { Static, TSchema } from "
|
|
2
|
-
import { Value } from "
|
|
1
|
+
import type { Static, TSchema } from "typebox";
|
|
2
|
+
import { Value } from "typebox/value";
|
|
3
3
|
|
|
4
4
|
export function isTypeBoxValue<T extends TSchema>(schema: T, value: unknown): value is Static<T> {
|
|
5
5
|
return Value.Check(schema, value);
|
|
@@ -51,11 +51,11 @@ export function safeParseTypeBoxJson<T extends TSchema>(
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
function formatTypeBoxError(schema: TSchema, value: unknown, context: string): string {
|
|
54
|
-
const firstError = Value.Errors(schema, value)
|
|
54
|
+
const firstError = Value.Errors(schema, value)[0];
|
|
55
55
|
if (!firstError) {
|
|
56
56
|
return `${context}: invalid value.`;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
const path = firstError.
|
|
59
|
+
const path = firstError.instancePath.length > 0 ? firstError.instancePath : "/";
|
|
60
60
|
return `${context}: ${path} ${firstError.message}`;
|
|
61
61
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-sessions",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Pi session search, ask, handoff, auto-titling, and indexing tools",
|
|
6
6
|
"license": "MIT",
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
"keywords": [
|
|
16
16
|
"pi-package"
|
|
17
17
|
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=20 <26"
|
|
20
|
+
},
|
|
18
21
|
"files": [
|
|
19
22
|
"extensions",
|
|
20
23
|
"images"
|
|
@@ -38,23 +41,25 @@
|
|
|
38
41
|
"prepare": "husky"
|
|
39
42
|
},
|
|
40
43
|
"devDependencies": {
|
|
41
|
-
"@biomejs/biome": "^2.4.
|
|
42
|
-
"@
|
|
43
|
-
"@
|
|
44
|
-
"@
|
|
45
|
-
"@
|
|
44
|
+
"@biomejs/biome": "^2.4.14",
|
|
45
|
+
"@earendil-works/pi-agent-core": "^0.78.0",
|
|
46
|
+
"@earendil-works/pi-ai": "^0.78.0",
|
|
47
|
+
"@earendil-works/pi-coding-agent": "^0.78.0",
|
|
48
|
+
"@earendil-works/pi-tui": "^0.78.0",
|
|
46
49
|
"@types/better-sqlite3": "^7.6.13",
|
|
47
|
-
"@types/node": "^25.6.
|
|
50
|
+
"@types/node": "^25.6.2",
|
|
48
51
|
"husky": "^9.1.7",
|
|
49
|
-
"lint-staged": "^
|
|
52
|
+
"lint-staged": "^17.0.3",
|
|
53
|
+
"typebox": "^1.1.38",
|
|
50
54
|
"typescript": "^6.0.3",
|
|
51
|
-
"vitest": "^4.1.
|
|
55
|
+
"vitest": "^4.1.5"
|
|
52
56
|
},
|
|
53
57
|
"peerDependencies": {
|
|
54
|
-
"@
|
|
55
|
-
"@
|
|
56
|
-
"@
|
|
57
|
-
"@
|
|
58
|
+
"@earendil-works/pi-agent-core": ">=0.78.0",
|
|
59
|
+
"@earendil-works/pi-ai": ">=0.78.0",
|
|
60
|
+
"@earendil-works/pi-coding-agent": ">=0.78.0",
|
|
61
|
+
"@earendil-works/pi-tui": ">=0.78.0",
|
|
62
|
+
"typebox": ">=1.1.24"
|
|
58
63
|
},
|
|
59
64
|
"lint-staged": {
|
|
60
65
|
"*.{js,cjs,mjs,jsx,ts,tsx,json,jsonc}": "biome check --write --no-errors-on-unmatched"
|