@oh-my-pi/pi-coding-agent 15.9.3 → 15.9.67
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/CHANGELOG.md +74 -1
- package/dist/types/cli/classify-install-target.d.ts +5 -1
- package/dist/types/config/keybindings.d.ts +4 -1
- package/dist/types/config/settings-schema.d.ts +24 -5
- package/dist/types/edit/file-snapshot-store.d.ts +1 -1
- package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +1 -0
- package/dist/types/eval/backend.d.ts +6 -6
- package/dist/types/eval/bridge-timeout.d.ts +27 -0
- package/dist/types/eval/idle-timeout.d.ts +16 -14
- package/dist/types/eval/js/executor.d.ts +3 -3
- package/dist/types/eval/py/executor.d.ts +2 -2
- package/dist/types/eval/py/spawn-options.d.ts +58 -0
- package/dist/types/modes/components/assistant-message.d.ts +16 -0
- package/dist/types/modes/components/copy-selector.d.ts +22 -0
- package/dist/types/modes/components/custom-editor.d.ts +3 -1
- package/dist/types/modes/components/error-banner.d.ts +11 -0
- package/dist/types/modes/components/model-selector.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +15 -0
- package/dist/types/modes/components/transcript-container.d.ts +1 -0
- package/dist/types/modes/components/user-message.d.ts +1 -1
- package/dist/types/modes/controllers/command-controller.d.ts +0 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/image-references.d.ts +17 -0
- package/dist/types/modes/interactive-mode.d.ts +8 -1
- package/dist/types/modes/types.d.ts +8 -1
- package/dist/types/modes/utils/copy-targets.d.ts +53 -0
- package/dist/types/modes/utils/ui-helpers.d.ts +1 -0
- package/dist/types/session/blob-store.d.ts +12 -11
- package/dist/types/session/session-manager.d.ts +5 -3
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/tiny/title-client.d.ts +16 -1
- package/dist/types/tool-discovery/mode.d.ts +8 -0
- package/dist/types/tools/archive-reader.d.ts +5 -1
- package/dist/types/tools/eval-render.d.ts +8 -0
- package/dist/types/tools/render-utils.d.ts +25 -0
- package/dist/types/tui/code-cell.d.ts +6 -0
- package/dist/types/tui/hyperlink.d.ts +12 -0
- package/dist/types/tui/output-block.d.ts +11 -0
- package/dist/types/web/search/render.d.ts +1 -2
- package/package.json +9 -9
- package/src/autoresearch/dashboard.ts +11 -21
- package/src/cli/classify-install-target.ts +31 -5
- package/src/cli/claude-trace-cli.ts +13 -1
- package/src/cli/plugin-cli.ts +45 -0
- package/src/cli/web-search-cli.ts +0 -1
- package/src/config/keybindings.ts +58 -1
- package/src/config/model-registry.ts +54 -4
- package/src/config/settings-schema.ts +25 -5
- package/src/debug/raw-sse.ts +18 -4
- package/src/edit/file-snapshot-store.ts +1 -1
- package/src/edit/index.ts +1 -1
- package/src/edit/renderer.ts +7 -7
- package/src/edit/streaming.ts +1 -1
- package/src/eval/__tests__/agent-bridge.test.ts +100 -27
- package/src/eval/__tests__/bridge-timeout.test.ts +64 -0
- package/src/eval/__tests__/idle-timeout.test.ts +26 -12
- package/src/eval/__tests__/kernel-spawn.test.ts +103 -0
- package/src/eval/__tests__/llm-bridge.test.ts +10 -10
- package/src/eval/__tests__/shared-executors.test.ts +2 -2
- package/src/eval/agent-bridge.ts +4 -5
- package/src/eval/backend.ts +6 -6
- package/src/eval/bridge-timeout.ts +44 -0
- package/src/eval/idle-timeout.ts +33 -15
- package/src/eval/js/executor.ts +10 -10
- package/src/eval/llm-bridge.ts +4 -5
- package/src/eval/py/executor.ts +6 -6
- package/src/eval/py/kernel.ts +11 -1
- package/src/eval/py/spawn-options.ts +126 -0
- package/src/eval/py/tool-bridge.ts +43 -5
- package/src/export/ttsr.ts +9 -0
- package/src/extensibility/custom-commands/bundled/ci-green/index.ts +31 -2
- package/src/extensibility/extensions/runner.ts +2 -0
- package/src/internal-urls/docs-index.generated.ts +9 -8
- package/src/lsp/client.ts +80 -2
- package/src/lsp/index.ts +38 -4
- package/src/lsp/render.ts +3 -3
- package/src/main.ts +8 -2
- package/src/modes/components/agent-dashboard.ts +13 -4
- package/src/modes/components/assistant-message.ts +44 -1
- package/src/modes/components/copy-selector.ts +249 -0
- package/src/modes/components/custom-editor.ts +14 -2
- package/src/modes/components/error-banner.ts +33 -0
- package/src/modes/components/extensions/extension-list.ts +17 -8
- package/src/modes/components/history-search.ts +19 -11
- package/src/modes/components/model-selector.ts +125 -29
- package/src/modes/components/oauth-selector.ts +28 -12
- package/src/modes/components/session-observer-overlay.ts +13 -15
- package/src/modes/components/session-selector.ts +24 -13
- package/src/modes/components/tool-execution.ts +71 -13
- package/src/modes/components/transcript-container.ts +93 -32
- package/src/modes/components/tree-selector.ts +19 -7
- package/src/modes/components/user-message-selector.ts +25 -14
- package/src/modes/components/user-message.ts +9 -2
- package/src/modes/controllers/command-controller.ts +0 -116
- package/src/modes/controllers/event-controller.ts +67 -12
- package/src/modes/controllers/input-controller.ts +33 -1
- package/src/modes/controllers/selector-controller.ts +38 -1
- package/src/modes/image-references.ts +111 -0
- package/src/modes/interactive-mode.ts +52 -17
- package/src/modes/theme/theme.ts +46 -10
- package/src/modes/types.ts +11 -2
- package/src/modes/utils/copy-targets.ts +254 -0
- package/src/modes/utils/ui-helpers.ts +23 -2
- package/src/prompts/ci-green-request.md +5 -3
- package/src/prompts/system/project-prompt.md +1 -0
- package/src/prompts/tools/ast-edit.md +1 -1
- package/src/prompts/tools/ast-grep.md +1 -1
- package/src/prompts/tools/read.md +1 -1
- package/src/prompts/tools/search.md +1 -1
- package/src/sdk.ts +17 -9
- package/src/session/agent-session.ts +43 -14
- package/src/session/blob-store.ts +96 -9
- package/src/session/session-manager.ts +19 -10
- package/src/slash-commands/builtin-registry.ts +3 -11
- package/src/system-prompt.ts +4 -0
- package/src/task/render.ts +38 -11
- package/src/tiny/title-client.ts +7 -1
- package/src/tool-discovery/mode.ts +24 -0
- package/src/tools/archive-reader.ts +339 -31
- package/src/tools/bash.ts +18 -8
- package/src/tools/browser/render.ts +5 -4
- package/src/tools/debug.ts +3 -3
- package/src/tools/eval-render.ts +24 -9
- package/src/tools/eval.ts +14 -19
- package/src/tools/fetch.ts +34 -14
- package/src/tools/gh.ts +65 -11
- package/src/tools/index.ts +6 -8
- package/src/tools/read.ts +65 -19
- package/src/tools/render-utils.ts +46 -0
- package/src/tools/search-tool-bm25.ts +4 -6
- package/src/tools/search.ts +60 -11
- package/src/tools/ssh.ts +21 -8
- package/src/tools/write.ts +17 -8
- package/src/tui/code-cell.ts +19 -4
- package/src/tui/hyperlink.ts +42 -7
- package/src/tui/output-block.ts +14 -0
- package/src/web/search/index.ts +2 -2
- package/src/web/search/render.ts +23 -55
- package/dist/types/eval/heartbeat.d.ts +0 -45
- package/src/eval/__tests__/heartbeat.test.ts +0 -84
- package/src/eval/heartbeat.ts +0 -74
- /package/dist/types/eval/__tests__/{heartbeat.test.d.ts → bridge-timeout.test.d.ts} +0 -0
|
@@ -18,7 +18,7 @@ Performs structural code search using AST matching via native ast-grep.
|
|
|
18
18
|
|
|
19
19
|
<output>
|
|
20
20
|
- Grouped matches with file path, byte range, line/column ranges, metavariable captures
|
|
21
|
-
- Match lines are numbered under a file snapshot tag header in hashline mode:
|
|
21
|
+
- Match lines are numbered under a file snapshot tag header in hashline mode: `[src/foo.ts#1A2B]`, `*42:content` for the matched line, ` 43:content` for context
|
|
22
22
|
- Summary counts (`totalMatches`, `filesWithMatches`, `filesSearched`) and parse issues when present
|
|
23
23
|
</output>
|
|
24
24
|
|
|
@@ -28,7 +28,7 @@ Append `:<sel>` to `path`. The bare path falls back to the default mode.
|
|
|
28
28
|
|
|
29
29
|
- Reading a directory path returns a depth-limited dirent listing.
|
|
30
30
|
{{#if IS_HL_MODE}}
|
|
31
|
-
- Reading a file with an explicit selector emits a file snapshot tag header and numbered lines:
|
|
31
|
+
- Reading a file with an explicit selector emits a file snapshot tag header and numbered lines: `[src/foo.ts#1A2B]` then `41:def alpha():`. Copy the `[PATH#TAG]` header for anchored edits; ops use bare line numbers. NEVER fabricate the tag.
|
|
32
32
|
{{else}}
|
|
33
33
|
{{#if IS_LINE_NUMBER_MODE}}
|
|
34
34
|
- Reading a file with an explicit selector returns lines prefixed with line numbers: `41|def alpha():`.
|
|
@@ -9,7 +9,7 @@ Searches files using powerful regex matching.
|
|
|
9
9
|
|
|
10
10
|
<output>
|
|
11
11
|
{{#if IS_HL_MODE}}
|
|
12
|
-
- Text output emits a file snapshot tag header per matched file plus numbered lines:
|
|
12
|
+
- Text output emits a file snapshot tag header per matched file plus numbered lines: `[src/login.ts#1A2B]`, `*42:if (user.id) {` (match), ` 43:return user;` (context). Copy the header for anchored edits; ops use bare line numbers.
|
|
13
13
|
{{else}}
|
|
14
14
|
{{#if IS_LINE_NUMBER_MODE}}
|
|
15
15
|
- Text output is line-number-prefixed
|
package/src/sdk.ts
CHANGED
|
@@ -130,6 +130,7 @@ import {
|
|
|
130
130
|
resolveThinkingLevelForModel,
|
|
131
131
|
toReasoningEffort,
|
|
132
132
|
} from "./thinking";
|
|
133
|
+
import { countToolsForAutoDiscovery, resolveEffectiveToolDiscoveryMode } from "./tool-discovery/mode";
|
|
133
134
|
import {
|
|
134
135
|
collectDiscoverableTools,
|
|
135
136
|
type DiscoverableTool,
|
|
@@ -157,6 +158,7 @@ import {
|
|
|
157
158
|
ResolveTool,
|
|
158
159
|
renderSearchToolBm25Description,
|
|
159
160
|
SearchTool,
|
|
161
|
+
SearchToolBm25Tool,
|
|
160
162
|
setPreferredImageProvider,
|
|
161
163
|
setPreferredSearchProvider,
|
|
162
164
|
type Tool,
|
|
@@ -1687,6 +1689,19 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1687
1689
|
}
|
|
1688
1690
|
}
|
|
1689
1691
|
|
|
1692
|
+
const effectiveDiscoveryMode = resolveEffectiveToolDiscoveryMode(
|
|
1693
|
+
settings,
|
|
1694
|
+
countToolsForAutoDiscovery(toolRegistry.keys()),
|
|
1695
|
+
);
|
|
1696
|
+
if (effectiveDiscoveryMode !== "off" && !toolRegistry.has("search_tool_bm25")) {
|
|
1697
|
+
const searchTool: Tool = new SearchToolBm25Tool(toolSession);
|
|
1698
|
+
toolRegistry.set(
|
|
1699
|
+
searchTool.name,
|
|
1700
|
+
new ExtensionToolWrapper(wrapToolWithMetaNotice(searchTool), extensionRunner) as Tool,
|
|
1701
|
+
);
|
|
1702
|
+
}
|
|
1703
|
+
const mcpDiscoveryEnabled = effectiveDiscoveryMode !== "off"; // back-compat: true when any discovery active
|
|
1704
|
+
|
|
1690
1705
|
const reloadSshTool = async (): Promise<AgentTool | null> => {
|
|
1691
1706
|
if (!requestedToolNameSet.has("ssh")) return null;
|
|
1692
1707
|
const sshTool = (await loadSshTool({
|
|
@@ -1773,6 +1788,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1773
1788
|
secretsEnabled,
|
|
1774
1789
|
workspaceTree: workspaceTreePromise,
|
|
1775
1790
|
memoryRootEnabled: memoryBackend.id === "local",
|
|
1791
|
+
model: settings.get("includeModelInPrompt") ? getActiveModelString() : undefined,
|
|
1776
1792
|
});
|
|
1777
1793
|
|
|
1778
1794
|
if (options.systemPrompt === undefined) {
|
|
@@ -1805,15 +1821,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
1805
1821
|
const requestedToolNames = explicitlyRequestedToolNames ?? toolNamesFromRegistry;
|
|
1806
1822
|
const normalizedRequested = requestedToolNames.filter(name => toolRegistry.has(name));
|
|
1807
1823
|
const requestedToolNameSet = new Set(normalizedRequested);
|
|
1808
|
-
// Effective discovery mode
|
|
1809
|
-
const toolsDiscoveryModeSetting = settings.get("tools.discoveryMode");
|
|
1810
|
-
const effectiveDiscoveryMode: "off" | "mcp-only" | "all" =
|
|
1811
|
-
toolsDiscoveryModeSetting !== "off"
|
|
1812
|
-
? (toolsDiscoveryModeSetting as "off" | "mcp-only" | "all")
|
|
1813
|
-
: settings.get("mcp.discoveryMode")
|
|
1814
|
-
? "mcp-only"
|
|
1815
|
-
: "off";
|
|
1816
|
-
const mcpDiscoveryEnabled = effectiveDiscoveryMode !== "off"; // back-compat: true when any discovery active
|
|
1824
|
+
// Effective discovery mode is resolved after the full registry exists so auto mode can count MCP/extension tools.
|
|
1817
1825
|
const defaultInactiveToolNames = new Set(
|
|
1818
1826
|
registeredTools.filter(tool => tool.definition.defaultInactive).map(tool => tool.definition.name),
|
|
1819
1827
|
);
|
|
@@ -91,6 +91,7 @@ import {
|
|
|
91
91
|
extractRetryHint,
|
|
92
92
|
getAgentDbPath,
|
|
93
93
|
getInstallId,
|
|
94
|
+
isBunTestRuntime,
|
|
94
95
|
isEnoent,
|
|
95
96
|
isUnexpectedSocketCloseMessage,
|
|
96
97
|
logger,
|
|
@@ -192,6 +193,7 @@ import {
|
|
|
192
193
|
toReasoningEffort,
|
|
193
194
|
} from "../thinking";
|
|
194
195
|
import { shutdownTinyTitleClient } from "../tiny/title-client";
|
|
196
|
+
import { countToolsForAutoDiscovery, resolveEffectiveToolDiscoveryMode } from "../tool-discovery/mode";
|
|
195
197
|
import {
|
|
196
198
|
buildDiscoverableToolSearchIndex,
|
|
197
199
|
collectDiscoverableTools,
|
|
@@ -960,6 +962,13 @@ export class AgentSession {
|
|
|
960
962
|
* the dominant cause of prompt-cache invalidation in long sessions.
|
|
961
963
|
*/
|
|
962
964
|
#lastAppliedToolSignature: string | undefined;
|
|
965
|
+
/**
|
|
966
|
+
* Model identifier (`provider/id`) currently rendered into `#baseSystemPrompt`.
|
|
967
|
+
* The prompt surfaces the active model to the agent, so a model switch must
|
|
968
|
+
* trigger a rebuild. Compared against the live model after every model change
|
|
969
|
+
* to decide whether the cached prompt is stale.
|
|
970
|
+
*/
|
|
971
|
+
#promptModelKey: string | undefined;
|
|
963
972
|
#mcpDiscoveryEnabled = false;
|
|
964
973
|
#discoverableMCPTools = new Map<string, DiscoverableTool>();
|
|
965
974
|
#selectedMCPToolNames = new Set<string>();
|
|
@@ -1028,6 +1037,7 @@ export class AgentSession {
|
|
|
1028
1037
|
|
|
1029
1038
|
#acquirePowerAssertion(): void {
|
|
1030
1039
|
if (process.platform !== "darwin") return;
|
|
1040
|
+
if (isBunTestRuntime()) return;
|
|
1031
1041
|
if (this.#powerAssertion) return;
|
|
1032
1042
|
const idle = this.settings.get("power.preventIdleSleep");
|
|
1033
1043
|
const system = this.settings.get("power.preventSystemSleep");
|
|
@@ -1173,6 +1183,7 @@ export class AgentSession {
|
|
|
1173
1183
|
this.#getMcpServerInstructions = config.getMcpServerInstructions;
|
|
1174
1184
|
this.#reloadSshTool = config.reloadSshTool;
|
|
1175
1185
|
this.#baseSystemPrompt = this.agent.state.systemPrompt;
|
|
1186
|
+
this.#promptModelKey = this.#currentPromptModelKey();
|
|
1176
1187
|
this.#mcpDiscoveryEnabled = config.mcpDiscoveryEnabled ?? false;
|
|
1177
1188
|
this.#setDiscoverableMCPTools(this.#collectDiscoverableMCPToolsFromRegistry());
|
|
1178
1189
|
this.#selectedMCPToolNames = new Set(config.initialSelectedMCPToolNames ?? []);
|
|
@@ -3264,9 +3275,21 @@ export class AgentSession {
|
|
|
3264
3275
|
return resolveEditMode(this.#getEditModeSession());
|
|
3265
3276
|
}
|
|
3266
3277
|
|
|
3267
|
-
|
|
3278
|
+
/**
|
|
3279
|
+
* Model key (`provider/id`) currently surfaced in the system prompt, or
|
|
3280
|
+
* undefined when the model is unset or `includeModelInPrompt` is disabled.
|
|
3281
|
+
*/
|
|
3282
|
+
#currentPromptModelKey(): string | undefined {
|
|
3283
|
+
if (!this.settings.get("includeModelInPrompt")) return undefined;
|
|
3284
|
+
return this.model ? formatModelString(this.model) : undefined;
|
|
3285
|
+
}
|
|
3286
|
+
|
|
3287
|
+
async #syncAfterModelChange(previousEditMode: EditMode): Promise<void> {
|
|
3268
3288
|
const currentEditMode = this.#resolveActiveEditMode();
|
|
3269
|
-
|
|
3289
|
+
const editModeChanged = previousEditMode !== currentEditMode && this.getActiveToolNames().includes("edit");
|
|
3290
|
+
// The system prompt may surface the active model; a switch makes the cached prompt stale.
|
|
3291
|
+
const modelChanged = this.#currentPromptModelKey() !== this.#promptModelKey;
|
|
3292
|
+
if (editModeChanged || modelChanged) {
|
|
3270
3293
|
await this.refreshBaseSystemPrompt();
|
|
3271
3294
|
}
|
|
3272
3295
|
}
|
|
@@ -3305,12 +3328,14 @@ export class AgentSession {
|
|
|
3305
3328
|
|
|
3306
3329
|
// ── Generic tool discovery (covers built-in + MCP + extension) ────────────
|
|
3307
3330
|
|
|
3308
|
-
/** Resolve effective discovery mode
|
|
3331
|
+
/** Resolve effective discovery mode from the current registry size. */
|
|
3309
3332
|
#resolveEffectiveDiscoveryMode(): "off" | "mcp-only" | "all" {
|
|
3310
|
-
const
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
|
|
3333
|
+
const mode = resolveEffectiveToolDiscoveryMode(
|
|
3334
|
+
this.settings,
|
|
3335
|
+
countToolsForAutoDiscovery(this.#toolRegistry.keys()),
|
|
3336
|
+
);
|
|
3337
|
+
if (mode !== "off") return mode;
|
|
3338
|
+
return this.#mcpDiscoveryEnabled ? "mcp-only" : "off";
|
|
3314
3339
|
}
|
|
3315
3340
|
|
|
3316
3341
|
isToolDiscoveryEnabled(): boolean {
|
|
@@ -3551,6 +3576,7 @@ export class AgentSession {
|
|
|
3551
3576
|
this.#baseSystemPrompt = built.systemPrompt;
|
|
3552
3577
|
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
3553
3578
|
this.#lastAppliedToolSignature = signature;
|
|
3579
|
+
this.#promptModelKey = this.#currentPromptModelKey();
|
|
3554
3580
|
}
|
|
3555
3581
|
}
|
|
3556
3582
|
if (options?.persistMCPSelection !== false) {
|
|
@@ -3633,6 +3659,7 @@ export class AgentSession {
|
|
|
3633
3659
|
const built = await this.#rebuildSystemPrompt(activeToolNames, this.#toolRegistry);
|
|
3634
3660
|
this.#baseSystemPrompt = built.systemPrompt;
|
|
3635
3661
|
this.agent.setSystemPrompt(this.#baseSystemPrompt);
|
|
3662
|
+
this.#promptModelKey = this.#currentPromptModelKey();
|
|
3636
3663
|
// Refresh the cached signature so a subsequent `#applyActiveToolsByName` with
|
|
3637
3664
|
// the same tool set does not re-rebuild on top of the explicit refresh we
|
|
3638
3665
|
// just performed (and conversely, a different set forces a fresh rebuild).
|
|
@@ -3692,7 +3719,7 @@ export class AgentSession {
|
|
|
3692
3719
|
* closure-captured ones cannot change at runtime regardless of skip behavior.
|
|
3693
3720
|
* For everything else, callers must explicitly call `refreshBaseSystemPrompt()`
|
|
3694
3721
|
* after side-effecting changes; see e.g. the memory hooks and
|
|
3695
|
-
* `#
|
|
3722
|
+
* `#syncAfterModelChange`.
|
|
3696
3723
|
*
|
|
3697
3724
|
* The current calendar date IS covered (appended as a segment) because
|
|
3698
3725
|
* `buildSystemPrompt` injects it into the prompt body (`Today is '{{date}}'`).
|
|
@@ -5284,7 +5311,7 @@ export class AgentSession {
|
|
|
5284
5311
|
// Re-apply thinking for the newly selected model. Prefer the model's
|
|
5285
5312
|
// configured defaultLevel; otherwise preserve the current level (or auto).
|
|
5286
5313
|
this.#reapplyThinkingLevel(model.thinking?.defaultLevel);
|
|
5287
|
-
await this.#
|
|
5314
|
+
await this.#syncAfterModelChange(previousEditMode);
|
|
5288
5315
|
}
|
|
5289
5316
|
|
|
5290
5317
|
/**
|
|
@@ -5318,7 +5345,7 @@ export class AgentSession {
|
|
|
5318
5345
|
} else {
|
|
5319
5346
|
this.#reapplyThinkingLevel(model.thinking?.defaultLevel);
|
|
5320
5347
|
}
|
|
5321
|
-
await this.#
|
|
5348
|
+
await this.#syncAfterModelChange(previousEditMode);
|
|
5322
5349
|
}
|
|
5323
5350
|
|
|
5324
5351
|
/**
|
|
@@ -5462,7 +5489,7 @@ export class AgentSession {
|
|
|
5462
5489
|
|
|
5463
5490
|
// Apply the scoped model's configured thinking level, preserving auto.
|
|
5464
5491
|
this.setThinkingLevel(this.#autoThinking ? AUTO_THINKING : next.thinkingLevel);
|
|
5465
|
-
await this.#
|
|
5492
|
+
await this.#syncAfterModelChange(previousEditMode);
|
|
5466
5493
|
|
|
5467
5494
|
return { model: next.model, thinkingLevel: this.thinkingLevel, isScoped: true };
|
|
5468
5495
|
}
|
|
@@ -5491,7 +5518,7 @@ export class AgentSession {
|
|
|
5491
5518
|
this.settings.getStorage()?.recordModelUsage(`${nextModel.provider}/${nextModel.id}`);
|
|
5492
5519
|
// Re-apply the current thinking level (or auto) for the newly selected model
|
|
5493
5520
|
this.#reapplyThinkingLevel();
|
|
5494
|
-
await this.#
|
|
5521
|
+
await this.#syncAfterModelChange(previousEditMode);
|
|
5495
5522
|
|
|
5496
5523
|
return { model: nextModel, thinkingLevel: this.thinkingLevel, isScoped: false };
|
|
5497
5524
|
}
|
|
@@ -8111,8 +8138,10 @@ export class AgentSession {
|
|
|
8111
8138
|
|
|
8112
8139
|
const currentSelector = this.model ? formatRetryFallbackSelector(this.model, this.thinkingLevel) : undefined;
|
|
8113
8140
|
if (!switchedCredential && currentSelector) {
|
|
8114
|
-
|
|
8115
|
-
|
|
8141
|
+
if (retrySettings.modelFallback) {
|
|
8142
|
+
this.#noteRetryFallbackCooldown(currentSelector, parsedRetryAfterMs, errorMessage);
|
|
8143
|
+
switchedModel = await this.#tryRetryModelFallback(currentSelector);
|
|
8144
|
+
}
|
|
8116
8145
|
if (switchedModel) {
|
|
8117
8146
|
delayMs = 0;
|
|
8118
8147
|
} else if (parsedRetryAfterMs && parsedRetryAfterMs > delayMs) {
|
|
@@ -5,19 +5,90 @@ import { isEnoent, logger } from "@oh-my-pi/pi-utils";
|
|
|
5
5
|
|
|
6
6
|
const BLOB_PREFIX = "blob:sha256:";
|
|
7
7
|
|
|
8
|
+
export interface BlobPutOptions {
|
|
9
|
+
/** Optional file extension for a sidecar hardlink/copy that OS openers can type-detect. */
|
|
10
|
+
extension?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
8
13
|
export interface BlobPutResult {
|
|
9
14
|
hash: string;
|
|
15
|
+
/** Canonical content-addressed path, always `<dir>/<sha256-hex>`. */
|
|
10
16
|
path: string;
|
|
17
|
+
/** Path with the requested extension when supplied, otherwise the canonical path. */
|
|
18
|
+
displayPath: string;
|
|
11
19
|
get ref(): string;
|
|
12
20
|
}
|
|
13
21
|
|
|
14
22
|
/**
|
|
15
23
|
* Content-addressed blob store for externalizing large binary data (images) from session JSONL files.
|
|
16
24
|
*
|
|
17
|
-
* Files are stored at `<dir>/<sha256-hex
|
|
18
|
-
*
|
|
19
|
-
*
|
|
25
|
+
* Files are stored canonically at `<dir>/<sha256-hex>`. Callers may also request
|
|
26
|
+
* a typed sidecar path (`<dir>/<sha256-hex>.<ext>`) for `file://` links and OS
|
|
27
|
+
* image viewers; blob refs and reads still address the extensionless hash path.
|
|
28
|
+
* The SHA-256 hash is computed over the raw binary data (not base64).
|
|
29
|
+
* Content-addressing makes writes idempotent and provides automatic deduplication
|
|
30
|
+
* across sessions.
|
|
20
31
|
*/
|
|
32
|
+
|
|
33
|
+
const IMAGE_EXTENSION_BY_MIME: Record<string, string> = {
|
|
34
|
+
"image/png": "png",
|
|
35
|
+
"image/jpeg": "jpg",
|
|
36
|
+
"image/jpg": "jpg",
|
|
37
|
+
"image/gif": "gif",
|
|
38
|
+
"image/webp": "webp",
|
|
39
|
+
"image/svg+xml": "svg",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function normalizeBlobExtension(extension: string | undefined): string | undefined {
|
|
43
|
+
if (!extension) return undefined;
|
|
44
|
+
const normalized = extension.startsWith(".") ? extension.slice(1) : extension;
|
|
45
|
+
if (normalized.length === 0 || normalized.length > 32) return undefined;
|
|
46
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(normalized)) return undefined;
|
|
47
|
+
return normalized.toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function ensureDisplayPath(blobPath: string, displayPath: string, data: Buffer): Promise<void> {
|
|
51
|
+
if (displayPath === blobPath) return;
|
|
52
|
+
try {
|
|
53
|
+
await fsp.link(blobPath, displayPath);
|
|
54
|
+
return;
|
|
55
|
+
} catch (err) {
|
|
56
|
+
if (typeof err === "object" && err !== null && "code" in err && err.code === "EEXIST") return;
|
|
57
|
+
logger.debug("Blob display hardlink failed; falling back to copy", {
|
|
58
|
+
blobPath,
|
|
59
|
+
displayPath,
|
|
60
|
+
error: err instanceof Error ? err.message : String(err),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
await Bun.write(displayPath, data);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function ensureDisplayPathSync(blobPath: string, displayPath: string, data: Buffer): void {
|
|
67
|
+
if (displayPath === blobPath) return;
|
|
68
|
+
try {
|
|
69
|
+
fs.linkSync(blobPath, displayPath);
|
|
70
|
+
return;
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (typeof err === "object" && err !== null && "code" in err && err.code === "EEXIST") return;
|
|
73
|
+
logger.debug("Blob display hardlink failed; falling back to copy", {
|
|
74
|
+
blobPath,
|
|
75
|
+
displayPath,
|
|
76
|
+
error: err instanceof Error ? err.message : String(err),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
fs.writeFileSync(displayPath, data);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function blobExtensionForImageMimeType(mimeType: string | undefined): string | undefined {
|
|
83
|
+
if (!mimeType) return undefined;
|
|
84
|
+
const lower = mimeType.toLowerCase();
|
|
85
|
+
const known = IMAGE_EXTENSION_BY_MIME[lower];
|
|
86
|
+
if (known) return known;
|
|
87
|
+
if (!lower.startsWith("image/")) return undefined;
|
|
88
|
+
const subtype = lower.slice("image/".length).split(";")[0]?.split("+")[0];
|
|
89
|
+
return normalizeBlobExtension(subtype);
|
|
90
|
+
}
|
|
91
|
+
|
|
21
92
|
export class BlobStore {
|
|
22
93
|
constructor(readonly dir: string) {}
|
|
23
94
|
|
|
@@ -25,18 +96,22 @@ export class BlobStore {
|
|
|
25
96
|
* Write binary data to the blob store.
|
|
26
97
|
* @returns SHA-256 hex hash of the data
|
|
27
98
|
*/
|
|
28
|
-
async put(data: Buffer): Promise<BlobPutResult> {
|
|
99
|
+
async put(data: Buffer, options?: BlobPutOptions): Promise<BlobPutResult> {
|
|
29
100
|
const hash = new Bun.SHA256().update(data).digest("hex");
|
|
30
101
|
const blobPath = path.join(this.dir, hash);
|
|
102
|
+
const extension = normalizeBlobExtension(options?.extension);
|
|
103
|
+
const displayPath = extension ? `${blobPath}.${extension}` : blobPath;
|
|
31
104
|
const result = {
|
|
32
105
|
hash,
|
|
33
106
|
path: blobPath,
|
|
107
|
+
displayPath,
|
|
34
108
|
get ref() {
|
|
35
109
|
return `${BLOB_PREFIX}${hash}`;
|
|
36
110
|
},
|
|
37
111
|
};
|
|
38
112
|
|
|
39
113
|
await Bun.write(blobPath, data);
|
|
114
|
+
await ensureDisplayPath(blobPath, displayPath, data);
|
|
40
115
|
return result;
|
|
41
116
|
}
|
|
42
117
|
|
|
@@ -45,18 +120,22 @@ export class BlobStore {
|
|
|
45
120
|
* cannot afford the microtask hops of the async version (e.g. OOM-safe session writes).
|
|
46
121
|
* Returns once the bytes are in the kernel page cache.
|
|
47
122
|
*/
|
|
48
|
-
putSync(data: Buffer): BlobPutResult {
|
|
123
|
+
putSync(data: Buffer, options?: BlobPutOptions): BlobPutResult {
|
|
49
124
|
const hash = new Bun.SHA256().update(data).digest("hex");
|
|
50
125
|
const blobPath = path.join(this.dir, hash);
|
|
126
|
+
const extension = normalizeBlobExtension(options?.extension);
|
|
127
|
+
const displayPath = extension ? `${blobPath}.${extension}` : blobPath;
|
|
51
128
|
const result = {
|
|
52
129
|
hash,
|
|
53
130
|
path: blobPath,
|
|
131
|
+
displayPath,
|
|
54
132
|
get ref() {
|
|
55
133
|
return `${BLOB_PREFIX}${hash}`;
|
|
56
134
|
},
|
|
57
135
|
};
|
|
58
136
|
fs.mkdirSync(this.dir, { recursive: true });
|
|
59
137
|
fs.writeFileSync(blobPath, data);
|
|
138
|
+
ensureDisplayPathSync(blobPath, displayPath, data);
|
|
60
139
|
return result;
|
|
61
140
|
}
|
|
62
141
|
|
|
@@ -120,17 +199,25 @@ export function externalizeImageDataUrlSync(blobStore: BlobStore, dataUrl: strin
|
|
|
120
199
|
* Externalize an image's base64 data to the blob store, returning a blob reference.
|
|
121
200
|
* If the data is already a blob reference, returns it unchanged.
|
|
122
201
|
*/
|
|
123
|
-
export async function externalizeImageData(
|
|
202
|
+
export async function externalizeImageData(
|
|
203
|
+
blobStore: BlobStore,
|
|
204
|
+
base64Data: string,
|
|
205
|
+
mimeType?: string,
|
|
206
|
+
): Promise<string> {
|
|
124
207
|
if (isBlobRef(base64Data)) return base64Data;
|
|
125
208
|
const buffer = Buffer.from(base64Data, "base64");
|
|
126
|
-
const { ref } = await blobStore.put(buffer
|
|
209
|
+
const { ref } = await blobStore.put(buffer, {
|
|
210
|
+
extension: blobExtensionForImageMimeType(mimeType),
|
|
211
|
+
});
|
|
127
212
|
return ref;
|
|
128
213
|
}
|
|
129
214
|
|
|
130
215
|
/** Synchronous variant of {@link externalizeImageData}. */
|
|
131
|
-
export function externalizeImageDataSync(blobStore: BlobStore, base64Data: string): string {
|
|
216
|
+
export function externalizeImageDataSync(blobStore: BlobStore, base64Data: string, mimeType?: string): string {
|
|
132
217
|
if (isBlobRef(base64Data)) return base64Data;
|
|
133
|
-
return blobStore.putSync(Buffer.from(base64Data, "base64")
|
|
218
|
+
return blobStore.putSync(Buffer.from(base64Data, "base64"), {
|
|
219
|
+
extension: blobExtensionForImageMimeType(mimeType),
|
|
220
|
+
}).ref;
|
|
134
221
|
}
|
|
135
222
|
|
|
136
223
|
/**
|
|
@@ -29,6 +29,7 @@ import {
|
|
|
29
29
|
} from "@oh-my-pi/pi-utils";
|
|
30
30
|
import { ArtifactManager } from "./artifacts";
|
|
31
31
|
import {
|
|
32
|
+
type BlobPutOptions,
|
|
32
33
|
type BlobPutResult,
|
|
33
34
|
BlobStore,
|
|
34
35
|
externalizeImageData,
|
|
@@ -336,6 +337,7 @@ export type ReadonlySessionManager = Pick<
|
|
|
336
337
|
| "getTree"
|
|
337
338
|
| "getUsageStatistics"
|
|
338
339
|
| "putBlob"
|
|
340
|
+
| "putBlobSync"
|
|
339
341
|
>;
|
|
340
342
|
|
|
341
343
|
function createSessionId(): string {
|
|
@@ -1219,7 +1221,7 @@ async function truncateForPersistence(obj: unknown, blobStore: BlobStore, key?:
|
|
|
1219
1221
|
if (key === TEXT_CONTENT_KEY && isImageBlock(item)) {
|
|
1220
1222
|
if (!isBlobRef(item.data) && item.data.length >= BLOB_EXTERNALIZE_THRESHOLD) {
|
|
1221
1223
|
changed = true;
|
|
1222
|
-
const blobRef = await externalizeImageData(blobStore, item.data);
|
|
1224
|
+
const blobRef = await externalizeImageData(blobStore, item.data, item.mimeType);
|
|
1223
1225
|
return { ...item, data: blobRef };
|
|
1224
1226
|
}
|
|
1225
1227
|
}
|
|
@@ -1313,13 +1315,15 @@ function truncateForPersistenceSync(obj: unknown, blobStore: BlobStore, key?: st
|
|
|
1313
1315
|
const result: unknown[] = new Array(obj.length);
|
|
1314
1316
|
for (let i = 0; i < obj.length; i++) {
|
|
1315
1317
|
const item = obj[i];
|
|
1316
|
-
if (
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1318
|
+
if (
|
|
1319
|
+
key === TEXT_CONTENT_KEY &&
|
|
1320
|
+
isImageBlock(item) &&
|
|
1321
|
+
!isBlobRef(item.data) &&
|
|
1322
|
+
item.data.length >= BLOB_EXTERNALIZE_THRESHOLD
|
|
1323
|
+
) {
|
|
1324
|
+
changed = true;
|
|
1325
|
+
result[i] = { ...item, data: externalizeImageDataSync(blobStore, item.data, item.mimeType) };
|
|
1326
|
+
continue;
|
|
1323
1327
|
}
|
|
1324
1328
|
const newItem = truncateForPersistenceSync(item, blobStore, key);
|
|
1325
1329
|
if (newItem !== item) changed = true;
|
|
@@ -1978,8 +1982,13 @@ export class SessionManager {
|
|
|
1978
1982
|
}
|
|
1979
1983
|
|
|
1980
1984
|
/** Puts a binary blob into the blob store and returns the blob reference */
|
|
1981
|
-
async putBlob(data: Buffer): Promise<BlobPutResult> {
|
|
1982
|
-
return this.#blobStore.put(data);
|
|
1985
|
+
async putBlob(data: Buffer, options?: BlobPutOptions): Promise<BlobPutResult> {
|
|
1986
|
+
return this.#blobStore.put(data, options);
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
/** Synchronous variant of {@link putBlob} for rebuild-only render paths. */
|
|
1990
|
+
putBlobSync(data: Buffer, options?: BlobPutOptions): BlobPutResult {
|
|
1991
|
+
return this.#blobStore.putSync(data, options);
|
|
1983
1992
|
}
|
|
1984
1993
|
|
|
1985
1994
|
captureState(): SessionManagerStateSnapshot {
|
|
@@ -392,17 +392,9 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
392
392
|
},
|
|
393
393
|
{
|
|
394
394
|
name: "copy",
|
|
395
|
-
description: "
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
{ name: "code", description: "Copy last code block" },
|
|
399
|
-
{ name: "all", description: "Copy all code blocks from last message" },
|
|
400
|
-
{ name: "cmd", description: "Copy last bash/python command" },
|
|
401
|
-
],
|
|
402
|
-
allowArgs: true,
|
|
403
|
-
handleTui: async (command, runtime) => {
|
|
404
|
-
const sub = command.args.trim().toLowerCase() || undefined;
|
|
405
|
-
await runtime.ctx.handleCopyCommand(sub);
|
|
395
|
+
description: "Pick text or code from the conversation to copy",
|
|
396
|
+
handleTui: (_command, runtime) => {
|
|
397
|
+
runtime.ctx.showCopySelector();
|
|
406
398
|
runtime.ctx.editor.setText("");
|
|
407
399
|
},
|
|
408
400
|
},
|
package/src/system-prompt.ts
CHANGED
|
@@ -363,6 +363,8 @@ export interface BuildSystemPromptOptions {
|
|
|
363
363
|
workspaceTree?: WorkspaceTree | Promise<WorkspaceTree>;
|
|
364
364
|
/** Whether the local memory://root summary is active. */
|
|
365
365
|
memoryRootEnabled?: boolean;
|
|
366
|
+
/** Active model identifier (e.g. "anthropic/claude-opus-4") surfaced to the agent. */
|
|
367
|
+
model?: string;
|
|
366
368
|
}
|
|
367
369
|
|
|
368
370
|
/** Result of building provider-facing system prompt messages. */
|
|
@@ -396,6 +398,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
396
398
|
secretsEnabled = false,
|
|
397
399
|
workspaceTree: providedWorkspaceTree,
|
|
398
400
|
memoryRootEnabled = false,
|
|
401
|
+
model,
|
|
399
402
|
} = options;
|
|
400
403
|
const resolvedCwd = cwd ?? getProjectDir();
|
|
401
404
|
|
|
@@ -566,6 +569,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}):
|
|
|
566
569
|
date,
|
|
567
570
|
dateTime,
|
|
568
571
|
cwd: promptCwd,
|
|
572
|
+
model: model ?? "",
|
|
569
573
|
intentTracing: !!intentField,
|
|
570
574
|
intentField: intentField ?? "",
|
|
571
575
|
mcpDiscoveryMode,
|
package/src/task/render.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { RenderResultOptions } from "../extensibility/custom-tools/types";
|
|
|
13
13
|
import { formatContextUsage } from "../modes/components/status-line/context-thresholds";
|
|
14
14
|
import type { Theme } from "../modes/theme/theme";
|
|
15
15
|
import {
|
|
16
|
+
capPreviewLines,
|
|
16
17
|
formatBadge,
|
|
17
18
|
formatDuration,
|
|
18
19
|
formatMoreItems,
|
|
@@ -117,6 +118,26 @@ function normalizeReportFindings(value: unknown): ReportFindingDetails[] {
|
|
|
117
118
|
return findings;
|
|
118
119
|
}
|
|
119
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Normalize the `yield` slot of `extractedToolData` into an array of
|
|
123
|
+
* yield-detail records. The subprocess executor always populates this slot as
|
|
124
|
+
* `unknown[]` (see `executor.ts` `extractData` handler), but the renderer
|
|
125
|
+
* MUST also tolerate a stray single object — optional chaining short-circuits
|
|
126
|
+
* on `null`/`undefined` only, so calling `.map` on a plain object would throw
|
|
127
|
+
* `TypeError: completeData?.map is not a function` and crash the TUI.
|
|
128
|
+
* A single object is wrapped as a 1-element array so the review verdict still
|
|
129
|
+
* renders; non-object primitives drop out.
|
|
130
|
+
*/
|
|
131
|
+
function normalizeYieldData(value: unknown): Array<{ data: unknown }> {
|
|
132
|
+
if (Array.isArray(value)) {
|
|
133
|
+
return value.filter((item): item is { data: unknown } => item !== null && typeof item === "object");
|
|
134
|
+
}
|
|
135
|
+
if (value !== null && typeof value === "object") {
|
|
136
|
+
return [value as { data: unknown }];
|
|
137
|
+
}
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
|
|
120
141
|
function formatJsonScalar(value: unknown, _theme: Theme): string {
|
|
121
142
|
if (value === null) return "null";
|
|
122
143
|
if (typeof value === "string") {
|
|
@@ -541,10 +562,11 @@ export function renderCall(
|
|
|
541
562
|
|
|
542
563
|
if (hasContext) {
|
|
543
564
|
lines.push(` ${branch} ${theme.fg("dim", "Context")}`);
|
|
544
|
-
|
|
565
|
+
const contextLines = context.split("\n").map(line => {
|
|
545
566
|
const content = line ? theme.fg("muted", replaceTabs(line)) : "";
|
|
546
|
-
|
|
547
|
-
}
|
|
567
|
+
return ` ${vertical} ${content}`;
|
|
568
|
+
});
|
|
569
|
+
lines.push(...capPreviewLines(contextLines, theme, { expanded: options.expanded, prefix: ` ${vertical} ` }));
|
|
548
570
|
}
|
|
549
571
|
|
|
550
572
|
// `Tasks` is the last child unless the isolation flag follows it.
|
|
@@ -671,12 +693,12 @@ function renderAgentProgress(
|
|
|
671
693
|
if (progress.extractedToolData) {
|
|
672
694
|
// For completed tasks, check for review verdict from yield tool
|
|
673
695
|
if (progress.status === "completed") {
|
|
674
|
-
const completeData = progress.extractedToolData.yield
|
|
696
|
+
const completeData = normalizeYieldData(progress.extractedToolData.yield);
|
|
675
697
|
const reportFindingData = normalizeReportFindings(progress.extractedToolData.report_finding);
|
|
676
698
|
const reviewData = completeData
|
|
677
|
-
|
|
699
|
+
.map(c => c.data as SubmitReviewDetails)
|
|
678
700
|
.filter(d => d && typeof d === "object" && "overall_correctness" in d);
|
|
679
|
-
if (reviewData
|
|
701
|
+
if (reviewData.length > 0) {
|
|
680
702
|
const summary = reviewData[reviewData.length - 1];
|
|
681
703
|
const findings = reportFindingData;
|
|
682
704
|
lines.push(...renderReviewResult(summary, findings, continuePrefix, expanded, theme));
|
|
@@ -912,16 +934,21 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
912
934
|
);
|
|
913
935
|
}
|
|
914
936
|
// Check for review result (yield with review schema + report_finding)
|
|
915
|
-
|
|
937
|
+
// Check for review result (yield with review schema + report_finding).
|
|
938
|
+
// `normalizeYieldData` guards against a stray non-array `yield` slot —
|
|
939
|
+
// optional chaining on `.map` only short-circuits on null/undefined and
|
|
940
|
+
// would otherwise crash the renderer with `TypeError: completeData?.map
|
|
941
|
+
// is not a function` when the slot is a plain object (see issue #1987).
|
|
942
|
+
const completeData = normalizeYieldData(result.extractedToolData?.yield);
|
|
916
943
|
const reportFindingData = normalizeReportFindings(result.extractedToolData?.report_finding);
|
|
917
944
|
|
|
918
945
|
// Extract review verdict from yield tool's data field if it matches SubmitReviewDetails
|
|
919
946
|
const reviewData = completeData
|
|
920
|
-
|
|
947
|
+
.map(c => c.data as SubmitReviewDetails)
|
|
921
948
|
.filter(d => d && typeof d === "object" && "overall_correctness" in d);
|
|
922
|
-
const submitReviewData = reviewData
|
|
949
|
+
const submitReviewData = reviewData.length > 0 ? reviewData : undefined;
|
|
923
950
|
|
|
924
|
-
if (submitReviewData
|
|
951
|
+
if (submitReviewData) {
|
|
925
952
|
// Use combined review renderer
|
|
926
953
|
const summary = submitReviewData[submitReviewData.length - 1];
|
|
927
954
|
const findings = reportFindingData;
|
|
@@ -929,7 +956,7 @@ function renderAgentResult(result: SingleResult, isLast: boolean, expanded: bool
|
|
|
929
956
|
return lines;
|
|
930
957
|
}
|
|
931
958
|
if (reportFindingData.length > 0) {
|
|
932
|
-
const hasCompleteData = completeData
|
|
959
|
+
const hasCompleteData = completeData.length > 0;
|
|
933
960
|
const message = hasCompleteData
|
|
934
961
|
? "Review verdict missing expected fields"
|
|
935
962
|
: "Review incomplete (yield not called)";
|
package/src/tiny/title-client.ts
CHANGED
|
@@ -261,6 +261,11 @@ export class TinyTitleClient {
|
|
|
261
261
|
#pending = new Map<string, PendingRequest>();
|
|
262
262
|
#progressListeners = new Set<(event: TinyTitleProgressEvent) => void>();
|
|
263
263
|
#nextRequestId = 0;
|
|
264
|
+
#spawnWorker: () => WorkerHandle;
|
|
265
|
+
|
|
266
|
+
constructor(spawnWorker: () => WorkerHandle = spawnTinyTitleWorker) {
|
|
267
|
+
this.#spawnWorker = spawnWorker;
|
|
268
|
+
}
|
|
264
269
|
|
|
265
270
|
onProgress(listener: (event: TinyTitleProgressEvent) => void): () => void {
|
|
266
271
|
this.#progressListeners.add(listener);
|
|
@@ -392,7 +397,7 @@ export class TinyTitleClient {
|
|
|
392
397
|
|
|
393
398
|
#ensureWorker(): WorkerHandle {
|
|
394
399
|
if (this.#worker) return this.#worker;
|
|
395
|
-
const worker =
|
|
400
|
+
const worker = this.#spawnWorker();
|
|
396
401
|
this.#worker = worker;
|
|
397
402
|
this.#unsubscribeMessage = worker.onMessage(message => this.#handleMessage(message));
|
|
398
403
|
this.#unsubscribeError = worker.onError(error => this.#handleWorkerError(error));
|
|
@@ -429,6 +434,7 @@ export class TinyTitleClient {
|
|
|
429
434
|
this.#emitProgress({ modelKey: pending.modelKey, status: "error" });
|
|
430
435
|
if (pending.kind === "generate" || pending.kind === "complete") pending.resolve(null);
|
|
431
436
|
else pending.resolve(false);
|
|
437
|
+
void this.terminate();
|
|
432
438
|
}
|
|
433
439
|
|
|
434
440
|
#emitProgress(event: TinyTitleProgressEvent): void {
|