pi-sessions 0.7.2 → 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 +71 -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} +67 -46
  12. package/extensions/session-auto-title/model.ts +25 -17
  13. package/extensions/session-auto-title/retitle.ts +26 -12
  14. package/extensions/session-auto-title/wizard.ts +12 -6
  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 +80 -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,23 @@
1
+ import type { ExpandableContentPresentation } from "../shared/rendering/expandable-content-layout.ts";
2
+ import type { RenderTheme } from "../shared/rendering/theme.ts";
3
+ import { formatSessionTitleOrShortId } from "../shared/session-ui.ts";
4
+ import type { SessionAskViewModel } from "./view-model.ts";
5
+
6
+ export function buildSessionAskPresentation(
7
+ model: SessionAskViewModel,
8
+ theme: RenderTheme,
9
+ ): ExpandableContentPresentation {
10
+ const identity = formatSessionTitleOrShortId(model.sessionName, model.sessionId);
11
+ return {
12
+ header: `title: ${theme.bold(identity)}`,
13
+ ...(model.question
14
+ ? {
15
+ metadata: [theme.fg("muted", `prompt: ${model.question}`)],
16
+ }
17
+ : {}),
18
+ body: {
19
+ text: model.answer,
20
+ collapsedRows: 6,
21
+ },
22
+ };
23
+ }
@@ -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,13 +1,12 @@
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
 
10
- const AUTO_TITLE_REQUEST_TIMEOUT_MS = 15_000;
11
10
  const AUTO_TITLE_MAX_TOKENS = 64;
12
11
  const AUTO_TITLE_CHAR_MAX = 80;
13
12
 
@@ -18,6 +17,12 @@ export interface AutoTitleFailure {
18
17
  message: string;
19
18
  }
20
19
 
20
+ export interface AutoTitleGeneration {
21
+ systemPrompt: string;
22
+ timeoutMs: number;
23
+ thinkingLevel: ThinkingLevel | undefined;
24
+ }
25
+
21
26
  export type AutoTitleGenerationResult =
22
27
  | {
23
28
  ok: true;
@@ -29,11 +34,11 @@ export type AutoTitleGenerationResult =
29
34
  };
30
35
 
31
36
  export async function generateAutoTitle(
32
- ctx: ExtensionContext,
37
+ modelRuntime: ModelRuntime,
33
38
  model: Model<Api>,
34
39
  context: AutoTitleContext,
35
40
  trigger: AutoTitleTrigger,
36
- systemPrompt: string,
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(), AUTO_TITLE_REQUEST_TIMEOUT_MS);
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,20 +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,
86
+ buildGeneration(resolution),
78
87
  );
79
88
  },
80
89
  ),
81
90
  });
82
91
 
83
- pi.on("session_start", async (_event: SessionStartEvent, ctx: ExtensionContext) => {
84
- sessionEpoch += 1;
85
- resolvedModel = resolveAutoTitleModel(ctx, settings.autoTitle.model)?.model;
86
- persistAutoTitleState(pi, controller.handleSessionStart(ctx));
87
- });
88
-
89
92
  pi.on("turn_end", async (_event: TurnEndEvent, ctx: ExtensionContext) => {
90
93
  const result = controller.handleTurnEnd(ctx);
91
94
  persistAutoTitleState(pi, result.persistedState);
@@ -94,16 +97,19 @@ export default function sessionAutoTitleExtension(pi: ExtensionAPI): void {
94
97
  return;
95
98
  }
96
99
 
100
+ const modelRuntime = await getModelRuntime(ctx.modelRegistry);
101
+ const resolution = resolveAutoTitleModel(modelRuntime, ctx.model, settings.autoTitle.model);
97
102
  titleWorkInFlight = runRetitlePlan({
98
103
  pi,
99
104
  controller,
100
105
  ctx,
101
- model: resolvedModel,
106
+ modelRuntime,
107
+ model: resolution?.model,
102
108
  isManual: false,
103
109
  existingPlan: result.plan,
104
- getSessionEpoch: () => sessionEpoch,
110
+ getSessionEpoch,
105
111
  notifyOnSuccess: false,
106
- systemPrompt: settings.autoTitle.prompt,
112
+ generation: buildGeneration(resolution),
107
113
  })
108
114
  .then((outcome) => {
109
115
  if (outcome.ok) {
@@ -124,29 +130,34 @@ export default function sessionAutoTitleExtension(pi: ExtensionAPI): void {
124
130
  });
125
131
  });
126
132
 
127
- pi.on("session_shutdown", async () => {
128
- sessionEpoch += 1;
129
- controller.handleSessionShutdown();
130
- titleWorkInFlight = undefined;
131
- resolvedModel = undefined;
132
- });
133
+ return {
134
+ onSessionStart(_event, ctx) {
135
+ persistAutoTitleState(pi, controller.handleSessionStart(ctx));
136
+ },
137
+ onSessionShutdown() {
138
+ controller.handleSessionShutdown();
139
+ titleWorkInFlight = undefined;
140
+ },
141
+ };
133
142
  }
134
143
 
135
144
  async function handleTitleInvocation(
136
145
  pi: ExtensionAPI,
137
146
  state: TitleRunState,
138
147
  ctx: ExtensionCommandContext,
148
+ modelRuntime: ModelRuntime,
139
149
  model: Model<Api> | undefined,
140
150
  invocation: RetitleCommandInvocation,
141
- systemPrompt: string,
151
+ generation: AutoTitleGeneration,
142
152
  ): Promise<RetitleCommandOutcome> {
143
153
  const retitleOpts = {
144
154
  pi,
145
155
  controller: state.controller,
146
156
  ctx,
157
+ modelRuntime,
147
158
  model,
148
159
  isManual: true,
149
- systemPrompt,
160
+ generation,
150
161
  getSessionEpoch: state.getSessionEpoch,
151
162
  };
152
163
 
@@ -165,7 +176,15 @@ async function handleTitleInvocation(
165
176
  return retitleCurrentSession();
166
177
  }
167
178
 
168
- return showRetitleWizard(pi, state.controller, ctx, model, state.getSessionEpoch, systemPrompt);
179
+ return showRetitleWizard(
180
+ pi,
181
+ state.controller,
182
+ ctx,
183
+ modelRuntime,
184
+ model,
185
+ state.getSessionEpoch,
186
+ generation,
187
+ );
169
188
  }
170
189
 
171
190
  if (invocation.scope === "this") {
@@ -177,9 +196,10 @@ async function handleTitleInvocation(
177
196
  pi,
178
197
  state.controller,
179
198
  ctx,
199
+ modelRuntime,
180
200
  model,
181
201
  state.getSessionEpoch,
182
- systemPrompt,
202
+ generation,
183
203
  {
184
204
  initialInvocation: {
185
205
  scope: invocation.scope,
@@ -196,11 +216,12 @@ async function handleTitleInvocation(
196
216
  pi,
197
217
  state.controller,
198
218
  ctx,
219
+ modelRuntime,
199
220
  model,
200
221
  scan,
201
222
  mode,
202
223
  state.getSessionEpoch,
203
- systemPrompt,
224
+ generation,
204
225
  );
205
226
  notifyBulkRetitleResult(ctx, scan, mode, result);
206
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,11 +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,
63
+ generation: AutoTitleGeneration,
61
64
  ): Promise<BulkRetitleResult> {
62
65
  const result: BulkRetitleResult = {
63
66
  attempted: 0,
@@ -82,9 +85,10 @@ export async function runBulkRetitle(
82
85
  pi,
83
86
  controller,
84
87
  ctx,
88
+ modelRuntime,
85
89
  model,
86
90
  isManual: true,
87
- systemPrompt,
91
+ generation,
88
92
  getSessionEpoch,
89
93
  notifyOnSuccess: false,
90
94
  });
@@ -92,7 +96,7 @@ export async function runBulkRetitle(
92
96
  continue;
93
97
  }
94
98
 
95
- const outcome = await retitleStoredSession(ctx, model, session.path, systemPrompt);
99
+ const outcome = await retitleStoredSession(modelRuntime, model, session.path, generation);
96
100
  result[outcome] += 1;
97
101
  }
98
102
 
@@ -162,9 +166,10 @@ export interface RetitlePlanOptions {
162
166
  pi: ExtensionAPI;
163
167
  controller: SessionAutoTitleController;
164
168
  ctx: ExtensionContext;
169
+ modelRuntime: ModelRuntime;
165
170
  model: Model<Api> | undefined;
166
171
  isManual: boolean;
167
- systemPrompt: string;
172
+ generation: AutoTitleGeneration;
168
173
  existingPlan?: AutoTitleTriggerPlan;
169
174
  getSessionEpoch?: () => number;
170
175
  notifyOnSuccess?: boolean;
@@ -173,8 +178,17 @@ export interface RetitlePlanOptions {
173
178
  export async function runRetitlePlan(
174
179
  options: RetitlePlanOptions,
175
180
  ): Promise<AutoTitleGenerationResult> {
176
- const { pi, controller, ctx, model, isManual, systemPrompt, existingPlan, getSessionEpoch } =
177
- options;
181
+ const {
182
+ pi,
183
+ controller,
184
+ ctx,
185
+ modelRuntime,
186
+ model,
187
+ isManual,
188
+ generation,
189
+ existingPlan,
190
+ getSessionEpoch,
191
+ } = options;
178
192
  const notifyOnSuccess = options.notifyOnSuccess ?? isManual;
179
193
 
180
194
  const plan = existingPlan ?? controller.handleManualRetitle(ctx);
@@ -206,11 +220,11 @@ export async function runRetitlePlan(
206
220
  },
207
221
  );
208
222
  const generatedTitle = await generateAutoTitle(
209
- ctx,
223
+ modelRuntime,
210
224
  model,
211
225
  titleContext,
212
226
  plan.reason,
213
- systemPrompt,
227
+ generation,
214
228
  );
215
229
  if (!generatedTitle.ok) {
216
230
  return generatedTitle;
@@ -253,10 +267,10 @@ export function persistAutoTitleState(
253
267
  }
254
268
 
255
269
  async function retitleStoredSession(
256
- ctx: ExtensionContext,
270
+ modelRuntime: ModelRuntime,
257
271
  model: Model<Api>,
258
272
  sessionPath: string,
259
- systemPrompt: string,
273
+ generation: AutoTitleGeneration,
260
274
  ): Promise<"retitled" | "unchanged" | "failed"> {
261
275
  const sessionManager = SessionManager.open(sessionPath);
262
276
  const currentTitle = sessionManager.getSessionName();
@@ -274,11 +288,11 @@ async function retitleStoredSession(
274
288
  currentTitle,
275
289
  };
276
290
  const generatedTitle = await generateAutoTitle(
277
- ctx,
291
+ modelRuntime,
278
292
  model,
279
293
  titleContext,
280
294
  plan.reason,
281
- systemPrompt,
295
+ generation,
282
296
  );
283
297
  if (!generatedTitle.ok) {
284
298
  return "failed";