jeopi-cli 16.4.2 → 16.4.4
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 -0
- package/dist/cli.js +3033 -2991
- package/dist/types/advisor/advise-tool.d.ts +5 -1
- package/dist/types/advisor/config.d.ts +4 -3
- package/dist/types/advisor/runtime.d.ts +15 -0
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/update-cli.d.ts +4 -1
- package/dist/types/cli/usage-error.d.ts +4 -0
- package/dist/types/commit/agentic/agent.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +3 -3
- package/dist/types/config/model-resolver.d.ts +7 -1
- package/dist/types/config/models-config.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +3 -13
- 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/internal-urls/memory-protocol.d.ts +6 -6
- package/dist/types/internal-urls/types.d.ts +7 -0
- package/dist/types/irc/bus.d.ts +4 -0
- package/dist/types/lsp/deferred-diagnostics.d.ts +11 -0
- package/dist/types/modes/components/move-overlay.d.ts +1 -1
- package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
- package/dist/types/modes/components/welcome.d.ts +4 -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 +27 -7
- package/dist/types/modes/ultrathink.d.ts +1 -1
- package/dist/types/modes/utils/transcript-render-helpers.d.ts +9 -0
- package/dist/types/modes/workflow.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +6 -0
- package/dist/types/session/auth-broker-config.d.ts +2 -2
- package/dist/types/tools/bash-skill-urls.d.ts +3 -2
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +3 -1
- package/dist/types/tools/browser/cmux/rpc.d.ts +20 -0
- package/dist/types/tools/browser/launch.d.ts +1 -0
- package/dist/types/tools/browser/run-cancellation.d.ts +17 -2
- package/dist/types/tools/browser/run-output.d.ts +25 -0
- package/dist/types/tools/browser/tab-worker.d.ts +22 -0
- package/dist/types/utils/changelog.d.ts +35 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +13 -0
- package/src/advisor/__tests__/config.test.ts +36 -0
- package/src/advisor/advise-tool.ts +6 -1
- package/src/advisor/config.ts +11 -8
- package/src/advisor/runtime.ts +127 -0
- package/src/autolearn/controller.ts +15 -3
- package/src/cli/args.ts +12 -0
- package/src/cli/flag-tables.ts +21 -7
- package/src/cli/update-cli.ts +63 -13
- package/src/cli/usage-error.ts +7 -0
- package/src/commands/acp.ts +11 -2
- package/src/commands/launch.ts +12 -3
- package/src/commit/agentic/agent.ts +4 -0
- package/src/commit/agentic/index.ts +46 -21
- package/src/config/config-file.ts +30 -12
- package/src/config/model-registry.ts +7 -7
- package/src/config/model-resolver.ts +24 -6
- package/src/config/models-config.ts +1 -1
- package/src/config/settings-schema.ts +3 -15
- package/src/edit/index.ts +10 -76
- package/src/edit/renderer.ts +13 -10
- package/src/eval/__tests__/agent-bridge.test.ts +19 -14
- package/src/eval/__tests__/bridge-timeout.test.ts +60 -0
- package/src/eval/executor-base.ts +84 -7
- package/src/eval/js/shared/rewrite-imports.ts +31 -13
- package/src/eval/py/tool-bridge.ts +13 -14
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/export/ttsr.ts +0 -3
- package/src/extensibility/custom-tools/loader.ts +70 -37
- 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/plugins/legacy-pi-bundled-keys.ts +9 -1
- package/src/extensibility/plugins/legacy-pi-bundled-registry.ts +30 -3
- package/src/extensibility/plugins/manager.ts +21 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/memory-protocol.ts +13 -8
- package/src/internal-urls/skill-protocol.ts +1 -1
- package/src/internal-urls/types.ts +7 -0
- package/src/irc/bus.ts +48 -18
- package/src/lsp/client.ts +7 -1
- package/src/lsp/deferred-diagnostics.ts +66 -0
- package/src/main.ts +13 -13
- package/src/mcp/tool-bridge.ts +57 -6
- package/src/mcp/transports/stdio.test.ts +45 -1
- package/src/mcp/transports/stdio.ts +6 -3
- package/src/modes/acp/acp-agent.ts +73 -2
- package/src/modes/acp/acp-event-mapper.ts +5 -0
- package/src/modes/acp/acp-mode.ts +11 -0
- package/src/modes/components/__tests__/move-overlay.test.ts +16 -1
- package/src/modes/components/advisor-config.ts +15 -7
- package/src/modes/components/chat-transcript-builder.ts +31 -11
- package/src/modes/components/move-overlay.ts +2 -3
- package/src/modes/components/plan-review-overlay.ts +7 -0
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/welcome.ts +13 -14
- package/src/modes/controllers/command-controller.ts +8 -10
- package/src/modes/controllers/event-controller.ts +58 -42
- package/src/modes/controllers/input-controller.ts +9 -6
- package/src/modes/controllers/selector-controller.ts +3 -2
- package/src/modes/controllers/tan-command-controller.ts +40 -1
- package/src/modes/interactive-mode.ts +13 -0
- 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/rpc/host-tools.ts +13 -0
- package/src/modes/rpc/rpc-mode.ts +130 -56
- package/src/modes/ultrathink.ts +6 -5
- package/src/modes/utils/transcript-render-helpers.ts +44 -0
- package/src/modes/utils/ui-helpers.ts +16 -1
- package/src/modes/workflow.ts +5 -4
- package/src/prompts/system/eager-task.md +2 -2
- package/src/prompts/system/tan-context-switch.md +17 -0
- package/src/prompts/tools/browser.md +5 -5
- package/src/sdk.ts +3 -0
- package/src/session/agent-session.ts +267 -36
- package/src/session/auth-broker-config.ts +4 -4
- package/src/session/snapcompact-inline.ts +3 -19
- package/src/session/streaming-output.ts +40 -12
- package/src/slash-commands/builtin-registry.ts +8 -8
- package/src/task/executor.ts +8 -0
- package/src/task/render.ts +51 -44
- package/src/tools/bash-skill-urls.ts +7 -4
- package/src/tools/bash.ts +30 -33
- package/src/tools/browser/cmux/cmux-tab.ts +49 -50
- package/src/tools/browser/cmux/rpc.ts +50 -0
- package/src/tools/browser/launch.ts +8 -4
- package/src/tools/browser/run-cancellation.ts +44 -5
- package/src/tools/browser/run-output.ts +76 -0
- package/src/tools/browser/tab-worker.ts +280 -133
- package/src/tools/eval-render.ts +16 -14
- package/src/tools/glob.ts +20 -6
- package/src/tools/grep.ts +1 -0
- package/src/tools/image-gen.ts +3 -4
- package/src/tools/irc.ts +27 -13
- package/src/tools/path-utils.ts +6 -1
- package/src/tools/read.ts +14 -7
- package/src/tools/write.ts +22 -5
- package/src/tools/yield.ts +29 -1
- package/src/utils/changelog.ts +106 -9
- package/src/utils/git.ts +0 -1
- package/src/utils/title-generator.ts +72 -7
- package/src/web/search/index.ts +14 -6
- package/src/web/search/providers/perplexity.ts +18 -2
- package/dist/types/tools/bash-command-fixup.d.ts +0 -11
- package/src/tools/bash-command-fixup.ts +0 -37
|
@@ -3,12 +3,14 @@ import * as path from "node:path";
|
|
|
3
3
|
import type { AssistantMessage } from "jeopi-ai";
|
|
4
4
|
import { prompt, Snowflake } from "jeopi-utils";
|
|
5
5
|
import backgroundTanDispatchPrompt from "../../prompts/system/background-tan-dispatch.md" with { type: "text" };
|
|
6
|
+
import tanContextSwitchPrompt from "../../prompts/system/tan-context-switch.md" with { type: "text" };
|
|
6
7
|
import { AgentRegistry, MAIN_AGENT_ID } from "../../registry/agent-registry";
|
|
7
8
|
import * as sdk from "../../sdk";
|
|
8
9
|
import type { AgentSession } from "../../session/agent-session";
|
|
9
10
|
import { BACKGROUND_TAN_DISPATCH_MESSAGE_TYPE } from "../../session/messages";
|
|
10
11
|
import { SessionManager } from "../../session/session-manager";
|
|
11
12
|
import { createMCPProxyTools, createSubagentSettings } from "../../task/executor";
|
|
13
|
+
import { USER_TODO_EDIT_CUSTOM_TYPE } from "../../tools/todo";
|
|
12
14
|
import type { InteractiveModeContext } from "../types";
|
|
13
15
|
|
|
14
16
|
const TAN_LABEL_PREVIEW_LENGTH = 80;
|
|
@@ -66,6 +68,11 @@ export class TanCommandController {
|
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
const parentSessionId = session.sessionId;
|
|
71
|
+
// Providers route on `promptCacheKey ?? sessionId`, so the parent's live
|
|
72
|
+
// requests may cache under a pinned key that differs from its session id
|
|
73
|
+
// (the parent being itself a fork/tan). Mirror exactly what the parent
|
|
74
|
+
// populated the cache under — same rule as advisor and handoff calls.
|
|
75
|
+
const parentPromptCacheKey = session.agent.promptCacheKey ?? parentSessionId;
|
|
69
76
|
const thinkingLevel = session.configuredThinkingLevel();
|
|
70
77
|
const systemPrompt = [...session.systemPrompt];
|
|
71
78
|
const toolNames = session.getActiveToolNames();
|
|
@@ -111,7 +118,7 @@ export class TanCommandController {
|
|
|
111
118
|
systemPrompt,
|
|
112
119
|
toolNames,
|
|
113
120
|
providerSessionId: `${parentSessionId}:tan:${Snowflake.next()}`,
|
|
114
|
-
providerPromptCacheKey:
|
|
121
|
+
providerPromptCacheKey: parentPromptCacheKey,
|
|
115
122
|
modelRegistry,
|
|
116
123
|
authStorage: modelRegistry.authStorage,
|
|
117
124
|
settings,
|
|
@@ -127,19 +134,51 @@ export class TanCommandController {
|
|
|
127
134
|
disableExtensionDiscovery: true,
|
|
128
135
|
});
|
|
129
136
|
clone = created.session;
|
|
137
|
+
clone.sessionManager.appendSessionInit({
|
|
138
|
+
systemPrompt: clone.systemPrompt.join("\n\n"),
|
|
139
|
+
task: trimmedWork,
|
|
140
|
+
tools: clone.getActiveToolNames(),
|
|
141
|
+
});
|
|
130
142
|
const abortClone = () => {
|
|
131
143
|
void clone?.abort();
|
|
132
144
|
};
|
|
133
145
|
signal.addEventListener("abort", abortClone, { once: true });
|
|
146
|
+
// The fork inherits the parent's todo list via session entries;
|
|
147
|
+
// its reminders would drag the tan back onto the parent's task.
|
|
148
|
+
// Clear runtime state and persist an empty edit so reloads agree.
|
|
149
|
+
clone.setTodoPhases([]);
|
|
150
|
+
cloneManager.appendCustomEntry(USER_TODO_EDIT_CUSTOM_TYPE, { phases: [] });
|
|
151
|
+
const injectContextSwitch = () => {
|
|
152
|
+
clone?.agent.appendMessage({
|
|
153
|
+
role: "developer",
|
|
154
|
+
content: tanContextSwitchPrompt,
|
|
155
|
+
attribution: "agent",
|
|
156
|
+
timestamp: Date.now(),
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
// Compaction summarizes the fork notice away with the rest of the
|
|
160
|
+
// history, after which the clone re-adopts the parent's task as its
|
|
161
|
+
// own (the summary blends both). Re-inject after every successful
|
|
162
|
+
// compaction so the fork boundary survives summarization.
|
|
163
|
+
const unsubscribeCompaction = clone.subscribe(event => {
|
|
164
|
+
if (event.type === "auto_compaction_end" && event.result && !event.aborted) {
|
|
165
|
+
injectContextSwitch();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
134
168
|
try {
|
|
135
169
|
if (signal.aborted) {
|
|
136
170
|
abortClone();
|
|
137
171
|
throw new Error("Aborted before execution");
|
|
138
172
|
}
|
|
173
|
+
// Inject a context-switch developer message so the clone knows
|
|
174
|
+
// it is a tangential fork — its parent owns the prior conversation;
|
|
175
|
+
// this agent must focus exclusively on the user's request.
|
|
176
|
+
injectContextSwitch();
|
|
139
177
|
await clone.prompt(trimmedWork, { attribution: "user" });
|
|
140
178
|
await clone.waitForIdle();
|
|
141
179
|
return extractAssistantText(clone.getLastAssistantMessage()) || "(no output)";
|
|
142
180
|
} finally {
|
|
181
|
+
unsubscribeCompaction();
|
|
143
182
|
signal.removeEventListener("abort", abortClone);
|
|
144
183
|
}
|
|
145
184
|
} finally {
|
|
@@ -118,6 +118,7 @@ import { formatPhaseDisplayName, todoMatchesAnyDescription } from "../tools/todo
|
|
|
118
118
|
import { ToolError } from "../tools/tool-errors";
|
|
119
119
|
import { vocalizer } from "../tts/vocalizer";
|
|
120
120
|
import { renderTreeList } from "../tui/tree-list";
|
|
121
|
+
import { copyToClipboard } from "../utils/clipboard";
|
|
121
122
|
import type { EventBus } from "../utils/event-bus";
|
|
122
123
|
import { getEditorCommand, openInEditor } from "../utils/external-editor";
|
|
123
124
|
import { getSessionAccentAnsi, getSessionAccentHex } from "../utils/session-color";
|
|
@@ -2391,6 +2392,7 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2391
2392
|
{
|
|
2392
2393
|
onPick: choice => finish(choice),
|
|
2393
2394
|
onCancel: () => finish(undefined),
|
|
2395
|
+
onCopyPlan: content => void this.#copyPlanToClipboard(content),
|
|
2394
2396
|
onExternalEditor: dialogOptions?.onExternalEditor,
|
|
2395
2397
|
onAnnotationExternalEditor: (draft, commit) => void this.#openPlanAnnotationInExternalEditor(draft, commit),
|
|
2396
2398
|
onPlanEdited: dialogOptions?.onPlanEdited,
|
|
@@ -2457,6 +2459,17 @@ export class InteractiveMode implements InteractiveModeContext {
|
|
|
2457
2459
|
return contextUsage !== undefined && contextUsage.percent > PLAN_KEEP_CONTEXT_DISABLE_THRESHOLD_PERCENT;
|
|
2458
2460
|
}
|
|
2459
2461
|
|
|
2462
|
+
async #copyPlanToClipboard(content: string): Promise<void> {
|
|
2463
|
+
try {
|
|
2464
|
+
await copyToClipboard(content);
|
|
2465
|
+
this.showStatus("Copied plan to clipboard");
|
|
2466
|
+
} catch (error) {
|
|
2467
|
+
this.showWarning(
|
|
2468
|
+
`Failed to copy plan to clipboard: ${error instanceof Error ? error.message : String(error)}`,
|
|
2469
|
+
);
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2460
2473
|
async #openPlanInExternalEditor(planFilePath: string): Promise<void> {
|
|
2461
2474
|
const editorCmd = getEditorCommand();
|
|
2462
2475
|
if (!editorCmd) {
|
|
@@ -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
|
@@ -62,13 +62,22 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
|
|
|
62
62
|
}
|
|
63
63
|
});
|
|
64
64
|
|
|
65
|
+
let wroteTextWorkingIndicator = false;
|
|
66
|
+
const writeTextWorkingIndicator = (): void => {
|
|
67
|
+
if (mode !== "text" || wroteTextWorkingIndicator) return;
|
|
68
|
+
process.stderr.write("Working...\n");
|
|
69
|
+
wroteTextWorkingIndicator = true;
|
|
70
|
+
};
|
|
71
|
+
|
|
65
72
|
// Send initial message with attachments
|
|
66
73
|
if (initialMessage !== undefined) {
|
|
74
|
+
writeTextWorkingIndicator();
|
|
67
75
|
await logger.time("print:prompt:initial", () => session.prompt(initialMessage, { images: initialImages }));
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
// Send remaining messages
|
|
71
79
|
for (const message of messages) {
|
|
80
|
+
writeTextWorkingIndicator();
|
|
72
81
|
await logger.time("print:prompt:next", () => session.prompt(message));
|
|
73
82
|
}
|
|
74
83
|
|
|
@@ -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;
|
|
@@ -125,6 +126,9 @@ export class RpcHostToolBridge {
|
|
|
125
126
|
if (signal?.aborted) {
|
|
126
127
|
return Promise.reject(new Error(`Host tool "${definition.name}" was aborted`));
|
|
127
128
|
}
|
|
129
|
+
if (this.#closedError) {
|
|
130
|
+
return Promise.reject(this.#closedError);
|
|
131
|
+
}
|
|
128
132
|
|
|
129
133
|
const id = Snowflake.next() as string;
|
|
130
134
|
const { promise, resolve, reject } = Promise.withResolvers<AgentToolResult<unknown>>();
|
|
@@ -183,4 +187,13 @@ export class RpcHostToolBridge {
|
|
|
183
187
|
pending.reject(error);
|
|
184
188
|
}
|
|
185
189
|
}
|
|
190
|
+
/** Reject active and future host tool requests after the RPC client disconnects. */
|
|
191
|
+
close(message: string): void {
|
|
192
|
+
if (!this.#closedError) this.#closedError = new Error(message);
|
|
193
|
+
const pendingCalls = Array.from(this.#pendingCalls.values());
|
|
194
|
+
this.#pendingCalls.clear();
|
|
195
|
+
for (const pending of pendingCalls) {
|
|
196
|
+
pending.reject(this.#closedError);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
186
199
|
}
|
|
@@ -58,6 +58,28 @@ export type PendingExtensionRequest = {
|
|
|
58
58
|
resolve: (response: RpcExtensionUIResponse) => void;
|
|
59
59
|
reject: (error: Error) => void;
|
|
60
60
|
};
|
|
61
|
+
/** Pending extension UI request map that can fail closed when the RPC client disconnects. */
|
|
62
|
+
export class RpcPendingExtensionRequests extends Map<string, PendingExtensionRequest> {
|
|
63
|
+
#closedError: Error | undefined;
|
|
64
|
+
|
|
65
|
+
override set(id: string, request: PendingExtensionRequest): this {
|
|
66
|
+
if (this.#closedError) {
|
|
67
|
+
request.reject(this.#closedError);
|
|
68
|
+
return this;
|
|
69
|
+
}
|
|
70
|
+
return super.set(id, request);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Reject every active and future extension UI request. */
|
|
74
|
+
rejectAll(message: string): void {
|
|
75
|
+
if (!this.#closedError) this.#closedError = new Error(message);
|
|
76
|
+
const requests = Array.from(this.values());
|
|
77
|
+
this.clear();
|
|
78
|
+
for (const request of requests) {
|
|
79
|
+
request.reject(this.#closedError);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
61
83
|
|
|
62
84
|
type RpcOutput = (
|
|
63
85
|
obj:
|
|
@@ -88,6 +110,7 @@ export type RpcSkillCommandResult = { agentInvoked: true };
|
|
|
88
110
|
export async function tryRunRpcSkillCommand(
|
|
89
111
|
session: RpcSkillCommandSession,
|
|
90
112
|
text: string,
|
|
113
|
+
streamingBehavior: "steer" | "followUp" = "steer",
|
|
91
114
|
): Promise<RpcSkillCommandResult | false> {
|
|
92
115
|
if (!session.skillsSettings?.enableSkillCommands) return false;
|
|
93
116
|
const parsed = parseSkillInvocation(text);
|
|
@@ -95,13 +118,16 @@ export async function tryRunRpcSkillCommand(
|
|
|
95
118
|
const skill = session.skills.find(candidate => candidate.name === parsed.name);
|
|
96
119
|
if (!skill) return false;
|
|
97
120
|
const built = await buildSkillPromptMessage(skill, parsed.args, "user");
|
|
98
|
-
await session.promptCustomMessage(
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
121
|
+
await session.promptCustomMessage(
|
|
122
|
+
{
|
|
123
|
+
customType: SKILL_PROMPT_MESSAGE_TYPE,
|
|
124
|
+
content: built.message,
|
|
125
|
+
display: true,
|
|
126
|
+
details: built.details,
|
|
127
|
+
attribution: "user",
|
|
128
|
+
},
|
|
129
|
+
{ streamingBehavior },
|
|
130
|
+
);
|
|
105
131
|
return { agentInvoked: true };
|
|
106
132
|
}
|
|
107
133
|
|
|
@@ -242,45 +268,50 @@ function isRpcExtensionUIResponse(value: unknown): value is RpcExtensionUIRespon
|
|
|
242
268
|
return value.type === "extension_ui_response" && typeof value.id === "string";
|
|
243
269
|
}
|
|
244
270
|
|
|
245
|
-
/**
|
|
246
|
-
|
|
247
|
-
*
|
|
248
|
-
* Bash commands are dispatched in the background so the caller (the stdin loop
|
|
249
|
-
* in {@link runRpcMode}) can keep reading subsequent frames while a shell
|
|
250
|
-
* command is still running. This lets a client send `abort_bash` (or any other
|
|
251
|
-
* command) while a long-running `bash` is in flight. Response correlation is
|
|
252
|
-
* preserved via each command's `id`; ordering across concurrent commands is
|
|
253
|
-
* not guaranteed and clients MUST match on `id`.
|
|
254
|
-
*
|
|
255
|
-
* @returns `undefined` when the frame was routed to a side-channel handler
|
|
256
|
-
* (extension UI response, host tool/URI frames) or dispatched in the
|
|
257
|
-
* background (`bash`). Otherwise a promise that resolves once the response
|
|
258
|
-
* for the command has been emitted via `output`. Errors from `handleCommand`
|
|
259
|
-
* on non-`bash` commands propagate; the caller is expected to wrap them.
|
|
260
|
-
*/
|
|
261
|
-
export function dispatchRpcInputFrame(parsed: unknown, deps: RpcInputFrameDeps): Promise<void> | undefined {
|
|
262
|
-
// Side-channel: extension UI responses resolve a pending dialog promise.
|
|
271
|
+
/** Dispatch side-channel frames that must overtake the serialized command queue. */
|
|
272
|
+
export function dispatchRpcControlFrame(parsed: unknown, deps: RpcInputFrameDeps): boolean {
|
|
263
273
|
if (isRpcExtensionUIResponse(parsed)) {
|
|
264
274
|
const pending = deps.pendingExtensionRequests.get(parsed.id);
|
|
265
275
|
if (pending) pending.resolve(parsed);
|
|
266
|
-
return
|
|
276
|
+
return true;
|
|
267
277
|
}
|
|
268
278
|
|
|
269
279
|
if (isRpcHostToolResult(parsed)) {
|
|
270
280
|
deps.onHostToolResult(parsed);
|
|
271
|
-
return
|
|
281
|
+
return true;
|
|
272
282
|
}
|
|
273
283
|
|
|
274
284
|
if (isRpcHostToolUpdate(parsed)) {
|
|
275
285
|
deps.onHostToolUpdate(parsed);
|
|
276
|
-
return
|
|
286
|
+
return true;
|
|
277
287
|
}
|
|
278
288
|
|
|
279
289
|
if (isRpcHostUriResult(parsed)) {
|
|
280
290
|
deps.onHostUriResult(parsed);
|
|
281
|
-
return
|
|
291
|
+
return true;
|
|
282
292
|
}
|
|
283
293
|
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Dispatch a single parsed frame from the RPC input stream.
|
|
299
|
+
*
|
|
300
|
+
* Bash commands are dispatched in the background so the caller can keep reading
|
|
301
|
+
* subsequent frames while a shell command is still running. This lets a client
|
|
302
|
+
* send `abort_bash` while a long-running `bash` is in flight. Response
|
|
303
|
+
* correlation is preserved via each command's `id`; ordering across concurrent
|
|
304
|
+
* commands is not guaranteed and clients MUST match on `id`.
|
|
305
|
+
*
|
|
306
|
+
* @returns `undefined` when the frame was routed to a side-channel handler
|
|
307
|
+
* (extension UI response, host tool/URI frames) or dispatched in the
|
|
308
|
+
* background (`bash`). Otherwise a promise that resolves once the response
|
|
309
|
+
* for the command has been emitted via `output`. Errors from `handleCommand`
|
|
310
|
+
* on non-`bash` commands propagate; the caller is expected to wrap them.
|
|
311
|
+
*/
|
|
312
|
+
export function dispatchRpcInputFrame(parsed: unknown, deps: RpcInputFrameDeps): Promise<void> | undefined {
|
|
313
|
+
if (dispatchRpcControlFrame(parsed, deps)) return undefined;
|
|
314
|
+
|
|
284
315
|
// Regular RPC command. The transport contract states each remaining frame
|
|
285
316
|
// is an {@link RpcCommand}; `handleCommand`'s `default` arm surfaces
|
|
286
317
|
// unknown discriminants as an error response, so we do not shape-check
|
|
@@ -309,6 +340,59 @@ export function dispatchRpcInputFrame(parsed: unknown, deps: RpcInputFrameDeps):
|
|
|
309
340
|
})();
|
|
310
341
|
}
|
|
311
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
|
+
if (dispatchRpcControlFrame(parsed, this.#deps)) return;
|
|
358
|
+
|
|
359
|
+
const command = parsed as RpcCommand;
|
|
360
|
+
if (command.type === "bash") {
|
|
361
|
+
dispatchRpcInputFrame(command, this.#deps);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const task = this.#tail.then(
|
|
366
|
+
() => this.#dispatchSerialCommand(command),
|
|
367
|
+
() => this.#dispatchSerialCommand(command),
|
|
368
|
+
);
|
|
369
|
+
this.#tail = task.catch(() => {});
|
|
370
|
+
this.#tasks.add(task);
|
|
371
|
+
void task.finally(() => {
|
|
372
|
+
this.#tasks.delete(task);
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Await every accepted serial command, including commands queued before EOF. */
|
|
377
|
+
async drain(): Promise<void> {
|
|
378
|
+
while (this.#tasks.size > 0) {
|
|
379
|
+
await Promise.allSettled(Array.from(this.#tasks));
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
async #dispatchSerialCommand(command: RpcCommand): Promise<void> {
|
|
384
|
+
try {
|
|
385
|
+
const awaited = dispatchRpcInputFrame(command, this.#deps);
|
|
386
|
+
if (awaited) await awaited;
|
|
387
|
+
} catch (err: unknown) {
|
|
388
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
389
|
+
this.#deps.output(this.#deps.errorResponse(command.id, command.type, message));
|
|
390
|
+
} finally {
|
|
391
|
+
await this.#afterSerialCommand?.();
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
312
396
|
/**
|
|
313
397
|
* Coordinates deferred shutdown with in-flight background input tasks.
|
|
314
398
|
*
|
|
@@ -547,7 +631,7 @@ export async function runRpcMode(
|
|
|
547
631
|
|
|
548
632
|
const extensionUserMessageTracker = new RpcExtensionUserMessageTracker();
|
|
549
633
|
|
|
550
|
-
const pendingExtensionRequests = new
|
|
634
|
+
const pendingExtensionRequests = new RpcPendingExtensionRequests();
|
|
551
635
|
const hostToolBridge = new RpcHostToolBridge(output);
|
|
552
636
|
const hostUriBridge = new RpcHostUriBridge(output);
|
|
553
637
|
const subagentRegistry = eventBus ? new RpcSubagentRegistry(eventBus, output) : undefined;
|
|
@@ -842,7 +926,7 @@ export async function runRpcMode(
|
|
|
842
926
|
// =================================================================
|
|
843
927
|
|
|
844
928
|
case "prompt": {
|
|
845
|
-
const skillResult = await tryRunRpcSkillCommand(session, command.message);
|
|
929
|
+
const skillResult = await tryRunRpcSkillCommand(session, command.message, command.streamingBehavior);
|
|
846
930
|
if (skillResult) {
|
|
847
931
|
return success(id, "prompt", skillResult);
|
|
848
932
|
}
|
|
@@ -1276,35 +1360,25 @@ export async function runRpcMode(
|
|
|
1276
1360
|
onHostUriResult: frame => hostUriBridge.handleResult(frame),
|
|
1277
1361
|
};
|
|
1278
1362
|
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1363
|
+
const inputDispatcher = new RpcInputDispatcher({
|
|
1364
|
+
deps: dispatchFrameDeps,
|
|
1365
|
+
afterSerialCommand: () => shutdownCoordinator.checkShutdownRequested(),
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
// Keep the stdin reader moving: side-channel frames dispatch immediately,
|
|
1369
|
+
// ordinary commands serialize through inputDispatcher, and bash remains
|
|
1370
|
+
// background-dispatched so abort_bash can overtake it.
|
|
1282
1371
|
for await (const parsed of readJsonl(Bun.stdin.stream())) {
|
|
1283
|
-
|
|
1284
|
-
const awaited = dispatchRpcInputFrame(parsed, dispatchFrameDeps);
|
|
1285
|
-
if (awaited) {
|
|
1286
|
-
await awaited;
|
|
1287
|
-
// Check for deferred shutdown request (idle between commands).
|
|
1288
|
-
// Background-dispatched bash frames skip this check so a later
|
|
1289
|
-
// abort_bash can still be read; the coordinator re-checks when
|
|
1290
|
-
// each tracked task settles, so a shutdown requested mid-bash
|
|
1291
|
-
// fires once the response frame is written even if no further
|
|
1292
|
-
// client frames arrive.
|
|
1293
|
-
await shutdownCoordinator.checkShutdownRequested();
|
|
1294
|
-
}
|
|
1295
|
-
} catch (e: unknown) {
|
|
1296
|
-
const message = e instanceof Error ? e.message : String(e);
|
|
1297
|
-
output(error(undefined, "parse", `Failed to parse command: ${message}`));
|
|
1298
|
-
}
|
|
1372
|
+
inputDispatcher.dispatch(parsed);
|
|
1299
1373
|
}
|
|
1300
1374
|
|
|
1301
|
-
//
|
|
1302
|
-
//
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
// stdin closed — RPC client is gone, exit cleanly
|
|
1306
|
-
hostToolBridge.rejectAllPending("RPC client disconnected before host tool execution completed");
|
|
1375
|
+
// stdin closed — RPC client is gone. Fail pending side-channel requests
|
|
1376
|
+
// first so active/queued commands can settle, then drain accepted work.
|
|
1377
|
+
pendingExtensionRequests.rejectAll("RPC client disconnected before extension UI response completed");
|
|
1378
|
+
hostToolBridge.close("RPC client disconnected before host tool execution completed");
|
|
1307
1379
|
hostUriBridge.clear("RPC client disconnected before host URI request completed");
|
|
1380
|
+
await inputDispatcher.drain();
|
|
1381
|
+
await shutdownCoordinator.drain();
|
|
1308
1382
|
subagentRegistry?.dispose();
|
|
1309
1383
|
process.exit(0);
|
|
1310
1384
|
}
|
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
|
});
|
|
@@ -130,6 +130,50 @@ export function assistantHasVisibleContent(message: AssistantAgentMessage): bool
|
|
|
130
130
|
);
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Split mixed assistant turns into visible text before tool execution and
|
|
135
|
+
* visible text segments that must render immediately after the preceding tool.
|
|
136
|
+
*/
|
|
137
|
+
export function splitAssistantMessageToolTimeline(message: AssistantAgentMessage): {
|
|
138
|
+
beforeTools: AssistantAgentMessage;
|
|
139
|
+
afterToolCalls: ReadonlyMap<string, AssistantAgentMessage>;
|
|
140
|
+
hasToolCalls: boolean;
|
|
141
|
+
} {
|
|
142
|
+
const beforeTools: AssistantAgentMessage["content"] = [];
|
|
143
|
+
const afterToolCalls = new Map<string, AssistantAgentMessage>();
|
|
144
|
+
let pendingAfterTool: AssistantAgentMessage["content"] = [];
|
|
145
|
+
let lastToolCallId: string | undefined;
|
|
146
|
+
let sawToolCall = false;
|
|
147
|
+
|
|
148
|
+
const displaySegment = (content: AssistantAgentMessage["content"]): AssistantAgentMessage => ({
|
|
149
|
+
...message,
|
|
150
|
+
content,
|
|
151
|
+
stopReason: "stop",
|
|
152
|
+
errorMessage: undefined,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const flushPendingAfterTool = () => {
|
|
156
|
+
if (!lastToolCallId || pendingAfterTool.length === 0) return;
|
|
157
|
+
afterToolCalls.set(lastToolCallId, displaySegment(pendingAfterTool));
|
|
158
|
+
pendingAfterTool = [];
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
for (const content of message.content) {
|
|
162
|
+
if (content.type === "toolCall") {
|
|
163
|
+
flushPendingAfterTool();
|
|
164
|
+
sawToolCall = true;
|
|
165
|
+
lastToolCallId = content.id;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (sawToolCall) pendingAfterTool.push(content);
|
|
169
|
+
else beforeTools.push(content);
|
|
170
|
+
}
|
|
171
|
+
flushPendingAfterTool();
|
|
172
|
+
|
|
173
|
+
if (!sawToolCall) return { beforeTools: message, afterToolCalls, hasToolCalls: false };
|
|
174
|
+
return { beforeTools: displaySegment(beforeTools), afterToolCalls, hasToolCalls: true };
|
|
175
|
+
}
|
|
176
|
+
|
|
133
177
|
/**
|
|
134
178
|
* Normalize raw tool-call arguments to a plain record, collapsing non-object or
|
|
135
179
|
* array values to an empty object.
|
|
@@ -56,6 +56,7 @@ import {
|
|
|
56
56
|
buildIrcMessageCard,
|
|
57
57
|
normalizeToolArgs,
|
|
58
58
|
resolveAssistantErrorMessage,
|
|
59
|
+
splitAssistantMessageToolTimeline,
|
|
59
60
|
} from "./transcript-render-helpers";
|
|
60
61
|
|
|
61
62
|
type TextBlock = { type: "text"; text: string };
|
|
@@ -350,7 +351,8 @@ export class UiHelpers {
|
|
|
350
351
|
if (message.role !== "toolResult") flushPendingUsage();
|
|
351
352
|
// Assistant messages need special handling for tool calls
|
|
352
353
|
if (message.role === "assistant") {
|
|
353
|
-
|
|
354
|
+
const timeline = splitAssistantMessageToolTimeline(message);
|
|
355
|
+
this.ctx.addMessageToChat(timeline.beforeTools);
|
|
354
356
|
const lastChild = this.ctx.chatContainer.children[this.ctx.chatContainer.children.length - 1];
|
|
355
357
|
const assistantComponent = lastChild instanceof AssistantMessageComponent ? lastChild : undefined;
|
|
356
358
|
if (assistantComponent) {
|
|
@@ -384,6 +386,7 @@ export class UiHelpers {
|
|
|
384
386
|
continue;
|
|
385
387
|
}
|
|
386
388
|
resolveWaitingPoll(content.name);
|
|
389
|
+
const afterToolSegment = timeline.afterToolCalls.get(content.id);
|
|
387
390
|
|
|
388
391
|
if (
|
|
389
392
|
content.name === "read" &&
|
|
@@ -404,6 +407,16 @@ export class UiHelpers {
|
|
|
404
407
|
false,
|
|
405
408
|
content.id,
|
|
406
409
|
);
|
|
410
|
+
} else if (afterToolSegment) {
|
|
411
|
+
if (!readGroup) {
|
|
412
|
+
readGroup = new ReadToolGroupComponent({
|
|
413
|
+
showContentPreview: this.ctx.settings.get("read.toolResultPreview"),
|
|
414
|
+
});
|
|
415
|
+
readGroup.setExpanded(this.ctx.toolOutputExpanded);
|
|
416
|
+
this.ctx.chatContainer.addChild(readGroup);
|
|
417
|
+
}
|
|
418
|
+
readGroup.updateArgs(content.arguments, content.id);
|
|
419
|
+
this.ctx.pendingTools.set(content.id, readGroup);
|
|
407
420
|
} else {
|
|
408
421
|
const normalizedArgs = normalizeToolArgs(content.arguments);
|
|
409
422
|
readToolCallArgs.set(content.id, normalizedArgs);
|
|
@@ -411,6 +424,7 @@ export class UiHelpers {
|
|
|
411
424
|
readToolCallAssistantComponents.set(content.id, assistantComponent);
|
|
412
425
|
}
|
|
413
426
|
}
|
|
427
|
+
if (afterToolSegment) this.ctx.addMessageToChat(afterToolSegment);
|
|
414
428
|
continue;
|
|
415
429
|
}
|
|
416
430
|
|
|
@@ -458,6 +472,7 @@ export class UiHelpers {
|
|
|
458
472
|
} else {
|
|
459
473
|
this.ctx.pendingTools.set(content.id, component);
|
|
460
474
|
}
|
|
475
|
+
if (afterToolSegment) this.ctx.addMessageToChat(afterToolSegment);
|
|
461
476
|
}
|
|
462
477
|
pendingUsage = this.ctx.settings.get("display.showTokenUsage") ? message.usage : undefined;
|
|
463
478
|
pendingUsageDuration = message.duration;
|