mixdog 0.8.1 → 0.9.1
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/README.md +47 -23
- package/package.json +34 -28
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +205 -33
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1222 -90
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -29
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +242 -98
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2211 -446
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +108 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
- package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +149 -51
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +0 -1
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
- package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -6
- package/src/runtime/channels/lib/tool-format.mjs +0 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +519 -118
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
- package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
- package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
- package/src/runtime/memory/lib/session-ingest.mjs +196 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +104 -473
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +44 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +139 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -95
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +1 -78
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4800 -2140
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +14 -4
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +428 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +597 -362
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +499 -0
- package/src/tui/components/tool-output-format.test.mjs +312 -0
- package/src/tui/dist/index.mjs +9192 -2399
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +671 -69
- package/src/tui/markdown/format-token.test.mjs +312 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +756 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +2 -13
- package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/ink.js +54 -8
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- package/src/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
package/src/tui/engine.mjs
CHANGED
|
@@ -8,11 +8,17 @@
|
|
|
8
8
|
import { performance } from 'node:perf_hooks';
|
|
9
9
|
import { SPINNER_VERBS } from './spinner-verbs.mjs';
|
|
10
10
|
import {
|
|
11
|
+
aggregateToolCategoryEntry,
|
|
11
12
|
classifyToolCategory,
|
|
12
13
|
formatAggregateDetail,
|
|
13
14
|
summarizeToolResult,
|
|
14
15
|
} from '../runtime/shared/tool-surface.mjs';
|
|
15
|
-
import { presentErrorText } from '../runtime/shared/err-text.mjs';
|
|
16
|
+
import { isBackgroundErrorOnlyBody, presentErrorText } from '../runtime/shared/err-text.mjs';
|
|
17
|
+
import {
|
|
18
|
+
isModelVisibleToolCompletionWrapper,
|
|
19
|
+
modelVisibleToolCompletionMessage,
|
|
20
|
+
} from '../runtime/shared/tool-execution-contract.mjs';
|
|
21
|
+
import { listThemes, getThemeSetting, setThemeSetting } from './theme.mjs';
|
|
16
22
|
|
|
17
23
|
const BOOT_PROFILE_ENABLED = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_BOOT_PROFILE || ''));
|
|
18
24
|
const BOOT_PROFILE_START = globalThis.__mixdogBootProfileStart || (globalThis.__mixdogBootProfileStart = performance.now());
|
|
@@ -75,6 +81,11 @@ const SESSION_RUNTIME_MODULE = import.meta.url.replace(/\\/g, '/').includes('/tu
|
|
|
75
81
|
? '../../mixdog-session-runtime.mjs'
|
|
76
82
|
: '../mixdog-session-runtime.mjs';
|
|
77
83
|
|
|
84
|
+
const TOOL_APPROVAL_TIMEOUT_MS = (() => {
|
|
85
|
+
const value = Number(process.env.MIXDOG_TOOL_APPROVAL_TIMEOUT_MS);
|
|
86
|
+
return Number.isFinite(value) && value > 0 ? Math.max(1000, Math.round(value)) : 120_000;
|
|
87
|
+
})();
|
|
88
|
+
|
|
78
89
|
let _idSeq = 0;
|
|
79
90
|
const nextId = () => `it_${++_idSeq}`;
|
|
80
91
|
|
|
@@ -97,21 +108,47 @@ function pickDoneVerb(turn) {
|
|
|
97
108
|
return TURN_DONE_VERBS[(turn * 5 + 2) % TURN_DONE_VERBS.length];
|
|
98
109
|
}
|
|
99
110
|
|
|
100
|
-
function
|
|
111
|
+
function formatElapsedSeconds(ms) {
|
|
101
112
|
const value = Math.max(0, Number(ms) || 0);
|
|
102
|
-
if (value
|
|
103
|
-
|
|
104
|
-
if (value < 1_000) return '';
|
|
105
|
-
return `${Math.floor(value / 1000)}s`;
|
|
113
|
+
if (value <= 0) return '0s';
|
|
114
|
+
return `${Math.max(1, Math.ceil(value / 1000))}s`;
|
|
106
115
|
}
|
|
107
116
|
|
|
108
|
-
function
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
if (
|
|
112
|
-
if (
|
|
113
|
-
if (
|
|
114
|
-
return
|
|
117
|
+
function compactEventLabel(event = {}) {
|
|
118
|
+
const status = String(event.status || '').toLowerCase();
|
|
119
|
+
const reactive = String(event.trigger || '').toLowerCase() === 'reactive';
|
|
120
|
+
if (status === 'failed') return reactive ? 'Compact failed (overflow retry)' : 'Compact failed';
|
|
121
|
+
if (status === 'skipped') return 'Compact skipped';
|
|
122
|
+
if (status === 'no_change') return 'Compact checked';
|
|
123
|
+
return reactive ? 'Compact complete (overflow recovery)' : 'Compact complete';
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function compactEventDetail(event = {}) {
|
|
127
|
+
// Keep the elapsed time as the lead detail, but no longer discard the rest of
|
|
128
|
+
// the compact metadata. Surface type/trigger and the boundary/pressure so the
|
|
129
|
+
// statusdone marker reflects what actually fired.
|
|
130
|
+
const parts = [];
|
|
131
|
+
const elapsed = formatElapsedSeconds(Number(event.durationMs ?? event.elapsedMs ?? 0));
|
|
132
|
+
if (elapsed) parts.push(elapsed);
|
|
133
|
+
const type = String(event.compactType || event.type || '').trim();
|
|
134
|
+
if (type && type !== 'semantic') parts.push(type);
|
|
135
|
+
const trigger = String(event.trigger || '').toLowerCase();
|
|
136
|
+
if (trigger === 'reactive') parts.push('reactive');
|
|
137
|
+
else if (trigger === 'manual') parts.push('manual');
|
|
138
|
+
const before = Number(event.beforeTokens ?? event.pressureTokens ?? 0);
|
|
139
|
+
const after = Number(event.afterTokens ?? 0);
|
|
140
|
+
const fmtTok = (n) => {
|
|
141
|
+
const v = Number(n) || 0;
|
|
142
|
+
if (v >= 1000) return `${(v / 1000).toFixed(v >= 10_000 ? 0 : 1)}k`;
|
|
143
|
+
return `${Math.round(v)}`;
|
|
144
|
+
};
|
|
145
|
+
if (before > 0 && after > 0 && after !== before) parts.push(`${fmtTok(before)}→${fmtTok(after)}`);
|
|
146
|
+
return parts.join(' · ');
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function projectNameFromPath(value) {
|
|
150
|
+
const text = String(value || '').replace(/[\\/]+$/, '');
|
|
151
|
+
return text.split(/[\\/]/).pop() || text || '(current)';
|
|
115
152
|
}
|
|
116
153
|
|
|
117
154
|
const FAILED_NOTICE_ACTIONS = new Map([
|
|
@@ -119,7 +156,7 @@ const FAILED_NOTICE_ACTIONS = new Map([
|
|
|
119
156
|
['auth-forget', 'forget auth'],
|
|
120
157
|
['auto-clear', 'update auto-clear'],
|
|
121
158
|
['autoclear', 'update auto-clear'],
|
|
122
|
-
['
|
|
159
|
+
['agent', 'run agent command'],
|
|
123
160
|
['channels', 'load channels'],
|
|
124
161
|
['channels update', 'update channels'],
|
|
125
162
|
['clear', 'clear chat'],
|
|
@@ -226,23 +263,110 @@ function polishNoticeText(text) {
|
|
|
226
263
|
function toolResultText(content) {
|
|
227
264
|
if (content == null) return '';
|
|
228
265
|
if (typeof content === 'string') return content;
|
|
229
|
-
const parts = Array.isArray(content)
|
|
230
|
-
? content
|
|
231
|
-
: (content && typeof content === 'object' && Array.isArray(content.content) ? content.content : null);
|
|
232
|
-
if (parts) {
|
|
233
|
-
return parts.map((c) => {
|
|
234
|
-
if (typeof c === 'string') return c;
|
|
235
|
-
if (c?.type === 'image') return `[image: ${c.mimeType || c.mediaType || c.source?.media_type || 'image'}]`;
|
|
236
|
-
return c?.text ?? '';
|
|
237
|
-
}).filter(Boolean).join('\n');
|
|
238
|
-
}
|
|
239
266
|
if (Array.isArray(content)) {
|
|
240
|
-
return content.map((c) => (
|
|
267
|
+
return content.map((c) => toolResultPartText(c)).filter((t) => t !== '').join('\n');
|
|
268
|
+
}
|
|
269
|
+
if (typeof content === 'object') {
|
|
270
|
+
if (Array.isArray(content.content)) {
|
|
271
|
+
const nested = content.content.map((c) => toolResultPartText(c)).filter((t) => t !== '').join('\n');
|
|
272
|
+
if (nested) return nested;
|
|
273
|
+
} else if (content.content != null && typeof content.content === 'object') {
|
|
274
|
+
const nested = toolResultPartText(content.content);
|
|
275
|
+
if (nested) return nested;
|
|
276
|
+
}
|
|
277
|
+
if (Array.isArray(content.parts)) {
|
|
278
|
+
const nested = content.parts.map((c) => toolResultPartText(c)).filter((t) => t !== '').join('\n');
|
|
279
|
+
if (nested) return nested;
|
|
280
|
+
}
|
|
281
|
+
const fromPart = toolResultPartText(content);
|
|
282
|
+
if (fromPart) return fromPart;
|
|
283
|
+
if (content?.type === 'tool_result') return '';
|
|
284
|
+
if (typeof content.text === 'string') return content.text;
|
|
285
|
+
if (typeof content.content === 'string') return content.content;
|
|
241
286
|
}
|
|
242
|
-
if (typeof content === 'object' && typeof content.text === 'string') return content.text;
|
|
243
287
|
try { return JSON.stringify(content); } catch { return String(content); }
|
|
244
288
|
}
|
|
245
289
|
|
|
290
|
+
const TOOL_RESULT_PART_MAX_DEPTH = 12;
|
|
291
|
+
const TOOL_RESULT_JSON_FALLBACK_MAX = 480;
|
|
292
|
+
|
|
293
|
+
function compactToolResultObjectFallback(obj) {
|
|
294
|
+
if (obj?.type === 'tool_result') return '';
|
|
295
|
+
try {
|
|
296
|
+
const json = JSON.stringify(obj);
|
|
297
|
+
if (!json || json === '{}') return '';
|
|
298
|
+
if (json.length <= TOOL_RESULT_JSON_FALLBACK_MAX) return json;
|
|
299
|
+
return `${json.slice(0, TOOL_RESULT_JSON_FALLBACK_MAX - 1)}…`;
|
|
300
|
+
} catch {
|
|
301
|
+
return String(obj);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function toolResultPartText(part, depth = 0) {
|
|
306
|
+
if (part == null) return '';
|
|
307
|
+
if (depth > TOOL_RESULT_PART_MAX_DEPTH) return '';
|
|
308
|
+
if (typeof part === 'string') return part;
|
|
309
|
+
if (part?.type === 'image' || part?.type === 'input_image') {
|
|
310
|
+
return `[image: ${part.mimeType || part.mediaType || part.source?.media_type || 'image'}]`;
|
|
311
|
+
}
|
|
312
|
+
if (part?.type === 'tool_result') {
|
|
313
|
+
const inner = part.content;
|
|
314
|
+
if (typeof inner === 'string') return inner;
|
|
315
|
+
if (Array.isArray(inner)) {
|
|
316
|
+
return inner.map((c) => toolResultPartText(c, depth + 1)).filter((t) => t !== '').join('\n');
|
|
317
|
+
}
|
|
318
|
+
if (inner != null && typeof inner === 'object') {
|
|
319
|
+
return toolResultPartText(inner, depth + 1);
|
|
320
|
+
}
|
|
321
|
+
return '';
|
|
322
|
+
}
|
|
323
|
+
if (part?.type === 'text' || part?.type === 'output_text' || part?.type === 'input_text') {
|
|
324
|
+
return part.text ?? '';
|
|
325
|
+
}
|
|
326
|
+
if (Array.isArray(part)) {
|
|
327
|
+
return part.map((c) => toolResultPartText(c, depth + 1)).filter((t) => t !== '').join('\n');
|
|
328
|
+
}
|
|
329
|
+
if (typeof part === 'object') {
|
|
330
|
+
if (Array.isArray(part.content)) {
|
|
331
|
+
const nested = part.content.map((c) => toolResultPartText(c, depth + 1)).filter((t) => t !== '').join('\n');
|
|
332
|
+
if (nested) return nested;
|
|
333
|
+
}
|
|
334
|
+
if (part.content != null && typeof part.content === 'object') {
|
|
335
|
+
const nested = toolResultPartText(part.content, depth + 1);
|
|
336
|
+
if (nested) return nested;
|
|
337
|
+
}
|
|
338
|
+
if (Array.isArray(part.parts)) {
|
|
339
|
+
const nested = part.parts.map((c) => toolResultPartText(c, depth + 1)).filter((t) => t !== '').join('\n');
|
|
340
|
+
if (nested) return nested;
|
|
341
|
+
}
|
|
342
|
+
if (typeof part.text === 'string' && part.text) return part.text;
|
|
343
|
+
if (typeof part.output === 'string' && part.output) return part.output;
|
|
344
|
+
if (typeof part.message === 'string' && part.message) return part.message;
|
|
345
|
+
if (typeof part.content === 'string') return part.content;
|
|
346
|
+
if (part.source?.type === 'base64' && part.source?.data) {
|
|
347
|
+
return `[image: ${part.source.media_type || part.source.mediaType || 'base64'}]`;
|
|
348
|
+
}
|
|
349
|
+
return compactToolResultObjectFallback(part);
|
|
350
|
+
}
|
|
351
|
+
return '';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function toolAggregateDetailFallback(detailText, rawResult) {
|
|
355
|
+
if (String(detailText || '').trim()) return detailText;
|
|
356
|
+
const raw = String(rawResult || '').replace(/\s+$/, '').trim();
|
|
357
|
+
if (!raw) return detailText;
|
|
358
|
+
const line = raw.split('\n').map((l) => l.trim()).find(Boolean) || '';
|
|
359
|
+
if (!line) return detailText;
|
|
360
|
+
return line.length > 160 ? `${line.slice(0, 157)}…` : line;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function toolGroupedDisplayFallback(resultText, text, rawText) {
|
|
364
|
+
if (String(resultText || '').trim()) return resultText;
|
|
365
|
+
const body = String(text || rawText || '').trim();
|
|
366
|
+
if (body) return text || rawText;
|
|
367
|
+
return resultText;
|
|
368
|
+
}
|
|
369
|
+
|
|
246
370
|
function toolErrorDisplay(value, surface = 'tool') {
|
|
247
371
|
const text = presentErrorText(value, { surface });
|
|
248
372
|
if (/^(?:Search failed|Fetch failed|No first response|The .+ went stale|(?:Web search agent|Agent|Tool) (?:stopped|was cancelled))/i.test(text)) {
|
|
@@ -284,7 +408,7 @@ function stripSyntheticAgentTags(text) {
|
|
|
284
408
|
const taskResult = textBetweenTag(value, 'result');
|
|
285
409
|
if (taskResult) return taskResult;
|
|
286
410
|
return value
|
|
287
|
-
.replace(/^
|
|
411
|
+
.replace(/^agent result[^\n]*(?:\n|$)/i, '')
|
|
288
412
|
.replace(/<\/?(?:final-answer|task-notification|task-id|tool-use-id|output-file|result|status|summary|usage|total_tokens|tool_uses|duration_ms|worktree|worktreePath|worktreeBranch)[^>]*>/gi, '')
|
|
289
413
|
.trim();
|
|
290
414
|
}
|
|
@@ -300,7 +424,7 @@ function splitBridgeEnvelope(text) {
|
|
|
300
424
|
};
|
|
301
425
|
}
|
|
302
426
|
|
|
303
|
-
function
|
|
427
|
+
function agentJobStatusText(parsed) {
|
|
304
428
|
if (!parsed) return '';
|
|
305
429
|
const parts = [];
|
|
306
430
|
if (parsed.status) parts.push(`status: ${parsed.status}`);
|
|
@@ -308,19 +432,55 @@ function bridgeJobStatusText(parsed) {
|
|
|
308
432
|
return parts.join(' · ');
|
|
309
433
|
}
|
|
310
434
|
|
|
311
|
-
function
|
|
435
|
+
function agentJobResultText(text, parsed = parseAgentJob(text)) {
|
|
312
436
|
const value = String(text ?? '').trim();
|
|
313
437
|
if (!value) return '';
|
|
314
438
|
if (parsed?.taskId) {
|
|
315
439
|
const { body } = splitBridgeEnvelope(value);
|
|
316
440
|
const cleanBody = stripSyntheticAgentTags(body);
|
|
317
441
|
if (cleanBody) return cleanBody;
|
|
318
|
-
return
|
|
442
|
+
return agentJobStatusText(parsed);
|
|
319
443
|
}
|
|
320
444
|
return stripSyntheticAgentTags(value) || value;
|
|
321
445
|
}
|
|
322
446
|
|
|
323
|
-
function
|
|
447
|
+
function parseAgentResultEnvelope(text, fallback = {}) {
|
|
448
|
+
const value = String(text ?? '').trim();
|
|
449
|
+
if (!/^agent result\b/i.test(value)) return null;
|
|
450
|
+
const [head = '', ...restLines] = value.split('\n');
|
|
451
|
+
const body = stripSyntheticAgentTags(restLines.join('\n'));
|
|
452
|
+
const attrs = {};
|
|
453
|
+
const attrRe = /([a-zA-Z][\w-]*)=("[^"]*"|'[^']*'|\S+)/g;
|
|
454
|
+
let match;
|
|
455
|
+
while ((match = attrRe.exec(head))) {
|
|
456
|
+
attrs[match[1].toLowerCase()] = String(match[2] || '').replace(/^["']|["']$/g, '');
|
|
457
|
+
}
|
|
458
|
+
const providerModel = /\s([a-zA-Z0-9_.-]+)\/([^\s]+)\s*$/i.exec(head);
|
|
459
|
+
const role = attrs.agent || attrs.role || fallback.role || fallback.agent || '';
|
|
460
|
+
return {
|
|
461
|
+
name: 'agent',
|
|
462
|
+
label: String(fallback.status || attrs.status || 'completed').toLowerCase(),
|
|
463
|
+
args: {
|
|
464
|
+
type: 'result',
|
|
465
|
+
status: fallback.status || attrs.status || 'completed',
|
|
466
|
+
task_id: fallback.taskId || attrs.task_id || attrs.taskid || undefined,
|
|
467
|
+
tag: fallback.tag || attrs.tag || undefined,
|
|
468
|
+
agent: role || undefined,
|
|
469
|
+
role: role || undefined,
|
|
470
|
+
provider: fallback.provider || attrs.provider || providerModel?.[1] || undefined,
|
|
471
|
+
model: fallback.model || attrs.model || providerModel?.[2] || undefined,
|
|
472
|
+
preset: fallback.preset || attrs.preset || undefined,
|
|
473
|
+
effort: fallback.effort || attrs.effort || undefined,
|
|
474
|
+
fast: fallback.fast ?? attrs.fast,
|
|
475
|
+
},
|
|
476
|
+
result: body || agentJobStatusText({ status: fallback.status || attrs.status || 'completed', taskId: fallback.taskId || attrs.task_id || attrs.taskid || '' }),
|
|
477
|
+
isError: /^(failed|error|timeout|cancelled|canceled|killed)$/i.test(fallback.status || attrs.status || ''),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
export { toolResultText, toolAggregateDetailFallback, toolGroupedDisplayFallback };
|
|
482
|
+
|
|
483
|
+
export function parseBackgroundTaskEnvelope(text) {
|
|
324
484
|
const value = String(text ?? '').trim();
|
|
325
485
|
if (!/^background task\b/i.test(value)) return null;
|
|
326
486
|
const allLines = value.split('\n');
|
|
@@ -334,9 +494,25 @@ function parseBackgroundTaskEnvelope(text) {
|
|
|
334
494
|
if (match) fields[match[1].toLowerCase()] = match[2].trim();
|
|
335
495
|
}
|
|
336
496
|
const surface = String(fields.surface || fields.operation || 'task').toLowerCase();
|
|
337
|
-
const name = surface === 'explore' || surface === 'search' || surface === 'shell' ? surface : 'task';
|
|
497
|
+
const name = surface === 'explore' || surface === 'search' || surface === 'shell' || surface === 'agent' ? surface : 'task';
|
|
338
498
|
const status = String(fields.status || '').toLowerCase();
|
|
339
499
|
const taskId = fields.task_id || fields.taskid || '';
|
|
500
|
+
const errorText = fields.error || '';
|
|
501
|
+
const agentResult = parseAgentResultEnvelope(body, {
|
|
502
|
+
status,
|
|
503
|
+
taskId,
|
|
504
|
+
tag: fields.tag || fields.label || '',
|
|
505
|
+
role: fields.role || fields.agent || '',
|
|
506
|
+
agent: fields.agent || '',
|
|
507
|
+
provider: fields.provider || '',
|
|
508
|
+
model: fields.model || '',
|
|
509
|
+
preset: fields.preset || '',
|
|
510
|
+
effort: fields.effort || '',
|
|
511
|
+
fast: fields.fast,
|
|
512
|
+
});
|
|
513
|
+
if (agentResult) return { ...agentResult, rawResult: value };
|
|
514
|
+
const errorOnlyBody = isBackgroundErrorOnlyBody(body, errorText);
|
|
515
|
+
const resultBody = body && !errorOnlyBody ? body : '';
|
|
340
516
|
return {
|
|
341
517
|
name,
|
|
342
518
|
label: status || 'notification',
|
|
@@ -345,30 +521,78 @@ function parseBackgroundTaskEnvelope(text) {
|
|
|
345
521
|
status,
|
|
346
522
|
task_id: taskId || undefined,
|
|
347
523
|
surface,
|
|
524
|
+
operation: fields.operation || undefined,
|
|
348
525
|
label: fields.label || undefined,
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
526
|
+
tag: fields.tag || undefined,
|
|
527
|
+
agent: fields.agent || fields.role || undefined,
|
|
528
|
+
role: fields.role || fields.agent || undefined,
|
|
529
|
+
provider: fields.provider || undefined,
|
|
530
|
+
model: fields.model || undefined,
|
|
531
|
+
preset: fields.preset || undefined,
|
|
532
|
+
effort: fields.effort || undefined,
|
|
533
|
+
fast: fields.fast || undefined,
|
|
534
|
+
error: errorText || undefined,
|
|
535
|
+
startedAt: fields.started || fields.startedat || undefined,
|
|
536
|
+
finishedAt: fields.finished || fields.finishedat || undefined,
|
|
537
|
+
},
|
|
538
|
+
result: resultBody || (!errorText ? [status ? `status: ${status}` : '', taskId ? `task_id: ${taskId}` : ''].filter(Boolean).join(' · ') : ''),
|
|
539
|
+
rawResult: value,
|
|
540
|
+
isError: /^(failed|error|timeout|cancelled|canceled|killed)$/i.test(status) || /^error:/i.test(body) || Boolean(errorText),
|
|
352
541
|
};
|
|
353
542
|
}
|
|
354
543
|
|
|
544
|
+
function isStatusOnlyAgentCompletionNotification(text) {
|
|
545
|
+
const background = parseBackgroundTaskEnvelope(text);
|
|
546
|
+
if (background?.name === 'agent' && /^(completed|cancelled|canceled)$/i.test(background.label || '')) {
|
|
547
|
+
return !(hasAgentResponseResultText(background.result) || hasAgentResponseResultText(text));
|
|
548
|
+
}
|
|
549
|
+
const parsed = parseAgentJob(text);
|
|
550
|
+
const result = agentJobResultText(text, parsed);
|
|
551
|
+
if (!parsed?.taskId || !/^(completed|cancelled|canceled)$/i.test(parsed.status || '')) return false;
|
|
552
|
+
return !(hasAgentResponseResultText(result) || hasAgentResponseResultText(text));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function hasAgentResponseResultText(text) {
|
|
556
|
+
const value = String(text || '').trim();
|
|
557
|
+
if (!value) return false;
|
|
558
|
+
if (/^status:\s*(?:running|pending|queued|completed|failed|cancelled|canceled)(?:\s*·\s*task_id:\s*\S+)?$/i.test(value)) return false;
|
|
559
|
+
if (/^(?:background task\b|agent task:|task_id:)/i.test(value) && !/\n\s*\n[\s\S]*\S/.test(value)) return false;
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
|
|
355
563
|
function bracketField(text, name) {
|
|
356
564
|
const re = new RegExp(`^\\[${name}:\\s*([^\\]]*)\\]`, 'mi');
|
|
357
565
|
return re.exec(String(text ?? ''))?.[1]?.trim() || '';
|
|
358
566
|
}
|
|
359
567
|
|
|
568
|
+
function toolResultStatus(text) {
|
|
569
|
+
const value = String(text ?? '');
|
|
570
|
+
const tagged = textBetweenTag(value, 'status');
|
|
571
|
+
if (tagged) return tagged.trim();
|
|
572
|
+
const bracketed = bracketField(value, 'status');
|
|
573
|
+
if (bracketed) return bracketed.trim();
|
|
574
|
+
const inline = /^(?:status|state):\s*([^\s·,;]+)/mi.exec(value);
|
|
575
|
+
return inline ? inline[1].trim() : '';
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function isErrorToolStatus(status) {
|
|
579
|
+
return /^(failed|error|timeout|cancelled|canceled|killed)$/i.test(String(status || '').trim());
|
|
580
|
+
}
|
|
581
|
+
|
|
360
582
|
function parseSyntheticAgentMessage(text) {
|
|
361
583
|
const value = String(text ?? '').trim();
|
|
362
584
|
if (!value) return null;
|
|
363
585
|
const finalAnswer = textBetweenTag(value, 'final-answer');
|
|
364
586
|
if (finalAnswer) {
|
|
365
587
|
return {
|
|
366
|
-
name: '
|
|
588
|
+
name: 'agent',
|
|
367
589
|
label: 'final',
|
|
368
590
|
args: { type: 'read', description: 'agent result' },
|
|
369
591
|
result: finalAnswer,
|
|
370
592
|
};
|
|
371
593
|
}
|
|
594
|
+
const agentResult = parseAgentResultEnvelope(value);
|
|
595
|
+
if (agentResult) return agentResult;
|
|
372
596
|
const backgroundTask = parseBackgroundTaskEnvelope(value);
|
|
373
597
|
if (backgroundTask) return backgroundTask;
|
|
374
598
|
const shellTaskId = bracketField(value, 'task_id');
|
|
@@ -384,15 +608,15 @@ function parseSyntheticAgentMessage(text) {
|
|
|
384
608
|
isError: /^(failed|error|timeout|cancelled|killed)$/i.test(status) || (exit && exit !== '0' && exit !== 'n/a'),
|
|
385
609
|
};
|
|
386
610
|
}
|
|
387
|
-
const
|
|
388
|
-
if (
|
|
389
|
-
const label =
|
|
390
|
-
const result =
|
|
611
|
+
const agentJob = parseAgentJob(value);
|
|
612
|
+
if (agentJob?.taskId) {
|
|
613
|
+
const label = agentJob.status || 'notification';
|
|
614
|
+
const result = agentJobResultText(value, agentJob);
|
|
391
615
|
return {
|
|
392
|
-
name: '
|
|
616
|
+
name: 'agent',
|
|
393
617
|
label,
|
|
394
|
-
args:
|
|
395
|
-
result: result ||
|
|
618
|
+
args: agentArgsWithResultMetadata({ type: agentJob.type || 'notification', description: 'agent notification' }, agentJob),
|
|
619
|
+
result: result || agentJobStatusText(agentJob) || 'agent notification',
|
|
396
620
|
isError: /^(failed|error|timeout|cancelled|killed)$/i.test(label),
|
|
397
621
|
};
|
|
398
622
|
}
|
|
@@ -402,7 +626,7 @@ function parseSyntheticAgentMessage(text) {
|
|
|
402
626
|
const taskId = textBetweenTag(value, 'task-id');
|
|
403
627
|
const result = stripSyntheticAgentTags(value);
|
|
404
628
|
return {
|
|
405
|
-
name: '
|
|
629
|
+
name: 'agent',
|
|
406
630
|
label: status,
|
|
407
631
|
taskId,
|
|
408
632
|
summary,
|
|
@@ -435,9 +659,9 @@ function parseToolArgs(args) {
|
|
|
435
659
|
|
|
436
660
|
const yieldToRenderer = () => new Promise((resolve) => setImmediate(resolve));
|
|
437
661
|
|
|
438
|
-
function
|
|
662
|
+
function parseAgentJob(text) {
|
|
439
663
|
const value = String(text || '');
|
|
440
|
-
const idMatch = /^
|
|
664
|
+
const idMatch = /^agent task:\s*([^\s]+)/m.exec(value) || /^task_id:\s*([^\s]+)/m.exec(value);
|
|
441
665
|
if (!idMatch) return null;
|
|
442
666
|
const statusMatch = /^status:\s*([^\s(]+)/m.exec(value);
|
|
443
667
|
const typeMatch = /^type:\s*(.+)$/m.exec(value);
|
|
@@ -468,20 +692,51 @@ function queuePriorityValue(value) {
|
|
|
468
692
|
}
|
|
469
693
|
|
|
470
694
|
function defaultQueuePriority(mode) {
|
|
471
|
-
|
|
695
|
+
// Queue priority defaults:
|
|
696
|
+
// - user/bashed prompt input defaults to `next`, so it can be attached at the
|
|
697
|
+
// next model-send boundary while a turn is active.
|
|
698
|
+
// - task notifications default to `later`, unless the caller explicitly marks
|
|
699
|
+
// them urgent (e.g. interactive shell stall/completion).
|
|
700
|
+
return mode === 'task-notification' ? 'later' : 'next';
|
|
472
701
|
}
|
|
473
702
|
|
|
474
703
|
function isQueuedEntryEditable(entry) {
|
|
475
|
-
|
|
704
|
+
const mode = entry?.mode || 'prompt';
|
|
705
|
+
return mode !== 'task-notification' && mode !== 'pending-resume';
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function isQueuedEntryVisible(entry) {
|
|
709
|
+
// state.queued drives the user-command wait list above the prompt. Background
|
|
710
|
+
// task completions stay in the internal pending queue, but should never look
|
|
711
|
+
// like commands typed by the user while they wait to be drained.
|
|
712
|
+
const mode = entry?.mode || 'prompt';
|
|
713
|
+
if (mode === 'pending-resume') return false;
|
|
714
|
+
return isQueuedEntryEditable(entry);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function isSlashQueuedEntry(entry) {
|
|
718
|
+
if (entry?.skipSlashCommands) return false;
|
|
719
|
+
const text = promptContentText(entry?.content ?? entry?.text ?? '');
|
|
720
|
+
return text.trim().startsWith('/');
|
|
476
721
|
}
|
|
477
722
|
|
|
478
723
|
function firstQueueLine(text) {
|
|
479
724
|
return String(text || '').split('\n').map((line) => line.trim()).find(Boolean) || '';
|
|
480
725
|
}
|
|
481
726
|
|
|
727
|
+
function shortTextFingerprint(text) {
|
|
728
|
+
const value = String(text || '').trim();
|
|
729
|
+
let hash = 2166136261;
|
|
730
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
731
|
+
hash ^= value.charCodeAt(i);
|
|
732
|
+
hash = Math.imul(hash, 16777619);
|
|
733
|
+
}
|
|
734
|
+
return (hash >>> 0).toString(36);
|
|
735
|
+
}
|
|
736
|
+
|
|
482
737
|
function notificationDisplayText(text) {
|
|
483
|
-
const parsed =
|
|
484
|
-
const result =
|
|
738
|
+
const parsed = parseAgentJob(text);
|
|
739
|
+
const result = agentJobResultText(text, parsed);
|
|
485
740
|
const synthetic = parseSyntheticAgentMessage(text);
|
|
486
741
|
return firstQueueLine(synthetic?.result || result || text) || 'agent notification';
|
|
487
742
|
}
|
|
@@ -499,6 +754,33 @@ function promptContentText(content) {
|
|
|
499
754
|
return String(content ?? '');
|
|
500
755
|
}
|
|
501
756
|
|
|
757
|
+
function timestampMs(value) {
|
|
758
|
+
if (value == null || value === '') return 0;
|
|
759
|
+
const n = Number(value);
|
|
760
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
761
|
+
const parsed = Date.parse(String(value));
|
|
762
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function hasModelVisibleConversation(session) {
|
|
766
|
+
const messages = Array.isArray(session?.messages) ? session.messages : [];
|
|
767
|
+
return messages.some((message) => {
|
|
768
|
+
const role = message?.role;
|
|
769
|
+
if (role !== 'user' && role !== 'assistant' && role !== 'tool') return false;
|
|
770
|
+
const text = promptContentText(message.content).trim();
|
|
771
|
+
if (role === 'user' && text.startsWith('<system-reminder>')) return false;
|
|
772
|
+
if (role === 'assistant' && text === '.' && !Array.isArray(message.toolCalls)) return false;
|
|
773
|
+
return !!text || role === 'assistant' || role === 'tool';
|
|
774
|
+
});
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
function sessionActivityTimestamp(session, fallback = 0) {
|
|
778
|
+
if (!hasModelVisibleConversation(session)) return 0;
|
|
779
|
+
return timestampMs(session?.lastUsedAt)
|
|
780
|
+
|| timestampMs(session?.updatedAt)
|
|
781
|
+
|| timestampMs(fallback);
|
|
782
|
+
}
|
|
783
|
+
|
|
502
784
|
function promptDisplayText(content, options = {}) {
|
|
503
785
|
if (typeof options.displayText === 'string') return options.displayText;
|
|
504
786
|
return promptContentText(content);
|
|
@@ -543,18 +825,76 @@ function callCommitCallbacks(entries) {
|
|
|
543
825
|
|
|
544
826
|
function notificationQueueKey(event, text, parsed) {
|
|
545
827
|
const meta = event?.meta && typeof event.meta === 'object' ? event.meta : {};
|
|
828
|
+
const synthetic = parseSyntheticAgentMessage(text);
|
|
829
|
+
if (synthetic?.name === 'agent' && String(synthetic.args?.type || '').toLowerCase() === 'result') {
|
|
830
|
+
const taskId = String(synthetic.args?.task_id || '').trim();
|
|
831
|
+
const executionId = String(meta.execution_id || '').trim();
|
|
832
|
+
const tag = String(synthetic.args?.tag || '').trim();
|
|
833
|
+
const resultId = taskId
|
|
834
|
+
? `task:${taskId}`
|
|
835
|
+
: executionId
|
|
836
|
+
? `exec:${executionId}`
|
|
837
|
+
: tag
|
|
838
|
+
? `tag:${tag}:${shortTextFingerprint(synthetic.result || text)}`
|
|
839
|
+
: '';
|
|
840
|
+
const role = String(synthetic.args?.agent || synthetic.args?.role || '').trim();
|
|
841
|
+
if (resultId || role) return ['agent-result', resultId, role].filter(Boolean).join(':');
|
|
842
|
+
}
|
|
546
843
|
const id = String(meta.execution_id || parsed?.taskId || '').trim();
|
|
547
844
|
if (!id) return '';
|
|
548
845
|
const type = String(meta.type || '').trim();
|
|
549
846
|
const status = String(meta.status || parsed?.status || '').trim();
|
|
550
847
|
const fallbackKind = String(text || '').split('\n', 1)[0]?.trim() || 'notification';
|
|
551
|
-
|
|
848
|
+
// Distinguish a body-carrying completion from a header-only preview that
|
|
849
|
+
// shares the same id/type/status. An early agent preview can arrive before
|
|
850
|
+
// the session is persisted (no result body); the canonical notification that
|
|
851
|
+
// follows DOES carry the body. Without this dimension the bodyless preview
|
|
852
|
+
// would claim the dedupe key and suppress the real result. A blank-line gap
|
|
853
|
+
// separates the task header block from the result body in the envelope.
|
|
854
|
+
const hasBody = /\n\s*\n[\s\S]*\S/.test(String(text || '')) ? 'b1' : 'b0';
|
|
855
|
+
return [id, type || fallbackKind, status, hasBody].filter(Boolean).join(':');
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/** Pure delivery plan for runtime.onNotification execution envelopes (tests + handler). */
|
|
859
|
+
export function resolveTuiRuntimeNotificationDelivery(event, text) {
|
|
860
|
+
const trimmed = String(text ?? '').trim();
|
|
861
|
+
if (!trimmed) return { action: 'ignore' };
|
|
862
|
+
const parsed = parseAgentJob(trimmed);
|
|
863
|
+
const meta = event?.meta && typeof event.meta === 'object' ? event.meta : {};
|
|
864
|
+
if (!isExecutionNotification(event, trimmed, parsed)) {
|
|
865
|
+
return { action: 'enqueue', displayText: trimmed, modelContent: trimmed };
|
|
866
|
+
}
|
|
867
|
+
if (isStatusOnlyAgentCompletionNotification(trimmed)) {
|
|
868
|
+
return { action: 'status-only', displayText: trimmed, modelContent: '' };
|
|
869
|
+
}
|
|
870
|
+
const modelContent = modelVisibleToolCompletionMessage(trimmed, meta);
|
|
871
|
+
return {
|
|
872
|
+
action: 'execution-ui',
|
|
873
|
+
displayText: trimmed,
|
|
874
|
+
modelContent,
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
function isExecutionNotification(event, text, parsed) {
|
|
879
|
+
const meta = event?.meta && typeof event.meta === 'object' ? event.meta : {};
|
|
880
|
+
if (meta.execution_id || meta.execution_surface) return true;
|
|
881
|
+
if (parseAgentResultEnvelope(text)) return true;
|
|
882
|
+
if (parseBackgroundTaskEnvelope(text)) return true;
|
|
883
|
+
return Boolean(parsed?.taskId && /^(?:agent task:|task_id:)/mi.test(String(text || '')));
|
|
552
884
|
}
|
|
553
885
|
|
|
554
|
-
function
|
|
886
|
+
function agentArgsWithResultMetadata(args, parsed) {
|
|
555
887
|
if (!parsed) return args;
|
|
556
888
|
const next = { ...(args && typeof args === 'object' ? args : {}) };
|
|
557
|
-
|
|
889
|
+
const requestedAction = String(next.type || next.action || next.mode || '').trim().toLowerCase();
|
|
890
|
+
if (parsed.type) {
|
|
891
|
+
// Job status envelopes report the original job type (usually "spawn").
|
|
892
|
+
// Preserve the user's current agent tool action ("status", "read", …) so
|
|
893
|
+
// manual checks render as "Reviewer status" instead of another
|
|
894
|
+
// "Spawning Reviewer" card. Keep the job type as metadata for detail.
|
|
895
|
+
if (!requestedAction || /^(notification|result|completion)$/i.test(requestedAction)) next.type = parsed.type;
|
|
896
|
+
else next.jobType = parsed.type;
|
|
897
|
+
}
|
|
558
898
|
if (parsed.status) next.status = parsed.status;
|
|
559
899
|
if (parsed.taskId) next.task_id = parsed.taskId;
|
|
560
900
|
if (parsed.role) next.role = parsed.role;
|
|
@@ -593,23 +933,22 @@ export async function createEngineSession({
|
|
|
593
933
|
const cwd = runtime.cwd || process.cwd();
|
|
594
934
|
const stateStartedAt = performance.now();
|
|
595
935
|
const autoClearState = () => runtime.getAutoClear?.() || runtime.autoClear || { enabled: true, idleMs: 60 * 60 * 1000 };
|
|
596
|
-
const
|
|
597
|
-
let
|
|
598
|
-
let
|
|
599
|
-
const
|
|
936
|
+
const AGENT_STATUS_CACHE_MS = 250;
|
|
937
|
+
let agentStatusCache = null;
|
|
938
|
+
let agentStatusCacheAt = 0;
|
|
939
|
+
const agentStatusState = ({ force = false } = {}) => {
|
|
600
940
|
const now = Date.now();
|
|
601
|
-
if (!force &&
|
|
602
|
-
const status = runtime.
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
bridgeScope: status.bridgeScope || null,
|
|
941
|
+
if (!force && agentStatusCache && now - agentStatusCacheAt < AGENT_STATUS_CACHE_MS) return agentStatusCache;
|
|
942
|
+
const status = runtime.agentStatus?.() || {};
|
|
943
|
+
agentStatusCache = {
|
|
944
|
+
agentWorkers: Array.isArray(status.agentWorkers) ? status.agentWorkers : [],
|
|
945
|
+
agentJobs: Array.isArray(status.agentJobs) ? status.agentJobs : [],
|
|
946
|
+
agentScope: status.agentScope || null,
|
|
608
947
|
};
|
|
609
|
-
|
|
610
|
-
return
|
|
948
|
+
agentStatusCacheAt = now;
|
|
949
|
+
return agentStatusCache;
|
|
611
950
|
};
|
|
612
|
-
const
|
|
951
|
+
const baseRouteState = () => ({
|
|
613
952
|
sessionId: runtime.id,
|
|
614
953
|
clientHostPid: runtime.clientHostPid || null,
|
|
615
954
|
model: runtime.model,
|
|
@@ -623,18 +962,37 @@ export async function createEngineSession({
|
|
|
623
962
|
effectiveContextWindowPercent: runtime.effectiveContextWindowPercent,
|
|
624
963
|
cwd: runtime.cwd || process.cwd(),
|
|
625
964
|
systemShell: runtime.systemShell || { source: 'auto', command: '', effective: '' },
|
|
965
|
+
searchRoute: runtime.getSearchRoute?.() || runtime.searchRoute || null,
|
|
626
966
|
autoClear: autoClearState(),
|
|
627
967
|
workflow: runtime.workflow || null,
|
|
628
968
|
});
|
|
629
969
|
|
|
630
|
-
const
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
970
|
+
const routeState = () => ({
|
|
971
|
+
...baseRouteState(),
|
|
972
|
+
displayContextWindow: state.displayContextWindow || 0,
|
|
973
|
+
compactBoundaryTokens: state.compactBoundaryTokens || 0,
|
|
974
|
+
autoCompactTokenLimit: state.autoCompactTokenLimit || 0,
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
function syncContextDisplayFields(ctx = null) {
|
|
978
|
+
const status = ctx || runtime.contextStatus?.() || null;
|
|
979
|
+
if (!status) return;
|
|
980
|
+
const displayWindow = Number(status.contextWindow || 0);
|
|
981
|
+
const compactBoundary = Number(status.compaction?.boundaryTokens || 0);
|
|
982
|
+
const autoCompact = Number(
|
|
983
|
+
status.compaction?.autoCompactTokenLimit
|
|
984
|
+
|| runtime.session?.autoCompactTokenLimit
|
|
985
|
+
|| 0,
|
|
986
|
+
);
|
|
987
|
+
if (displayWindow > 0) state.displayContextWindow = displayWindow;
|
|
988
|
+
if (compactBoundary > 0) state.compactBoundaryTokens = compactBoundary;
|
|
989
|
+
if (autoCompact > 0) state.autoCompactTokenLimit = autoCompact;
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
const initialAgentState = {
|
|
993
|
+
agentWorkers: [],
|
|
994
|
+
agentJobs: [],
|
|
995
|
+
agentScope: null,
|
|
638
996
|
};
|
|
639
997
|
let state = {
|
|
640
998
|
items: [],
|
|
@@ -645,25 +1003,74 @@ export async function createEngineSession({
|
|
|
645
1003
|
spinner: null,
|
|
646
1004
|
queued: [],
|
|
647
1005
|
thinking: null,
|
|
1006
|
+
toolApproval: null,
|
|
648
1007
|
lastTurn: null,
|
|
649
1008
|
stats: createSessionStats(),
|
|
650
|
-
...
|
|
651
|
-
|
|
1009
|
+
...baseRouteState(),
|
|
1010
|
+
displayContextWindow: 0,
|
|
1011
|
+
compactBoundaryTokens: 0,
|
|
1012
|
+
autoCompactTokenLimit: 0,
|
|
1013
|
+
...initialAgentState,
|
|
652
1014
|
toolMode: runtime.toolMode,
|
|
653
1015
|
cwd,
|
|
1016
|
+
themeEpoch: 0,
|
|
654
1017
|
};
|
|
1018
|
+
bootProfile('engine:route-state-ready', { ms: (performance.now() - stateStartedAt).toFixed(1) });
|
|
655
1019
|
bootProfile('engine:state-ready', { ms: (performance.now() - stateStartedAt).toFixed(1) });
|
|
1020
|
+
let pendingSessionReset = false;
|
|
656
1021
|
const syncContextStats = ({ allowEstimated = false } = {}) => {
|
|
1022
|
+
if (pendingSessionReset) return null;
|
|
657
1023
|
const ctx = runtime.contextStatus?.() || null;
|
|
1024
|
+
if (!ctx) return null;
|
|
1025
|
+
syncContextDisplayFields(ctx);
|
|
658
1026
|
const hasProviderUsage = Number(state.stats.latestPromptTokens || state.stats.latestInputTokens || state.stats.inputTokens || 0) > 0;
|
|
659
|
-
const
|
|
660
|
-
const
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
1027
|
+
const hasApiContextUsage = Number(ctx?.lastApiRequestTokens ?? ctx?.usage?.lastContextTokens ?? 0) > 0;
|
|
1028
|
+
const hasTurnActivity = state.busy === true
|
|
1029
|
+
|| state.spinner != null
|
|
1030
|
+
|| state.thinking != null;
|
|
1031
|
+
const isFreshSession = !hasProviderUsage && !hasApiContextUsage && !hasTurnActivity;
|
|
1032
|
+
if (isFreshSession) {
|
|
1033
|
+
state.stats.currentEstimatedContextTokens = 0;
|
|
1034
|
+
state.stats.currentContextTokens = 0;
|
|
1035
|
+
state.stats.currentContextSource = null;
|
|
1036
|
+
state.stats.currentContextUpdatedAt = Date.now();
|
|
1037
|
+
return ctx;
|
|
1038
|
+
}
|
|
1039
|
+
const estimatedTokens = Math.max(0, Number(ctx.currentEstimatedTokens ?? ctx.usedTokens ?? 0));
|
|
1040
|
+
const usedTokens = Math.max(0, Number(ctx.usedTokens ?? estimatedTokens ?? 0));
|
|
1041
|
+
const usedSource = String(ctx.usedSource || '').toLowerCase();
|
|
1042
|
+
const shouldPublishEstimate = allowEstimated && (
|
|
1043
|
+
usedSource === 'estimated'
|
|
1044
|
+
|| Number(ctx.currentEstimatedTokens) > 0
|
|
1045
|
+
|| usedTokens > 0
|
|
1046
|
+
);
|
|
1047
|
+
if (!allowEstimated && !hasProviderUsage && usedSource !== 'last_api_request') return ctx;
|
|
1048
|
+
if (shouldPublishEstimate) {
|
|
1049
|
+
state.stats.currentEstimatedContextTokens = estimatedTokens;
|
|
1050
|
+
state.stats.currentContextSource = 'estimated';
|
|
1051
|
+
state.stats.currentContextTokens = 0;
|
|
1052
|
+
} else if (allowEstimated && (hasProviderUsage || hasApiContextUsage || hasTurnActivity)) {
|
|
1053
|
+
state.stats.currentEstimatedContextTokens = estimatedTokens;
|
|
1054
|
+
state.stats.currentContextSource = usedSource || (estimatedTokens > 0 ? 'estimated' : null);
|
|
1055
|
+
const publishedSource = String(state.stats.currentContextSource || '').toLowerCase();
|
|
1056
|
+
if (publishedSource === 'last_api_request') {
|
|
1057
|
+
const apiUsed = Math.max(0, Number(ctx.lastApiRequestTokens ?? usedTokens ?? 0));
|
|
1058
|
+
state.stats.currentContextTokens = apiUsed;
|
|
1059
|
+
} else if (publishedSource === 'estimated') {
|
|
1060
|
+
state.stats.currentContextTokens = 0;
|
|
1061
|
+
} else {
|
|
1062
|
+
state.stats.currentContextTokens = usedTokens > 0 ? usedTokens : 0;
|
|
1063
|
+
}
|
|
1064
|
+
} else {
|
|
1065
|
+
state.stats.currentEstimatedContextTokens = 0;
|
|
1066
|
+
if (usedSource === 'last_api_request' && Number(ctx.lastApiRequestTokens ?? usedTokens ?? 0) > 0) {
|
|
1067
|
+
state.stats.currentContextTokens = Math.max(0, Number(ctx.lastApiRequestTokens ?? usedTokens ?? 0));
|
|
1068
|
+
state.stats.currentContextSource = 'last_api_request';
|
|
1069
|
+
} else {
|
|
1070
|
+
state.stats.currentContextTokens = 0;
|
|
1071
|
+
state.stats.currentContextSource = null;
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
667
1074
|
state.stats.currentContextUpdatedAt = Date.now();
|
|
668
1075
|
return ctx;
|
|
669
1076
|
};
|
|
@@ -671,7 +1078,24 @@ export async function createEngineSession({
|
|
|
671
1078
|
syncContextStats({ allowEstimated: true });
|
|
672
1079
|
bootProfile('engine:context-ready', { ms: (performance.now() - contextStartedAt).toFixed(1) });
|
|
673
1080
|
const listeners = new Set();
|
|
674
|
-
|
|
1081
|
+
// Coalesce store notifications: a single onToolCall batch / finalize path
|
|
1082
|
+
// fires many set() calls in one synchronous block (aggregate header sync,
|
|
1083
|
+
// spinner, item pushes). Notifying React on every set() painted the
|
|
1084
|
+
// intermediate layouts ("툭툭" header/count jitter). Collapsing the
|
|
1085
|
+
// notifications into one microtask means listeners see only the final state
|
|
1086
|
+
// of the current tick — "draw twice, keep the last one". getState() stays
|
|
1087
|
+
// synchronous and always returns the latest snapshot, so useSyncExternalStore
|
|
1088
|
+
// never tears.
|
|
1089
|
+
let emitScheduled = false;
|
|
1090
|
+
const flushEmit = () => {
|
|
1091
|
+
emitScheduled = false;
|
|
1092
|
+
for (const l of listeners) l();
|
|
1093
|
+
};
|
|
1094
|
+
const emit = () => {
|
|
1095
|
+
if (emitScheduled) return;
|
|
1096
|
+
emitScheduled = true;
|
|
1097
|
+
queueMicrotask(flushEmit);
|
|
1098
|
+
};
|
|
675
1099
|
const set = (patch) => {
|
|
676
1100
|
if (!patch || typeof patch !== 'object') return false;
|
|
677
1101
|
let changed = false;
|
|
@@ -703,38 +1127,36 @@ export async function createEngineSession({
|
|
|
703
1127
|
if (item?.id != null) itemIndexById.set(item.id, index);
|
|
704
1128
|
set({ items });
|
|
705
1129
|
};
|
|
706
|
-
const
|
|
707
|
-
const idSet = new Set((ids || []).filter((id) => id != null));
|
|
708
|
-
if (idSet.size === 0) return false;
|
|
709
|
-
const items = state.items.filter((item) => !idSet.has(item?.id));
|
|
710
|
-
if (items.length === state.items.length) return false;
|
|
711
|
-
set({ items: replaceItems(items) });
|
|
712
|
-
return true;
|
|
713
|
-
};
|
|
714
|
-
const pushUserOrSyntheticItem = (text, id = nextId()) => {
|
|
1130
|
+
const upsertSyntheticToolItem = (text, id = nextId(), parsed = null) => {
|
|
715
1131
|
const synthetic = parseSyntheticAgentMessage(text);
|
|
716
|
-
if (!synthetic)
|
|
717
|
-
pushItem({ kind: 'user', id, text });
|
|
718
|
-
return;
|
|
719
|
-
}
|
|
1132
|
+
if (!synthetic) return false;
|
|
720
1133
|
const label = synthetic.label || 'notification';
|
|
1134
|
+
const args = synthetic.args || {
|
|
1135
|
+
type: label,
|
|
1136
|
+
task_id: synthetic.taskId || parsed?.taskId || undefined,
|
|
1137
|
+
description: synthetic.summary || 'agent notification',
|
|
1138
|
+
};
|
|
1139
|
+
const isError = synthetic.isError ?? /^(failed|error|timeout|killed|cancelled)$/i.test(label);
|
|
721
1140
|
pushItem({
|
|
722
1141
|
kind: 'tool',
|
|
723
1142
|
id,
|
|
724
|
-
name: synthetic.name || '
|
|
725
|
-
args
|
|
726
|
-
type: label,
|
|
727
|
-
task_id: synthetic.taskId || undefined,
|
|
728
|
-
description: synthetic.summary || 'agent notification',
|
|
729
|
-
},
|
|
1143
|
+
name: synthetic.name || 'agent',
|
|
1144
|
+
args,
|
|
730
1145
|
result: synthetic.result,
|
|
731
|
-
|
|
1146
|
+
rawResult: synthetic.rawResult ?? text,
|
|
1147
|
+
isError,
|
|
732
1148
|
expanded: false,
|
|
733
1149
|
count: 1,
|
|
734
1150
|
completedCount: 1,
|
|
735
1151
|
startedAt: Date.now(),
|
|
736
1152
|
completedAt: Date.now(),
|
|
737
1153
|
});
|
|
1154
|
+
return true;
|
|
1155
|
+
};
|
|
1156
|
+
const pushUserOrSyntheticItem = (text, id = nextId()) => {
|
|
1157
|
+
if (isModelVisibleToolCompletionWrapper(text)) return;
|
|
1158
|
+
if (upsertSyntheticToolItem(text, id)) return;
|
|
1159
|
+
pushItem({ kind: 'user', id, text });
|
|
738
1160
|
};
|
|
739
1161
|
const pushToast = (text, tone = 'info', ttlMs = 3000) => {
|
|
740
1162
|
const id = nextId();
|
|
@@ -759,6 +1181,80 @@ export async function createEngineSession({
|
|
|
759
1181
|
pushItem({ kind: 'notice', id, text: value, tone });
|
|
760
1182
|
return id;
|
|
761
1183
|
};
|
|
1184
|
+
const toolApprovalQueue = [];
|
|
1185
|
+
let activeToolApproval = null;
|
|
1186
|
+
function normalizeToolApprovalRequest(input = {}, id = nextId()) {
|
|
1187
|
+
const now = Date.now();
|
|
1188
|
+
const timeoutMs = TOOL_APPROVAL_TIMEOUT_MS;
|
|
1189
|
+
return {
|
|
1190
|
+
id,
|
|
1191
|
+
name: String(input?.name || input?.tool_name || 'tool'),
|
|
1192
|
+
args: input?.args ?? input?.tool_input ?? null,
|
|
1193
|
+
cwd: input?.cwd || null,
|
|
1194
|
+
sessionId: input?.sessionId || input?.session_id || null,
|
|
1195
|
+
toolCallId: input?.toolCallId || input?.tool_use_id || null,
|
|
1196
|
+
reason: String(input?.reason || input?.message || 'approval requested by hook').trim(),
|
|
1197
|
+
requestedAt: now,
|
|
1198
|
+
timeoutMs,
|
|
1199
|
+
expiresAt: now + timeoutMs,
|
|
1200
|
+
};
|
|
1201
|
+
}
|
|
1202
|
+
function presentNextToolApproval() {
|
|
1203
|
+
if (activeToolApproval || disposed) return;
|
|
1204
|
+
const entry = toolApprovalQueue.shift();
|
|
1205
|
+
if (!entry) {
|
|
1206
|
+
if (state.toolApproval) set({ toolApproval: null });
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
1209
|
+
activeToolApproval = entry;
|
|
1210
|
+
entry.timer = setTimeout(() => {
|
|
1211
|
+
finishToolApproval(entry.id, false, 'approval timed out');
|
|
1212
|
+
}, entry.request.timeoutMs);
|
|
1213
|
+
entry.timer.unref?.();
|
|
1214
|
+
set({ toolApproval: entry.request });
|
|
1215
|
+
}
|
|
1216
|
+
function finishToolApproval(id, approved, reason = '') {
|
|
1217
|
+
const targetId = String(id || '');
|
|
1218
|
+
if (activeToolApproval && activeToolApproval.id === targetId) {
|
|
1219
|
+
const entry = activeToolApproval;
|
|
1220
|
+
activeToolApproval = null;
|
|
1221
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
1222
|
+
set({ toolApproval: null });
|
|
1223
|
+
try { entry.resolve({ approved: approved === true, reason: String(reason || '') }); } catch {}
|
|
1224
|
+
presentNextToolApproval();
|
|
1225
|
+
return true;
|
|
1226
|
+
}
|
|
1227
|
+
const index = toolApprovalQueue.findIndex((entry) => entry.id === targetId);
|
|
1228
|
+
if (index >= 0) {
|
|
1229
|
+
const [entry] = toolApprovalQueue.splice(index, 1);
|
|
1230
|
+
if (entry?.timer) clearTimeout(entry.timer);
|
|
1231
|
+
try { entry.resolve({ approved: approved === true, reason: String(reason || '') }); } catch {}
|
|
1232
|
+
return true;
|
|
1233
|
+
}
|
|
1234
|
+
return false;
|
|
1235
|
+
}
|
|
1236
|
+
function denyAllToolApprovals(reason = 'approval cancelled') {
|
|
1237
|
+
if (activeToolApproval) {
|
|
1238
|
+
const entry = activeToolApproval;
|
|
1239
|
+
activeToolApproval = null;
|
|
1240
|
+
if (entry.timer) clearTimeout(entry.timer);
|
|
1241
|
+
try { entry.resolve({ approved: false, reason }); } catch {}
|
|
1242
|
+
}
|
|
1243
|
+
while (toolApprovalQueue.length > 0) {
|
|
1244
|
+
const entry = toolApprovalQueue.shift();
|
|
1245
|
+
if (entry?.timer) clearTimeout(entry.timer);
|
|
1246
|
+
try { entry.resolve({ approved: false, reason }); } catch {}
|
|
1247
|
+
}
|
|
1248
|
+
if (state.toolApproval) set({ toolApproval: null });
|
|
1249
|
+
}
|
|
1250
|
+
function requestToolApproval(input = {}) {
|
|
1251
|
+
if (disposed) return Promise.resolve({ approved: false, reason: 'runtime disposed' });
|
|
1252
|
+
return new Promise((resolve) => {
|
|
1253
|
+
const id = nextId();
|
|
1254
|
+
toolApprovalQueue.push({ id, request: normalizeToolApprovalRequest(input, id), resolve, timer: null });
|
|
1255
|
+
presentNextToolApproval();
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
762
1258
|
const patchItem = (id, patch) => {
|
|
763
1259
|
let index = itemIndexById.get(id);
|
|
764
1260
|
if (!Number.isInteger(index) || state.items[index]?.id !== id) {
|
|
@@ -782,6 +1278,17 @@ export async function createEngineSession({
|
|
|
782
1278
|
};
|
|
783
1279
|
const toastTimers = new Set();
|
|
784
1280
|
let disposed = false;
|
|
1281
|
+
const runtimePulseTimer = setInterval(() => {
|
|
1282
|
+
if (disposed) return;
|
|
1283
|
+
if (pendingSessionReset) return;
|
|
1284
|
+
syncContextStats({ allowEstimated: true });
|
|
1285
|
+
set({
|
|
1286
|
+
...routeState(),
|
|
1287
|
+
stats: { ...state.stats },
|
|
1288
|
+
...agentStatusState(),
|
|
1289
|
+
});
|
|
1290
|
+
}, 2000);
|
|
1291
|
+
runtimePulseTimer.unref?.();
|
|
785
1292
|
|
|
786
1293
|
function clearToastTimers() {
|
|
787
1294
|
for (const timer of toastTimers) {
|
|
@@ -794,18 +1301,46 @@ export async function createEngineSession({
|
|
|
794
1301
|
let lastUserActivityAt = Date.now();
|
|
795
1302
|
let autoClearRunning = false;
|
|
796
1303
|
const pendingNotificationKeys = new Set();
|
|
1304
|
+
const displayedExecutionNotificationKeys = new Set();
|
|
1305
|
+
let executionResumeKickDeferred = false;
|
|
1306
|
+
|
|
1307
|
+
function kickExecutionPendingResume() {
|
|
1308
|
+
if (disposed) return;
|
|
1309
|
+
if (state.busy) {
|
|
1310
|
+
executionResumeKickDeferred = true;
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
if (pending.some((entry) => entry.mode === 'pending-resume')) {
|
|
1314
|
+
executionResumeKickDeferred = true;
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
executionResumeKickDeferred = false;
|
|
1318
|
+
pending.push(makeQueueEntry('', { mode: 'pending-resume', priority: 'next' }));
|
|
1319
|
+
void drain();
|
|
1320
|
+
}
|
|
797
1321
|
|
|
798
|
-
function
|
|
799
|
-
|
|
1322
|
+
function flushDeferredExecutionPendingResumeKick() {
|
|
1323
|
+
if (!executionResumeKickDeferred || disposed || state.busy) return;
|
|
1324
|
+
kickExecutionPendingResume();
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
function scheduleExecutionPendingResumeKick() {
|
|
1328
|
+
// notifyFnForSession enqueues the model-visible body after onNotification
|
|
1329
|
+
// returns; defer the kick so askSession pre-drain sees session pending.
|
|
1330
|
+
queueMicrotask(() => kickExecutionPendingResume());
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
function updateAgentJobCard(itemId, text, isError = false) {
|
|
1334
|
+
const parsed = parseAgentJob(text);
|
|
800
1335
|
const current = state.items.find((it) => it.id === itemId);
|
|
801
|
-
const rawDisplayText =
|
|
802
|
-
const displayText = isError ? toolErrorDisplay(rawDisplayText, '
|
|
1336
|
+
const rawDisplayText = agentJobResultText(text, parsed) || String(text ?? '').trim();
|
|
1337
|
+
const displayText = isError ? toolErrorDisplay(rawDisplayText, 'agent') : rawDisplayText;
|
|
803
1338
|
patchItem(itemId, {
|
|
804
1339
|
result: displayText,
|
|
805
1340
|
text: displayText,
|
|
806
1341
|
isError,
|
|
807
1342
|
errorCount: isError ? 1 : 0,
|
|
808
|
-
...(parsed ? { args:
|
|
1343
|
+
...(parsed ? { args: agentArgsWithResultMetadata(current?.args, parsed) } : {}),
|
|
809
1344
|
});
|
|
810
1345
|
}
|
|
811
1346
|
|
|
@@ -814,23 +1349,38 @@ export async function createEngineSession({
|
|
|
814
1349
|
if (disposed) return;
|
|
815
1350
|
const text = String(event?.content ?? event?.text ?? event ?? '').trim();
|
|
816
1351
|
if (!text) return;
|
|
817
|
-
const parsed =
|
|
1352
|
+
const parsed = parseAgentJob(text);
|
|
818
1353
|
const notificationKey = notificationQueueKey(event, text, parsed);
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
1354
|
+
const delivery = resolveTuiRuntimeNotificationDelivery(event, text);
|
|
1355
|
+
if (delivery.action === 'ignore') return;
|
|
1356
|
+
if (delivery.action === 'status-only') {
|
|
1357
|
+
if (parsed?.taskId) set(agentStatusState({ force: true }));
|
|
1358
|
+
return true;
|
|
1359
|
+
}
|
|
1360
|
+
if (delivery.action === 'execution-ui') {
|
|
1361
|
+
const firstDelivery = !notificationKey || !displayedExecutionNotificationKeys.has(notificationKey);
|
|
1362
|
+
if (firstDelivery) {
|
|
1363
|
+
if (notificationKey) displayedExecutionNotificationKeys.add(notificationKey);
|
|
1364
|
+
pushUserOrSyntheticItem(delivery.displayText, nextId());
|
|
827
1365
|
}
|
|
1366
|
+
if (parsed?.taskId) set(agentStatusState({ force: true }));
|
|
1367
|
+
if (String(delivery.modelContent || '').trim()) {
|
|
1368
|
+
scheduleExecutionPendingResumeKick();
|
|
1369
|
+
}
|
|
1370
|
+
return true;
|
|
828
1371
|
}
|
|
829
|
-
|
|
1372
|
+
if (parsed?.taskId) {
|
|
1373
|
+
set(agentStatusState({ force: true }));
|
|
1374
|
+
}
|
|
1375
|
+
const modelContent = String(delivery.modelContent ?? delivery.displayText ?? text).trim();
|
|
1376
|
+
if (!modelContent) return true;
|
|
1377
|
+
enqueue(modelContent, {
|
|
830
1378
|
mode: 'task-notification',
|
|
831
1379
|
priority: 'next',
|
|
832
1380
|
key: notificationKey || undefined,
|
|
1381
|
+
displayText: delivery.displayText || text,
|
|
833
1382
|
});
|
|
1383
|
+
return true;
|
|
834
1384
|
});
|
|
835
1385
|
}
|
|
836
1386
|
|
|
@@ -852,6 +1402,10 @@ export async function createEngineSession({
|
|
|
852
1402
|
...uniqueReasons.slice(1),
|
|
853
1403
|
].join('\n');
|
|
854
1404
|
}
|
|
1405
|
+
for (const result of group.results || []) {
|
|
1406
|
+
const line = String(result?.text || '').trim();
|
|
1407
|
+
if (line) return result.text;
|
|
1408
|
+
}
|
|
855
1409
|
return '';
|
|
856
1410
|
}
|
|
857
1411
|
|
|
@@ -869,6 +1423,7 @@ export async function createEngineSession({
|
|
|
869
1423
|
function aggregateRawResult(calls) {
|
|
870
1424
|
const chunks = [];
|
|
871
1425
|
for (const rec of calls || []) {
|
|
1426
|
+
if (rec?.resolved !== true) continue;
|
|
872
1427
|
const text = String(rec?.resultText || '').replace(/\s+$/, '');
|
|
873
1428
|
if (!text.trim()) continue;
|
|
874
1429
|
const label = String(rec?.name || rec?.category || 'tool').trim() || 'tool';
|
|
@@ -877,6 +1432,19 @@ export async function createEngineSession({
|
|
|
877
1432
|
return chunks.join('\n\n');
|
|
878
1433
|
}
|
|
879
1434
|
|
|
1435
|
+
function aggregateDisplayDetail(summaries, allCalls) {
|
|
1436
|
+
const fromSummaries = formatAggregateDetail(summaries);
|
|
1437
|
+
if (String(fromSummaries || '').trim()) return fromSummaries;
|
|
1438
|
+
let line = '';
|
|
1439
|
+
for (const rec of allCalls || []) {
|
|
1440
|
+
const text = String(rec?.resultText || '').replace(/\s+$/, '');
|
|
1441
|
+
line = text.split('\n').map((l) => l.trim()).find(Boolean) || '';
|
|
1442
|
+
if (line) break;
|
|
1443
|
+
}
|
|
1444
|
+
if (!line) return '';
|
|
1445
|
+
return line.length > 160 ? `${line.slice(0, 157)}…` : line;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
880
1448
|
function aggregateBucketForCategory(category) {
|
|
881
1449
|
switch (category) {
|
|
882
1450
|
case 'Read':
|
|
@@ -915,21 +1483,31 @@ export async function createEngineSession({
|
|
|
915
1483
|
if (!card || card.done) return false;
|
|
916
1484
|
const callId = toolResultCallId(message) || card.callId;
|
|
917
1485
|
if (callId && done.has(callId)) return false;
|
|
1486
|
+
// A result for this card arrived (possibly before its deferred push delay
|
|
1487
|
+
// elapsed) — surface the card now so the patch below has a live item and the
|
|
1488
|
+
// fast tool paints a completed card directly, no pending placeholder stage.
|
|
1489
|
+
// ensureVisible flushes this card AND every earlier-created still-deferred
|
|
1490
|
+
// card in order, so transcript order always matches call order.
|
|
1491
|
+
(card.aggregate?.ensureVisible || card.ensureVisible)?.();
|
|
918
1492
|
const rawText = toolResultText(message?.content);
|
|
919
|
-
const isError = message?.isError === true || message?.toolKind === 'error' || /^\s*\[?error/i.test(rawText);
|
|
1493
|
+
const isError = message?.isError === true || message?.toolKind === 'error' || /^\s*\[?error/i.test(rawText) || isErrorToolStatus(toolResultStatus(rawText));
|
|
920
1494
|
const text = isError ? toolErrorDisplay(rawText, card?.name || 'tool') : rawText;
|
|
921
1495
|
|
|
922
1496
|
// Aggregate card handling — collect semantic summaries per call
|
|
923
1497
|
const aggregate = card.aggregate;
|
|
924
1498
|
if (aggregate && card.itemId === aggregate.itemId) {
|
|
925
1499
|
const callRec = callId ? aggregate.calls.get(callId) : null;
|
|
926
|
-
if (callRec)
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
callRec.resolved = true;
|
|
1500
|
+
if (!callRec) return false;
|
|
1501
|
+
if (callRec.resolved) {
|
|
1502
|
+
card.done = true;
|
|
1503
|
+
if (callId) done.add(callId);
|
|
1504
|
+
return false;
|
|
932
1505
|
}
|
|
1506
|
+
callRec.summary = !isError ? summarizeToolResult(callRec.name, callRec.args, rawText, isError) : null;
|
|
1507
|
+
assignAggregateSummaryOrder(aggregate, callRec);
|
|
1508
|
+
callRec.isError = isError;
|
|
1509
|
+
callRec.resultText = text;
|
|
1510
|
+
callRec.resolved = true;
|
|
933
1511
|
const allCalls = [...aggregate.calls.values()];
|
|
934
1512
|
const completed = allCalls.filter((r) => r.resolved).length;
|
|
935
1513
|
const errors = allCalls.filter((r) => r.isError).length;
|
|
@@ -939,36 +1517,42 @@ export async function createEngineSession({
|
|
|
939
1517
|
const succeeded = completed - errors;
|
|
940
1518
|
detailText = succeeded > 0 ? `${succeeded} Ok · ${errors} Failed` : `${errors} Failed`;
|
|
941
1519
|
} else {
|
|
942
|
-
detailText =
|
|
1520
|
+
detailText = aggregateDisplayDetail(summaries, allCalls);
|
|
1521
|
+
if (errors > 0) {
|
|
1522
|
+
detailText = detailText ? `${detailText} · ${errors} Failed` : `${errors} Failed`;
|
|
1523
|
+
}
|
|
943
1524
|
}
|
|
944
1525
|
const currentItem = state.items.find((it) => it.id === card.itemId);
|
|
945
|
-
const
|
|
1526
|
+
const earlyCompleted = allCalls.filter((r) => r.resolved || r.completedEarly).length;
|
|
1527
|
+
const visualCompleted = Math.max(completed, earlyCompleted, Math.min(allCalls.length, Number(currentItem?.completedCount || 0)));
|
|
946
1528
|
const rawResult = aggregateRawResult(allCalls);
|
|
1529
|
+
const displayDetail = toolAggregateDetailFallback(detailText, rawResult);
|
|
947
1530
|
patchItem(card.itemId, {
|
|
948
|
-
result:
|
|
949
|
-
text:
|
|
1531
|
+
result: displayDetail,
|
|
1532
|
+
text: displayDetail,
|
|
950
1533
|
rawResult: rawResult || null,
|
|
951
1534
|
isError: errors > 0,
|
|
952
1535
|
errorCount: errors,
|
|
953
1536
|
count: allCalls.length,
|
|
954
1537
|
completedCount: visualCompleted,
|
|
955
|
-
completedAt: Date.now(),
|
|
1538
|
+
completedAt: Number(currentItem?.completedAt) || Date.now(),
|
|
956
1539
|
});
|
|
957
1540
|
card.done = true;
|
|
958
1541
|
if (callId) done.add(callId);
|
|
959
1542
|
return true;
|
|
960
1543
|
}
|
|
961
1544
|
|
|
962
|
-
// Non-aggregate (legacy
|
|
1545
|
+
// Non-aggregate (legacy agent-job cards, etc.)
|
|
963
1546
|
const group = toolGroups.get(card.itemId) || { count: 1, completed: 0, errors: 0, results: [] };
|
|
964
1547
|
group.completed = Math.min(group.count, group.completed + 1);
|
|
965
1548
|
group.errors += isError ? 1 : 0;
|
|
966
1549
|
group.results.push({ text, isError });
|
|
967
1550
|
toolGroups.set(card.itemId, group);
|
|
968
1551
|
const resultText = groupedToolResultText(group);
|
|
1552
|
+
const displayResult = toolGroupedDisplayFallback(resultText, text, rawText);
|
|
969
1553
|
const patch = {
|
|
970
|
-
result:
|
|
971
|
-
text:
|
|
1554
|
+
result: displayResult,
|
|
1555
|
+
text: displayResult,
|
|
972
1556
|
isError: group.errors > 0,
|
|
973
1557
|
errorCount: group.errors,
|
|
974
1558
|
count: group.count,
|
|
@@ -976,11 +1560,16 @@ export async function createEngineSession({
|
|
|
976
1560
|
completedAt: Date.now(),
|
|
977
1561
|
};
|
|
978
1562
|
if (group.count <= 1) {
|
|
979
|
-
const
|
|
980
|
-
if (
|
|
1563
|
+
const body = String(text || rawText || '').trim();
|
|
1564
|
+
if (body) patch.rawResult = text || rawText;
|
|
1565
|
+
const parsedAgent = parseAgentJob(rawText);
|
|
1566
|
+
if (parsedAgent) {
|
|
1567
|
+
patch.args = agentArgsWithResultMetadata(state.items.find((it) => it.id === card.itemId)?.args, parsedAgent);
|
|
1568
|
+
set(agentStatusState({ force: true }));
|
|
1569
|
+
}
|
|
981
1570
|
}
|
|
982
1571
|
patchItem(card.itemId, patch);
|
|
983
|
-
if (group.count <= 1)
|
|
1572
|
+
if (group.count <= 1) updateAgentJobCard(card.itemId, rawText, isError);
|
|
984
1573
|
card.done = true;
|
|
985
1574
|
if (callId) done.add(callId);
|
|
986
1575
|
return true;
|
|
@@ -1017,6 +1606,9 @@ export async function createEngineSession({
|
|
|
1017
1606
|
if (!finalize) return;
|
|
1018
1607
|
for (const card of toolCards || []) {
|
|
1019
1608
|
if (card.done) continue;
|
|
1609
|
+
// Finalize must surface any still-deferred card before patching its result
|
|
1610
|
+
// so the completed/cancelled card is never silently dropped.
|
|
1611
|
+
(card.aggregate?.ensureVisible || card.ensureVisible)?.();
|
|
1020
1612
|
// Aggregate finalize — mark any remaining calls as done
|
|
1021
1613
|
const aggregate = card.aggregate;
|
|
1022
1614
|
if (aggregate && card.itemId === aggregate.itemId) {
|
|
@@ -1026,11 +1618,12 @@ export async function createEngineSession({
|
|
|
1026
1618
|
const totalCompleted = remaining > 0 ? completed + remaining : completed;
|
|
1027
1619
|
const errors = allCalls.filter((r) => r.isError).length;
|
|
1028
1620
|
const summaries = aggregateSummaries(aggregate);
|
|
1029
|
-
const detailText =
|
|
1621
|
+
const detailText = aggregateDisplayDetail(summaries, allCalls);
|
|
1030
1622
|
const rawResult = aggregateRawResult(allCalls);
|
|
1623
|
+
const displayDetail = toolAggregateDetailFallback(detailText, rawResult);
|
|
1031
1624
|
patchItem(card.itemId, {
|
|
1032
|
-
result:
|
|
1033
|
-
text:
|
|
1625
|
+
result: displayDetail,
|
|
1626
|
+
text: displayDetail,
|
|
1034
1627
|
rawResult: rawResult || null,
|
|
1035
1628
|
isError: errors > 0,
|
|
1036
1629
|
errorCount: errors,
|
|
@@ -1084,13 +1677,90 @@ export async function createEngineSession({
|
|
|
1084
1677
|
let thinkingSegmentStartedAt = 0;
|
|
1085
1678
|
let accumulatedThinkingMs = 0;
|
|
1086
1679
|
let cancelled = false;
|
|
1680
|
+
let askResult = null;
|
|
1681
|
+
let turnFinishedNormally = false;
|
|
1682
|
+
const itemsAtTurnStart = state.items.length;
|
|
1087
1683
|
const cardByCallId = new Map();
|
|
1088
1684
|
const toolCards = [];
|
|
1089
1685
|
const toolGroups = new Map();
|
|
1090
1686
|
const resultsDone = new Set();
|
|
1687
|
+
// Streaming providers can deliver eager onToolResult before onToolCall registers
|
|
1688
|
+
// cards (send() still in flight). Hold those by callId until the batch lands.
|
|
1689
|
+
const earlyResultBuffer = new Map();
|
|
1091
1690
|
const aggregateCards = []; // active aggregate cards in the current consecutive tool block
|
|
1691
|
+
const aggregateByBucket = new Map(); // tail-continuation cache; append only while still the last visible item
|
|
1092
1692
|
let openAggregateCard = null;
|
|
1093
|
-
|
|
1693
|
+
|
|
1694
|
+
// ── Deferred tool-card push (scroll/text sync) ────────────────────────────
|
|
1695
|
+
// A tool card used to enter the transcript the instant onToolCall fired,
|
|
1696
|
+
// reserving its estimated height (margin+header+detail) while ToolExecution
|
|
1697
|
+
// only painted blank placeholder rows for TOOL_PENDING_SHOW_DELAY_MS. With a
|
|
1698
|
+
// bottom-fixed viewport that shoved the body up BEFORE any glyph appeared, so
|
|
1699
|
+
// the scroll ran ahead of the text. We now hold each card off-screen for the
|
|
1700
|
+
// same delay and push it only when its real header/detail will paint (delay
|
|
1701
|
+
// elapsed) OR a result lands first (fast tool → completed card, no pending
|
|
1702
|
+
// flicker). Either way the pushed spec is stamped `deferredDisplayReady` so
|
|
1703
|
+
// ToolExecution renders the real header + 'Running' detail immediately
|
|
1704
|
+
// instead of the blank pre-delay placeholder — this matters for the
|
|
1705
|
+
// result-forced chain push (flushDeferredUpTo), where earlier-seq sibling
|
|
1706
|
+
// cards are pushed alongside the result-bearing one before their own delay
|
|
1707
|
+
// elapses and would otherwise paint an empty reserved band.
|
|
1708
|
+
// Mirrors components/ToolExecution.jsx TOOL_PENDING_SHOW_DELAY_MS.
|
|
1709
|
+
const TOOL_CARD_PUSH_DELAY_MS = 1000;
|
|
1710
|
+
let deferredSeqCounter = 0;
|
|
1711
|
+
const deferredEntries = []; // creation-order list; each is pushed at most once
|
|
1712
|
+
// Push this entry AND every earlier-created still-deferred entry, in order,
|
|
1713
|
+
// so transcript order always matches call order even when a later card's
|
|
1714
|
+
// result/timer fires before an earlier one's.
|
|
1715
|
+
const flushDeferredUpTo = (entry) => {
|
|
1716
|
+
if (!entry) return;
|
|
1717
|
+
for (const e of deferredEntries) {
|
|
1718
|
+
if (e.seq > entry.seq) break;
|
|
1719
|
+
if (e.pushed) continue;
|
|
1720
|
+
e.pushed = true;
|
|
1721
|
+
if (e.timer) { clearTimeout(e.timer); e.timer = null; }
|
|
1722
|
+
try { e.push(); } catch {}
|
|
1723
|
+
}
|
|
1724
|
+
};
|
|
1725
|
+
const registerDeferredCard = (card) => {
|
|
1726
|
+
const entry = {
|
|
1727
|
+
seq: deferredSeqCounter++,
|
|
1728
|
+
pushed: false,
|
|
1729
|
+
timer: null,
|
|
1730
|
+
push: () => { card.pushed = true; if (card.spec) { card.spec.deferredDisplayReady = true; pushItem(card.spec); } },
|
|
1731
|
+
};
|
|
1732
|
+
card.deferred = entry;
|
|
1733
|
+
card.ensureVisible = () => flushDeferredUpTo(entry);
|
|
1734
|
+
deferredEntries.push(entry);
|
|
1735
|
+
entry.timer = setTimeout(() => {
|
|
1736
|
+
entry.timer = null;
|
|
1737
|
+
if (disposed) return;
|
|
1738
|
+
flushDeferredUpTo(entry);
|
|
1739
|
+
}, TOOL_CARD_PUSH_DELAY_MS);
|
|
1740
|
+
entry.timer.unref?.();
|
|
1741
|
+
};
|
|
1742
|
+
const registerDeferredAggregate = (aggregate) => {
|
|
1743
|
+
const entry = {
|
|
1744
|
+
seq: deferredSeqCounter++,
|
|
1745
|
+
pushed: false,
|
|
1746
|
+
timer: null,
|
|
1747
|
+
push: () => { aggregate.pushed = true; if (aggregate.pendingSpec) { aggregate.pendingSpec.deferredDisplayReady = true; pushItem(aggregate.pendingSpec); } },
|
|
1748
|
+
};
|
|
1749
|
+
aggregate.deferred = entry;
|
|
1750
|
+
aggregate.ensureVisible = () => flushDeferredUpTo(entry);
|
|
1751
|
+
deferredEntries.push(entry);
|
|
1752
|
+
entry.timer = setTimeout(() => {
|
|
1753
|
+
entry.timer = null;
|
|
1754
|
+
if (disposed) return;
|
|
1755
|
+
flushDeferredUpTo(entry);
|
|
1756
|
+
}, TOOL_CARD_PUSH_DELAY_MS);
|
|
1757
|
+
entry.timer.unref?.();
|
|
1758
|
+
};
|
|
1759
|
+
const clearDeferredTimers = () => {
|
|
1760
|
+
for (const e of deferredEntries) {
|
|
1761
|
+
if (e.timer) { clearTimeout(e.timer); e.timer = null; }
|
|
1762
|
+
}
|
|
1763
|
+
};
|
|
1094
1764
|
|
|
1095
1765
|
const markPromptCommitted = () => {
|
|
1096
1766
|
if (activePromptRestore) {
|
|
@@ -1109,9 +1779,13 @@ export async function createEngineSession({
|
|
|
1109
1779
|
const ids = new Set();
|
|
1110
1780
|
for (const card of toolCards || []) {
|
|
1111
1781
|
if (card?.itemId) ids.add(card.itemId);
|
|
1782
|
+
// Seal not-yet-pushed specs too, so a card that pushes later (timer)
|
|
1783
|
+
// enters already-finalized instead of flashing the active header form.
|
|
1784
|
+
if (card && card.pushed === false && card.spec) card.spec.headerFinalized = true;
|
|
1112
1785
|
}
|
|
1113
1786
|
for (const aggregate of aggregateCards || []) {
|
|
1114
1787
|
if (aggregate?.itemId) ids.add(aggregate.itemId);
|
|
1788
|
+
if (aggregate && aggregate.pushed === false && aggregate.pendingSpec) aggregate.pendingSpec.headerFinalized = true;
|
|
1115
1789
|
}
|
|
1116
1790
|
if (ids.size === 0) return false;
|
|
1117
1791
|
let changed = false;
|
|
@@ -1128,13 +1802,15 @@ export async function createEngineSession({
|
|
|
1128
1802
|
for (const aggregate of aggregateCards) {
|
|
1129
1803
|
const allCalls = [...aggregate.calls.values()];
|
|
1130
1804
|
if (allCalls.length === 0) continue;
|
|
1805
|
+
aggregate.ensureVisible?.();
|
|
1131
1806
|
const errors = allCalls.filter((r) => r.isError).length;
|
|
1132
1807
|
const summaries = aggregateSummaries(aggregate);
|
|
1133
|
-
const detailText =
|
|
1808
|
+
const detailText = aggregateDisplayDetail(summaries, allCalls);
|
|
1134
1809
|
const rawResult = aggregateRawResult(allCalls);
|
|
1810
|
+
const displayDetail = toolAggregateDetailFallback(detailText, rawResult);
|
|
1135
1811
|
patchItem(aggregate.itemId, {
|
|
1136
|
-
result:
|
|
1137
|
-
text:
|
|
1812
|
+
result: displayDetail,
|
|
1813
|
+
text: displayDetail,
|
|
1138
1814
|
rawResult: rawResult || null,
|
|
1139
1815
|
isError: errors > 0,
|
|
1140
1816
|
count: allCalls.length,
|
|
@@ -1151,8 +1827,38 @@ export async function createEngineSession({
|
|
|
1151
1827
|
openAggregateCard = null;
|
|
1152
1828
|
};
|
|
1153
1829
|
|
|
1830
|
+
const isAggregateTail = (aggregate) => {
|
|
1831
|
+
if (!aggregate?.itemId) return false;
|
|
1832
|
+
const last = state.items[state.items.length - 1];
|
|
1833
|
+
return last?.kind === 'tool' && last.aggregate === true && last.id === aggregate.itemId;
|
|
1834
|
+
};
|
|
1835
|
+
|
|
1836
|
+
const rememberActiveAggregate = (aggregate) => {
|
|
1837
|
+
if (!aggregate) return;
|
|
1838
|
+
if (!aggregateCards.includes(aggregate)) aggregateCards.push(aggregate);
|
|
1839
|
+
aggregateByBucket.set(aggregate.bucket, aggregate);
|
|
1840
|
+
};
|
|
1841
|
+
|
|
1154
1842
|
const ensureAggregateCard = (bucket) => {
|
|
1155
|
-
|
|
1843
|
+
// Reuse the open aggregate when it is either still the transcript tail OR
|
|
1844
|
+
// has not been pushed yet. The not-yet-pushed case matters for parallel
|
|
1845
|
+
// tool batches: every call in one onToolCall batch is collected first and
|
|
1846
|
+
// the card is pushed once afterward (syncAggregateHeader), so the 2nd+
|
|
1847
|
+
// same-bucket call in the SAME batch must merge even though the card is
|
|
1848
|
+
// not yet visible in state.items (isAggregateTail would be false).
|
|
1849
|
+
if (openAggregateCard?.bucket === bucket
|
|
1850
|
+
&& (!openAggregateCard.pushed || isAggregateTail(openAggregateCard))) return openAggregateCard;
|
|
1851
|
+
// If the previous aggregate was finalized/closed but is still the tail of
|
|
1852
|
+
// the transcript (or was never pushed), continue that exact card instead
|
|
1853
|
+
// of pushing a duplicate directly below it. Never reach past a visible
|
|
1854
|
+
// assistant/tool/status item: that would make an older card's count change
|
|
1855
|
+
// "in the middle" of history.
|
|
1856
|
+
const tailAggregate = aggregateByBucket.get(bucket);
|
|
1857
|
+
if (tailAggregate && (!tailAggregate.pushed || isAggregateTail(tailAggregate))) {
|
|
1858
|
+
openAggregateCard = tailAggregate;
|
|
1859
|
+
rememberActiveAggregate(tailAggregate);
|
|
1860
|
+
return tailAggregate;
|
|
1861
|
+
}
|
|
1156
1862
|
const itemId = nextId();
|
|
1157
1863
|
const aggregate = {
|
|
1158
1864
|
itemId,
|
|
@@ -1164,7 +1870,10 @@ export async function createEngineSession({
|
|
|
1164
1870
|
pushed: false,
|
|
1165
1871
|
startedAt: Date.now(),
|
|
1166
1872
|
};
|
|
1167
|
-
|
|
1873
|
+
// Arm the deferred push once at creation; syncAggregateHeader only keeps
|
|
1874
|
+
// pendingSpec current until the timer/result flushes it in call order.
|
|
1875
|
+
registerDeferredAggregate(aggregate);
|
|
1876
|
+
rememberActiveAggregate(aggregate);
|
|
1168
1877
|
openAggregateCard = aggregate;
|
|
1169
1878
|
return aggregate;
|
|
1170
1879
|
};
|
|
@@ -1174,15 +1883,17 @@ export async function createEngineSession({
|
|
|
1174
1883
|
const patch = {
|
|
1175
1884
|
args: { categoryOrder: aggregate.categoryOrder.slice() },
|
|
1176
1885
|
count: aggregate.calls.size,
|
|
1177
|
-
completedCount: [...aggregate.calls.values()].filter((r) => r.resolved).length,
|
|
1886
|
+
completedCount: [...aggregate.calls.values()].filter((r) => r.resolved || r.completedEarly).length,
|
|
1178
1887
|
categories: Object.fromEntries(aggregate.categories),
|
|
1179
1888
|
};
|
|
1180
1889
|
if (aggregate.pushed) {
|
|
1181
1890
|
patchItem(aggregate.itemId, patch);
|
|
1182
1891
|
return;
|
|
1183
1892
|
}
|
|
1184
|
-
|
|
1185
|
-
|
|
1893
|
+
// Not yet visible: keep the latest header spec current. The deferred entry
|
|
1894
|
+
// (armed at creation) pushes pendingSpec when its timer fires or a result
|
|
1895
|
+
// forces it visible, preserving call order via flushDeferredUpTo.
|
|
1896
|
+
aggregate.pendingSpec = {
|
|
1186
1897
|
kind: 'tool',
|
|
1187
1898
|
id: aggregate.itemId,
|
|
1188
1899
|
name: '__aggregate__',
|
|
@@ -1194,14 +1905,20 @@ export async function createEngineSession({
|
|
|
1194
1905
|
expanded: false,
|
|
1195
1906
|
headerFinalized: false,
|
|
1196
1907
|
startedAt: aggregate.startedAt || Date.now(),
|
|
1197
|
-
}
|
|
1908
|
+
};
|
|
1198
1909
|
};
|
|
1199
1910
|
|
|
1200
|
-
const ensureAssistant = () => {
|
|
1911
|
+
const ensureAssistant = (initialText = '') => {
|
|
1201
1912
|
if (!currentAssistantId) {
|
|
1202
1913
|
currentAssistantId = nextId();
|
|
1203
|
-
currentAssistantText
|
|
1204
|
-
|
|
1914
|
+
// Do NOT reset currentAssistantText here. The first onTextDelta has
|
|
1915
|
+
// already accumulated the opening chunk before this batched flush runs;
|
|
1916
|
+
// wiping it dropped the leading characters and forced a later set() to
|
|
1917
|
+
// re-add them. Segment resets are owned by closeAssistantSegment().
|
|
1918
|
+
// Seed the new row with the already-visible text so the ● gutter and the
|
|
1919
|
+
// first body line appear in the SAME set()/emit() — no empty "●-only"
|
|
1920
|
+
// row that scrolls once on its own and again when the body lands.
|
|
1921
|
+
pushItem({ kind: 'assistant', id: currentAssistantId, text: String(initialText || ''), streaming: true });
|
|
1205
1922
|
}
|
|
1206
1923
|
return currentAssistantId;
|
|
1207
1924
|
};
|
|
@@ -1211,6 +1928,19 @@ export async function createEngineSession({
|
|
|
1211
1928
|
currentAssistantText = '';
|
|
1212
1929
|
};
|
|
1213
1930
|
|
|
1931
|
+
const commitAssistantSegment = ({ sealToolBlock = false } = {}) => {
|
|
1932
|
+
const text = currentAssistantText || '';
|
|
1933
|
+
if (!text.trim()) {
|
|
1934
|
+
closeAssistantSegment();
|
|
1935
|
+
return false;
|
|
1936
|
+
}
|
|
1937
|
+
if (sealToolBlock) clearAggregateContinuation();
|
|
1938
|
+
const id = currentAssistantId || ensureAssistant(text);
|
|
1939
|
+
patchItem(id, { text, streaming: false });
|
|
1940
|
+
closeAssistantSegment();
|
|
1941
|
+
return true;
|
|
1942
|
+
};
|
|
1943
|
+
|
|
1214
1944
|
const startThinkingSegment = () => {
|
|
1215
1945
|
const now = Date.now();
|
|
1216
1946
|
if (!thinkingStartedAt) thinkingStartedAt = now;
|
|
@@ -1232,11 +1962,17 @@ export async function createEngineSession({
|
|
|
1232
1962
|
// batch accumulated text and flush at most once per STREAM_BATCH_INTERVAL_MS
|
|
1233
1963
|
// (≈16ms / 60fps cap). A forced flush happens before any tool call,
|
|
1234
1964
|
// finalization, or error so those code paths see the correct text state.
|
|
1235
|
-
|
|
1965
|
+
// Flush cadence for streamed text/thinking. 8ms (~120fps) matches the Ink
|
|
1966
|
+
// render maxFps (index.jsx render({ maxFps: 120 })), so a queued batch is
|
|
1967
|
+
// never held back waiting for the next Ink frame. 16ms (~60fps) left every
|
|
1968
|
+
// other Ink frame idle, which made fast provider streams visibly land in
|
|
1969
|
+
// coarse chunks ("10 chars at a time").
|
|
1970
|
+
const STREAM_BATCH_INTERVAL_MS = 8;
|
|
1236
1971
|
let _batchTimer = null;
|
|
1237
1972
|
let _pendingTextFlush = false; // true when a text/spinner update is queued
|
|
1238
1973
|
let _pendingThinkFlush = false; // true when a thinking update is queued
|
|
1239
1974
|
let _pendingThinkingLastEndedAt = 0;
|
|
1975
|
+
let compactingActive = false;
|
|
1240
1976
|
|
|
1241
1977
|
const flushStreamBatch = () => {
|
|
1242
1978
|
if (_batchTimer !== null) {
|
|
@@ -1245,22 +1981,45 @@ export async function createEngineSession({
|
|
|
1245
1981
|
}
|
|
1246
1982
|
if (_pendingTextFlush) {
|
|
1247
1983
|
_pendingTextFlush = false;
|
|
1248
|
-
|
|
1249
|
-
//
|
|
1250
|
-
//
|
|
1984
|
+
// Show only COMPLETED lines while streaming. The in-progress trailing
|
|
1985
|
+
// line stays hidden until its '\n' arrives, so the visible text never
|
|
1986
|
+
// grows a glyph at a time (no "무"→pause→"무슨 일…" partial reveal, no
|
|
1987
|
+
// CJK-width reflow jitter). The final non-streaming patch
|
|
1988
|
+
// (streaming:false) always carries the full text, so the tail line that
|
|
1989
|
+
// never got a newline still lands once at finalize.
|
|
1990
|
+
const newlineIdx = currentAssistantText.lastIndexOf('\n');
|
|
1991
|
+
const streamingVisibleText = newlineIdx >= 0
|
|
1992
|
+
? currentAssistantText.slice(0, newlineIdx + 1)
|
|
1993
|
+
: '';
|
|
1251
1994
|
const patch = {};
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1995
|
+
// Do NOT create the assistant row (and scroll the transcript) before
|
|
1996
|
+
// there is a completed line with VISIBLE content to show. Until the
|
|
1997
|
+
// first '\n' the only pending state is the spinner; the row appears
|
|
1998
|
+
// together with its first visible line, so no empty "●-only" row
|
|
1999
|
+
// flashes/scrolls ahead of text. `.trim()` also guards the
|
|
2000
|
+
// whitespace-only case: a response that opens with leading newlines
|
|
2001
|
+
// ("\n\n# …") completes a blank line first, whose estimated height
|
|
2002
|
+
// still reserves rows and scrolls the transcript, but Markdown trims
|
|
2003
|
+
// the body to nothing — so the scroll advances onto an empty band for
|
|
2004
|
+
// a few seconds until a non-blank line lands. Don't create the row
|
|
2005
|
+
// until there is real content to paint.
|
|
2006
|
+
if (currentAssistantId || streamingVisibleText.trim()) {
|
|
2007
|
+
const id = ensureAssistant(streamingVisibleText);
|
|
2008
|
+
// Emit the accumulated assistant text and spinner update together so a
|
|
2009
|
+
// streaming batch costs one set() → one emit() → one React reconcile.
|
|
2010
|
+
const index = state.items.findIndex((it) => it.id === id);
|
|
2011
|
+
if (index >= 0) {
|
|
2012
|
+
const current = state.items[index];
|
|
2013
|
+
if (!Object.is(current.text, streamingVisibleText) || current.streaming !== true) {
|
|
2014
|
+
const items = state.items.slice();
|
|
2015
|
+
items[index] = { ...current, text: streamingVisibleText, streaming: true };
|
|
2016
|
+
patch.items = items;
|
|
2017
|
+
}
|
|
1259
2018
|
}
|
|
1260
2019
|
}
|
|
1261
2020
|
const responseLengthVal = assistantText.length + thinkingText.length;
|
|
1262
2021
|
if (state.spinner) {
|
|
1263
|
-
patch.spinner = { ...state.spinner, responseLength: responseLengthVal, thinking: false, thinkingLastEndedAt: _pendingThinkingLastEndedAt || state.spinner.thinkingLastEndedAt, mode: 'responding' };
|
|
2022
|
+
patch.spinner = { ...state.spinner, responseLength: responseLengthVal, thinking: false, thinkingLastEndedAt: _pendingThinkingLastEndedAt || state.spinner.thinkingLastEndedAt, mode: compactingActive ? 'compacting' : 'responding' };
|
|
1264
2023
|
}
|
|
1265
2024
|
if (Object.keys(patch).length > 0) set(patch);
|
|
1266
2025
|
_pendingThinkingLastEndedAt = 0;
|
|
@@ -1269,9 +2028,11 @@ export async function createEngineSession({
|
|
|
1269
2028
|
_pendingThinkFlush = false;
|
|
1270
2029
|
const responseLengthVal = assistantText.length + thinkingText.length;
|
|
1271
2030
|
const thinkingElapsedMs = accumulatedThinkingMs + (thinkingSegmentStartedAt ? Math.max(0, Date.now() - thinkingSegmentStartedAt) : 0);
|
|
1272
|
-
const patch = { thinking: thinkingText };
|
|
2031
|
+
const patch = { thinking: compactingActive ? null : thinkingText };
|
|
1273
2032
|
if (state.spinner) {
|
|
1274
|
-
patch.spinner =
|
|
2033
|
+
patch.spinner = compactingActive
|
|
2034
|
+
? { ...state.spinner, responseLength: responseLengthVal, thinking: false, thinkingAccumulatedMs: accumulatedThinkingMs, thinkingElapsedMs, thinkingLastEndedAt: state.spinner.thinkingLastEndedAt || 0, mode: 'compacting' }
|
|
2035
|
+
: { ...state.spinner, responseLength: responseLengthVal, thinking: true, thinkingStartedAt, thinkingSegmentStartedAt, thinkingAccumulatedMs: accumulatedThinkingMs, thinkingElapsedMs, thinkingLastEndedAt: 0, mode: 'thinking' };
|
|
1275
2036
|
}
|
|
1276
2037
|
set(patch);
|
|
1277
2038
|
}
|
|
@@ -1283,10 +2044,87 @@ export async function createEngineSession({
|
|
|
1283
2044
|
if (_batchTimer?.unref) _batchTimer.unref(); // don't prevent process exit
|
|
1284
2045
|
};
|
|
1285
2046
|
|
|
2047
|
+
// __earlyNotify: show 1-line summary + completedCount immediately; defer
|
|
2048
|
+
// rawResult/expand and resultsDone to the history flush.
|
|
2049
|
+
const markToolCardCompletedState = (callId, message) => {
|
|
2050
|
+
const card = cardByCallId.get(callId);
|
|
2051
|
+
if (!card) return;
|
|
2052
|
+
const aggregate = card.aggregate;
|
|
2053
|
+
if (aggregate && card.itemId === aggregate.itemId) {
|
|
2054
|
+
const callRec = aggregate.calls.get(callId);
|
|
2055
|
+
if (!callRec || callRec.resolved || callRec.completedEarly) return;
|
|
2056
|
+
aggregate.ensureVisible?.();
|
|
2057
|
+
const rawText = toolResultText(message?.content);
|
|
2058
|
+
const isError = message?.isError === true || message?.toolKind === 'error' || /^\s*\[?error/i.test(rawText) || isErrorToolStatus(toolResultStatus(rawText));
|
|
2059
|
+
const text = isError ? toolErrorDisplay(rawText, callRec.name || 'tool') : rawText;
|
|
2060
|
+
callRec.summary = !isError ? summarizeToolResult(callRec.name, callRec.args, rawText, isError) : null;
|
|
2061
|
+
assignAggregateSummaryOrder(aggregate, callRec);
|
|
2062
|
+
callRec.isError = isError;
|
|
2063
|
+
callRec.resultText = text;
|
|
2064
|
+
callRec.completedEarly = true;
|
|
2065
|
+
const allCalls = [...aggregate.calls.values()];
|
|
2066
|
+
const completedCount = allCalls.filter((r) => r.resolved || r.completedEarly).length;
|
|
2067
|
+
const errors = allCalls.filter((r) => r.isError).length;
|
|
2068
|
+
const summaries = aggregateSummaries(aggregate);
|
|
2069
|
+
let detailText;
|
|
2070
|
+
if (errors > 0 && summaries.length === 0) {
|
|
2071
|
+
const succeeded = completedCount - errors;
|
|
2072
|
+
detailText = succeeded > 0 ? `${succeeded} Ok · ${errors} Failed` : `${errors} Failed`;
|
|
2073
|
+
} else {
|
|
2074
|
+
detailText = aggregateDisplayDetail(summaries, allCalls);
|
|
2075
|
+
if (errors > 0) {
|
|
2076
|
+
detailText = detailText ? `${detailText} · ${errors} Failed` : `${errors} Failed`;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
const rawResult = aggregateRawResult(allCalls);
|
|
2080
|
+
const displayDetail = toolAggregateDetailFallback(detailText, rawResult);
|
|
2081
|
+
const currentItem = state.items.find((it) => it.id === card.itemId);
|
|
2082
|
+
const visualCompleted = Math.max(
|
|
2083
|
+
completedCount,
|
|
2084
|
+
Math.min(allCalls.length, Number(currentItem?.completedCount || 0)),
|
|
2085
|
+
);
|
|
2086
|
+
const patch = {
|
|
2087
|
+
result: displayDetail,
|
|
2088
|
+
text: displayDetail,
|
|
2089
|
+
isError: errors > 0,
|
|
2090
|
+
errorCount: errors,
|
|
2091
|
+
count: allCalls.length,
|
|
2092
|
+
completedCount: visualCompleted,
|
|
2093
|
+
};
|
|
2094
|
+
if (visualCompleted >= allCalls.length) {
|
|
2095
|
+
patch.completedAt = Number(currentItem?.completedAt) || Date.now();
|
|
2096
|
+
}
|
|
2097
|
+
patchItem(card.itemId, patch);
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
// Non-aggregate eager tools are rare; flipping completedCount without
|
|
2101
|
+
// result changes pending/detail rendering and risks row jitter — wait for
|
|
2102
|
+
// the real history flush (unchanged behavior).
|
|
2103
|
+
};
|
|
2104
|
+
|
|
2105
|
+
const deliverToolResultMessage = (message) => {
|
|
2106
|
+
if (message?.__earlyNotify === true) {
|
|
2107
|
+
const earlyCallId = toolResultCallId(message);
|
|
2108
|
+
if (earlyCallId) markToolCardCompletedState(earlyCallId, message);
|
|
2109
|
+
return;
|
|
2110
|
+
}
|
|
2111
|
+
flushToolResults([message], toolCards, cardByCallId, toolGroups, resultsDone);
|
|
2112
|
+
};
|
|
2113
|
+
|
|
1286
2114
|
try {
|
|
1287
2115
|
const { result, session } = await runtime.ask(userText, {
|
|
1288
2116
|
drainSteering: () => drainPendingSteering(),
|
|
1289
2117
|
onSteerMessage: (text) => {
|
|
2118
|
+
// Steering can be injected after a terminal no-tool response has
|
|
2119
|
+
// already streamed but before runTurn finalizes. Seal the current
|
|
2120
|
+
// assistant segment first so the steered user turn and the next
|
|
2121
|
+
// assistant response do not get visually merged into one bubble.
|
|
2122
|
+
flushStreamBatch();
|
|
2123
|
+
if (currentAssistantId) {
|
|
2124
|
+
patchItem(currentAssistantId, { text: currentAssistantText || assistantText, streaming: false });
|
|
2125
|
+
closeAssistantSegment();
|
|
2126
|
+
}
|
|
2127
|
+
assistantText = '';
|
|
1290
2128
|
const value = String(text || '').trim();
|
|
1291
2129
|
if (value) {
|
|
1292
2130
|
finalizeToolHeaders();
|
|
@@ -1295,17 +2133,20 @@ export async function createEngineSession({
|
|
|
1295
2133
|
},
|
|
1296
2134
|
onToolCall: async (_iter, calls) => {
|
|
1297
2135
|
markPromptCommitted();
|
|
2136
|
+
// Always flush any buffered mid-turn assistant text before the tool
|
|
2137
|
+
// card appears. Without this, when neither a thinking panel nor a
|
|
2138
|
+
// spinner is active the buffered text was dropped by the following
|
|
2139
|
+
// closeAssistantSegment(), so the message above the tool card vanished.
|
|
2140
|
+
flushStreamBatch();
|
|
1298
2141
|
if (thinkingText && state.thinking) {
|
|
1299
2142
|
const thinkingLastEndedAt = closeThinkingSegment();
|
|
1300
|
-
flushStreamBatch(); // flush any buffered text/thinking before the tool card appears
|
|
1301
2143
|
set({ thinking: null, spinner: state.spinner ? { ...state.spinner, thinking: false, thinkingAccumulatedMs: accumulatedThinkingMs, thinkingLastEndedAt, mode: 'tool-use' } : state.spinner });
|
|
1302
2144
|
} else if (state.spinner) {
|
|
1303
|
-
flushStreamBatch(); // flush any buffered text before the tool card appears
|
|
1304
2145
|
set({ spinner: { ...state.spinner, mode: 'tool-use' } });
|
|
1305
2146
|
}
|
|
1306
2147
|
const batchCalls = (calls || []).filter(Boolean);
|
|
1307
2148
|
if (batchCalls.length === 0) return;
|
|
1308
|
-
|
|
2149
|
+
commitAssistantSegment({ sealToolBlock: true });
|
|
1309
2150
|
|
|
1310
2151
|
const touchedAggregates = new Set();
|
|
1311
2152
|
for (let i = 0; i < batchCalls.length; i++) {
|
|
@@ -1314,26 +2155,37 @@ export async function createEngineSession({
|
|
|
1314
2155
|
const args = toolCallArgs(c);
|
|
1315
2156
|
const category = classifyToolCategory(name, args);
|
|
1316
2157
|
const bucket = aggregateBucketForCategory(category);
|
|
2158
|
+
const categoryEntry = aggregateToolCategoryEntry(name, args, category);
|
|
1317
2159
|
const callId = toolCallId(c);
|
|
1318
2160
|
const callKey = callId || `__tool_${toolCards.length}_${i}`;
|
|
1319
2161
|
|
|
1320
2162
|
if (!bucket) {
|
|
1321
2163
|
openAggregateCard = null;
|
|
1322
2164
|
const itemId = nextId();
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
2165
|
+
// Defer the visible push: hold the spec and only enter the
|
|
2166
|
+
// transcript when the real header/detail will paint (delay
|
|
2167
|
+
// elapsed) or its result lands first. Avoids reserving blank
|
|
2168
|
+
// placeholder height that scrolls the body ahead of the glyphs.
|
|
2169
|
+
const card = {
|
|
2170
|
+
itemId,
|
|
2171
|
+
callId: callKey,
|
|
2172
|
+
done: false,
|
|
2173
|
+
pushed: false,
|
|
2174
|
+
spec: {
|
|
2175
|
+
kind: 'tool',
|
|
2176
|
+
id: itemId,
|
|
2177
|
+
name,
|
|
2178
|
+
args,
|
|
2179
|
+
result: null,
|
|
2180
|
+
isError: false,
|
|
2181
|
+
expanded: false,
|
|
2182
|
+
headerFinalized: false,
|
|
2183
|
+
count: 1,
|
|
2184
|
+
completedCount: 0,
|
|
2185
|
+
startedAt: Date.now(),
|
|
2186
|
+
},
|
|
2187
|
+
};
|
|
2188
|
+
registerDeferredCard(card);
|
|
1337
2189
|
if (callId) {
|
|
1338
2190
|
cardByCallId.set(callId, card);
|
|
1339
2191
|
}
|
|
@@ -1342,9 +2194,13 @@ export async function createEngineSession({
|
|
|
1342
2194
|
}
|
|
1343
2195
|
|
|
1344
2196
|
const aggregateCard = ensureAggregateCard(bucket);
|
|
1345
|
-
if (!aggregateCard.categories.has(
|
|
1346
|
-
|
|
1347
|
-
aggregateCard.
|
|
2197
|
+
if (!aggregateCard.categories.has(categoryEntry.key)) aggregateCard.categoryOrder.push(categoryEntry.key);
|
|
2198
|
+
const prevCategory = aggregateCard.categories.get(categoryEntry.key);
|
|
2199
|
+
aggregateCard.categories.set(categoryEntry.key, {
|
|
2200
|
+
...categoryEntry,
|
|
2201
|
+
count: Number(prevCategory?.count || 0) + Number(categoryEntry.count || 1),
|
|
2202
|
+
});
|
|
2203
|
+
aggregateCard.calls.set(callKey, { name, args, category, summary: null, summarySeq: null, isError: false, resultText: null, resolved: false, completedEarly: false });
|
|
1348
2204
|
touchedAggregates.add(aggregateCard);
|
|
1349
2205
|
const card = { itemId: aggregateCard.itemId, callId: callKey, done: false, aggregate: aggregateCard };
|
|
1350
2206
|
if (callId) {
|
|
@@ -1356,21 +2212,62 @@ export async function createEngineSession({
|
|
|
1356
2212
|
for (const aggregateCard of touchedAggregates) {
|
|
1357
2213
|
syncAggregateHeader(aggregateCard);
|
|
1358
2214
|
}
|
|
2215
|
+
for (const [bufferedCallId, bufferedMessage] of earlyResultBuffer) {
|
|
2216
|
+
if (!cardByCallId.has(bufferedCallId)) continue;
|
|
2217
|
+
deliverToolResultMessage(bufferedMessage);
|
|
2218
|
+
earlyResultBuffer.delete(bufferedCallId);
|
|
2219
|
+
}
|
|
1359
2220
|
await yieldToRenderer();
|
|
1360
2221
|
},
|
|
1361
2222
|
onToolResult: (message) => {
|
|
1362
|
-
|
|
2223
|
+
const callId = toolResultCallId(message);
|
|
2224
|
+
if (callId && !cardByCallId.has(callId) && !resultsDone.has(callId)) {
|
|
2225
|
+
earlyResultBuffer.set(callId, message);
|
|
2226
|
+
return;
|
|
2227
|
+
}
|
|
2228
|
+
deliverToolResultMessage(message);
|
|
2229
|
+
},
|
|
2230
|
+
onToolApproval: async (request) => {
|
|
2231
|
+
markPromptCommitted();
|
|
2232
|
+
flushStreamBatch();
|
|
2233
|
+
if (state.spinner) set({ spinner: { ...state.spinner, mode: 'tool-approval' } });
|
|
2234
|
+
return await requestToolApproval(request);
|
|
2235
|
+
},
|
|
2236
|
+
onCompactEvent: (event) => {
|
|
2237
|
+
flushStreamBatch();
|
|
2238
|
+
pushItem({
|
|
2239
|
+
kind: 'statusdone',
|
|
2240
|
+
id: nextId(),
|
|
2241
|
+
label: compactEventLabel(event),
|
|
2242
|
+
detail: compactEventDetail(event),
|
|
2243
|
+
});
|
|
1363
2244
|
},
|
|
1364
2245
|
onStageChange: (stage) => {
|
|
1365
2246
|
if (!state.spinner) return;
|
|
1366
2247
|
const value = String(stage || '');
|
|
2248
|
+
if (value === 'compacting') {
|
|
2249
|
+
compactingActive = true;
|
|
2250
|
+
const thinkingLastEndedAt = closeThinkingSegment();
|
|
2251
|
+
_pendingThinkFlush = false;
|
|
2252
|
+
set({
|
|
2253
|
+
thinking: null,
|
|
2254
|
+
spinner: {
|
|
2255
|
+
...state.spinner,
|
|
2256
|
+
thinking: false,
|
|
2257
|
+
thinkingSegmentStartedAt: 0,
|
|
2258
|
+
thinkingAccumulatedMs: accumulatedThinkingMs,
|
|
2259
|
+
thinkingLastEndedAt: thinkingLastEndedAt || state.spinner.thinkingLastEndedAt || 0,
|
|
2260
|
+
mode: 'compacting',
|
|
2261
|
+
},
|
|
2262
|
+
});
|
|
2263
|
+
return;
|
|
2264
|
+
}
|
|
2265
|
+
if (value === 'requesting' || value === 'streaming') compactingActive = false;
|
|
1367
2266
|
const mode = value === 'requesting'
|
|
1368
2267
|
? 'requesting'
|
|
1369
2268
|
: value === 'streaming'
|
|
1370
2269
|
? (state.spinner.thinking ? 'thinking' : 'responding')
|
|
1371
|
-
:
|
|
1372
|
-
? 'compacting'
|
|
1373
|
-
: null;
|
|
2270
|
+
: null;
|
|
1374
2271
|
if (!mode || state.spinner.mode === mode) return;
|
|
1375
2272
|
set({ spinner: { ...state.spinner, mode } });
|
|
1376
2273
|
},
|
|
@@ -1380,15 +2277,40 @@ export async function createEngineSession({
|
|
|
1380
2277
|
markPromptCommitted();
|
|
1381
2278
|
const thinkingLastEndedAt = closeThinkingSegment();
|
|
1382
2279
|
if (state.thinking) set({ thinking: null }); // collapse thinking panel immediately, no batch delay
|
|
1383
|
-
if (textChunk.trim()) clearAggregateContinuation();
|
|
1384
2280
|
assistantText += textChunk;
|
|
1385
|
-
ensureAssistant(); // create the assistant item in state immediately so the slot exists
|
|
1386
2281
|
currentAssistantText += textChunk;
|
|
1387
|
-
// Accumulate text
|
|
2282
|
+
// Accumulate text and schedule a batched flush (≤1 render per
|
|
2283
|
+
// STREAM_BATCH_INTERVAL_MS). Without scheduling, mid-turn text only
|
|
2284
|
+
// surfaced via the tool-call/finalize flush, so a text→tool segment
|
|
2285
|
+
// with no spinner/thinking dropped the message above the tool card.
|
|
1388
2286
|
_pendingTextFlush = true;
|
|
1389
2287
|
if (thinkingLastEndedAt) _pendingThinkingLastEndedAt = thinkingLastEndedAt;
|
|
1390
2288
|
scheduleStreamFlush();
|
|
1391
2289
|
},
|
|
2290
|
+
onAssistantText: (text) => {
|
|
2291
|
+
// Mid-turn assistant text that precedes a tool call. Providers that
|
|
2292
|
+
// stream via onTextDelta already accumulated it into assistantText;
|
|
2293
|
+
// providers that only return the final response.content (no deltas)
|
|
2294
|
+
// never fired onTextDelta, so without this the preamble shows nothing
|
|
2295
|
+
// before the tool card. De-dup against already-streamed text so the
|
|
2296
|
+
// streaming path is unaffected.
|
|
2297
|
+
const full = String(text ?? '');
|
|
2298
|
+
if (!full.trim()) return;
|
|
2299
|
+
// If the streaming path already produced text for THIS segment,
|
|
2300
|
+
// onTextDelta owns the render — content is the same accumulated text
|
|
2301
|
+
// (or a superset), so skip to avoid double-printing the preamble.
|
|
2302
|
+
// Do not check turn-global assistantText: earlier closed preambles stay
|
|
2303
|
+
// there across tool calls, and would suppress later non-streaming
|
|
2304
|
+
// preambles even though currentAssistantText has been reset.
|
|
2305
|
+
if (currentAssistantText.trim()) return;
|
|
2306
|
+
markPromptCommitted();
|
|
2307
|
+
closeThinkingSegment();
|
|
2308
|
+
if (state.thinking) set({ thinking: null });
|
|
2309
|
+
assistantText += full;
|
|
2310
|
+
currentAssistantText += full;
|
|
2311
|
+
_pendingTextFlush = true;
|
|
2312
|
+
flushStreamBatch();
|
|
2313
|
+
},
|
|
1392
2314
|
onReasoningDelta: (chunk) => {
|
|
1393
2315
|
if (String(chunk ?? '')) markPromptCommitted();
|
|
1394
2316
|
startThinkingSegment();
|
|
@@ -1399,7 +2321,7 @@ export async function createEngineSession({
|
|
|
1399
2321
|
},
|
|
1400
2322
|
onUsageDelta: (delta) => {
|
|
1401
2323
|
applyUsageDelta(state.stats, delta);
|
|
1402
|
-
syncContextStats();
|
|
2324
|
+
syncContextStats({ allowEstimated: true });
|
|
1403
2325
|
const currentTurnInput = Math.max(0, state.stats.inputTokens - inputBaseline);
|
|
1404
2326
|
const currentTurnOutput = Math.max(0, state.stats.outputTokens - outputBaseline);
|
|
1405
2327
|
if (state.spinner) {
|
|
@@ -1409,24 +2331,30 @@ export async function createEngineSession({
|
|
|
1409
2331
|
}
|
|
1410
2332
|
},
|
|
1411
2333
|
});
|
|
2334
|
+
askResult = result;
|
|
1412
2335
|
markPromptCommitted();
|
|
1413
|
-
if (result?.postTurnCompact?.changed) autoCompactStatus = result.postTurnCompact;
|
|
1414
2336
|
|
|
1415
2337
|
flushToolResults(session?.messages || [], toolCards, cardByCallId, toolGroups, resultsDone, { finalize: true });
|
|
1416
2338
|
finalizeToolHeaders();
|
|
1417
2339
|
flushStreamBatch(); // force-flush any batched streaming text before finalization writes
|
|
1418
|
-
syncContextStats();
|
|
1419
|
-
|
|
1420
|
-
const finalText =
|
|
1421
|
-
if (
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
2340
|
+
syncContextStats({ allowEstimated: true });
|
|
2341
|
+
|
|
2342
|
+
const finalText = result?.content != null ? String(result.content) : '';
|
|
2343
|
+
if (finalText.trim()) {
|
|
2344
|
+
// The persisted transcript is written from the provider's final content,
|
|
2345
|
+
// while the live TUI row is fed by streaming deltas. If a provider/parser
|
|
2346
|
+
// misses or suppresses an early delta, keeping the streamed buffer here
|
|
2347
|
+
// leaves the final on-screen assistant row missing leading characters even
|
|
2348
|
+
// though the transcript is correct. Always reconcile the active segment to
|
|
2349
|
+
// the final provider text when it is available.
|
|
2350
|
+
const id = currentAssistantId || ensureAssistant(finalText);
|
|
1426
2351
|
currentAssistantText = finalText;
|
|
1427
2352
|
patchItem(id, { text: finalText, streaming: false });
|
|
2353
|
+
} else if (currentAssistantId && (currentAssistantText.trim() || assistantText.trim())) {
|
|
2354
|
+
const streamedText = currentAssistantText || assistantText;
|
|
2355
|
+
patchItem(currentAssistantId, { text: streamedText, streaming: false });
|
|
1428
2356
|
}
|
|
1429
|
-
|
|
2357
|
+
turnFinishedNormally = true;
|
|
1430
2358
|
} catch (error) {
|
|
1431
2359
|
flushStreamBatch(); // ensure any batched text lands before the error notice
|
|
1432
2360
|
if (error?.name === 'SessionClosedError') {
|
|
@@ -1446,6 +2374,17 @@ export async function createEngineSession({
|
|
|
1446
2374
|
pushNotice(toolErrorDisplay(error, 'turn'), 'error');
|
|
1447
2375
|
}
|
|
1448
2376
|
} finally {
|
|
2377
|
+
denyAllToolApprovals(cancelled ? 'turn cancelled' : 'turn finished');
|
|
2378
|
+
// Flush any still-deferred tool cards into the transcript and cancel their
|
|
2379
|
+
// pending push timers so nothing fires (or leaks) after the turn ends. The
|
|
2380
|
+
// finalize path above already patches results onto visible cards; this just
|
|
2381
|
+
// guarantees every registered card is materialized before the turn closes.
|
|
2382
|
+
if (deferredEntries.length) {
|
|
2383
|
+
const last = deferredEntries[deferredEntries.length - 1];
|
|
2384
|
+
if (last) flushDeferredUpTo(last);
|
|
2385
|
+
clearDeferredTimers();
|
|
2386
|
+
}
|
|
2387
|
+
const producedTranscriptItem = state.items.length > itemsAtTurnStart;
|
|
1449
2388
|
const reclaimed = cancelled && activePromptRestore?.reclaimed === true;
|
|
1450
2389
|
activePromptRestore = null;
|
|
1451
2390
|
closeThinkingSegment();
|
|
@@ -1453,25 +2392,24 @@ export async function createEngineSession({
|
|
|
1453
2392
|
const thinkingElapsedMs = thinkingStartedAt ? accumulatedThinkingMs : 0;
|
|
1454
2393
|
const finalOutputTokens = Math.max(0, Number(state.spinner?.outputTokens || 0), Math.round(Number(state.spinner?.responseLength || 0) / 4));
|
|
1455
2394
|
const turnStatus = cancelled ? 'cancelled' : 'done';
|
|
2395
|
+
const resultContent = askResult?.content != null ? String(askResult.content).trim() : '';
|
|
2396
|
+
const assistantOutput = (currentAssistantText || assistantText || '').trim();
|
|
2397
|
+
// Suppress only true pending-resume no-ops: no transcript items added and no model output; cancelled/error turns and any visible turn stay marked.
|
|
2398
|
+
const isNoOpTurn = turnFinishedNormally
|
|
2399
|
+
&& !cancelled
|
|
2400
|
+
&& toolCards.length === 0
|
|
2401
|
+
&& !resultContent
|
|
2402
|
+
&& !assistantOutput
|
|
2403
|
+
&& !producedTranscriptItem;
|
|
2404
|
+
if (!isNoOpTurn) {
|
|
2405
|
+
state.stats.turns = (state.stats.turns || 0) + 1;
|
|
2406
|
+
}
|
|
1456
2407
|
// Pin the post-think summary into the transcript right after this turn's
|
|
1457
2408
|
// output so it scrolls up with the answer and stays in the scrollback,
|
|
1458
|
-
//
|
|
2409
|
+
// in scrollback. (Previously TurnDone rendered only in the
|
|
1459
2410
|
// bottom-fixed live-status slot and vanished on the next turn.)
|
|
1460
|
-
if (!reclaimed) {
|
|
2411
|
+
if (!reclaimed && !isNoOpTurn) {
|
|
1461
2412
|
pushItem({ kind: 'turndone', id: nextId(), elapsedMs, status: turnStatus, outputTokens: finalOutputTokens, thinkingElapsedMs, verb: pickDoneVerb(turnIndex) });
|
|
1462
|
-
if (!cancelled && autoCompactStatus?.changed) {
|
|
1463
|
-
const before = formatTokenCount(autoCompactStatus.beforeTokens || autoCompactStatus.pressureTokens || 0);
|
|
1464
|
-
const after = formatTokenCount(autoCompactStatus.afterTokens || 0);
|
|
1465
|
-
const trigger = formatTokenCount(autoCompactStatus.triggerTokens || 0);
|
|
1466
|
-
pushItem({
|
|
1467
|
-
kind: 'statusdone',
|
|
1468
|
-
id: nextId(),
|
|
1469
|
-
label: 'Auto-compact complete',
|
|
1470
|
-
detail: [`context ${before}→${after}`, trigger !== '0' ? `trigger ${trigger}` : '']
|
|
1471
|
-
.filter(Boolean)
|
|
1472
|
-
.join(' · '),
|
|
1473
|
-
});
|
|
1474
|
-
}
|
|
1475
2413
|
}
|
|
1476
2414
|
set({
|
|
1477
2415
|
busy: false,
|
|
@@ -1481,8 +2419,9 @@ export async function createEngineSession({
|
|
|
1481
2419
|
stats: { ...state.stats },
|
|
1482
2420
|
...routeState(),
|
|
1483
2421
|
toolMode: runtime.toolMode,
|
|
1484
|
-
...
|
|
2422
|
+
...agentStatusState({ force: true }),
|
|
1485
2423
|
});
|
|
2424
|
+
flushDeferredExecutionPendingResumeKick();
|
|
1486
2425
|
}
|
|
1487
2426
|
return cancelled ? 'cancelled' : 'done';
|
|
1488
2427
|
}
|
|
@@ -1504,6 +2443,7 @@ export async function createEngineSession({
|
|
|
1504
2443
|
mode,
|
|
1505
2444
|
priority,
|
|
1506
2445
|
key: options.key || null,
|
|
2446
|
+
skipSlashCommands: options.skipSlashCommands === true,
|
|
1507
2447
|
displayText: mode === 'task-notification' ? notificationDisplayText(displayText) : String(displayText || ''),
|
|
1508
2448
|
};
|
|
1509
2449
|
}
|
|
@@ -1512,7 +2452,8 @@ export async function createEngineSession({
|
|
|
1512
2452
|
const ids = new Set(entries.map((entry) => entry.id));
|
|
1513
2453
|
const keys = entries.map((entry) => entry.key).filter(Boolean);
|
|
1514
2454
|
for (const key of keys) pendingNotificationKeys.delete(key);
|
|
1515
|
-
|
|
2455
|
+
const queued = state.queued.filter((q) => !ids.has(q.id));
|
|
2456
|
+
if (queued.length !== state.queued.length) set({ queued });
|
|
1516
2457
|
}
|
|
1517
2458
|
|
|
1518
2459
|
function requeueEntriesFront(entries) {
|
|
@@ -1531,16 +2472,19 @@ export async function createEngineSession({
|
|
|
1531
2472
|
}
|
|
1532
2473
|
if (restored.length === 0) return false;
|
|
1533
2474
|
pending.unshift(...restored);
|
|
1534
|
-
|
|
2475
|
+
const visible = restored.filter(isQueuedEntryVisible);
|
|
2476
|
+
if (visible.length > 0) set({ queued: [...visible, ...state.queued] });
|
|
1535
2477
|
return true;
|
|
1536
2478
|
}
|
|
1537
2479
|
|
|
1538
|
-
function dequeueQueueBatch(maxPriority = 'later') {
|
|
2480
|
+
function dequeueQueueBatch(maxPriority = 'later', options = {}) {
|
|
1539
2481
|
if (pending.length === 0) return [];
|
|
1540
2482
|
const max = queuePriorityValue(maxPriority);
|
|
2483
|
+
const predicate = typeof options.predicate === 'function' ? options.predicate : () => true;
|
|
1541
2484
|
let bestPriority = Infinity;
|
|
1542
2485
|
let targetMode = null;
|
|
1543
2486
|
for (const entry of pending) {
|
|
2487
|
+
if (!predicate(entry)) continue;
|
|
1544
2488
|
const p = queuePriorityValue(entry.priority);
|
|
1545
2489
|
if (p > max) continue;
|
|
1546
2490
|
if (p < bestPriority) {
|
|
@@ -1552,7 +2496,7 @@ export async function createEngineSession({
|
|
|
1552
2496
|
const batch = [];
|
|
1553
2497
|
for (let i = 0; i < pending.length;) {
|
|
1554
2498
|
const entry = pending[i];
|
|
1555
|
-
if ((entry.mode || 'prompt') === targetMode && queuePriorityValue(entry.priority) === bestPriority) {
|
|
2499
|
+
if (predicate(entry) && (entry.mode || 'prompt') === targetMode && queuePriorityValue(entry.priority) === bestPriority) {
|
|
1556
2500
|
batch.push(entry);
|
|
1557
2501
|
pending.splice(i, 1);
|
|
1558
2502
|
} else {
|
|
@@ -1568,7 +2512,7 @@ export async function createEngineSession({
|
|
|
1568
2512
|
draining = true;
|
|
1569
2513
|
try {
|
|
1570
2514
|
while (pending.length > 0) {
|
|
1571
|
-
// Drain one priority/mode bucket at a time
|
|
2515
|
+
// Drain one priority/mode bucket at a time (unified command queue):
|
|
1572
2516
|
// unified command queue semantics: prompt steering stays editable and
|
|
1573
2517
|
// task notifications stay non-editable but model-visible.
|
|
1574
2518
|
const batch = dequeueQueueBatch('later');
|
|
@@ -1576,6 +2520,7 @@ export async function createEngineSession({
|
|
|
1576
2520
|
const ids = new Set(batch.map((e) => e.id));
|
|
1577
2521
|
const merged = mergePromptContents(batch);
|
|
1578
2522
|
for (const entry of batch) {
|
|
2523
|
+
if (entry.mode === 'pending-resume') continue;
|
|
1579
2524
|
pushUserOrSyntheticItem(entry.text, entry.id);
|
|
1580
2525
|
}
|
|
1581
2526
|
const nonEditable = batch.filter((entry) => !isQueuedEntryEditable(entry));
|
|
@@ -1596,6 +2541,8 @@ export async function createEngineSession({
|
|
|
1596
2541
|
}
|
|
1597
2542
|
} finally {
|
|
1598
2543
|
draining = false;
|
|
2544
|
+
if (pending.length > 0) void drain();
|
|
2545
|
+
else flushDeferredExecutionPendingResumeKick();
|
|
1599
2546
|
}
|
|
1600
2547
|
}
|
|
1601
2548
|
function enqueue(text, options = {}) {
|
|
@@ -1605,13 +2552,17 @@ export async function createEngineSession({
|
|
|
1605
2552
|
pendingNotificationKeys.add(entry.key);
|
|
1606
2553
|
}
|
|
1607
2554
|
pending.push(entry);
|
|
1608
|
-
set({ queued: [...state.queued, entry] });
|
|
2555
|
+
if (isQueuedEntryVisible(entry)) set({ queued: [...state.queued, entry] });
|
|
1609
2556
|
void drain();
|
|
1610
2557
|
return true;
|
|
1611
2558
|
}
|
|
1612
2559
|
|
|
1613
2560
|
function drainPendingSteering() {
|
|
1614
|
-
|
|
2561
|
+
// Mid-turn steering drain:
|
|
2562
|
+
// getCommandsByMaxPriority('next') and exclude slash commands. Slash
|
|
2563
|
+
// commands must run through the normal command processor after the turn,
|
|
2564
|
+
// not be sent to the model as plain text.
|
|
2565
|
+
const batch = dequeueQueueBatch('next', { predicate: (entry) => !isSlashQueuedEntry(entry) });
|
|
1615
2566
|
if (batch.length === 0) return [];
|
|
1616
2567
|
const out = batch
|
|
1617
2568
|
.map((entry) => {
|
|
@@ -1631,24 +2582,20 @@ export async function createEngineSession({
|
|
|
1631
2582
|
async function autoClearBeforeSubmit() {
|
|
1632
2583
|
const cfg = autoClearState();
|
|
1633
2584
|
const now = Date.now();
|
|
1634
|
-
const
|
|
2585
|
+
const activityAt = sessionActivityTimestamp(runtime.session, lastUserActivityAt);
|
|
2586
|
+
const idleMs = activityAt ? now - activityAt : 0;
|
|
1635
2587
|
if (!cfg.enabled || state.busy || pending.length > 0 || autoClearRunning || idleMs < cfg.idleMs) {
|
|
1636
|
-
lastUserActivityAt = now;
|
|
2588
|
+
if (!activityAt) lastUserActivityAt = now;
|
|
1637
2589
|
return false;
|
|
1638
2590
|
}
|
|
1639
2591
|
autoClearRunning = true;
|
|
2592
|
+
const startedAt = Date.now();
|
|
2593
|
+
set({ commandStatus: { active: true, verb: 'Auto-clearing idle conversation', startedAt, mode: 'auto-clear' } });
|
|
1640
2594
|
try {
|
|
1641
|
-
|
|
1642
|
-
await runtime.clear();
|
|
2595
|
+
await runtime.clear({ compactType: cfg.compactType || null, requireCompactSuccess: !!cfg.compactType });
|
|
1643
2596
|
resetStats();
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
const thresholdLabel = formatIdleDuration(cfg.idleMs);
|
|
1647
|
-
const beforeTokens = Number(beforeContext?.usedTokens || beforeContext?.currentEstimatedTokens || beforeContext?.usage?.lastContextTokens || 0);
|
|
1648
|
-
const afterTokens = Number(afterContext?.usedTokens || afterContext?.currentEstimatedTokens || afterContext?.usage?.lastContextTokens || 0);
|
|
1649
|
-
const contextDetail = beforeTokens > 0 || afterTokens > 0
|
|
1650
|
-
? `context ${formatTokenCount(beforeTokens)}→${formatTokenCount(afterTokens)}`
|
|
1651
|
-
: 'context reset';
|
|
2597
|
+
clearUiActivityBeforeContextSync();
|
|
2598
|
+
syncContextStats({ allowEstimated: true });
|
|
1652
2599
|
set({
|
|
1653
2600
|
items: replaceItems([]),
|
|
1654
2601
|
toasts: [],
|
|
@@ -1663,17 +2610,23 @@ export async function createEngineSession({
|
|
|
1663
2610
|
kind: 'statusdone',
|
|
1664
2611
|
id: nextId(),
|
|
1665
2612
|
label: 'Auto-clear complete',
|
|
1666
|
-
detail:
|
|
1667
|
-
.filter(Boolean)
|
|
1668
|
-
.join(' · '),
|
|
2613
|
+
detail: formatElapsedSeconds(Date.now() - startedAt),
|
|
1669
2614
|
});
|
|
1670
2615
|
return true;
|
|
1671
2616
|
} catch (error) {
|
|
1672
|
-
|
|
2617
|
+
const message = presentErrorText(error, { surface: 'auto-clear' });
|
|
2618
|
+
pushItem({
|
|
2619
|
+
kind: 'statusdone',
|
|
2620
|
+
id: nextId(),
|
|
2621
|
+
label: 'Auto-clear skipped',
|
|
2622
|
+
detail: `conversation kept · ${message}`,
|
|
2623
|
+
});
|
|
2624
|
+
pushNotice(`auto-clear skipped: ${message}`, 'error');
|
|
1673
2625
|
return false;
|
|
1674
2626
|
} finally {
|
|
1675
2627
|
lastUserActivityAt = Date.now();
|
|
1676
2628
|
autoClearRunning = false;
|
|
2629
|
+
set({ commandStatus: null });
|
|
1677
2630
|
}
|
|
1678
2631
|
}
|
|
1679
2632
|
|
|
@@ -1698,6 +2651,63 @@ export async function createEngineSession({
|
|
|
1698
2651
|
state.stats = createSessionStats();
|
|
1699
2652
|
return state.stats;
|
|
1700
2653
|
};
|
|
2654
|
+
const clearUiActivityBeforeContextSync = () => {
|
|
2655
|
+
state.items = replaceItems([]);
|
|
2656
|
+
state.toasts = [];
|
|
2657
|
+
state.queued = [];
|
|
2658
|
+
state.thinking = null;
|
|
2659
|
+
state.spinner = null;
|
|
2660
|
+
state.lastTurn = null;
|
|
2661
|
+
state.busy = false;
|
|
2662
|
+
};
|
|
2663
|
+
const resetTuiForPendingSessionReset = () => {
|
|
2664
|
+
pendingSessionReset = true;
|
|
2665
|
+
clearUiActivityBeforeContextSync();
|
|
2666
|
+
resetStats();
|
|
2667
|
+
state.stats.currentContextTokens = 0;
|
|
2668
|
+
state.stats.currentEstimatedContextTokens = 0;
|
|
2669
|
+
state.stats.currentContextSource = null;
|
|
2670
|
+
state.stats.currentContextUpdatedAt = Date.now();
|
|
2671
|
+
state.displayContextWindow = 0;
|
|
2672
|
+
state.compactBoundaryTokens = 0;
|
|
2673
|
+
state.autoCompactTokenLimit = 0;
|
|
2674
|
+
};
|
|
2675
|
+
const snapshotTuiBeforeSessionReset = () => ({
|
|
2676
|
+
items: state.items.slice(),
|
|
2677
|
+
toasts: state.toasts.slice(),
|
|
2678
|
+
queued: state.queued.slice(),
|
|
2679
|
+
thinking: state.thinking,
|
|
2680
|
+
spinner: state.spinner,
|
|
2681
|
+
lastTurn: state.lastTurn,
|
|
2682
|
+
busy: state.busy,
|
|
2683
|
+
stats: { ...state.stats },
|
|
2684
|
+
sessionId: state.sessionId,
|
|
2685
|
+
});
|
|
2686
|
+
const restoreTuiAfterFailedSessionReset = (snapshot) => {
|
|
2687
|
+
if (!snapshot) return;
|
|
2688
|
+
pendingSessionReset = false;
|
|
2689
|
+
state.items = replaceItems(snapshot.items);
|
|
2690
|
+
state.toasts = snapshot.toasts.slice();
|
|
2691
|
+
state.queued = snapshot.queued.slice();
|
|
2692
|
+
state.thinking = snapshot.thinking;
|
|
2693
|
+
state.spinner = snapshot.spinner;
|
|
2694
|
+
state.lastTurn = snapshot.lastTurn;
|
|
2695
|
+
state.busy = snapshot.busy;
|
|
2696
|
+
state.stats = { ...snapshot.stats };
|
|
2697
|
+
syncContextStats({ allowEstimated: true });
|
|
2698
|
+
set({
|
|
2699
|
+
items: state.items,
|
|
2700
|
+
toasts: state.toasts,
|
|
2701
|
+
queued: state.queued,
|
|
2702
|
+
thinking: state.thinking,
|
|
2703
|
+
spinner: state.spinner,
|
|
2704
|
+
lastTurn: state.lastTurn,
|
|
2705
|
+
busy: state.busy,
|
|
2706
|
+
...routeState(),
|
|
2707
|
+
stats: { ...state.stats },
|
|
2708
|
+
...agentStatusState(),
|
|
2709
|
+
});
|
|
2710
|
+
};
|
|
1701
2711
|
const resetStatsAndSyncContext = () => {
|
|
1702
2712
|
resetStats();
|
|
1703
2713
|
syncContextStats({ allowEstimated: true });
|
|
@@ -1715,11 +2725,10 @@ export async function createEngineSession({
|
|
|
1715
2725
|
const t = promptDisplayText(text, options).trim();
|
|
1716
2726
|
if (!t || state.commandBusy) return false;
|
|
1717
2727
|
const mode = options.mode || 'prompt';
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
const priority = options.priority;
|
|
2728
|
+
// Prompt input queued while a turn is active keeps the
|
|
2729
|
+
// default `next` priority, so it is injected at the next tool/model
|
|
2730
|
+
// boundary. Explicit options.priority still wins.
|
|
2731
|
+
const priority = options.priority;
|
|
1723
2732
|
const queueOptions = {
|
|
1724
2733
|
...options,
|
|
1725
2734
|
mode,
|
|
@@ -1738,8 +2747,8 @@ export async function createEngineSession({
|
|
|
1738
2747
|
if (state.commandBusy) return false;
|
|
1739
2748
|
set({ commandBusy: true });
|
|
1740
2749
|
try {
|
|
1741
|
-
await runtime.setRoute({ model: m });
|
|
1742
|
-
|
|
2750
|
+
await runtime.setRoute({ model: m }, { applyToCurrentSession: true });
|
|
2751
|
+
syncContextStats({ allowEstimated: true });
|
|
1743
2752
|
set({ ...routeState(), stats: { ...state.stats } });
|
|
1744
2753
|
return true;
|
|
1745
2754
|
} finally {
|
|
@@ -1787,34 +2796,75 @@ export async function createEngineSession({
|
|
|
1787
2796
|
})
|
|
1788
2797
|
.catch((error) => pushNotice(toolErrorDisplay(error, 'tool'), 'error'));
|
|
1789
2798
|
},
|
|
1790
|
-
toggleBridgeMode: () => {
|
|
1791
|
-
const mode = runtime.toggleBridgeMode();
|
|
1792
|
-
set(bridgeStatusState({ force: true }));
|
|
1793
|
-
pushNotice(`bridge mode -> ${mode}`, 'info');
|
|
1794
|
-
return mode;
|
|
1795
|
-
},
|
|
1796
|
-
setBridgeMode: (mode) => {
|
|
1797
|
-
const next = runtime.setBridgeMode(mode);
|
|
1798
|
-
set(bridgeStatusState({ force: true }));
|
|
1799
|
-
return next;
|
|
1800
|
-
},
|
|
1801
2799
|
getAutoClear: () => autoClearState(),
|
|
1802
2800
|
setAutoClear: (input = {}) => {
|
|
1803
2801
|
const next = runtime.setAutoClear?.(input) || autoClearState();
|
|
1804
2802
|
set({ autoClear: next });
|
|
1805
2803
|
return next;
|
|
1806
2804
|
},
|
|
1807
|
-
|
|
2805
|
+
getProfile: () => runtime.getProfile?.() || { title: '', language: 'system', languages: [] },
|
|
2806
|
+
setProfile: (input = {}) => {
|
|
2807
|
+
const next = runtime.setProfile?.(input) || runtime.getProfile?.() || null;
|
|
2808
|
+
return next;
|
|
2809
|
+
},
|
|
2810
|
+
getCompactionSettings: () => {
|
|
2811
|
+
return runtime.getCompactionSettings?.() || {};
|
|
2812
|
+
},
|
|
2813
|
+
setCompactionSettings: async (input = {}) => {
|
|
2814
|
+
if (state.commandBusy) return null;
|
|
2815
|
+
set({ commandBusy: true });
|
|
2816
|
+
try {
|
|
2817
|
+
const next = runtime.setCompactionSettings?.(input) || {};
|
|
2818
|
+
syncContextStats({ allowEstimated: true });
|
|
2819
|
+
set({ ...routeState(), stats: { ...state.stats } });
|
|
2820
|
+
return next;
|
|
2821
|
+
} finally {
|
|
2822
|
+
set({ commandBusy: false });
|
|
2823
|
+
}
|
|
2824
|
+
},
|
|
2825
|
+
getMemorySettings: () => {
|
|
2826
|
+
return runtime.getMemorySettings?.() || { enabled: true };
|
|
2827
|
+
},
|
|
2828
|
+
setMemoryEnabled: async (enabled) => {
|
|
1808
2829
|
if (state.commandBusy) return null;
|
|
1809
2830
|
set({ commandBusy: true });
|
|
1810
2831
|
try {
|
|
1811
|
-
const
|
|
1812
|
-
|
|
2832
|
+
const next = await runtime.setMemoryEnabled?.(enabled);
|
|
2833
|
+
syncContextStats({ allowEstimated: true });
|
|
2834
|
+
set({ ...routeState(), stats: { ...state.stats } });
|
|
2835
|
+
return next;
|
|
2836
|
+
} finally {
|
|
2837
|
+
set({ commandBusy: false });
|
|
2838
|
+
}
|
|
2839
|
+
},
|
|
2840
|
+
getChannelSettings: (options = {}) => {
|
|
2841
|
+
return runtime.getChannelSettings?.(options) || {
|
|
2842
|
+
enabled: true,
|
|
2843
|
+
...(options?.includeStatus === false ? {} : { status: runtime.getChannelWorkerStatus?.() }),
|
|
2844
|
+
};
|
|
2845
|
+
},
|
|
2846
|
+
setChannelsEnabled: async (enabled) => {
|
|
2847
|
+
if (state.commandBusy) return null;
|
|
2848
|
+
set({ commandBusy: true });
|
|
2849
|
+
try {
|
|
2850
|
+
const next = await runtime.setChannelsEnabled?.(enabled);
|
|
2851
|
+
set({ ...routeState(), stats: { ...state.stats } });
|
|
2852
|
+
return next;
|
|
2853
|
+
} finally {
|
|
2854
|
+
set({ commandBusy: false });
|
|
2855
|
+
}
|
|
2856
|
+
},
|
|
2857
|
+
agentControl: async (args = {}) => {
|
|
2858
|
+
if (state.commandBusy) return null;
|
|
2859
|
+
set({ commandBusy: true });
|
|
2860
|
+
try {
|
|
2861
|
+
const result = await runtime.agentControl(args);
|
|
2862
|
+
const text = String(result ?? '').trim();
|
|
1813
2863
|
const itemId = nextId();
|
|
1814
2864
|
pushItem({
|
|
1815
2865
|
kind: 'tool',
|
|
1816
2866
|
id: itemId,
|
|
1817
|
-
name: '
|
|
2867
|
+
name: 'agent',
|
|
1818
2868
|
args,
|
|
1819
2869
|
result: null,
|
|
1820
2870
|
isError: false,
|
|
@@ -1823,8 +2873,8 @@ export async function createEngineSession({
|
|
|
1823
2873
|
completedCount: 0,
|
|
1824
2874
|
startedAt: Date.now(),
|
|
1825
2875
|
});
|
|
1826
|
-
|
|
1827
|
-
set(
|
|
2876
|
+
updateAgentJobCard(itemId, text, /^error:/i.test(text));
|
|
2877
|
+
set(agentStatusState({ force: true }));
|
|
1828
2878
|
return result;
|
|
1829
2879
|
} finally {
|
|
1830
2880
|
set({ commandBusy: false });
|
|
@@ -1845,10 +2895,12 @@ export async function createEngineSession({
|
|
|
1845
2895
|
);
|
|
1846
2896
|
return result;
|
|
1847
2897
|
},
|
|
1848
|
-
setCwd: (path) => {
|
|
2898
|
+
setCwd: (path, options = {}) => {
|
|
1849
2899
|
const next = runtime.setCwd(path);
|
|
1850
2900
|
set({ cwd: next });
|
|
1851
|
-
|
|
2901
|
+
if (options?.notice !== false) {
|
|
2902
|
+
pushNotice(options?.message || `Project set: ${projectNameFromPath(next)}`, 'info');
|
|
2903
|
+
}
|
|
1852
2904
|
return next;
|
|
1853
2905
|
},
|
|
1854
2906
|
getSystemShell: () => {
|
|
@@ -2065,14 +3117,36 @@ export async function createEngineSession({
|
|
|
2065
3117
|
},
|
|
2066
3118
|
compact: async () => {
|
|
2067
3119
|
if (state.commandBusy) return null;
|
|
3120
|
+
if (state.busy) {
|
|
3121
|
+
pushNotice('Compact skipped: turn in progress', 'info');
|
|
3122
|
+
return { changed: false, reason: 'compact skipped: turn in progress' };
|
|
3123
|
+
}
|
|
2068
3124
|
const startedAt = Date.now();
|
|
2069
3125
|
set({ commandBusy: true, commandStatus: { active: true, verb: 'Compacting conversation', startedAt, mode: 'compacting' } });
|
|
2070
3126
|
try {
|
|
2071
|
-
const result = await runtime.compact({
|
|
3127
|
+
const result = await runtime.compact({ recoverAgent: true });
|
|
2072
3128
|
syncContextStats({ allowEstimated: true });
|
|
2073
3129
|
set({ ...routeState(), stats: { ...state.stats } });
|
|
2074
3130
|
if (result) {
|
|
2075
|
-
pushItem({
|
|
3131
|
+
pushItem({
|
|
3132
|
+
kind: 'statusdone',
|
|
3133
|
+
id: nextId(),
|
|
3134
|
+
label: result.error ? 'Compact failed' : (result.changed === false ? 'Compact checked' : 'Compact complete'),
|
|
3135
|
+
detail: compactEventDetail({
|
|
3136
|
+
stage: 'manual',
|
|
3137
|
+
trigger: 'manual',
|
|
3138
|
+
status: result.error ? 'failed' : (result.changed === false ? 'no_change' : 'compacted'),
|
|
3139
|
+
compactType: result.compactType,
|
|
3140
|
+
beforeTokens: result.beforeTokens,
|
|
3141
|
+
afterTokens: result.afterTokens,
|
|
3142
|
+
beforeMessages: result.beforeMessages,
|
|
3143
|
+
afterMessages: result.afterMessages,
|
|
3144
|
+
semantic: result.semanticCompact,
|
|
3145
|
+
recallFastTrack: result.recallFastTrack,
|
|
3146
|
+
durationMs: Date.now() - startedAt,
|
|
3147
|
+
error: result.error,
|
|
3148
|
+
}),
|
|
3149
|
+
});
|
|
2076
3150
|
}
|
|
2077
3151
|
return result;
|
|
2078
3152
|
} finally {
|
|
@@ -2081,6 +3155,7 @@ export async function createEngineSession({
|
|
|
2081
3155
|
},
|
|
2082
3156
|
abort: () => {
|
|
2083
3157
|
if (!state.busy) return false;
|
|
3158
|
+
denyAllToolApprovals('interrupted by user');
|
|
2084
3159
|
const restoreState = activePromptRestore;
|
|
2085
3160
|
const restoreText = restoreState?.restorable ? restoreState.text : '';
|
|
2086
3161
|
const restorePastedImages = restoreState?.restorable && restoreState?.pastedImages ? restoreState.pastedImages : null;
|
|
@@ -2107,12 +3182,49 @@ export async function createEngineSession({
|
|
|
2107
3182
|
}
|
|
2108
3183
|
return { aborted, restoreText, pastedImages: restorePastedImages };
|
|
2109
3184
|
},
|
|
3185
|
+
resolveToolApproval: (id, decision = {}) => {
|
|
3186
|
+
const approved = decision === true || decision?.approved === true;
|
|
3187
|
+
return finishToolApproval(id, approved, decision?.reason || (approved ? 'approved by user' : 'denied by user'));
|
|
3188
|
+
},
|
|
2110
3189
|
listPresets: () => {
|
|
2111
3190
|
return runtime.listPresets();
|
|
2112
3191
|
},
|
|
2113
3192
|
listProviderModels: (options = {}) => {
|
|
2114
3193
|
return runtime.listProviderModels(options);
|
|
2115
3194
|
},
|
|
3195
|
+
getSearchRoute: () => {
|
|
3196
|
+
return runtime.getSearchRoute?.() || runtime.searchRoute || null;
|
|
3197
|
+
},
|
|
3198
|
+
listSearchModels: (options = {}) => {
|
|
3199
|
+
return runtime.listSearchModels?.(options) || [];
|
|
3200
|
+
},
|
|
3201
|
+
setSearchRoute: async (opts) => {
|
|
3202
|
+
if (state.commandBusy) return null;
|
|
3203
|
+
const beforeRouteState = routeState();
|
|
3204
|
+
const optimisticSearchRoute = opts?.provider && opts?.model
|
|
3205
|
+
? {
|
|
3206
|
+
provider: String(opts.provider).trim(),
|
|
3207
|
+
model: String(opts.model).trim(),
|
|
3208
|
+
...(opts.effort ? { effort: opts.effort } : {}),
|
|
3209
|
+
...(opts.fast === true ? { fast: true } : {}),
|
|
3210
|
+
...(opts.toolType ? { toolType: opts.toolType } : {}),
|
|
3211
|
+
}
|
|
3212
|
+
: null;
|
|
3213
|
+
set({ commandBusy: true });
|
|
3214
|
+
try {
|
|
3215
|
+
if (optimisticSearchRoute?.provider && optimisticSearchRoute.model) {
|
|
3216
|
+
set({ searchRoute: optimisticSearchRoute });
|
|
3217
|
+
}
|
|
3218
|
+
const result = await runtime.setSearchRoute?.(opts);
|
|
3219
|
+
set({ ...routeState(), stats: { ...state.stats } });
|
|
3220
|
+
return result;
|
|
3221
|
+
} catch (e) {
|
|
3222
|
+
set({ searchRoute: beforeRouteState.searchRoute || null });
|
|
3223
|
+
throw e;
|
|
3224
|
+
} finally {
|
|
3225
|
+
set({ commandBusy: false });
|
|
3226
|
+
}
|
|
3227
|
+
},
|
|
2116
3228
|
listAgents: () => {
|
|
2117
3229
|
return runtime.listAgents?.() || [];
|
|
2118
3230
|
},
|
|
@@ -2141,13 +3253,24 @@ export async function createEngineSession({
|
|
|
2141
3253
|
if (state.commandBusy) return null;
|
|
2142
3254
|
set({ commandBusy: true });
|
|
2143
3255
|
try {
|
|
2144
|
-
const result = runtime.setWorkflow?.(workflowId);
|
|
3256
|
+
const result = await runtime.setWorkflow?.(workflowId);
|
|
2145
3257
|
set({ ...routeState(), stats: { ...state.stats } });
|
|
2146
3258
|
return result;
|
|
2147
3259
|
} finally {
|
|
2148
3260
|
set({ commandBusy: false });
|
|
2149
3261
|
}
|
|
2150
3262
|
},
|
|
3263
|
+
// Theme is a TUI-local concern (no runtime round-trip). listThemes returns
|
|
3264
|
+
// picker metadata; getTheme reports the active id; setTheme applies the
|
|
3265
|
+
// palette in-place + persists ui.theme and bumps a themeEpoch so the React
|
|
3266
|
+
// tree re-renders (markdown/status/spinner colorizers re-resolve).
|
|
3267
|
+
listThemes: () => listThemes(),
|
|
3268
|
+
getTheme: () => getThemeSetting(),
|
|
3269
|
+
setTheme: (id, options = {}) => {
|
|
3270
|
+
const applied = setThemeSetting(id, options);
|
|
3271
|
+
set({ themeEpoch: (state.themeEpoch || 0) + 1 });
|
|
3272
|
+
return applied;
|
|
3273
|
+
},
|
|
2151
3274
|
setAgentRoute: async (agentId, opts) => {
|
|
2152
3275
|
return await runtime.setAgentRoute?.(agentId, opts);
|
|
2153
3276
|
},
|
|
@@ -2187,6 +3310,17 @@ export async function createEngineSession({
|
|
|
2187
3310
|
set({ commandBusy: false });
|
|
2188
3311
|
}
|
|
2189
3312
|
},
|
|
3313
|
+
beginOAuthProviderLogin: async (provider) => {
|
|
3314
|
+
if (state.commandBusy) throw new Error('command busy');
|
|
3315
|
+
set({ commandBusy: true });
|
|
3316
|
+
try {
|
|
3317
|
+
const result = await runtime.beginOAuthProviderLogin(provider);
|
|
3318
|
+
pushNotice(`provider oauth started: ${result.provider}`, 'info');
|
|
3319
|
+
return result;
|
|
3320
|
+
} finally {
|
|
3321
|
+
set({ commandBusy: false });
|
|
3322
|
+
}
|
|
3323
|
+
},
|
|
2190
3324
|
saveProviderApiKey: (provider, secret) => {
|
|
2191
3325
|
const result = runtime.saveProviderApiKey(provider, secret);
|
|
2192
3326
|
pushNotice(`provider api key saved: ${result.provider}`, 'info');
|
|
@@ -2298,8 +3432,11 @@ export async function createEngineSession({
|
|
|
2298
3432
|
if (state.commandBusy) return false;
|
|
2299
3433
|
set({ commandBusy: true });
|
|
2300
3434
|
try {
|
|
2301
|
-
|
|
2302
|
-
|
|
3435
|
+
const routeOpts = opts && typeof opts === 'object' ? opts : {};
|
|
3436
|
+
const applyToCurrentSession = routeOpts.applyToCurrentSession === false ? false : true;
|
|
3437
|
+
const { applyToCurrentSession: _drop, ...nextRoute } = routeOpts;
|
|
3438
|
+
await runtime.setRoute(nextRoute, { applyToCurrentSession });
|
|
3439
|
+
if (applyToCurrentSession) syncContextStats({ allowEstimated: true });
|
|
2303
3440
|
set({ ...routeState(), stats: { ...state.stats } });
|
|
2304
3441
|
return true;
|
|
2305
3442
|
} finally {
|
|
@@ -2311,13 +3448,31 @@ export async function createEngineSession({
|
|
|
2311
3448
|
if (state.commandBusy) return false;
|
|
2312
3449
|
set({ commandBusy: true });
|
|
2313
3450
|
clearToastTimers();
|
|
3451
|
+
const rollbackSnapshot = snapshotTuiBeforeSessionReset();
|
|
3452
|
+
resetTuiForPendingSessionReset();
|
|
3453
|
+
set({
|
|
3454
|
+
items: state.items,
|
|
3455
|
+
toasts: state.toasts,
|
|
3456
|
+
queued: state.queued,
|
|
3457
|
+
thinking: null,
|
|
3458
|
+
spinner: null,
|
|
3459
|
+
lastTurn: null,
|
|
3460
|
+
sessionId: null,
|
|
3461
|
+
stats: { ...state.stats },
|
|
3462
|
+
});
|
|
2314
3463
|
try {
|
|
2315
|
-
await runtime.clear({
|
|
3464
|
+
await runtime.clear({ recoverAgent: true });
|
|
3465
|
+
clearUiActivityBeforeContextSync();
|
|
3466
|
+
pendingSessionReset = false;
|
|
2316
3467
|
resetStatsAndSyncContext();
|
|
2317
3468
|
set({ items: replaceItems([]), toasts: [], queued: [], thinking: null, spinner: null, lastTurn: null, ...routeState(), stats: { ...state.stats } });
|
|
2318
3469
|
lastUserActivityAt = Date.now();
|
|
2319
3470
|
return true;
|
|
3471
|
+
} catch (error) {
|
|
3472
|
+
restoreTuiAfterFailedSessionReset(rollbackSnapshot);
|
|
3473
|
+
throw error;
|
|
2320
3474
|
} finally {
|
|
3475
|
+
pendingSessionReset = false;
|
|
2321
3476
|
set({ commandBusy: false });
|
|
2322
3477
|
}
|
|
2323
3478
|
},
|
|
@@ -2328,12 +3483,30 @@ export async function createEngineSession({
|
|
|
2328
3483
|
if (state.commandBusy) return false;
|
|
2329
3484
|
set({ commandBusy: true });
|
|
2330
3485
|
clearToastTimers();
|
|
3486
|
+
const rollbackSnapshot = snapshotTuiBeforeSessionReset();
|
|
3487
|
+
resetTuiForPendingSessionReset();
|
|
3488
|
+
set({
|
|
3489
|
+
items: state.items,
|
|
3490
|
+
toasts: state.toasts,
|
|
3491
|
+
queued: state.queued,
|
|
3492
|
+
thinking: null,
|
|
3493
|
+
spinner: null,
|
|
3494
|
+
lastTurn: null,
|
|
3495
|
+
sessionId: null,
|
|
3496
|
+
stats: { ...state.stats },
|
|
3497
|
+
});
|
|
2331
3498
|
try {
|
|
2332
3499
|
await runtime.newSession();
|
|
3500
|
+
clearUiActivityBeforeContextSync();
|
|
3501
|
+
pendingSessionReset = false;
|
|
2333
3502
|
resetStatsAndSyncContext();
|
|
2334
3503
|
set({ items: replaceItems([]), toasts: [], queued: [], thinking: null, spinner: null, lastTurn: null, ...routeState(), stats: { ...state.stats } });
|
|
2335
3504
|
return true;
|
|
3505
|
+
} catch (error) {
|
|
3506
|
+
restoreTuiAfterFailedSessionReset(rollbackSnapshot);
|
|
3507
|
+
throw error;
|
|
2336
3508
|
} finally {
|
|
3509
|
+
pendingSessionReset = false;
|
|
2337
3510
|
set({ commandBusy: false });
|
|
2338
3511
|
}
|
|
2339
3512
|
},
|
|
@@ -2359,13 +3532,14 @@ export async function createEngineSession({
|
|
|
2359
3532
|
items.push({
|
|
2360
3533
|
kind: 'tool',
|
|
2361
3534
|
id: nextId(),
|
|
2362
|
-
name: synthetic.name || '
|
|
3535
|
+
name: synthetic.name || 'agent',
|
|
2363
3536
|
args: synthetic.args || {
|
|
2364
3537
|
type: label,
|
|
2365
3538
|
task_id: synthetic.taskId || undefined,
|
|
2366
3539
|
description: synthetic.summary || 'agent notification',
|
|
2367
3540
|
},
|
|
2368
3541
|
result: synthetic.result,
|
|
3542
|
+
rawResult: synthetic.rawResult ?? text,
|
|
2369
3543
|
isError: synthetic.isError ?? /^(failed|error|killed|cancelled)$/i.test(label),
|
|
2370
3544
|
expanded: false,
|
|
2371
3545
|
count: 1,
|
|
@@ -2401,8 +3575,10 @@ export async function createEngineSession({
|
|
|
2401
3575
|
if (disposed) return;
|
|
2402
3576
|
disposed = true;
|
|
2403
3577
|
clearToastTimers();
|
|
3578
|
+
try { clearInterval(runtimePulseTimer); } catch {}
|
|
2404
3579
|
try { unsubscribeRuntimeNotifications?.(); } catch {}
|
|
2405
3580
|
unsubscribeRuntimeNotifications = null;
|
|
3581
|
+
denyAllToolApprovals('runtime closing');
|
|
2406
3582
|
await runtime.close(reason, options);
|
|
2407
3583
|
listeners.clear();
|
|
2408
3584
|
},
|