@oh-my-pi/pi-coding-agent 16.5.0 → 16.5.2
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 +91 -0
- package/dist/cli.js +3556 -3504
- package/dist/types/advisor/advise-tool.d.ts +12 -1
- package/dist/types/advisor/runtime.d.ts +41 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/update-cli.d.ts +4 -1
- package/dist/types/cli/usage-cli.d.ts +3 -0
- package/dist/types/cli/usage-error.d.ts +4 -0
- package/dist/types/config/api-key-resolver.d.ts +2 -2
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/model-resolver.d.ts +8 -1
- package/dist/types/config/models-config.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +10 -0
- package/dist/types/discovery/substitute-plugin-root.d.ts +22 -0
- package/dist/types/eval/__tests__/process-entry-import.test.d.ts +1 -0
- package/dist/types/eval/backend.d.ts +3 -3
- package/dist/types/eval/bridge-timeout.d.ts +9 -1
- package/dist/types/eval/js/context-manager.d.ts +5 -3
- package/dist/types/eval/js/process-entry.d.ts +6 -0
- package/dist/types/eval/js/worker-core.d.ts +15 -1
- package/dist/types/eval/py/spawn-options.d.ts +10 -0
- package/dist/types/eval/py/tool-bridge.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/wrapper.d.ts +3 -6
- package/dist/types/extensibility/plugins/bun-git-cache.d.ts +3 -0
- package/dist/types/goals/guided-setup.d.ts +12 -0
- package/dist/types/internal-urls/history-protocol.d.ts +3 -2
- package/dist/types/internal-urls/memory-protocol.d.ts +6 -7
- package/dist/types/internal-urls/registry-helpers.d.ts +19 -0
- package/dist/types/main.d.ts +1 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/oauth-flow.d.ts +2 -0
- package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +1 -0
- package/dist/types/modes/components/agent-hub.d.ts +10 -0
- package/dist/types/modes/components/dynamic-border.d.ts +5 -3
- package/dist/types/modes/components/login-dialog.d.ts +2 -0
- package/dist/types/modes/components/mcp-add-wizard.d.ts +1 -0
- package/dist/types/modes/components/read-tool-group.d.ts +0 -2
- package/dist/types/modes/components/transcript-container.d.ts +3 -2
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -0
- package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
- package/dist/types/modes/orchestrate.d.ts +1 -1
- package/dist/types/modes/rpc/host-tools.d.ts +2 -0
- package/dist/types/modes/rpc/rpc-mode.d.ts +26 -6
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/modes/ultrathink.d.ts +1 -1
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +12 -0
- package/dist/types/modes/workflow.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +6 -0
- package/dist/types/session/exit-diagnostics.d.ts +11 -0
- package/dist/types/session/messages.d.ts +15 -0
- package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +11 -0
- package/dist/types/subprocess/worker-client.d.ts +6 -0
- package/dist/types/tools/bash-skill-urls.d.ts +1 -0
- package/dist/types/tools/grep.d.ts +0 -2
- package/dist/types/tools/read.d.ts +0 -4
- package/dist/types/web/search/provider.d.ts +10 -3
- package/dist/types/web/search/providers/codex.d.ts +5 -4
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +917 -42
- package/src/advisor/advise-tool.ts +17 -1
- package/src/advisor/runtime.ts +270 -67
- package/src/autolearn/controller.ts +15 -3
- package/src/autoresearch/dashboard.ts +2 -2
- package/src/cli/args.ts +12 -0
- package/src/cli/auth-broker-cli.ts +30 -11
- package/src/cli/auth-gateway-cli.ts +5 -1
- package/src/cli/config-cli.ts +15 -3
- package/src/cli/dry-balance-cli.ts +14 -4
- package/src/cli/flag-tables.ts +21 -7
- package/src/cli/update-cli.ts +62 -11
- package/src/cli/usage-cli.ts +58 -5
- package/src/cli/usage-error.ts +7 -0
- package/src/cli.ts +23 -1
- package/src/commands/acp.ts +11 -2
- package/src/commands/launch.ts +12 -3
- package/src/commands/token.ts +3 -1
- package/src/config/api-key-resolver.ts +12 -3
- package/src/config/config-file.ts +30 -12
- package/src/config/model-registry.ts +7 -7
- package/src/config/model-resolver.ts +21 -7
- package/src/config/models-config.ts +1 -1
- package/src/config/settings-schema.ts +10 -0
- package/src/cursor.ts +2 -0
- package/src/discovery/claude-plugins.ts +9 -3
- package/src/discovery/omp-plugins.ts +6 -2
- package/src/discovery/substitute-plugin-root.ts +32 -0
- package/src/eval/__tests__/agent-bridge.test.ts +19 -14
- package/src/eval/__tests__/bridge-timeout.test.ts +106 -0
- package/src/eval/__tests__/js-context-manager.test.ts +158 -1
- package/src/eval/__tests__/kernel-spawn.test.ts +12 -0
- package/src/eval/__tests__/prelude-agent.test.ts +20 -0
- package/src/eval/__tests__/process-entry-import.test.ts +27 -0
- package/src/eval/agent-bridge.ts +121 -116
- package/src/eval/backend.ts +3 -3
- package/src/eval/bridge-timeout.ts +20 -2
- package/src/eval/executor-base.ts +85 -7
- package/src/eval/jl/kernel.ts +2 -1
- package/src/eval/js/context-manager.ts +109 -32
- package/src/eval/js/process-entry.ts +27 -0
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/js/worker-core.ts +70 -9
- package/src/eval/js/worker-entry.ts +1 -1
- package/src/eval/py/__tests__/prelude.test.ts +72 -0
- package/src/eval/py/kernel.ts +2 -1
- package/src/eval/py/prelude.py +28 -1
- package/src/eval/py/spawn-options.ts +13 -0
- package/src/eval/py/tool-bridge.ts +13 -14
- package/src/eval/rb/kernel.ts +2 -1
- package/src/exec/bash-executor.ts +30 -43
- package/src/extensibility/custom-tools/types.ts +3 -0
- package/src/extensibility/extensions/runner.ts +3 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/extensions/wrapper.ts +18 -18
- package/src/extensibility/plugins/bun-git-cache.ts +91 -0
- package/src/extensibility/plugins/legacy-pi-compat.ts +32 -16
- package/src/extensibility/plugins/manager.ts +28 -7
- package/src/goals/guided-setup.ts +29 -1
- package/src/internal-urls/history-protocol.ts +95 -15
- package/src/internal-urls/memory-protocol.ts +13 -9
- package/src/internal-urls/registry-helpers.ts +50 -1
- package/src/launch/broker.ts +38 -25
- package/src/lsp/client.ts +7 -1
- package/src/main.ts +29 -0
- package/src/mcp/oauth-discovery.ts +20 -1
- package/src/mcp/oauth-flow.ts +3 -1
- package/src/mcp/tool-bridge.ts +57 -6
- package/src/modes/components/__tests__/dynamic-border.test.ts +55 -0
- package/src/modes/components/agent-dashboard.ts +2 -2
- package/src/modes/components/agent-hub.ts +15 -2
- package/src/modes/components/agent-transcript-viewer.ts +2 -2
- package/src/modes/components/chat-transcript-builder.ts +26 -4
- package/src/modes/components/dynamic-border.ts +9 -6
- package/src/modes/components/extensions/extension-list.ts +2 -2
- package/src/modes/components/hook-selector.ts +10 -4
- package/src/modes/components/login-dialog.ts +5 -0
- package/src/modes/components/mcp-add-wizard.ts +5 -0
- package/src/modes/components/plan-review-overlay.ts +11 -11
- package/src/modes/components/read-tool-group.ts +1 -8
- package/src/modes/components/status-line/component.ts +10 -1
- package/src/modes/components/transcript-container.ts +110 -7
- package/src/modes/controllers/command-controller.ts +12 -4
- package/src/modes/controllers/event-controller.ts +80 -15
- package/src/modes/controllers/input-controller.ts +4 -2
- package/src/modes/controllers/mcp-command-controller.ts +6 -7
- package/src/modes/controllers/selector-controller.ts +24 -5
- package/src/modes/controllers/todo-command-controller.ts +18 -14
- package/src/modes/interactive-mode.ts +7 -3
- package/src/modes/magic-keyword-boundary.ts +23 -0
- package/src/modes/orchestrate.ts +6 -5
- package/src/modes/print-mode.ts +9 -0
- package/src/modes/prompt-action-autocomplete.ts +6 -1
- package/src/modes/rpc/host-tools.ts +15 -0
- package/src/modes/rpc/rpc-mode.ts +123 -48
- package/src/modes/types.ts +1 -1
- package/src/modes/ultrathink.ts +6 -5
- package/src/modes/utils/transcript-render-helpers.ts +54 -0
- package/src/modes/utils/ui-helpers.ts +27 -1
- package/src/modes/workflow.ts +6 -5
- package/src/prompts/advisor/system.md +1 -0
- package/src/prompts/system/system-prompt.md +1 -0
- package/src/prompts/tools/eval.md +2 -2
- package/src/prompts/tools/grep.md +1 -2
- package/src/prompts/tools/read.md +2 -4
- package/src/sdk.ts +61 -34
- package/src/session/agent-session.ts +283 -39
- package/src/session/exit-diagnostics.ts +108 -0
- package/src/session/messages.test.ts +66 -0
- package/src/session/messages.ts +37 -0
- package/src/session/streaming-output.ts +40 -12
- package/src/slash-commands/helpers/active-oauth-account.ts +22 -2
- package/src/slash-commands/helpers/logout.ts +23 -3
- package/src/slash-commands/helpers/usage-report.ts +14 -2
- package/src/subprocess/worker-client.ts +9 -2
- package/src/system-prompt.test.ts +36 -0
- package/src/system-prompt.ts +1 -1
- package/src/task/executor.ts +8 -0
- package/src/task/render.test.ts +36 -0
- package/src/task/render.ts +55 -43
- package/src/tools/bash-skill-urls.ts +4 -1
- package/src/tools/bash.ts +1 -0
- package/src/tools/browser/registry.ts +17 -3
- package/src/tools/eval.ts +14 -9
- package/src/tools/gh.ts +3 -1
- package/src/tools/grep.ts +5 -45
- package/src/tools/path-utils.ts +7 -1
- package/src/tools/read.ts +23 -74
- package/src/tools/write.ts +82 -9
- package/src/tools/yield.ts +29 -1
- package/src/utils/title-generator.ts +10 -6
- package/src/web/search/index.ts +39 -22
- package/src/web/search/provider.ts +33 -16
- package/src/web/search/providers/codex.ts +68 -21
- package/src/web/search/providers/perplexity-auth.ts +20 -11
- package/src/web/search/providers/perplexity.ts +14 -2
|
@@ -116,16 +116,18 @@ function findTaskFuzzy(phases: TodoPhase[], query: string): { task: TodoItem; ph
|
|
|
116
116
|
// Build system reminder
|
|
117
117
|
// =============================================================================
|
|
118
118
|
|
|
119
|
-
function buildSystemReminder(action: string, phases: TodoPhase[]): string {
|
|
119
|
+
function buildSystemReminder(action: string, phases: TodoPhase[], removed = false): string {
|
|
120
120
|
const md = phases.length === 0 ? "(empty)" : phasesToMarkdown(phases).trimEnd();
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
121
|
+
const lines = ["<system-reminder>", `The user manually modified the todo list (${action}).`];
|
|
122
|
+
if (removed) {
|
|
123
|
+
lines.push(
|
|
124
|
+
phases.length === 0
|
|
125
|
+
? "The user intentionally cleared the todo list. Do NOT recreate or re-populate it unless the user explicitly asks; continue the current request without a todo list."
|
|
126
|
+
: "The user intentionally removed the entries no longer shown below. Do NOT re-add them unless the user explicitly asks.",
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
lines.push("Current todo list:", "", md, "</system-reminder>");
|
|
130
|
+
return lines.join("\n");
|
|
129
131
|
}
|
|
130
132
|
|
|
131
133
|
export class TodoCommandController {
|
|
@@ -366,7 +368,7 @@ export class TodoCommandController {
|
|
|
366
368
|
const current = this.#currentPhases();
|
|
367
369
|
const trimmed = rest.trim();
|
|
368
370
|
if (!trimmed) {
|
|
369
|
-
this.#commit([], "/todo rm (all)");
|
|
371
|
+
this.#commit([], "/todo rm (all)", { removed: true });
|
|
370
372
|
this.ctx.showStatus("Cleared all todos.");
|
|
371
373
|
return;
|
|
372
374
|
}
|
|
@@ -377,7 +379,7 @@ export class TodoCommandController {
|
|
|
377
379
|
this.ctx.showError(errors.join("; "));
|
|
378
380
|
return;
|
|
379
381
|
}
|
|
380
|
-
this.#commit(phases, `/todo rm ${taskHit.task.content}
|
|
382
|
+
this.#commit(phases, `/todo rm ${taskHit.task.content}`, { removed: true });
|
|
381
383
|
this.ctx.showStatus(`Removed: ${taskHit.task.content}`);
|
|
382
384
|
return;
|
|
383
385
|
}
|
|
@@ -388,7 +390,7 @@ export class TodoCommandController {
|
|
|
388
390
|
this.ctx.showError(errors.join("; "));
|
|
389
391
|
return;
|
|
390
392
|
}
|
|
391
|
-
this.#commit(phases, `/todo rm ${phaseHit.name}
|
|
393
|
+
this.#commit(phases, `/todo rm ${phaseHit.name}`, { removed: true });
|
|
392
394
|
this.ctx.showStatus(`Removed phase: ${phaseHit.name}`);
|
|
393
395
|
return;
|
|
394
396
|
}
|
|
@@ -454,7 +456,7 @@ export class TodoCommandController {
|
|
|
454
456
|
}
|
|
455
457
|
}
|
|
456
458
|
|
|
457
|
-
#commit(nextPhases: TodoPhase[], action: string): void {
|
|
459
|
+
#commit(nextPhases: TodoPhase[], action: string, opts?: { removed?: boolean }): void {
|
|
458
460
|
// 1. In-memory + UI state
|
|
459
461
|
this.ctx.session.setTodoPhases(nextPhases);
|
|
460
462
|
this.ctx.setTodos(nextPhases);
|
|
@@ -463,7 +465,9 @@ export class TodoCommandController {
|
|
|
463
465
|
this.ctx.sessionManager.appendCustomEntry(USER_TODO_EDIT_CUSTOM_TYPE, { phases: nextPhases });
|
|
464
466
|
|
|
465
467
|
// 3. Inject system reminder so the agent learns about the change next turn.
|
|
466
|
-
|
|
468
|
+
// Removals carry explicit intent so the agent does not rebuild the
|
|
469
|
+
// cleared/removed items on its next turn (issue #5258).
|
|
470
|
+
const reminderText = buildSystemReminder(action, nextPhases, opts?.removed ?? false);
|
|
467
471
|
const message = {
|
|
468
472
|
role: "developer" as const,
|
|
469
473
|
content: [{ type: "text" as const, text: reminderText }],
|
|
@@ -71,7 +71,7 @@ import type {
|
|
|
71
71
|
import type { CompactOptions } from "../extensibility/extensions/types";
|
|
72
72
|
import type { Skill } from "../extensibility/skills";
|
|
73
73
|
import { loadSlashCommands } from "../extensibility/slash-commands";
|
|
74
|
-
import { type GuidedGoalMessage, runGuidedGoalTurn } from "../goals/guided-setup";
|
|
74
|
+
import { type GuidedGoalMessage, newGuidedGoalSessionId, runGuidedGoalTurn } from "../goals/guided-setup";
|
|
75
75
|
import type { Goal, GoalModeState } from "../goals/state";
|
|
76
76
|
import { resolveLocalUrlToPath } from "../internal-urls";
|
|
77
77
|
import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "../lsp/startup-events";
|
|
@@ -3115,8 +3115,12 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
3115
3115
|
|
|
3116
3116
|
const messages: GuidedGoalMessage[] = [{ role: "user", content: initial }];
|
|
3117
3117
|
let latestDraftObjective: string | undefined;
|
|
3118
|
+
// One Codex side session for the whole interview: every follow-up turn
|
|
3119
|
+
// reuses it so a multi-question interview shares a single websocket-only
|
|
3120
|
+
// Codex socket instead of leaking one per turn (#5471 review).
|
|
3121
|
+
const guidedGoalSessionId = newGuidedGoalSessionId(this.session);
|
|
3118
3122
|
for (let turn = 0; turn < 6; turn++) {
|
|
3119
|
-
const result = await runGuidedGoalTurn(this.session, { messages });
|
|
3123
|
+
const result = await runGuidedGoalTurn(this.session, { messages, sideSessionId: guidedGoalSessionId });
|
|
3120
3124
|
if (result.objective?.trim()) latestDraftObjective = result.objective.trim();
|
|
3121
3125
|
if (result.kind === "question") {
|
|
3122
3126
|
messages.push({ role: "assistant", content: result.question });
|
|
@@ -4156,7 +4160,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
4156
4160
|
await this.#selectorController.showDebugSelector();
|
|
4157
4161
|
}
|
|
4158
4162
|
|
|
4159
|
-
showAgentHub(options?: { requireContent?: boolean }): void {
|
|
4163
|
+
showAgentHub(options?: { requireContent?: boolean; armCloseTap?: boolean }): void {
|
|
4160
4164
|
this.#selectorController.showAgentHub(this.#observerRegistry, options);
|
|
4161
4165
|
}
|
|
4162
4166
|
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** Characters that bind a magic keyword into an identifier or path segment. */
|
|
2
|
+
const LEFT_BOUNDARY = String.raw`(?<![\p{L}\p{N}_./\\-])(?<!::)`;
|
|
3
|
+
|
|
4
|
+
/** Characters that cannot immediately follow a standalone magic keyword. */
|
|
5
|
+
const RIGHT_BOUNDARY = String.raw`(?![\p{L}\p{N}_/\\-])(?!\.[\p{L}\p{N}_-])(?!\()`;
|
|
6
|
+
|
|
7
|
+
/** Escape a literal string for safe insertion into a RegExp source. */
|
|
8
|
+
function escapeRegExp(value: string): string {
|
|
9
|
+
return value.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build a case-sensitive magic-keyword matcher for prose punctuation boundaries.
|
|
14
|
+
*
|
|
15
|
+
* Sentence punctuation and quotes may touch the keyword, but letters, digits,
|
|
16
|
+
* underscores, slashes, backslashes, hyphens, file-extension dots, symbol
|
|
17
|
+
* references (`foo::keyword`), and immediate call parentheses (`keyword()`)
|
|
18
|
+
* keep the occurrence embedded in code rather than prose.
|
|
19
|
+
*/
|
|
20
|
+
export function magicKeywordRegex(keyword: string, flags = ""): RegExp {
|
|
21
|
+
const normalizedFlags = flags.includes("u") ? flags : `${flags}u`;
|
|
22
|
+
return new RegExp(`${LEFT_BOUNDARY}${escapeRegExp(keyword)}${RIGHT_BOUNDARY}`, normalizedFlags);
|
|
23
|
+
}
|
package/src/modes/orchestrate.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import orchestrateNotice from "../prompts/system/orchestrate-notice.md" with { type: "text" };
|
|
2
2
|
import { createGradientHighlighter, type KeywordHighlighter } from "./gradient-highlight";
|
|
3
|
+
import { magicKeywordRegex } from "./magic-keyword-boundary";
|
|
3
4
|
import { keywordInProse } from "./markdown-prose";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -8,21 +9,21 @@ import { keywordInProse } from "./markdown-prose";
|
|
|
8
9
|
* Typing the standalone word in the input editor paints it with a cool
|
|
9
10
|
* teal→violet gradient ({@link highlightOrchestrate}); submitting a message that
|
|
10
11
|
* mentions it appends a hidden {@link ORCHESTRATE_NOTICE} that switches the model
|
|
11
|
-
* into multi-agent orchestration mode. Matching is
|
|
12
|
+
* into multi-agent orchestration mode. Matching is prose-delimited and
|
|
12
13
|
* case-sensitive (lowercase only), so "orchestrated", "Orchestrate", or a path
|
|
13
14
|
* like "orchestrate.ts" never trigger either behavior. Replaces the former
|
|
14
15
|
* `/orchestrate` slash command.
|
|
15
16
|
*/
|
|
16
17
|
|
|
17
|
-
// Detection: lowercase keyword flanked by whitespace or a string edge.
|
|
18
|
-
const ORCHESTRATE_WORD =
|
|
18
|
+
// Detection: lowercase keyword flanked by prose punctuation, whitespace, or a string edge.
|
|
19
|
+
const ORCHESTRATE_WORD = magicKeywordRegex("orchestrate");
|
|
19
20
|
|
|
20
21
|
/** Hidden system notice appended after a user message that mentions "orchestrate". */
|
|
21
22
|
export const ORCHESTRATE_NOTICE: string = orchestrateNotice.trim();
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Whether `text` contains the standalone keyword "orchestrate" (lowercase,
|
|
25
|
-
*
|
|
26
|
+
* prose-delimited) in prose — never inside a code block, inline code span,
|
|
26
27
|
* or XML/HTML section.
|
|
27
28
|
*/
|
|
28
29
|
export function containsOrchestrate(text: string): boolean {
|
|
@@ -36,7 +37,7 @@ export function containsOrchestrate(text: string): boolean {
|
|
|
36
37
|
*/
|
|
37
38
|
export const highlightOrchestrate: KeywordHighlighter = createGradientHighlighter({
|
|
38
39
|
probe: /orchestrate/,
|
|
39
|
-
highlight:
|
|
40
|
+
highlight: magicKeywordRegex("orchestrate", "g"),
|
|
40
41
|
stops: 14,
|
|
41
42
|
hue: t => 150 + t * 130,
|
|
42
43
|
});
|
package/src/modes/print-mode.ts
CHANGED
|
@@ -111,13 +111,22 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
111
111
|
}
|
|
112
112
|
});
|
|
113
113
|
|
|
114
|
+
let wroteTextWorkingIndicator = false;
|
|
115
|
+
const writeTextWorkingIndicator = (): void => {
|
|
116
|
+
if (mode !== "text" || wroteTextWorkingIndicator) return;
|
|
117
|
+
process.stderr.write("Working...\n");
|
|
118
|
+
wroteTextWorkingIndicator = true;
|
|
119
|
+
};
|
|
120
|
+
|
|
114
121
|
// Send initial message with attachments
|
|
115
122
|
if (initialMessage !== undefined) {
|
|
123
|
+
writeTextWorkingIndicator();
|
|
116
124
|
await logger.time("print:prompt:initial", () => session.prompt(initialMessage, { images: initialImages }));
|
|
117
125
|
}
|
|
118
126
|
|
|
119
127
|
// Send remaining messages
|
|
120
128
|
for (const message of messages) {
|
|
129
|
+
writeTextWorkingIndicator();
|
|
121
130
|
await logger.time("print:prompt:next", () => session.prompt(message));
|
|
122
131
|
}
|
|
123
132
|
|
|
@@ -156,7 +156,12 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
|
|
|
156
156
|
const commandName = commandText.slice(1, spaceIndex);
|
|
157
157
|
const command = this.#commands.find(cmd => cmd.name === commandName || cmd.aliases?.includes(commandName));
|
|
158
158
|
if (command && (!("allowArgs" in command) || command.allowArgs !== false)) {
|
|
159
|
-
|
|
159
|
+
const argumentSuggestions = await this.#baseProvider.getSuggestions(lines, cursorLine, cursorCol);
|
|
160
|
+
if (argumentSuggestions) return argumentSuggestions;
|
|
161
|
+
// No slash-argument completion for this input: fall through to
|
|
162
|
+
// internal-url completion only. `#` prompt-action tokens stay
|
|
163
|
+
// literal text inside slash command arguments.
|
|
164
|
+
return getInternalUrlSuggestions(textBeforeCursor, this.#basePath);
|
|
160
165
|
}
|
|
161
166
|
}
|
|
162
167
|
|
|
@@ -75,6 +75,7 @@ export class RpcHostToolBridge {
|
|
|
75
75
|
#output: RpcHostToolOutput;
|
|
76
76
|
#definitions = new Map<string, RpcHostToolDefinition>();
|
|
77
77
|
#pendingCalls = new Map<string, PendingHostToolCall>();
|
|
78
|
+
#closedError: Error | undefined;
|
|
78
79
|
|
|
79
80
|
constructor(output: RpcHostToolOutput) {
|
|
80
81
|
this.#output = output;
|
|
@@ -126,6 +127,10 @@ export class RpcHostToolBridge {
|
|
|
126
127
|
return Promise.reject(new Error(`Host tool "${definition.name}" was aborted`));
|
|
127
128
|
}
|
|
128
129
|
|
|
130
|
+
if (this.#closedError) {
|
|
131
|
+
return Promise.reject(this.#closedError);
|
|
132
|
+
}
|
|
133
|
+
|
|
129
134
|
const id = Snowflake.next() as string;
|
|
130
135
|
const { promise, resolve, reject } = Promise.withResolvers<AgentToolResult<unknown>>();
|
|
131
136
|
let settled = false;
|
|
@@ -183,4 +188,14 @@ export class RpcHostToolBridge {
|
|
|
183
188
|
pending.reject(error);
|
|
184
189
|
}
|
|
185
190
|
}
|
|
191
|
+
|
|
192
|
+
/** Reject active and future host tool requests after the RPC client disconnects. */
|
|
193
|
+
close(message: string): void {
|
|
194
|
+
if (!this.#closedError) this.#closedError = new Error(message);
|
|
195
|
+
const pendingCalls = Array.from(this.#pendingCalls.values());
|
|
196
|
+
this.#pendingCalls.clear();
|
|
197
|
+
for (const pending of pendingCalls) {
|
|
198
|
+
pending.reject(this.#closedError);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
186
201
|
}
|
|
@@ -59,6 +59,29 @@ export type PendingExtensionRequest = {
|
|
|
59
59
|
reject: (error: Error) => void;
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
+
/** Pending extension UI request map that can fail closed when the RPC client disconnects. */
|
|
63
|
+
export class RpcPendingExtensionRequests extends Map<string, PendingExtensionRequest> {
|
|
64
|
+
#closedError: Error | undefined;
|
|
65
|
+
|
|
66
|
+
override set(id: string, request: PendingExtensionRequest): this {
|
|
67
|
+
if (this.#closedError) {
|
|
68
|
+
request.reject(this.#closedError);
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
return super.set(id, request);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Reject every active and future extension UI request. */
|
|
75
|
+
rejectAll(message: string): void {
|
|
76
|
+
if (!this.#closedError) this.#closedError = new Error(message);
|
|
77
|
+
const requests = Array.from(this.values());
|
|
78
|
+
this.clear();
|
|
79
|
+
for (const request of requests) {
|
|
80
|
+
request.reject(this.#closedError);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
62
85
|
type RpcOutput = (
|
|
63
86
|
obj:
|
|
64
87
|
| RpcResponse
|
|
@@ -246,45 +269,49 @@ function isRpcExtensionUIResponse(value: unknown): value is RpcExtensionUIRespon
|
|
|
246
269
|
return value.type === "extension_ui_response" && typeof value.id === "string";
|
|
247
270
|
}
|
|
248
271
|
|
|
249
|
-
/**
|
|
250
|
-
|
|
251
|
-
*
|
|
252
|
-
* Bash commands are dispatched in the background so the caller (the stdin loop
|
|
253
|
-
* in {@link runRpcMode}) can keep reading subsequent frames while a shell
|
|
254
|
-
* command is still running. This lets a client send `abort_bash` (or any other
|
|
255
|
-
* command) while a long-running `bash` is in flight. Response correlation is
|
|
256
|
-
* preserved via each command's `id`; ordering across concurrent commands is
|
|
257
|
-
* not guaranteed and clients MUST match on `id`.
|
|
258
|
-
*
|
|
259
|
-
* @returns `undefined` when the frame was routed to a side-channel handler
|
|
260
|
-
* (extension UI response, host tool/URI frames) or dispatched in the
|
|
261
|
-
* background (`bash`). Otherwise a promise that resolves once the response
|
|
262
|
-
* for the command has been emitted via `output`. Errors from `handleCommand`
|
|
263
|
-
* on non-`bash` commands propagate; the caller is expected to wrap them.
|
|
264
|
-
*/
|
|
265
|
-
export function dispatchRpcInputFrame(parsed: unknown, deps: RpcInputFrameDeps): Promise<void> | undefined {
|
|
266
|
-
// Side-channel: extension UI responses resolve a pending dialog promise.
|
|
272
|
+
/** Dispatch side-channel frames that must overtake the serialized command queue. */
|
|
273
|
+
export function dispatchRpcControlFrame(parsed: unknown, deps: RpcInputFrameDeps): boolean {
|
|
267
274
|
if (isRpcExtensionUIResponse(parsed)) {
|
|
268
275
|
const pending = deps.pendingExtensionRequests.get(parsed.id);
|
|
269
276
|
if (pending) pending.resolve(parsed);
|
|
270
|
-
return
|
|
277
|
+
return true;
|
|
271
278
|
}
|
|
272
279
|
|
|
273
280
|
if (isRpcHostToolResult(parsed)) {
|
|
274
281
|
deps.onHostToolResult(parsed);
|
|
275
|
-
return
|
|
282
|
+
return true;
|
|
276
283
|
}
|
|
277
284
|
|
|
278
285
|
if (isRpcHostToolUpdate(parsed)) {
|
|
279
286
|
deps.onHostToolUpdate(parsed);
|
|
280
|
-
return
|
|
287
|
+
return true;
|
|
281
288
|
}
|
|
282
289
|
|
|
283
290
|
if (isRpcHostUriResult(parsed)) {
|
|
284
291
|
deps.onHostUriResult(parsed);
|
|
285
|
-
return
|
|
292
|
+
return true;
|
|
286
293
|
}
|
|
287
294
|
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Dispatch a single parsed frame from the RPC input stream.
|
|
300
|
+
*
|
|
301
|
+
* Bash commands are dispatched in the background so the caller can keep reading
|
|
302
|
+
* subsequent frames while a shell command is still running. This lets a client
|
|
303
|
+
* send `abort_bash` while a long-running `bash` is in flight. Response
|
|
304
|
+
* correlation is preserved via each command's `id`; ordering across concurrent
|
|
305
|
+
* commands is not guaranteed and clients MUST match on `id`.
|
|
306
|
+
*
|
|
307
|
+
* @returns `undefined` when the frame was routed to a side-channel handler
|
|
308
|
+
* (extension UI response, host tool/URI frames) or dispatched in the
|
|
309
|
+
* background (`bash`). Otherwise a promise that resolves once the response
|
|
310
|
+
* for the command has been emitted via `output`. Errors from `handleCommand`
|
|
311
|
+
* on non-`bash` commands propagate; the caller is expected to wrap them.
|
|
312
|
+
*/
|
|
313
|
+
export function dispatchRpcInputFrame(parsed: unknown, deps: RpcInputFrameDeps): Promise<void> | undefined {
|
|
314
|
+
if (dispatchRpcControlFrame(parsed, deps)) return undefined;
|
|
288
315
|
// Regular RPC command. The transport contract states each remaining frame
|
|
289
316
|
// is an {@link RpcCommand}; `handleCommand`'s `default` arm surfaces
|
|
290
317
|
// unknown discriminants as an error response, so we do not shape-check
|
|
@@ -313,6 +340,64 @@ export function dispatchRpcInputFrame(parsed: unknown, deps: RpcInputFrameDeps):
|
|
|
313
340
|
})();
|
|
314
341
|
}
|
|
315
342
|
|
|
343
|
+
/** Serializes ordinary RPC commands while allowing control frames to dispatch immediately. */
|
|
344
|
+
export class RpcInputDispatcher {
|
|
345
|
+
#tail: Promise<void> = Promise.resolve();
|
|
346
|
+
#tasks = new Set<Promise<void>>();
|
|
347
|
+
readonly #deps: RpcInputFrameDeps;
|
|
348
|
+
readonly #afterSerialCommand: (() => Promise<void>) | undefined;
|
|
349
|
+
|
|
350
|
+
constructor(options: { deps: RpcInputFrameDeps; afterSerialCommand?: () => Promise<void> }) {
|
|
351
|
+
this.#deps = options.deps;
|
|
352
|
+
this.#afterSerialCommand = options.afterSerialCommand;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Accept a parsed input frame without blocking the stdin reader. */
|
|
356
|
+
dispatch(parsed: unknown): void {
|
|
357
|
+
try {
|
|
358
|
+
if (dispatchRpcControlFrame(parsed, this.#deps)) return;
|
|
359
|
+
|
|
360
|
+
const command = parsed as RpcCommand;
|
|
361
|
+
if (command.type === "bash") {
|
|
362
|
+
dispatchRpcInputFrame(command, this.#deps);
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const task = this.#tail.then(
|
|
367
|
+
() => this.#dispatchSerialCommand(command),
|
|
368
|
+
() => this.#dispatchSerialCommand(command),
|
|
369
|
+
);
|
|
370
|
+
this.#tail = task.catch(() => {});
|
|
371
|
+
this.#tasks.add(task);
|
|
372
|
+
void task.finally(() => {
|
|
373
|
+
this.#tasks.delete(task);
|
|
374
|
+
});
|
|
375
|
+
} catch (err: unknown) {
|
|
376
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
377
|
+
this.#deps.output(this.#deps.errorResponse(undefined, "parse", `Failed to parse command: ${message}`));
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Await every accepted serial command, including commands queued before EOF. */
|
|
382
|
+
async drain(): Promise<void> {
|
|
383
|
+
while (this.#tasks.size > 0) {
|
|
384
|
+
await Promise.allSettled(Array.from(this.#tasks));
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
async #dispatchSerialCommand(command: RpcCommand): Promise<void> {
|
|
389
|
+
try {
|
|
390
|
+
const awaited = dispatchRpcInputFrame(command, this.#deps);
|
|
391
|
+
if (awaited) await awaited;
|
|
392
|
+
} catch (err: unknown) {
|
|
393
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
394
|
+
this.#deps.output(this.#deps.errorResponse(command.id, command.type, message));
|
|
395
|
+
} finally {
|
|
396
|
+
await this.#afterSerialCommand?.();
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
316
401
|
/**
|
|
317
402
|
* Coordinates deferred shutdown with in-flight background input tasks.
|
|
318
403
|
*
|
|
@@ -551,7 +636,7 @@ export async function runRpcMode(
|
|
|
551
636
|
|
|
552
637
|
const extensionUserMessageTracker = new RpcExtensionUserMessageTracker();
|
|
553
638
|
|
|
554
|
-
const pendingExtensionRequests = new
|
|
639
|
+
const pendingExtensionRequests = new RpcPendingExtensionRequests();
|
|
555
640
|
const hostToolBridge = new RpcHostToolBridge(output);
|
|
556
641
|
const hostUriBridge = new RpcHostUriBridge(output);
|
|
557
642
|
const subagentRegistry = eventBus ? new RpcSubagentRegistry(eventBus, output) : undefined;
|
|
@@ -1279,35 +1364,25 @@ export async function runRpcMode(
|
|
|
1279
1364
|
onHostUriResult: frame => hostUriBridge.handleResult(frame),
|
|
1280
1365
|
};
|
|
1281
1366
|
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1367
|
+
const inputDispatcher = new RpcInputDispatcher({
|
|
1368
|
+
deps: dispatchFrameDeps,
|
|
1369
|
+
afterSerialCommand: () => shutdownCoordinator.checkShutdownRequested(),
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
// Keep the stdin reader moving: side-channel frames dispatch immediately,
|
|
1373
|
+
// ordinary commands serialize through inputDispatcher, and bash remains
|
|
1374
|
+
// background-dispatched so abort_bash can overtake it.
|
|
1285
1375
|
for await (const parsed of readJsonl(Bun.stdin.stream())) {
|
|
1286
|
-
|
|
1287
|
-
const awaited = dispatchRpcInputFrame(parsed, dispatchFrameDeps);
|
|
1288
|
-
if (awaited) {
|
|
1289
|
-
await awaited;
|
|
1290
|
-
// Check for deferred shutdown request (idle between commands).
|
|
1291
|
-
// Background-dispatched bash frames skip this check so a later
|
|
1292
|
-
// abort_bash can still be read; the coordinator re-checks when
|
|
1293
|
-
// each tracked task settles, so a shutdown requested mid-bash
|
|
1294
|
-
// fires once the response frame is written even if no further
|
|
1295
|
-
// client frames arrive.
|
|
1296
|
-
await shutdownCoordinator.checkShutdownRequested();
|
|
1297
|
-
}
|
|
1298
|
-
} catch (e: unknown) {
|
|
1299
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
1300
|
-
output(error(undefined, "parse", `Failed to parse command: ${message}`));
|
|
1301
|
-
}
|
|
1376
|
+
inputDispatcher.dispatch(parsed);
|
|
1302
1377
|
}
|
|
1303
1378
|
|
|
1304
|
-
//
|
|
1305
|
-
//
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
// stdin closed — RPC client is gone, exit cleanly
|
|
1309
|
-
hostToolBridge.rejectAllPending("RPC client disconnected before host tool execution completed");
|
|
1379
|
+
// stdin closed — RPC client is gone. Fail pending side-channel requests
|
|
1380
|
+
// first so active/queued commands can settle, then drain accepted work.
|
|
1381
|
+
pendingExtensionRequests.rejectAll("RPC client disconnected before extension UI response completed");
|
|
1382
|
+
hostToolBridge.close("RPC client disconnected before host tool execution completed");
|
|
1310
1383
|
hostUriBridge.clear("RPC client disconnected before host URI request completed");
|
|
1384
|
+
await inputDispatcher.drain();
|
|
1385
|
+
await shutdownCoordinator.drain();
|
|
1311
1386
|
subagentRegistry?.dispose();
|
|
1312
1387
|
process.exit(0);
|
|
1313
1388
|
}
|
package/src/modes/types.ts
CHANGED
|
@@ -368,7 +368,7 @@ export interface InteractiveModeContext {
|
|
|
368
368
|
showProviderSetup(): Promise<void>;
|
|
369
369
|
showHookConfirm(title: string, message: string): Promise<boolean>;
|
|
370
370
|
showDebugSelector(): Promise<void>;
|
|
371
|
-
showAgentHub(options?: { requireContent?: boolean }): void;
|
|
371
|
+
showAgentHub(options?: { requireContent?: boolean; armCloseTap?: boolean }): void;
|
|
372
372
|
resetObserverRegistry(): void;
|
|
373
373
|
|
|
374
374
|
// Input handling
|
package/src/modes/ultrathink.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import ultrathinkNotice from "../prompts/system/ultrathink-notice.md" with { type: "text" };
|
|
2
2
|
import { createGradientHighlighter, type KeywordHighlighter } from "./gradient-highlight";
|
|
3
|
+
import { magicKeywordRegex } from "./magic-keyword-boundary";
|
|
3
4
|
import { keywordInProse } from "./markdown-prose";
|
|
4
5
|
|
|
5
6
|
/**
|
|
@@ -8,20 +9,20 @@ import { keywordInProse } from "./markdown-prose";
|
|
|
8
9
|
* Typing the standalone word in the input editor paints it with a rainbow
|
|
9
10
|
* gradient ({@link highlightUltrathink}); submitting a message that mentions it
|
|
10
11
|
* appends a hidden {@link ULTRATHINK_NOTICE} nudging the model toward careful
|
|
11
|
-
* multi-step reasoning. Matching is
|
|
12
|
+
* multi-step reasoning. Matching is prose-delimited and case-sensitive
|
|
12
13
|
* (lowercase only), so "ultrathinking", "Ultrathink", or "ultrathink.ts" never
|
|
13
14
|
* trigger either behavior.
|
|
14
15
|
*/
|
|
15
16
|
|
|
16
|
-
// Detection: lowercase keyword flanked by whitespace or a string edge.
|
|
17
|
-
const ULTRATHINK_WORD =
|
|
17
|
+
// Detection: lowercase keyword flanked by prose punctuation, whitespace, or a string edge.
|
|
18
|
+
const ULTRATHINK_WORD = magicKeywordRegex("ultrathink");
|
|
18
19
|
|
|
19
20
|
/** Hidden system notice appended after a user message that mentions "ultrathink". */
|
|
20
21
|
export const ULTRATHINK_NOTICE: string = ultrathinkNotice.trim();
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* Whether `text` contains the standalone keyword "ultrathink" (lowercase,
|
|
24
|
-
*
|
|
25
|
+
* prose-delimited) in prose — never inside a code block, inline code span,
|
|
25
26
|
* or XML/HTML section.
|
|
26
27
|
*/
|
|
27
28
|
export function containsUltrathink(text: string): boolean {
|
|
@@ -35,7 +36,7 @@ export function containsUltrathink(text: string): boolean {
|
|
|
35
36
|
*/
|
|
36
37
|
export const highlightUltrathink: KeywordHighlighter = createGradientHighlighter({
|
|
37
38
|
probe: /ultrathink/,
|
|
38
|
-
highlight:
|
|
39
|
+
highlight: magicKeywordRegex("ultrathink", "g"),
|
|
39
40
|
stops: 14,
|
|
40
41
|
hue: t => t * 330,
|
|
41
42
|
});
|
|
@@ -136,6 +136,60 @@ export function assistantHasVisibleContent(message: AssistantAgentMessage): bool
|
|
|
136
136
|
);
|
|
137
137
|
}
|
|
138
138
|
|
|
139
|
+
/**
|
|
140
|
+
* Split mixed assistant turns into visible text before tool execution and
|
|
141
|
+
* visible text segments that must render immediately after the preceding tool.
|
|
142
|
+
* Cursor can return intro text, tool calls, progress text, and the final answer
|
|
143
|
+
* in one assistant message; keeping every text block in the leading assistant
|
|
144
|
+
* block buries post-tool text above tool results in the transcript.
|
|
145
|
+
*/
|
|
146
|
+
export function splitAssistantMessageToolTimeline(message: AssistantAgentMessage): {
|
|
147
|
+
beforeTools: AssistantAgentMessage;
|
|
148
|
+
afterToolCalls: ReadonlyMap<string, AssistantAgentMessage>;
|
|
149
|
+
hasToolCalls: boolean;
|
|
150
|
+
} {
|
|
151
|
+
const beforeTools: AssistantAgentMessage["content"] = [];
|
|
152
|
+
const afterToolCalls = new Map<string, AssistantAgentMessage>();
|
|
153
|
+
let pendingAfterTool: AssistantAgentMessage["content"] = [];
|
|
154
|
+
let lastToolCallId: string | undefined;
|
|
155
|
+
let sawToolCall = false;
|
|
156
|
+
|
|
157
|
+
const displaySegment = (content: AssistantAgentMessage["content"]): AssistantAgentMessage => ({
|
|
158
|
+
...message,
|
|
159
|
+
content,
|
|
160
|
+
stopReason: "stop",
|
|
161
|
+
errorMessage: undefined,
|
|
162
|
+
retryRecovery: undefined,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const flushPendingAfterTool = () => {
|
|
166
|
+
if (!lastToolCallId || pendingAfterTool.length === 0) return;
|
|
167
|
+
afterToolCalls.set(lastToolCallId, displaySegment(pendingAfterTool));
|
|
168
|
+
pendingAfterTool = [];
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
for (const content of message.content) {
|
|
172
|
+
if (content.type === "toolCall") {
|
|
173
|
+
flushPendingAfterTool();
|
|
174
|
+
sawToolCall = true;
|
|
175
|
+
lastToolCallId = content.id;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (sawToolCall) {
|
|
179
|
+
pendingAfterTool.push(content);
|
|
180
|
+
} else {
|
|
181
|
+
beforeTools.push(content);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
flushPendingAfterTool();
|
|
185
|
+
|
|
186
|
+
if (!sawToolCall) {
|
|
187
|
+
return { beforeTools: message, afterToolCalls, hasToolCalls: false };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return { beforeTools: displaySegment(beforeTools), afterToolCalls, hasToolCalls: true };
|
|
191
|
+
}
|
|
192
|
+
|
|
139
193
|
/**
|
|
140
194
|
* Normalize raw tool-call arguments to a plain record, collapsing non-object or
|
|
141
195
|
* array values to an empty object.
|
|
@@ -57,6 +57,7 @@ import {
|
|
|
57
57
|
buildIrcMessageCard,
|
|
58
58
|
normalizeToolArgs,
|
|
59
59
|
resolveAssistantErrorPresentation,
|
|
60
|
+
splitAssistantMessageToolTimeline,
|
|
60
61
|
} from "./transcript-render-helpers";
|
|
61
62
|
|
|
62
63
|
type TextBlock = { type: "text"; text: string };
|
|
@@ -251,7 +252,10 @@ export class UiHelpers {
|
|
|
251
252
|
break;
|
|
252
253
|
}
|
|
253
254
|
case "assistant": {
|
|
254
|
-
const assistantComponent = createAssistantMessageComponent(
|
|
255
|
+
const assistantComponent = createAssistantMessageComponent(
|
|
256
|
+
this.ctx,
|
|
257
|
+
splitAssistantMessageToolTimeline(message).beforeTools,
|
|
258
|
+
);
|
|
255
259
|
this.ctx.chatContainer.addChild(assistantComponent);
|
|
256
260
|
break;
|
|
257
261
|
}
|
|
@@ -357,6 +361,7 @@ export class UiHelpers {
|
|
|
357
361
|
if (message.role !== "toolResult") flushPendingUsage();
|
|
358
362
|
// Assistant messages need special handling for tool calls
|
|
359
363
|
if (message.role === "assistant") {
|
|
364
|
+
const timeline = splitAssistantMessageToolTimeline(message);
|
|
360
365
|
this.ctx.addMessageToChat(message);
|
|
361
366
|
const lastChild = this.ctx.chatContainer.children[this.ctx.chatContainer.children.length - 1];
|
|
362
367
|
const assistantComponent = lastChild instanceof AssistantMessageComponent ? lastChild : undefined;
|
|
@@ -383,6 +388,11 @@ export class UiHelpers {
|
|
|
383
388
|
const errorPresentation = resolveAssistantErrorPresentation(message, this.ctx.viewSession.retryAttempt);
|
|
384
389
|
const hasErrorStop = errorPresentation.kind === "full";
|
|
385
390
|
const errorMessage = hasErrorStop ? errorPresentation.text : null;
|
|
391
|
+
const appendAssistantSegment = (segment: AssistantMessage | undefined) => {
|
|
392
|
+
if (!segment || !assistantHasVisibleContent(segment)) return;
|
|
393
|
+
const component = createAssistantMessageComponent(this.ctx, segment);
|
|
394
|
+
this.ctx.chatContainer.addChild(component);
|
|
395
|
+
};
|
|
386
396
|
|
|
387
397
|
// Render tool call components
|
|
388
398
|
for (const content of message.content) {
|
|
@@ -390,6 +400,7 @@ export class UiHelpers {
|
|
|
390
400
|
continue;
|
|
391
401
|
}
|
|
392
402
|
resolveWaitingPoll(content.name);
|
|
403
|
+
const afterToolSegment = timeline.afterToolCalls.get(content.id);
|
|
393
404
|
|
|
394
405
|
if (
|
|
395
406
|
content.name === "read" &&
|
|
@@ -410,6 +421,19 @@ export class UiHelpers {
|
|
|
410
421
|
false,
|
|
411
422
|
content.id,
|
|
412
423
|
);
|
|
424
|
+
} else if (afterToolSegment) {
|
|
425
|
+
if (!readGroup) {
|
|
426
|
+
readGroup = new ReadToolGroupComponent({
|
|
427
|
+
showContentPreview: this.ctx.settings.get("read.toolResultPreview"),
|
|
428
|
+
});
|
|
429
|
+
readGroup.setExpanded(this.ctx.toolOutputExpanded);
|
|
430
|
+
this.ctx.chatContainer.addChild(readGroup);
|
|
431
|
+
}
|
|
432
|
+
readGroup.updateArgs(content.arguments, content.id);
|
|
433
|
+
this.ctx.pendingTools.set(content.id, readGroup);
|
|
434
|
+
if (assistantComponent) {
|
|
435
|
+
readToolCallAssistantComponents.set(content.id, assistantComponent);
|
|
436
|
+
}
|
|
413
437
|
} else {
|
|
414
438
|
const normalizedArgs = normalizeToolArgs(content.arguments);
|
|
415
439
|
readToolCallArgs.set(content.id, normalizedArgs);
|
|
@@ -417,6 +441,7 @@ export class UiHelpers {
|
|
|
417
441
|
readToolCallAssistantComponents.set(content.id, assistantComponent);
|
|
418
442
|
}
|
|
419
443
|
}
|
|
444
|
+
appendAssistantSegment(afterToolSegment);
|
|
420
445
|
continue;
|
|
421
446
|
}
|
|
422
447
|
|
|
@@ -464,6 +489,7 @@ export class UiHelpers {
|
|
|
464
489
|
} else {
|
|
465
490
|
this.ctx.pendingTools.set(content.id, component);
|
|
466
491
|
}
|
|
492
|
+
appendAssistantSegment(afterToolSegment);
|
|
467
493
|
}
|
|
468
494
|
// Dangling toolCalls (no result on the resolved path — failed or
|
|
469
495
|
// retried turns, results on sibling branches) were stripped by the
|