pi-sessions 0.8.0 → 0.9.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 (104) hide show
  1. package/README.md +70 -37
  2. package/extensions/pi-sessions.ts +125 -0
  3. package/extensions/session-ask/agent.ts +34 -14
  4. package/extensions/{session-ask.ts → session-ask/install.ts} +73 -90
  5. package/extensions/session-ask/navigate.ts +3 -1
  6. package/extensions/session-ask/presenter.ts +23 -0
  7. package/extensions/session-ask/renderer.ts +63 -0
  8. package/extensions/session-ask/tool-contract.ts +27 -0
  9. package/extensions/session-ask/view-model.ts +18 -0
  10. package/extensions/session-auto-title/generate.ts +18 -18
  11. package/extensions/{session-auto-title.ts → session-auto-title/install.ts} +60 -53
  12. package/extensions/session-auto-title/model.ts +25 -17
  13. package/extensions/session-auto-title/retitle.ts +17 -18
  14. package/extensions/session-auto-title/wizard.ts +12 -11
  15. package/extensions/session-handoff/board-view-model.ts +181 -0
  16. package/extensions/session-handoff/board.ts +663 -0
  17. package/extensions/session-handoff/bootstrap.ts +174 -0
  18. package/extensions/session-handoff/extract.ts +101 -122
  19. package/extensions/session-handoff/install.ts +225 -0
  20. package/extensions/session-handoff/kickoff.ts +92 -0
  21. package/extensions/session-handoff/launch/backend.ts +16 -0
  22. package/extensions/session-handoff/launch/deferred.ts +20 -0
  23. package/extensions/session-handoff/launch/ghostty.ts +99 -0
  24. package/extensions/session-handoff/launch/resolution.ts +57 -0
  25. package/extensions/session-handoff/launch/shell.ts +7 -0
  26. package/extensions/session-handoff/launch/tmux.ts +31 -0
  27. package/extensions/session-handoff/launch-options.ts +61 -0
  28. package/extensions/session-handoff/launch-target.ts +91 -0
  29. package/extensions/session-handoff/metadata.ts +152 -63
  30. package/extensions/session-handoff/model.ts +58 -0
  31. package/extensions/session-handoff/query.ts +0 -1
  32. package/extensions/session-handoff/receipt.ts +96 -0
  33. package/extensions/session-handoff/review.ts +2 -28
  34. package/extensions/session-handoff/spawn.ts +76 -181
  35. package/extensions/session-handoff/strong-modal.ts +13 -0
  36. package/extensions/session-handoff/tool-contract.ts +12 -0
  37. package/extensions/session-handoff/tool-presenter.ts +35 -0
  38. package/extensions/session-handoff/tool-renderer.ts +66 -0
  39. package/extensions/session-handoff/tool-schema.ts +54 -0
  40. package/extensions/session-handoff/tool-view-model.ts +49 -0
  41. package/extensions/session-handoff/tool.ts +206 -0
  42. package/extensions/session-handoff/ui.ts +66 -0
  43. package/extensions/session-hooks/install.ts +75 -0
  44. package/extensions/{session-index.ts → session-index/install.ts} +9 -7
  45. package/extensions/session-messaging/broker/process.ts +21 -28
  46. package/extensions/session-messaging/install.ts +78 -0
  47. package/extensions/session-messaging/pi/cancel-session-presenter.ts +64 -0
  48. package/extensions/session-messaging/pi/incoming-message-presenter.ts +32 -0
  49. package/extensions/session-messaging/pi/incoming-message-view-model.ts +20 -0
  50. package/extensions/session-messaging/pi/incoming-runtime.ts +27 -23
  51. package/extensions/session-messaging/pi/message-contracts.ts +32 -0
  52. package/extensions/session-messaging/pi/renderer.ts +19 -12
  53. package/extensions/session-messaging/pi/send-message-presenter.ts +39 -0
  54. package/extensions/session-messaging/pi/send-message-view-model.ts +54 -0
  55. package/extensions/session-messaging/pi/service.ts +262 -81
  56. package/extensions/session-messaging/pi/tools.ts +184 -40
  57. package/extensions/session-search/extract.ts +60 -14
  58. package/extensions/session-search/hooks.ts +10 -13
  59. package/extensions/session-search/install.ts +373 -0
  60. package/extensions/session-search/renderer.ts +195 -0
  61. package/extensions/session-search/tool-contract.ts +51 -0
  62. package/extensions/shared/composition.ts +20 -0
  63. package/extensions/shared/errors.ts +3 -0
  64. package/extensions/shared/model-resolution.ts +73 -0
  65. package/extensions/shared/model-runtime.ts +33 -0
  66. package/extensions/shared/model.ts +7 -10
  67. package/extensions/shared/rendering/collapsible-text.ts +89 -0
  68. package/extensions/shared/rendering/expandable-content-layout.ts +84 -0
  69. package/extensions/shared/rendering/theme.ts +5 -0
  70. package/extensions/shared/session-broker/client.ts +30 -29
  71. package/extensions/shared/session-broker/protocol.ts +128 -10
  72. package/extensions/shared/session-index/common.ts +5 -5
  73. package/extensions/shared/session-index/lineage.ts +0 -3
  74. package/extensions/shared/session-index/schema.ts +0 -1
  75. package/extensions/shared/session-index/scoring.ts +1 -2
  76. package/extensions/shared/session-index/search.ts +11 -4
  77. package/extensions/shared/session-index/store.ts +4 -9
  78. package/extensions/shared/settings.ts +76 -38
  79. package/extensions/shared/text.ts +5 -0
  80. package/extensions/shared/thinking-levels.ts +22 -0
  81. package/extensions/shared/tmux.ts +205 -0
  82. package/extensions/subagents/cancel.ts +111 -0
  83. package/extensions/subagents/classify.ts +52 -0
  84. package/extensions/subagents/install.ts +344 -0
  85. package/extensions/subagents/launch-target.ts +75 -0
  86. package/extensions/subagents/ledger.ts +217 -0
  87. package/extensions/subagents/reconcile.ts +386 -0
  88. package/extensions/subagents/report-message-presenter.ts +36 -0
  89. package/extensions/subagents/report-message-renderer.ts +21 -0
  90. package/extensions/subagents/report-message-view-model.ts +29 -0
  91. package/extensions/subagents/report.ts +159 -0
  92. package/extensions/subagents/roster.ts +262 -0
  93. package/extensions/subagents/wake.ts +173 -0
  94. package/images/handoff-board-subagents.png +0 -0
  95. package/images/handoff-board-user-sessions.png +0 -0
  96. package/images/handoff-subagent-report.png +0 -0
  97. package/images/session-handoff-tool.png +0 -0
  98. package/package.json +10 -16
  99. package/extensions/session-handoff.ts +0 -741
  100. package/extensions/session-hooks.ts +0 -75
  101. package/extensions/session-messaging/pi/message-view.ts +0 -131
  102. package/extensions/session-messaging.ts +0 -30
  103. package/extensions/session-search.ts +0 -419
  104. package/extensions/shared/session-broker/active.ts +0 -26
@@ -0,0 +1,63 @@
1
+ import type {
2
+ AgentToolResult,
3
+ Theme,
4
+ ToolRenderResultOptions,
5
+ } from "@earendil-works/pi-coding-agent";
6
+ import { type Component, Text } from "@earendil-works/pi-tui";
7
+ import { ExpandableContentLayout } from "../shared/rendering/expandable-content-layout.ts";
8
+ import { formatSessionTitleOrShortId } from "../shared/session-ui.ts";
9
+ import { safeParseTypeBoxValue } from "../shared/typebox.ts";
10
+ import { buildSessionAskPresentation } from "./presenter.ts";
11
+ import {
12
+ SESSION_ASK_PROGRESS_DETAILS_SCHEMA,
13
+ SESSION_ASK_RESULT_DETAILS_SCHEMA,
14
+ } from "./tool-contract.ts";
15
+ import { buildSessionAskView } from "./view-model.ts";
16
+
17
+ interface SessionAskToolRendererState {
18
+ layout?: ExpandableContentLayout | undefined;
19
+ }
20
+
21
+ interface SessionAskToolRenderContext {
22
+ state: SessionAskToolRendererState;
23
+ isError: boolean;
24
+ }
25
+
26
+ export function renderSessionAskResult(
27
+ result: AgentToolResult<unknown>,
28
+ options: ToolRenderResultOptions,
29
+ theme: Theme,
30
+ context: SessionAskToolRenderContext,
31
+ ): Component {
32
+ const content = result.content[0];
33
+ if (content?.type !== "text") {
34
+ return new Text(theme.fg("error", "No session output"), 0, 0);
35
+ }
36
+
37
+ if (context.isError) {
38
+ return new Text(theme.fg("error", content.text), 0, 0);
39
+ }
40
+
41
+ if (options.isPartial) {
42
+ const details = safeParseTypeBoxValue(SESSION_ASK_PROGRESS_DETAILS_SCHEMA, result.details);
43
+ const lines = [theme.bold(theme.fg("warning", "Reading session..."))];
44
+ if (details) {
45
+ const identity = formatSessionTitleOrShortId(details.sessionName, details.sessionId);
46
+ lines.push(`title: ${theme.fg("accent", identity)}`);
47
+ }
48
+ if (details?.question) {
49
+ lines.push(theme.fg("muted", `prompt: ${details.question}`));
50
+ }
51
+ return new Text(lines.join("\n"), 0, 0);
52
+ }
53
+
54
+ const details = safeParseTypeBoxValue(SESSION_ASK_RESULT_DETAILS_SCHEMA, result.details);
55
+ if (!details) {
56
+ return new Text(content.text, 0, 0);
57
+ }
58
+
59
+ const layout = context.state.layout ?? new ExpandableContentLayout(theme);
60
+ context.state.layout = layout;
61
+ layout.update(buildSessionAskPresentation(buildSessionAskView(details), theme), options.expanded);
62
+ return layout;
63
+ }
@@ -0,0 +1,27 @@
1
+ import { type Static, Type } from "typebox";
2
+
3
+ export const SESSION_ASK_RELEVANT_FILE_SCHEMA = Type.Object({
4
+ path: Type.String(),
5
+ reason: Type.String(),
6
+ });
7
+
8
+ export const SESSION_ASK_PROGRESS_DETAILS_SCHEMA = Type.Object({
9
+ question: Type.String({ minLength: 1 }),
10
+ sessionId: Type.String({ minLength: 1 }),
11
+ sessionName: Type.String(),
12
+ sessionPath: Type.String({ minLength: 1 }),
13
+ });
14
+
15
+ export const SESSION_ASK_RESULT_DETAILS_SCHEMA = Type.Object({
16
+ answer: Type.String({ minLength: 1 }),
17
+ debugSessionPath: Type.Optional(Type.String({ minLength: 1 })),
18
+ question: Type.String({ minLength: 1 }),
19
+ relevantFiles: Type.Array(SESSION_ASK_RELEVANT_FILE_SCHEMA),
20
+ sessionId: Type.String({ minLength: 1 }),
21
+ sessionName: Type.String(),
22
+ sessionPath: Type.String({ minLength: 1 }),
23
+ });
24
+
25
+ export type SessionAskRelevantFile = Static<typeof SESSION_ASK_RELEVANT_FILE_SCHEMA>;
26
+ export type SessionAskProgressDetails = Static<typeof SESSION_ASK_PROGRESS_DETAILS_SCHEMA>;
27
+ export type SessionAskResultDetails = Static<typeof SESSION_ASK_RESULT_DETAILS_SCHEMA>;
@@ -0,0 +1,18 @@
1
+ import { normalizeOptionalText } from "../shared/text.ts";
2
+ import type { SessionAskResultDetails } from "./tool-contract.ts";
3
+
4
+ export interface SessionAskViewModel {
5
+ sessionId: string;
6
+ sessionName?: string | undefined;
7
+ question: string;
8
+ answer: string;
9
+ }
10
+
11
+ export function buildSessionAskView(details: SessionAskResultDetails): SessionAskViewModel {
12
+ return {
13
+ sessionId: details.sessionId.trim(),
14
+ sessionName: normalizeOptionalText(details.sessionName),
15
+ question: details.question.trim(),
16
+ answer: details.answer.trim(),
17
+ };
18
+ }
@@ -1,9 +1,9 @@
1
1
  import { appendFileSync, mkdirSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
4
5
  import type { Api, Model, TextContent, UserMessage } from "@earendil-works/pi-ai";
5
- import { completeSimple } from "@earendil-works/pi-ai/compat";
6
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
6
+ import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
7
7
  import type { AutoTitleContext } from "./context.ts";
8
8
  import type { AutoTitleTrigger } from "./state.ts";
9
9
 
@@ -17,6 +17,12 @@ export interface AutoTitleFailure {
17
17
  message: string;
18
18
  }
19
19
 
20
+ export interface AutoTitleGeneration {
21
+ systemPrompt: string;
22
+ timeoutMs: number;
23
+ thinkingLevel: ThinkingLevel | undefined;
24
+ }
25
+
20
26
  export type AutoTitleGenerationResult =
21
27
  | {
22
28
  ok: true;
@@ -28,12 +34,11 @@ export type AutoTitleGenerationResult =
28
34
  };
29
35
 
30
36
  export async function generateAutoTitle(
31
- ctx: ExtensionContext,
37
+ modelRuntime: ModelRuntime,
32
38
  model: Model<Api>,
33
39
  context: AutoTitleContext,
34
40
  trigger: AutoTitleTrigger,
35
- systemPrompt: string,
36
- timeoutMs: number,
41
+ generation: AutoTitleGeneration,
37
42
  ): Promise<AutoTitleGenerationResult> {
38
43
  if (!context.conversationText) {
39
44
  return {
@@ -46,16 +51,11 @@ export async function generateAutoTitle(
46
51
  };
47
52
  }
48
53
 
49
- const auth = await ctx.modelRegistry.getApiKeyAndHeaders(model);
50
- if (!auth.ok) {
51
- return {
52
- ok: false,
53
- failure: createAutoTitleFailure(trigger, model, "Failed to authenticate auto-title model."),
54
- };
55
- }
56
-
57
54
  const shouldPreserveTitle = trigger === "periodic" && Boolean(context.currentTitle?.trim());
58
- const resolvedSystemPrompt = buildAutoTitleSystemPrompt(systemPrompt, shouldPreserveTitle);
55
+ const resolvedSystemPrompt = buildAutoTitleSystemPrompt(
56
+ generation.systemPrompt,
57
+ shouldPreserveTitle,
58
+ );
59
59
  const userPrompt = buildAutoTitlePrompt(context, resolvedSystemPrompt, shouldPreserveTitle);
60
60
  const message: UserMessage = {
61
61
  role: "user",
@@ -64,7 +64,8 @@ export async function generateAutoTitle(
64
64
  };
65
65
 
66
66
  const abortController = new AbortController();
67
- const timeoutId = setTimeout(() => abortController.abort(), timeoutMs);
67
+ const timeoutId = setTimeout(() => abortController.abort(), generation.timeoutMs);
68
+ const thinkingLevel = generation.thinkingLevel;
68
69
 
69
70
  try {
70
71
  const requestContext = {
@@ -72,10 +73,9 @@ export async function generateAutoTitle(
72
73
  messages: [message],
73
74
  };
74
75
  writeAutoTitleDebugRequest(model, trigger, requestContext);
75
- const response = await completeSimple(model, requestContext, {
76
- ...(auth.apiKey && { apiKey: auth.apiKey }),
77
- ...(auth.headers && { headers: auth.headers }),
76
+ const response = await modelRuntime.completeSimple(model, requestContext, {
78
77
  maxTokens: AUTO_TITLE_MAX_TOKENS,
78
+ ...(thinkingLevel && thinkingLevel !== "off" ? { reasoning: thinkingLevel } : {}),
79
79
  signal: abortController.signal,
80
80
  });
81
81
  writeAutoTitleDebugResponse(model, trigger, response);
@@ -3,37 +3,30 @@ import type {
3
3
  ExtensionAPI,
4
4
  ExtensionCommandContext,
5
5
  ExtensionContext,
6
- SessionStartEvent,
6
+ ModelRuntime,
7
7
  TurnEndEvent,
8
8
  } from "@earendil-works/pi-coding-agent";
9
+ import type { SessionLifecycle } from "../shared/composition.ts";
10
+ import type { ModelRuntimeProvider } from "../shared/model-runtime.ts";
11
+ import { isTuiMode } from "../shared/pi-mode.ts";
12
+ import type { SessionSettings } from "../shared/settings.ts";
9
13
  import {
10
14
  createSessionAutoTitleCommandHandler,
11
15
  getRetitleArgumentCompletions,
12
16
  type RetitleCommandInvocation,
13
17
  type RetitleCommandOutcome,
14
- } from "./session-auto-title/command.ts";
15
- import {
16
- createSessionAutoTitleController,
17
- type SessionAutoTitleController,
18
- } from "./session-auto-title/controller.ts";
19
- import { resolveAutoTitleModel } from "./session-auto-title/model.ts";
18
+ } from "./command.ts";
19
+ import { createSessionAutoTitleController, type SessionAutoTitleController } from "./controller.ts";
20
+ import type { AutoTitleGeneration } from "./generate.ts";
21
+ import { type AutoTitleModelResolution, resolveAutoTitleModel } from "./model.ts";
20
22
  import {
21
23
  buildRetitleScopeScan,
22
24
  notifyBulkRetitleResult,
23
25
  persistAutoTitleState,
24
26
  runBulkRetitle,
25
27
  runRetitlePlan,
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
-
31
- export {
32
- createSessionAutoTitleCommandHandler,
33
- getRetitleArgumentCompletions,
34
- parseRetitleCommand,
35
- TITLE_USAGE,
36
- } from "./session-auto-title/command.ts";
28
+ } from "./retitle.ts";
29
+ import { showRetitleWizard } from "./wizard.ts";
37
30
 
38
31
  interface TitleRunState {
39
32
  controller: SessionAutoTitleController;
@@ -42,12 +35,26 @@ interface TitleRunState {
42
35
  clearInFlight: () => void;
43
36
  }
44
37
 
45
- export default function sessionAutoTitleExtension(pi: ExtensionAPI): void {
46
- const settings = loadSettings();
38
+ export function installAutoTitle(
39
+ pi: ExtensionAPI,
40
+ deps: {
41
+ settings: SessionSettings;
42
+ getModelRuntime: ModelRuntimeProvider;
43
+ getSessionEpoch: () => number;
44
+ },
45
+ ): SessionLifecycle {
46
+ const { settings, getModelRuntime, getSessionEpoch } = deps;
47
47
  const controller = createSessionAutoTitleController(settings.autoTitle);
48
- let sessionEpoch = 0;
49
48
  let titleWorkInFlight: Promise<void> | undefined;
50
- let resolvedModel: Model<Api> | undefined;
49
+
50
+ // An explicit thinkingLevel setting overrides a thinking suffix on the configured model.
51
+ const buildGeneration = (
52
+ resolution: AutoTitleModelResolution | undefined,
53
+ ): AutoTitleGeneration => ({
54
+ systemPrompt: settings.autoTitle.prompt,
55
+ timeoutMs: settings.autoTitle.timeoutMs,
56
+ thinkingLevel: settings.autoTitle.thinkingLevel ?? resolution?.thinkingLevel,
57
+ });
51
58
 
52
59
  pi.registerCommand("title", {
53
60
  description: "Generate titles for this session, this folder, or all of Pi",
@@ -58,12 +65,13 @@ export default function sessionAutoTitleExtension(pi: ExtensionAPI): void {
58
65
  await titleWorkInFlight;
59
66
  }
60
67
 
61
- const model = resolvedModel ?? resolveAutoTitleModel(ctx, settings.autoTitle.model)?.model;
68
+ const modelRuntime = await getModelRuntime(ctx.modelRegistry);
69
+ const resolution = resolveAutoTitleModel(modelRuntime, ctx.model, settings.autoTitle.model);
62
70
  return handleTitleInvocation(
63
71
  pi,
64
72
  {
65
73
  controller,
66
- getSessionEpoch: () => sessionEpoch,
74
+ getSessionEpoch,
67
75
  setInFlight: (work) => {
68
76
  titleWorkInFlight = work;
69
77
  },
@@ -72,21 +80,15 @@ export default function sessionAutoTitleExtension(pi: ExtensionAPI): void {
72
80
  },
73
81
  },
74
82
  ctx,
75
- model,
83
+ modelRuntime,
84
+ resolution?.model,
76
85
  invocation,
77
- settings.autoTitle.prompt,
78
- settings.autoTitle.timeoutMs,
86
+ buildGeneration(resolution),
79
87
  );
80
88
  },
81
89
  ),
82
90
  });
83
91
 
84
- pi.on("session_start", async (_event: SessionStartEvent, ctx: ExtensionContext) => {
85
- sessionEpoch += 1;
86
- resolvedModel = resolveAutoTitleModel(ctx, settings.autoTitle.model)?.model;
87
- persistAutoTitleState(pi, controller.handleSessionStart(ctx));
88
- });
89
-
90
92
  pi.on("turn_end", async (_event: TurnEndEvent, ctx: ExtensionContext) => {
91
93
  const result = controller.handleTurnEnd(ctx);
92
94
  persistAutoTitleState(pi, result.persistedState);
@@ -95,17 +97,19 @@ export default function sessionAutoTitleExtension(pi: ExtensionAPI): void {
95
97
  return;
96
98
  }
97
99
 
100
+ const modelRuntime = await getModelRuntime(ctx.modelRegistry);
101
+ const resolution = resolveAutoTitleModel(modelRuntime, ctx.model, settings.autoTitle.model);
98
102
  titleWorkInFlight = runRetitlePlan({
99
103
  pi,
100
104
  controller,
101
105
  ctx,
102
- model: resolvedModel,
106
+ modelRuntime,
107
+ model: resolution?.model,
103
108
  isManual: false,
104
109
  existingPlan: result.plan,
105
- getSessionEpoch: () => sessionEpoch,
110
+ getSessionEpoch,
106
111
  notifyOnSuccess: false,
107
- systemPrompt: settings.autoTitle.prompt,
108
- timeoutMs: settings.autoTitle.timeoutMs,
112
+ generation: buildGeneration(resolution),
109
113
  })
110
114
  .then((outcome) => {
111
115
  if (outcome.ok) {
@@ -126,31 +130,34 @@ export default function sessionAutoTitleExtension(pi: ExtensionAPI): void {
126
130
  });
127
131
  });
128
132
 
129
- pi.on("session_shutdown", async () => {
130
- sessionEpoch += 1;
131
- controller.handleSessionShutdown();
132
- titleWorkInFlight = undefined;
133
- resolvedModel = undefined;
134
- });
133
+ return {
134
+ onSessionStart(_event, ctx) {
135
+ persistAutoTitleState(pi, controller.handleSessionStart(ctx));
136
+ },
137
+ onSessionShutdown() {
138
+ controller.handleSessionShutdown();
139
+ titleWorkInFlight = undefined;
140
+ },
141
+ };
135
142
  }
136
143
 
137
144
  async function handleTitleInvocation(
138
145
  pi: ExtensionAPI,
139
146
  state: TitleRunState,
140
147
  ctx: ExtensionCommandContext,
148
+ modelRuntime: ModelRuntime,
141
149
  model: Model<Api> | undefined,
142
150
  invocation: RetitleCommandInvocation,
143
- systemPrompt: string,
144
- timeoutMs: number,
151
+ generation: AutoTitleGeneration,
145
152
  ): Promise<RetitleCommandOutcome> {
146
153
  const retitleOpts = {
147
154
  pi,
148
155
  controller: state.controller,
149
156
  ctx,
157
+ modelRuntime,
150
158
  model,
151
159
  isManual: true,
152
- systemPrompt,
153
- timeoutMs,
160
+ generation,
154
161
  getSessionEpoch: state.getSessionEpoch,
155
162
  };
156
163
 
@@ -173,10 +180,10 @@ async function handleTitleInvocation(
173
180
  pi,
174
181
  state.controller,
175
182
  ctx,
183
+ modelRuntime,
176
184
  model,
177
185
  state.getSessionEpoch,
178
- systemPrompt,
179
- timeoutMs,
186
+ generation,
180
187
  );
181
188
  }
182
189
 
@@ -189,10 +196,10 @@ async function handleTitleInvocation(
189
196
  pi,
190
197
  state.controller,
191
198
  ctx,
199
+ modelRuntime,
192
200
  model,
193
201
  state.getSessionEpoch,
194
- systemPrompt,
195
- timeoutMs,
202
+ generation,
196
203
  {
197
204
  initialInvocation: {
198
205
  scope: invocation.scope,
@@ -209,12 +216,12 @@ async function handleTitleInvocation(
209
216
  pi,
210
217
  state.controller,
211
218
  ctx,
219
+ modelRuntime,
212
220
  model,
213
221
  scan,
214
222
  mode,
215
223
  state.getSessionEpoch,
216
- systemPrompt,
217
- timeoutMs,
224
+ generation,
218
225
  );
219
226
  notifyBulkRetitleResult(ctx, scan, mode, result);
220
227
  return "success";
@@ -1,12 +1,15 @@
1
+ import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
1
2
  import type { Api, Model } from "@earendil-works/pi-ai";
2
- import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
- import { findModelByReference } from "../shared/model.ts";
4
- import { ModelReference } from "../shared/settings.ts";
5
-
6
- const DEFAULT_AUTO_TITLE_FALLBACK_MODELS: readonly ModelReference[] = [
7
- new ModelReference("google", "gemini-flash-lite-latest"),
8
- new ModelReference("anthropic", "claude-haiku-4-5"),
9
- new ModelReference("openai", "gpt-5.4-mini"),
3
+ import type { ModelRuntime } from "@earendil-works/pi-coding-agent";
4
+ import { type ExactModelReference, findModelByReference } from "../shared/model.ts";
5
+ import { resolveAuthenticatedModel } from "../shared/model-resolution.ts";
6
+ import { freshenModel } from "../shared/model-runtime.ts";
7
+
8
+ const DEFAULT_AUTO_TITLE_FALLBACK_MODELS: readonly ExactModelReference[] = [
9
+ { provider: "openai-codex", modelId: "gpt-5.6-luna" },
10
+ { provider: "openai", modelId: "gpt-5.6-luna" },
11
+ { provider: "anthropic", modelId: "claude-haiku-4-5" },
12
+ { provider: "google", modelId: "gemini-flash-lite-latest" },
10
13
  ] as const;
11
14
 
12
15
  export type AutoTitleModelSource = "configured" | "fallback" | "current";
@@ -14,24 +17,29 @@ export type AutoTitleModelSource = "configured" | "fallback" | "current";
14
17
  export interface AutoTitleModelResolution {
15
18
  model: Model<Api>;
16
19
  source: AutoTitleModelSource;
20
+ thinkingLevel?: ThinkingLevel | undefined;
17
21
  }
18
22
 
19
23
  export function resolveAutoTitleModel(
20
- ctx: ExtensionContext,
21
- configuredModel: ModelReference | undefined,
24
+ modelRuntime: ModelRuntime,
25
+ currentModel: Model<Api> | undefined,
26
+ configuredModel: string | undefined,
22
27
  ): AutoTitleModelResolution | undefined {
23
- const availableModels = ctx.modelRegistry.getAvailable();
24
-
25
28
  if (configuredModel) {
26
- const configuredMatch = findModelByReference(availableModels, configuredModel);
27
- if (configuredMatch) {
29
+ const configured = resolveAuthenticatedModel({
30
+ modelRuntime,
31
+ modelPattern: configuredModel,
32
+ });
33
+ if (configured.ok) {
28
34
  return {
29
- model: configuredMatch,
35
+ model: configured.model,
30
36
  source: "configured",
37
+ thinkingLevel: configured.thinkingLevel,
31
38
  };
32
39
  }
33
40
  }
34
41
 
42
+ const availableModels = modelRuntime.getAvailableSnapshot();
35
43
  for (const fallbackReference of DEFAULT_AUTO_TITLE_FALLBACK_MODELS) {
36
44
  const fallbackMatch = findModelByReference(availableModels, fallbackReference);
37
45
  if (fallbackMatch) {
@@ -42,12 +50,12 @@ export function resolveAutoTitleModel(
42
50
  }
43
51
  }
44
52
 
45
- if (!ctx.model) {
53
+ if (!currentModel) {
46
54
  return undefined;
47
55
  }
48
56
 
49
57
  return {
50
- model: ctx.model,
58
+ model: freshenModel(modelRuntime, currentModel),
51
59
  source: "current",
52
60
  };
53
61
  }
@@ -3,6 +3,7 @@ import type {
3
3
  ExtensionAPI,
4
4
  ExtensionCommandContext,
5
5
  ExtensionContext,
6
+ ModelRuntime,
6
7
  SessionInfo,
7
8
  } from "@earendil-works/pi-coding-agent";
8
9
  import { SessionManager } from "@earendil-works/pi-coding-agent";
@@ -10,6 +11,7 @@ import type { RetitleMode, RetitleScope } from "./command.ts";
10
11
  import { buildAutoTitleContext } from "./context.ts";
11
12
  import type { AutoTitleTriggerPlan, SessionAutoTitleController } from "./controller.ts";
12
13
  import {
14
+ type AutoTitleGeneration,
13
15
  type AutoTitleGenerationResult,
14
16
  createAutoTitleFailure,
15
17
  generateAutoTitle,
@@ -53,12 +55,12 @@ export async function runBulkRetitle(
53
55
  pi: ExtensionAPI,
54
56
  controller: SessionAutoTitleController,
55
57
  ctx: ExtensionCommandContext,
58
+ modelRuntime: ModelRuntime,
56
59
  model: Model<Api> | undefined,
57
60
  scan: RetitleScopeScan,
58
61
  mode: RetitleMode,
59
62
  getSessionEpoch: () => number,
60
- systemPrompt: string,
61
- timeoutMs: number,
63
+ generation: AutoTitleGeneration,
62
64
  ): Promise<BulkRetitleResult> {
63
65
  const result: BulkRetitleResult = {
64
66
  attempted: 0,
@@ -83,10 +85,10 @@ export async function runBulkRetitle(
83
85
  pi,
84
86
  controller,
85
87
  ctx,
88
+ modelRuntime,
86
89
  model,
87
90
  isManual: true,
88
- systemPrompt,
89
- timeoutMs,
91
+ generation,
90
92
  getSessionEpoch,
91
93
  notifyOnSuccess: false,
92
94
  });
@@ -94,7 +96,7 @@ export async function runBulkRetitle(
94
96
  continue;
95
97
  }
96
98
 
97
- const outcome = await retitleStoredSession(ctx, model, session.path, systemPrompt, timeoutMs);
99
+ const outcome = await retitleStoredSession(modelRuntime, model, session.path, generation);
98
100
  result[outcome] += 1;
99
101
  }
100
102
 
@@ -164,10 +166,10 @@ export interface RetitlePlanOptions {
164
166
  pi: ExtensionAPI;
165
167
  controller: SessionAutoTitleController;
166
168
  ctx: ExtensionContext;
169
+ modelRuntime: ModelRuntime;
167
170
  model: Model<Api> | undefined;
168
171
  isManual: boolean;
169
- systemPrompt: string;
170
- timeoutMs: number;
172
+ generation: AutoTitleGeneration;
171
173
  existingPlan?: AutoTitleTriggerPlan;
172
174
  getSessionEpoch?: () => number;
173
175
  notifyOnSuccess?: boolean;
@@ -180,10 +182,10 @@ export async function runRetitlePlan(
180
182
  pi,
181
183
  controller,
182
184
  ctx,
185
+ modelRuntime,
183
186
  model,
184
187
  isManual,
185
- systemPrompt,
186
- timeoutMs,
188
+ generation,
187
189
  existingPlan,
188
190
  getSessionEpoch,
189
191
  } = options;
@@ -218,12 +220,11 @@ export async function runRetitlePlan(
218
220
  },
219
221
  );
220
222
  const generatedTitle = await generateAutoTitle(
221
- ctx,
223
+ modelRuntime,
222
224
  model,
223
225
  titleContext,
224
226
  plan.reason,
225
- systemPrompt,
226
- timeoutMs,
227
+ generation,
227
228
  );
228
229
  if (!generatedTitle.ok) {
229
230
  return generatedTitle;
@@ -266,11 +267,10 @@ export function persistAutoTitleState(
266
267
  }
267
268
 
268
269
  async function retitleStoredSession(
269
- ctx: ExtensionContext,
270
+ modelRuntime: ModelRuntime,
270
271
  model: Model<Api>,
271
272
  sessionPath: string,
272
- systemPrompt: string,
273
- timeoutMs: number,
273
+ generation: AutoTitleGeneration,
274
274
  ): Promise<"retitled" | "unchanged" | "failed"> {
275
275
  const sessionManager = SessionManager.open(sessionPath);
276
276
  const currentTitle = sessionManager.getSessionName();
@@ -288,12 +288,11 @@ async function retitleStoredSession(
288
288
  currentTitle,
289
289
  };
290
290
  const generatedTitle = await generateAutoTitle(
291
- ctx,
291
+ modelRuntime,
292
292
  model,
293
293
  titleContext,
294
294
  plan.reason,
295
- systemPrompt,
296
- timeoutMs,
295
+ generation,
297
296
  );
298
297
  if (!generatedTitle.ok) {
299
298
  return "failed";