pi-sessions 0.5.1 → 0.7.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.
Files changed (57) hide show
  1. package/README.md +22 -1
  2. package/extensions/session-ask/agent.ts +400 -0
  3. package/extensions/session-ask/navigate.ts +880 -0
  4. package/extensions/session-ask.ts +82 -134
  5. package/extensions/session-auto-title/command.ts +1 -1
  6. package/extensions/session-auto-title/controller.ts +3 -3
  7. package/extensions/session-auto-title/generate.ts +2 -2
  8. package/extensions/session-auto-title/model.ts +4 -12
  9. package/extensions/session-auto-title/retitle.ts +5 -5
  10. package/extensions/session-auto-title/state.ts +1 -1
  11. package/extensions/session-auto-title/wizard.ts +4 -4
  12. package/extensions/session-auto-title.ts +8 -8
  13. package/extensions/session-handoff/extract.ts +18 -5
  14. package/extensions/session-handoff/metadata.ts +4 -1
  15. package/extensions/session-handoff/picker.ts +52 -6
  16. package/extensions/session-handoff/query.ts +78 -62
  17. package/extensions/session-handoff/refs.ts +14 -20
  18. package/extensions/session-handoff/spawn.ts +1 -1
  19. package/extensions/session-handoff.ts +54 -35
  20. package/extensions/session-hooks.ts +3 -3
  21. package/extensions/session-index.ts +4 -4
  22. package/extensions/session-messaging/broker/process.ts +302 -0
  23. package/extensions/session-messaging/broker/spawn.ts +164 -0
  24. package/extensions/session-messaging/pi/incoming-runtime.ts +114 -0
  25. package/extensions/session-messaging/pi/message-contracts.ts +36 -0
  26. package/extensions/session-messaging/pi/message-view.ts +131 -0
  27. package/extensions/session-messaging/pi/renderer.ts +16 -0
  28. package/extensions/session-messaging/pi/service.ts +370 -0
  29. package/extensions/session-messaging/pi/tools.ts +92 -0
  30. package/extensions/session-messaging.ts +30 -0
  31. package/extensions/session-search/extract.ts +90 -440
  32. package/extensions/session-search/hooks.ts +20 -28
  33. package/extensions/session-search/normalize.ts +1 -1
  34. package/extensions/session-search/reindex.ts +3 -2
  35. package/extensions/session-search.ts +161 -132
  36. package/extensions/shared/model.ts +18 -0
  37. package/extensions/shared/session-broker/active.ts +26 -0
  38. package/extensions/shared/session-broker/client.ts +253 -0
  39. package/extensions/shared/session-broker/framing.ts +72 -0
  40. package/extensions/shared/session-broker/protocol.ts +95 -0
  41. package/extensions/shared/session-broker/socket-path.ts +30 -0
  42. package/extensions/shared/session-index/access.ts +128 -0
  43. package/extensions/shared/session-index/common.ts +46 -51
  44. package/extensions/shared/session-index/index.ts +6 -5
  45. package/extensions/shared/session-index/lineage.ts +19 -2
  46. package/extensions/shared/session-index/query/ast.ts +40 -0
  47. package/extensions/shared/session-index/query/compiler.ts +146 -0
  48. package/extensions/shared/session-index/query/lexer.ts +140 -0
  49. package/extensions/shared/session-index/query/parser.ts +178 -0
  50. package/extensions/shared/session-index/schema.ts +25 -9
  51. package/extensions/shared/session-index/scoring.ts +80 -0
  52. package/extensions/shared/session-index/search.ts +555 -281
  53. package/extensions/shared/session-index/sqlite.ts +16 -9
  54. package/extensions/shared/session-index/store.ts +14 -23
  55. package/extensions/shared/settings.ts +62 -5
  56. package/extensions/shared/text.ts +50 -0
  57. package/package.json +4 -3
@@ -1,22 +1,14 @@
1
- import type { Message } from "@earendil-works/pi-ai";
2
- import { complete } from "@earendil-works/pi-ai/compat";
3
1
  import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
4
- import { Text } from "@earendil-works/pi-tui";
2
+ import { getKeybindings, type Keybinding, Text } from "@earendil-works/pi-tui";
5
3
  import { Type } from "typebox";
6
- import { type RenderedSessionTree, renderSessionTreeMarkdown } from "./session-search/extract.js";
4
+ import { runSessionAskAgent } from "./session-ask/agent.ts";
7
5
  import {
8
- getIndexStatus,
9
6
  getSessionById,
10
- INDEX_SCHEMA_VERSION,
11
- openIndexDatabase,
12
7
  type SessionLineageRow,
13
- } from "./shared/session-index/index.js";
14
- import { formatSessionTitleOrShortId, isExactSessionId } from "./shared/session-ui.js";
15
- import { loadSettings } from "./shared/settings.js";
16
-
17
- const SESSION_ASK_SYSTEM_PROMPT = `You are analyzing a Pi coding session transcript. The transcript includes the entire session tree, including abandoned branches and summaries.
18
-
19
- Answer the user's question using only the session contents. Be specific and concise. Include exact file paths, decisions, and outcomes when present. If the answer is not in the session, say so clearly.`;
8
+ withSessionIndex,
9
+ } from "./shared/session-index/index.ts";
10
+ import { formatSessionTitleOrShortId, isExactSessionId } from "./shared/session-ui.ts";
11
+ import { loadSettings } from "./shared/settings.ts";
20
12
 
21
13
  const COLLAPSED_ANSWER_PREVIEW_ROWS = 6;
22
14
 
@@ -25,21 +17,21 @@ interface SessionAskToolParams {
25
17
  question: string;
26
18
  }
27
19
 
20
+ interface SessionAskRelevantFile {
21
+ path: string;
22
+ reason: string;
23
+ }
24
+
28
25
  interface SessionAskToolDetails {
29
- cancelled?: boolean | undefined;
30
- error?: boolean | undefined;
31
26
  answer?: string | undefined;
27
+ debugSessionPath?: string | undefined;
32
28
  question?: string | undefined;
29
+ relevantFiles?: SessionAskRelevantFile[] | undefined;
33
30
  sessionId?: string | undefined;
34
31
  sessionName?: string | undefined;
35
32
  sessionPath?: string | undefined;
36
33
  }
37
34
 
38
- interface TextContentBlock {
39
- type: "text";
40
- text: string;
41
- }
42
-
43
35
  export default function sessionAskExtension(pi: ExtensionAPI): void {
44
36
  const settings = loadSettings();
45
37
 
@@ -61,32 +53,17 @@ export default function sessionAskExtension(pi: ExtensionAPI): void {
61
53
  async execute(_toolCallId, params: SessionAskToolParams, signal, onUpdate, ctx) {
62
54
  const sessionId = params.session.trim();
63
55
  if (!sessionId) {
64
- return errorResult("session_ask requires a session id.", { error: true });
56
+ throw new Error("session_ask requires a session id.");
65
57
  }
66
58
 
67
59
  const question = params.question.trim();
68
60
  if (!question) {
69
- return errorResult("session_ask requires a question.", { error: true, sessionId });
61
+ throw new Error("session_ask requires a question.");
70
62
  }
71
63
 
72
64
  const resolvedTarget = resolveSessionAskTarget(sessionId, settings.index.path);
73
65
  if (!resolvedTarget.resolved) {
74
- return errorResult(resolvedTarget.error ?? "Unable to resolve session id.", {
75
- error: true,
76
- sessionId,
77
- question,
78
- });
79
- }
80
-
81
- if (!ctx.model) {
82
- return errorResult("No active model is available for session_ask.", { error: true });
83
- }
84
-
85
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(ctx.model);
86
- if (!auth.ok || !auth.apiKey) {
87
- return errorResult(`No API key is available for ${ctx.model.provider}/${ctx.model.id}.`, {
88
- error: true,
89
- });
66
+ throw new Error(resolvedTarget.error ?? "Unable to resolve session id.");
90
67
  }
91
68
 
92
69
  const progressDetails: SessionAskToolDetails = {
@@ -100,68 +77,41 @@ export default function sessionAskExtension(pi: ExtensionAPI): void {
100
77
  details: progressDetails,
101
78
  });
102
79
 
103
- let rendered: RenderedSessionTree;
104
- try {
105
- rendered = renderSessionTreeMarkdown(resolvedTarget.resolved.sessionPath);
106
- } catch (error) {
107
- return errorResult(formatSessionAskLoadError(resolvedTarget.resolved.sessionPath, error), {
108
- error: true,
109
- sessionId,
110
- sessionPath: resolvedTarget.resolved.sessionPath,
111
- question,
112
- });
113
- }
114
-
115
- const loadedDetails: SessionAskToolDetails = {
116
- question,
117
- sessionId: rendered.sessionId,
118
- sessionName: rendered.sessionName,
119
- sessionPath: resolvedTarget.resolved.sessionPath,
120
- };
121
80
  onUpdate?.({
122
81
  content: [
123
82
  {
124
83
  type: "text",
125
- text: formatSessionAskHeader(rendered.sessionId, rendered.sessionName, question),
84
+ text: formatSessionAskHeader(
85
+ resolvedTarget.resolved.sessionId,
86
+ resolvedTarget.resolved.sessionName,
87
+ question,
88
+ ),
126
89
  },
127
90
  ],
128
- details: loadedDetails,
91
+ details: progressDetails,
129
92
  });
130
93
 
131
- const userMessage: Message = {
132
- role: "user",
133
- content: [
134
- {
135
- type: "text",
136
- text: [`## Session`, rendered.markdown, "", "## Question", question].join("\n"),
137
- },
138
- ],
139
- timestamp: Date.now(),
140
- };
141
-
142
- const response = await complete(
143
- ctx.model,
144
- { systemPrompt: SESSION_ASK_SYSTEM_PROMPT, messages: [userMessage] },
145
- signal
146
- ? {
147
- apiKey: auth.apiKey,
148
- ...(auth.headers ? { headers: auth.headers } : {}),
149
- signal,
150
- }
151
- : {
152
- apiKey: auth.apiKey,
153
- ...(auth.headers ? { headers: auth.headers } : {}),
154
- },
155
- );
94
+ const agentResult = await runSessionAskAgent({
95
+ ctx,
96
+ target: resolvedTarget.resolved,
97
+ question,
98
+ indexPath: settings.index.path,
99
+ askSettings: settings.ask,
100
+ thinkingLevel: pi.getThinkingLevel(),
101
+ signal,
102
+ });
156
103
 
157
- if (response.stopReason === "aborted") {
158
- return errorResult("Session ask was cancelled.", { cancelled: true });
104
+ if (signal?.aborted) {
105
+ throw new Error("Session ask was cancelled.");
159
106
  }
160
107
 
161
- const answer = collectTextBlocks(response.content).join("\n").trim();
108
+ const answer = agentResult?.answer || "Could not determine an answer from the session.";
109
+ const relevantFiles = agentResult?.relevantFiles ?? [];
162
110
 
163
111
  const details: SessionAskToolDetails = {
164
112
  answer,
113
+ debugSessionPath: agentResult?.debugSessionPath,
114
+ relevantFiles,
165
115
  sessionId: resolvedTarget.resolved.sessionId,
166
116
  sessionName: resolvedTarget.resolved.sessionName,
167
117
  sessionPath: resolvedTarget.resolved.sessionPath,
@@ -172,21 +122,29 @@ export default function sessionAskExtension(pi: ExtensionAPI): void {
172
122
  {
173
123
  type: "text",
174
124
  text: [
175
- formatSessionAskHeader(rendered.sessionId, rendered.sessionName, question),
176
- answer || "No answer generated.",
125
+ formatSessionAskHeader(
126
+ resolvedTarget.resolved.sessionId,
127
+ resolvedTarget.resolved.sessionName,
128
+ question,
129
+ ),
130
+ formatSessionAskAnswer(answer, relevantFiles, agentResult?.debugSessionPath),
177
131
  ].join("\n\n"),
178
132
  },
179
133
  ],
180
134
  details,
181
135
  };
182
136
  },
183
- renderResult(result, { expanded, isPartial }, theme) {
137
+ renderResult(result, { expanded, isPartial }, theme, context) {
184
138
  const details = result.details as SessionAskToolDetails | undefined;
185
139
  const content = result.content[0];
186
140
  if (content?.type !== "text") {
187
141
  return new Text(theme.fg("error", "No session output"), 0, 0);
188
142
  }
189
143
 
144
+ if (context.isError) {
145
+ return new Text(theme.fg("error", content.text), 0, 0);
146
+ }
147
+
190
148
  if (isPartial) {
191
149
  const lines = [theme.bold(theme.fg("warning", "Reading session..."))];
192
150
  if (details?.sessionId || details?.sessionName) {
@@ -199,14 +157,6 @@ export default function sessionAskExtension(pi: ExtensionAPI): void {
199
157
  return new Text(lines.join("\n"), 0, 0);
200
158
  }
201
159
 
202
- if (details?.cancelled) {
203
- return new Text(theme.fg("warning", content.text), 0, 0);
204
- }
205
-
206
- if (details?.error) {
207
- return new Text(theme.fg("error", content.text), 0, 0);
208
- }
209
-
210
160
  const answer = (details?.answer ?? "").trim() || "No answer generated.";
211
161
  const identity = formatSessionTitleOrShortId(details?.sessionName, details?.sessionId);
212
162
  const lines = [`title: ${theme.bold(identity)}`];
@@ -234,30 +184,20 @@ function resolveSessionAskTarget(
234
184
  };
235
185
  }
236
186
 
237
- const status = getIndexStatus(indexPath);
238
- if (!status.exists || status.schemaVersion !== INDEX_SCHEMA_VERSION) {
239
- return {
240
- error: `Session index missing or incompatible at ${indexPath}. Run /session-index and press r to rebuild it.`,
241
- };
242
- }
243
-
244
- const db = openIndexDatabase(status.dbPath, { create: false });
245
187
  try {
246
- const row = getSessionById(db, sessionId);
247
- if (!row) {
248
- return { error: `No indexed session found for id: ${sessionId}` };
249
- }
188
+ return withSessionIndex(indexPath, { mode: "read", required: true }, ({ db }) => {
189
+ const row = getSessionById(db, sessionId);
190
+ if (!row) {
191
+ return { error: `No indexed session found for id: ${sessionId}` };
192
+ }
250
193
 
251
- return { resolved: row };
252
- } finally {
253
- db.close();
194
+ return { resolved: row };
195
+ });
196
+ } catch (error) {
197
+ return { error: error instanceof Error ? error.message : String(error) };
254
198
  }
255
199
  }
256
200
 
257
- function collectTextBlocks(content: Array<{ type: string; text?: string }>): string[] {
258
- return content.filter(isTextContentBlock).map((block) => block.text);
259
- }
260
-
261
201
  function formatSessionAskHeader(sessionId: string, sessionName: string, question: string): string {
262
202
  return [
263
203
  `session: ${sessionId}`,
@@ -266,14 +206,20 @@ function formatSessionAskHeader(sessionId: string, sessionName: string, question
266
206
  ].join("\n");
267
207
  }
268
208
 
269
- function errorResult(
270
- text: string,
271
- details: SessionAskToolDetails,
272
- ): { content: Array<{ type: "text"; text: string }>; details: SessionAskToolDetails } {
273
- return {
274
- content: [{ type: "text", text }],
275
- details,
276
- };
209
+ function formatSessionAskAnswer(
210
+ answer: string,
211
+ relevantFiles: SessionAskRelevantFile[],
212
+ debugSessionPath: string | undefined,
213
+ ): string {
214
+ const lines = [answer || "No answer generated."];
215
+ if (relevantFiles.length > 0) {
216
+ lines.push("", "Relevant files:");
217
+ lines.push(...relevantFiles.map((file) => `- ${file.path}: ${file.reason}`));
218
+ }
219
+ if (debugSessionPath) {
220
+ lines.push("", `debug_session: ${debugSessionPath}`);
221
+ }
222
+ return lines.join("\n");
277
223
  }
278
224
 
279
225
  function formatSessionAskAnswerPreview(answer: string, expanded: boolean, theme: Theme): string[] {
@@ -282,17 +228,19 @@ function formatSessionAskAnswerPreview(answer: string, expanded: boolean, theme:
282
228
  return lines;
283
229
  }
284
230
 
285
- return [...lines.slice(0, COLLAPSED_ANSWER_PREVIEW_ROWS), theme.fg("dim", "...")];
231
+ return [
232
+ ...lines.slice(0, COLLAPSED_ANSWER_PREVIEW_ROWS),
233
+ formatOverflowHint(lines.length - COLLAPSED_ANSWER_PREVIEW_ROWS, lines.length, theme),
234
+ ];
286
235
  }
287
236
 
288
- function formatSessionAskLoadError(sessionPath: string, error: unknown): string {
289
- if (error instanceof Error && "code" in error && error.code === "ENOENT") {
290
- return `Session file not found: ${sessionPath}`;
291
- }
292
-
293
- return `Unable to load session file: ${sessionPath}`;
237
+ function formatOverflowHint(remaining: number, total: number, theme: Theme): string {
238
+ return `${theme.fg("muted", `... (${remaining} more lines, ${total} total,`)} ${theme.fg(
239
+ "dim",
240
+ formatKeyHint("app.tools.expand"),
241
+ )}${theme.fg("muted", " to expand)")}`;
294
242
  }
295
243
 
296
- function isTextContentBlock(content: { type: string; text?: string }): content is TextContentBlock {
297
- return content.type === "text" && typeof content.text === "string";
244
+ function formatKeyHint(keybinding: Keybinding): string {
245
+ return getKeybindings().getKeys(keybinding).join("/");
298
246
  }
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { AutocompleteItem } from "@earendil-works/pi-tui";
3
- import { isTuiMode } from "../shared/pi-mode.js";
3
+ import { isTuiMode } from "../shared/pi-mode.ts";
4
4
 
5
5
  export const TITLE_USAGE = "Usage: /title [this|folder|pi] [-f]";
6
6
 
@@ -1,13 +1,13 @@
1
1
  import { buildSessionContext, type ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import type { AutoTitleSettings } from "../shared/settings.js";
3
- import type { AutoTitleFailure } from "./generate.js";
2
+ import type { AutoTitleSettings } from "../shared/settings.ts";
3
+ import type { AutoTitleFailure } from "./generate.ts";
4
4
  import {
5
5
  type AutoTitleMode,
6
6
  type AutoTitlePersistedState,
7
7
  type AutoTitleTrigger,
8
8
  createAutoTitleState,
9
9
  getLatestAutoTitleState,
10
- } from "./state.js";
10
+ } from "./state.ts";
11
11
 
12
12
  export interface SessionAutoTitleStateSnapshot {
13
13
  currentSessionFile: string | undefined;
@@ -4,8 +4,8 @@ import { join } from "node:path";
4
4
  import type { Api, Model, TextContent, UserMessage } from "@earendil-works/pi-ai";
5
5
  import { completeSimple } from "@earendil-works/pi-ai/compat";
6
6
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
7
- import type { AutoTitleContext } from "./context.js";
8
- import type { AutoTitleTrigger } from "./state.js";
7
+ import type { AutoTitleContext } from "./context.ts";
8
+ import type { AutoTitleTrigger } from "./state.ts";
9
9
 
10
10
  const AUTO_TITLE_REQUEST_TIMEOUT_MS = 15_000;
11
11
  const AUTO_TITLE_MAX_TOKENS = 64;
@@ -1,6 +1,7 @@
1
1
  import type { Api, Model } from "@earendil-works/pi-ai";
2
2
  import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { ModelReference } from "../shared/settings.js";
3
+ import { findModelByReference } from "../shared/model.ts";
4
+ import { ModelReference } from "../shared/settings.ts";
4
5
 
5
6
  const DEFAULT_AUTO_TITLE_FALLBACK_MODELS: readonly ModelReference[] = [
6
7
  new ModelReference("google", "gemini-flash-lite-latest"),
@@ -22,7 +23,7 @@ export function resolveAutoTitleModel(
22
23
  const availableModels = ctx.modelRegistry.getAvailable();
23
24
 
24
25
  if (configuredModel) {
25
- const configuredMatch = findMatchingModel(availableModels, configuredModel);
26
+ const configuredMatch = findModelByReference(availableModels, configuredModel);
26
27
  if (configuredMatch) {
27
28
  return {
28
29
  model: configuredMatch,
@@ -32,7 +33,7 @@ export function resolveAutoTitleModel(
32
33
  }
33
34
 
34
35
  for (const fallbackReference of DEFAULT_AUTO_TITLE_FALLBACK_MODELS) {
35
- const fallbackMatch = findMatchingModel(availableModels, fallbackReference);
36
+ const fallbackMatch = findModelByReference(availableModels, fallbackReference);
36
37
  if (fallbackMatch) {
37
38
  return {
38
39
  model: fallbackMatch,
@@ -50,12 +51,3 @@ export function resolveAutoTitleModel(
50
51
  source: "current",
51
52
  };
52
53
  }
53
-
54
- function findMatchingModel(
55
- availableModels: Model<Api>[],
56
- reference: ModelReference,
57
- ): Model<Api> | undefined {
58
- return availableModels.find(
59
- (model) => model.provider === reference.provider && model.id === reference.modelId,
60
- );
61
- }
@@ -6,19 +6,19 @@ import type {
6
6
  SessionInfo,
7
7
  } from "@earendil-works/pi-coding-agent";
8
8
  import { SessionManager } from "@earendil-works/pi-coding-agent";
9
- import type { RetitleMode, RetitleScope } from "./command.js";
10
- import { buildAutoTitleContext } from "./context.js";
11
- import type { AutoTitleTriggerPlan, SessionAutoTitleController } from "./controller.js";
9
+ import type { RetitleMode, RetitleScope } from "./command.ts";
10
+ import { buildAutoTitleContext } from "./context.ts";
11
+ import type { AutoTitleTriggerPlan, SessionAutoTitleController } from "./controller.ts";
12
12
  import {
13
13
  type AutoTitleGenerationResult,
14
14
  createAutoTitleFailure,
15
15
  generateAutoTitle,
16
- } from "./generate.js";
16
+ } from "./generate.ts";
17
17
  import {
18
18
  AUTO_TITLE_STATE_CUSTOM_TYPE,
19
19
  type AutoTitlePersistedState,
20
20
  createAutoTitleState,
21
- } from "./state.js";
21
+ } from "./state.ts";
22
22
 
23
23
  export interface RetitleScopeScan {
24
24
  scope: Exclude<RetitleScope, "this">;
@@ -1,6 +1,6 @@
1
1
  import type { SessionEntry } from "@earendil-works/pi-coding-agent";
2
2
  import { type Static, Type } from "typebox";
3
- import { safeParseTypeBoxValue } from "../shared/typebox.js";
3
+ import { safeParseTypeBoxValue } from "../shared/typebox.ts";
4
4
 
5
5
  export const AUTO_TITLE_STATE_CUSTOM_TYPE = "pi-sessions.auto-title";
6
6
  export const AUTO_TITLE_STATE_VERSION = 1;
@@ -7,9 +7,9 @@ import {
7
7
  } from "@earendil-works/pi-coding-agent";
8
8
  import type { Focusable, TUI } from "@earendil-works/pi-tui";
9
9
  import { matchesKey, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
10
- import type { RetitleCommandOutcome, RetitleMode, RetitleScope } from "./command.js";
11
- import type { AutoRetitleStatus, SessionAutoTitleController } from "./controller.js";
12
- import type { AutoTitleFailure } from "./generate.js";
10
+ import type { RetitleCommandOutcome, RetitleMode, RetitleScope } from "./command.ts";
11
+ import type { AutoRetitleStatus, SessionAutoTitleController } from "./controller.ts";
12
+ import type { AutoTitleFailure } from "./generate.ts";
13
13
  import {
14
14
  buildBulkRetitleMessage,
15
15
  buildRetitleScopeScan,
@@ -20,7 +20,7 @@ import {
20
20
  type RetitleScopeScan,
21
21
  runBulkRetitle,
22
22
  runRetitlePlan,
23
- } from "./retitle.js";
23
+ } from "./retitle.ts";
24
24
 
25
25
  interface RetitleWizardOptions {
26
26
  initialInvocation?: {
@@ -11,29 +11,29 @@ import {
11
11
  getRetitleArgumentCompletions,
12
12
  type RetitleCommandInvocation,
13
13
  type RetitleCommandOutcome,
14
- } from "./session-auto-title/command.js";
14
+ } from "./session-auto-title/command.ts";
15
15
  import {
16
16
  createSessionAutoTitleController,
17
17
  type SessionAutoTitleController,
18
- } from "./session-auto-title/controller.js";
19
- import { resolveAutoTitleModel } from "./session-auto-title/model.js";
18
+ } from "./session-auto-title/controller.ts";
19
+ import { resolveAutoTitleModel } from "./session-auto-title/model.ts";
20
20
  import {
21
21
  buildRetitleScopeScan,
22
22
  notifyBulkRetitleResult,
23
23
  persistAutoTitleState,
24
24
  runBulkRetitle,
25
25
  runRetitlePlan,
26
- } from "./session-auto-title/retitle.js";
27
- import { showRetitleWizard } from "./session-auto-title/wizard.js";
28
- import { isTuiMode } from "./shared/pi-mode.js";
29
- import { loadSettings } from "./shared/settings.js";
26
+ } from "./session-auto-title/retitle.ts";
27
+ import { showRetitleWizard } from "./session-auto-title/wizard.ts";
28
+ import { isTuiMode } from "./shared/pi-mode.ts";
29
+ import { loadSettings } from "./shared/settings.ts";
30
30
 
31
31
  export {
32
32
  createSessionAutoTitleCommandHandler,
33
33
  getRetitleArgumentCompletions,
34
34
  parseRetitleCommand,
35
35
  TITLE_USAGE,
36
- } from "./session-auto-title/command.js";
36
+ } from "./session-auto-title/command.ts";
37
37
 
38
38
  interface TitleRunState {
39
39
  controller: SessionAutoTitleController;
@@ -19,7 +19,7 @@ import {
19
19
  serializeConversation,
20
20
  } from "@earendil-works/pi-coding-agent";
21
21
  import { type Static, Type } from "typebox";
22
- import { parseTypeBoxValue } from "../shared/typebox.js";
22
+ import { parseTypeBoxValue } from "../shared/typebox.ts";
23
23
 
24
24
  const MAX_RELEVANT_FILES = 12;
25
25
  const MAX_OPEN_QUESTIONS = 8;
@@ -88,6 +88,7 @@ export async function generateHandoffDraft(
88
88
  goal: string,
89
89
  thinkingLevel: ThinkingLevel | undefined,
90
90
  signal?: AbortSignal,
91
+ requestResponse = false,
91
92
  ): Promise<HandoffDraftResult | undefined> {
92
93
  if (!ctx.model) {
93
94
  throw new Error("No model is available for handoff.");
@@ -99,6 +100,7 @@ export async function generateHandoffDraft(
99
100
  goal,
100
101
  thinkingLevel,
101
102
  signal,
103
+ requestResponse,
102
104
  );
103
105
  }
104
106
 
@@ -108,6 +110,7 @@ export async function generateHandoffDraftFromSessionManager(
108
110
  goal: string,
109
111
  thinkingLevel: ThinkingLevel | undefined,
110
112
  signal?: AbortSignal,
113
+ requestResponse = false,
111
114
  ): Promise<HandoffDraftResult | undefined> {
112
115
  if (!ctx.model) {
113
116
  throw new Error("No model is available for handoff.");
@@ -139,7 +142,7 @@ export async function generateHandoffDraftFromSessionManager(
139
142
  const sessionPath = sourceSessionManager.getSessionFile();
140
143
 
141
144
  return {
142
- draft: assembleHandoffDraft(sessionId, sessionPath, handoffContext, goal),
145
+ draft: assembleHandoffDraft(sessionId, sessionPath, handoffContext, goal, requestResponse),
143
146
  context: handoffContext,
144
147
  sessionId,
145
148
  sessionPath,
@@ -241,8 +244,9 @@ export function assembleHandoffDraft(
241
244
  sessionPath: string | undefined,
242
245
  handoffContext: HandoffContext,
243
246
  goal: string,
247
+ requestResponse = false,
244
248
  ): string {
245
- const sections = [buildContinuityLine(sessionId, sessionPath)];
249
+ const sections = [buildContinuityLine(sessionId, sessionPath, requestResponse)];
246
250
  const nextTask = handoffContext.nextTask.trim() || goal.trim();
247
251
 
248
252
  if (nextTask) {
@@ -326,8 +330,17 @@ function extractHandoffContextFromArguments(
326
330
  };
327
331
  }
328
332
 
329
- function buildContinuityLine(sessionId: string, _sessionPath: string | undefined): string {
330
- return `Continuing work from session ${sessionId}. When you lack specific information you can use session_ask.`;
333
+ function buildContinuityLine(
334
+ sessionId: string,
335
+ _sessionPath: string | undefined,
336
+ requestResponse: boolean,
337
+ ): string {
338
+ const base = `Continuing work from session ${sessionId}. When you lack specific information you can use session_ask.`;
339
+ if (!requestResponse) {
340
+ return base;
341
+ }
342
+
343
+ return `${base} When this work is complete, send that session a completion report with session_send_message.`;
331
344
  }
332
345
 
333
346
  function normalizeStringArray(value: unknown, limit: number): string[] {
@@ -1,7 +1,7 @@
1
1
  import { Buffer } from "node:buffer";
2
2
  import type { CustomEntry, SessionEntry } from "@earendil-works/pi-coding-agent";
3
3
  import { type Static, Type } from "typebox";
4
- import { safeParseTypeBoxValue } from "../shared/typebox.js";
4
+ import { safeParseTypeBoxValue } from "../shared/typebox.ts";
5
5
 
6
6
  export const HANDOFF_METADATA_CUSTOM_TYPE = "pi-sessions.handoff";
7
7
  export const HANDOFF_BOOTSTRAP_ENV = "PI_SESSIONS_HANDOFF_BOOTSTRAP";
@@ -30,6 +30,7 @@ export const CHILD_GENERATED_HANDOFF_BOOTSTRAP_SCHEMA = Type.Object({
30
30
  goal: Type.String(),
31
31
  title: Type.String(),
32
32
  parentSessionFile: Type.String(),
33
+ requestResponse: Type.Optional(Type.Boolean()),
33
34
  });
34
35
 
35
36
  export const HANDOFF_BOOTSTRAP_SCHEMA = Type.Union([
@@ -80,6 +81,7 @@ export function createChildGeneratedHandoffBootstrap(options: {
80
81
  goal: string;
81
82
  title: string;
82
83
  parentSessionFile: string;
84
+ requestResponse?: boolean | undefined;
83
85
  }): ChildGeneratedHandoffBootstrap {
84
86
  return {
85
87
  mode: "generate",
@@ -87,6 +89,7 @@ export function createChildGeneratedHandoffBootstrap(options: {
87
89
  goal: options.goal.trim(),
88
90
  title: options.title.trim(),
89
91
  parentSessionFile: options.parentSessionFile,
92
+ ...(options.requestResponse === undefined ? {} : { requestResponse: options.requestResponse }),
90
93
  };
91
94
  }
92
95