mixdog 0.9.1 → 0.9.3
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/package.json +9 -1
- package/scripts/_bench-cwc.json +20 -0
- package/scripts/agent-loop-policy-test.mjs +37 -0
- package/scripts/agent-parallel-smoke.mjs +54 -10
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
- package/scripts/internal-comms-bench.mjs +727 -0
- package/scripts/internal-comms-smoke.mjs +75 -0
- package/scripts/lead-workflow-smoke.mjs +4 -4
- package/scripts/live-worker-smoke.mjs +9 -9
- package/scripts/output-style-bench.mjs +285 -0
- package/scripts/output-style-smoke.mjs +13 -10
- package/scripts/patch-replay.mjs +90 -0
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +310 -67
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +20 -9
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +17 -9
- package/src/app.mjs +10 -6
- package/src/defaults/{hidden-roles.json → agents.json} +7 -7
- package/src/examples/schedules/SCHEDULE.example.md +32 -0
- package/src/examples/webhooks/WEBHOOK.example.md +40 -0
- package/src/headless-role.mjs +14 -14
- package/src/help.mjs +1 -0
- package/src/lib/rules-builder.cjs +32 -54
- package/src/mixdog-session-runtime.mjs +1040 -2036
- package/src/output-styles/default.md +12 -7
- package/src/output-styles/minimal.md +25 -0
- package/src/output-styles/oneline.md +21 -0
- package/src/output-styles/simple.md +10 -9
- package/src/repl.mjs +17 -7
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +8 -12
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +13 -2
- package/src/rules/shared/01-tool.md +23 -12
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
- package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
- package/src/runtime/agent/orchestrator/config.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +182 -67
- package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
- package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
- package/src/runtime/agent/orchestrator/mcp/client.mjs +100 -18
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
- package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +513 -1000
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +232 -1152
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +14 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +70 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
- package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +437 -1439
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/config.mjs +54 -2
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/telegram-format.mjs +280 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +465 -345
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +121 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/lib/session-ingest.mjs +107 -0
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +10 -7
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/channel-notification-routing.mjs +12 -0
- package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
- package/src/runtime/shared/config.mjs +9 -0
- package/src/runtime/shared/llm/http-agent.mjs +12 -5
- package/src/runtime/shared/schedules-store.mjs +21 -19
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +156 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/session-runtime/config-helpers.mjs +209 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/output-styles.mjs +124 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool.mjs +302 -117
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +10 -292
- package/src/standalone/explore-tool.mjs +12 -5
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +25 -3
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2883 -778
- package/src/tui/components/ConfirmBar.jsx +47 -0
- package/src/tui/components/ContextPanel.jsx +5 -3
- package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
- package/src/tui/components/Markdown.jsx +22 -98
- package/src/tui/components/Message.jsx +14 -35
- package/src/tui/components/Picker.jsx +87 -12
- package/src/tui/components/PromptInput.jsx +355 -22
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +8 -5
- package/src/tui/components/Spinner.jsx +7 -7
- package/src/tui/components/StatusLine.jsx +40 -21
- package/src/tui/components/TextEntryPanel.jsx +51 -7
- package/src/tui/components/ToolExecution.jsx +183 -101
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +161 -23
- package/src/tui/components/tool-output-format.test.mjs +87 -0
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +6731 -2333
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +569 -948
- package/src/tui/index.jsx +117 -7
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +469 -0
- package/src/tui/markdown/format-token.mjs +128 -76
- package/src/tui/markdown/format-token.test.mjs +61 -19
- package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
- package/src/tui/markdown/render-ansi.test.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +167 -0
- package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
- package/src/tui/markdown/table-layout.mjs +9 -9
- package/src/tui/paste-attachments.mjs +36 -9
- package/src/tui/paste-fix.test.mjs +119 -0
- package/src/tui/prompt-history-store.mjs +129 -0
- package/src/tui/prompt-history-store.test.mjs +52 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
- package/src/tui/theme.mjs +41 -657
- package/src/tui/themes/base.mjs +86 -0
- package/src/tui/themes/basic.mjs +85 -0
- package/src/tui/themes/catppuccin.mjs +72 -0
- package/src/tui/themes/dracula.mjs +70 -0
- package/src/tui/themes/everforest.mjs +71 -0
- package/src/tui/themes/gruvbox.mjs +71 -0
- package/src/tui/themes/index.mjs +71 -0
- package/src/tui/themes/indigo.mjs +78 -0
- package/src/tui/themes/kanagawa.mjs +80 -0
- package/src/tui/themes/light.mjs +81 -0
- package/src/tui/themes/nord.mjs +72 -0
- package/src/tui/themes/onedark.mjs +16 -0
- package/src/tui/themes/rosepine.mjs +70 -0
- package/src/tui/themes/teal.mjs +80 -0
- package/src/tui/themes/tokyonight.mjs +79 -0
- package/src/tui/themes/utils.mjs +106 -0
- package/src/tui/themes/warm.mjs +79 -0
- package/src/tui/transcript-tool-failures.mjs +13 -2
- package/src/ui/markdown.mjs +1 -1
- package/src/ui/model-display.mjs +2 -2
- package/src/ui/statusline.mjs +75 -27
- package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
- package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
- package/src/workflows/default/WORKFLOW.md +46 -12
- package/src/workflows/sequential/WORKFLOW.md +51 -0
- package/src/workflows/solo/WORKFLOW.md +12 -1
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +100 -12
- package/vendor/ink/build/measure-text.js +4 -1
- package/vendor/ink/build/output.js +115 -9
- package/vendor/ink/build/render-node-to-output.js +4 -1
- package/vendor/ink/build/render.js +4 -0
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
package/src/tui/engine.mjs
CHANGED
|
@@ -4,76 +4,75 @@
|
|
|
4
4
|
* Runs mixdog's session manager outside React and exposes a tiny subscribable
|
|
5
5
|
* store. The React/ink layer consumes it via useSyncExternalStore
|
|
6
6
|
* (see hooks/useEngine.mjs).
|
|
7
|
+
*
|
|
8
|
+
* Pure/stateless helpers live in ./engine/* (boot-profile, session-stats,
|
|
9
|
+
* labels, notice-text, tool-result-text, tool-call-fields, agent-envelope,
|
|
10
|
+
* queue-helpers) and are re-exported here so the public surface is unchanged.
|
|
11
|
+
* This file keeps the stateful createEngineSession store + notification plan.
|
|
7
12
|
*/
|
|
8
13
|
import { performance } from 'node:perf_hooks';
|
|
9
|
-
import { SPINNER_VERBS } from './spinner-verbs.mjs';
|
|
10
14
|
import {
|
|
11
15
|
aggregateToolCategoryEntry,
|
|
12
16
|
classifyToolCategory,
|
|
13
17
|
formatAggregateDetail,
|
|
14
18
|
summarizeToolResult,
|
|
15
19
|
} from '../runtime/shared/tool-surface.mjs';
|
|
16
|
-
import { isBackgroundErrorOnlyBody, presentErrorText } from '../runtime/shared/err-text.mjs';
|
|
17
20
|
import {
|
|
18
21
|
isModelVisibleToolCompletionWrapper,
|
|
19
22
|
modelVisibleToolCompletionMessage,
|
|
20
23
|
} from '../runtime/shared/tool-execution-contract.mjs';
|
|
24
|
+
import { presentErrorText } from '../runtime/shared/err-text.mjs';
|
|
21
25
|
import { listThemes, getThemeSetting, setThemeSetting } from './theme.mjs';
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
stats.latestCacheWriteTokens = cacheWriteTokens;
|
|
73
|
-
stats.latestPromptTokens = promptTokens;
|
|
74
|
-
stats.costUsd += num(delta.costUsd);
|
|
75
|
-
return stats;
|
|
76
|
-
}
|
|
26
|
+
import { resetAllStreamingMarkdownStablePrefixes } from './markdown/streaming-markdown.mjs';
|
|
27
|
+
import { bootProfile } from './engine/boot-profile.mjs';
|
|
28
|
+
import { createSessionStats, applyUsageDelta } from './engine/session-stats.mjs';
|
|
29
|
+
import {
|
|
30
|
+
pickVerb,
|
|
31
|
+
pickDoneVerb,
|
|
32
|
+
formatElapsedSeconds,
|
|
33
|
+
compactEventLabel,
|
|
34
|
+
compactEventDetail,
|
|
35
|
+
projectNameFromPath,
|
|
36
|
+
} from './engine/labels.mjs';
|
|
37
|
+
import { polishNoticeText } from './engine/notice-text.mjs';
|
|
38
|
+
import {
|
|
39
|
+
toolResultText,
|
|
40
|
+
toolAggregateDetailFallback,
|
|
41
|
+
toolGroupedDisplayFallback,
|
|
42
|
+
toolErrorDisplay,
|
|
43
|
+
} from './engine/tool-result-text.mjs';
|
|
44
|
+
import {
|
|
45
|
+
toolCallId,
|
|
46
|
+
toolResultCallId,
|
|
47
|
+
toolCallName,
|
|
48
|
+
toolCallArgs,
|
|
49
|
+
} from './engine/tool-call-fields.mjs';
|
|
50
|
+
import {
|
|
51
|
+
parseAgentJob,
|
|
52
|
+
parseAgentResultEnvelope,
|
|
53
|
+
parseBackgroundTaskEnvelope,
|
|
54
|
+
parseSyntheticAgentMessage,
|
|
55
|
+
isStatusOnlyAgentCompletionNotification,
|
|
56
|
+
agentJobResultText,
|
|
57
|
+
agentArgsWithResultMetadata,
|
|
58
|
+
toolResultStatus,
|
|
59
|
+
isErrorToolStatus,
|
|
60
|
+
} from './engine/agent-envelope.mjs';
|
|
61
|
+
import {
|
|
62
|
+
queuePriorityValue,
|
|
63
|
+
defaultQueuePriority,
|
|
64
|
+
isQueuedEntryEditable,
|
|
65
|
+
isQueuedEntryVisible,
|
|
66
|
+
isSlashQueuedEntry,
|
|
67
|
+
shortTextFingerprint,
|
|
68
|
+
notificationDisplayText,
|
|
69
|
+
sessionActivityTimestamp,
|
|
70
|
+
promptDisplayText,
|
|
71
|
+
mergePromptContents,
|
|
72
|
+
mergePastedImages,
|
|
73
|
+
mergePastedTexts,
|
|
74
|
+
callCommitCallbacks,
|
|
75
|
+
} from './engine/queue-helpers.mjs';
|
|
77
76
|
|
|
78
77
|
// Source tests resolve from src/tui/engine.mjs; the built bundle resolves from
|
|
79
78
|
// src/tui/dist/index.mjs.
|
|
@@ -89,739 +88,21 @@ const TOOL_APPROVAL_TIMEOUT_MS = (() => {
|
|
|
89
88
|
let _idSeq = 0;
|
|
90
89
|
const nextId = () => `it_${++_idSeq}`;
|
|
91
90
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const TURN_DONE_VERBS = [
|
|
97
|
-
'Thought',
|
|
98
|
-
'Reasoned',
|
|
99
|
-
'Mapped',
|
|
100
|
-
'Checked',
|
|
101
|
-
'Solved',
|
|
102
|
-
'Composed',
|
|
103
|
-
'Synthesized',
|
|
104
|
-
'Wrapped',
|
|
105
|
-
];
|
|
106
|
-
|
|
107
|
-
function pickDoneVerb(turn) {
|
|
108
|
-
return TURN_DONE_VERBS[(turn * 5 + 2) % TURN_DONE_VERBS.length];
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function formatElapsedSeconds(ms) {
|
|
112
|
-
const value = Math.max(0, Number(ms) || 0);
|
|
113
|
-
if (value <= 0) return '0s';
|
|
114
|
-
return `${Math.max(1, Math.ceil(value / 1000))}s`;
|
|
115
|
-
}
|
|
116
|
-
|
|
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)';
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const FAILED_NOTICE_ACTIONS = new Map([
|
|
155
|
-
['api key save', 'save API key'],
|
|
156
|
-
['auth-forget', 'forget auth'],
|
|
157
|
-
['auto-clear', 'update auto-clear'],
|
|
158
|
-
['autoclear', 'update auto-clear'],
|
|
159
|
-
['agent', 'run agent command'],
|
|
160
|
-
['channels', 'load channels'],
|
|
161
|
-
['channels update', 'update channels'],
|
|
162
|
-
['clear', 'clear chat'],
|
|
163
|
-
['compact', 'compact context'],
|
|
164
|
-
['copy', 'copy'],
|
|
165
|
-
['core memory', 'load core memory'],
|
|
166
|
-
['cwd', 'update working directory'],
|
|
167
|
-
['effort switch', 'switch effort'],
|
|
168
|
-
['fast', 'update fast mode'],
|
|
169
|
-
['hook rule update', 'update hook rule'],
|
|
170
|
-
['hook toggle', 'toggle hook'],
|
|
171
|
-
['hook update', 'update hook'],
|
|
172
|
-
['hooks status', 'load hooks'],
|
|
173
|
-
['local provider update', 'update local provider'],
|
|
174
|
-
['mcp add', 'add MCP server'],
|
|
175
|
-
['mcp reconnect', 'reconnect MCP server'],
|
|
176
|
-
['mcp status', 'load MCP status'],
|
|
177
|
-
['mcp toggle', 'toggle MCP server'],
|
|
178
|
-
['memory', 'run memory command'],
|
|
179
|
-
['memory status', 'load memory status'],
|
|
180
|
-
['model save', 'save model'],
|
|
181
|
-
['model switch', 'switch model'],
|
|
182
|
-
['oauth code', 'finish OAuth login'],
|
|
183
|
-
['oauth login', 'start OAuth login'],
|
|
184
|
-
['output style switch', 'switch output style'],
|
|
185
|
-
['OpenAI usage auth save', 'save OpenAI usage auth'],
|
|
186
|
-
['OpenCode Go usage auth save', 'save OpenCode Go usage auth'],
|
|
187
|
-
['plugin add', 'add plugin'],
|
|
188
|
-
['plugin MCP enable', 'enable plugin MCP'],
|
|
189
|
-
['plugin uninstall', 'uninstall plugin'],
|
|
190
|
-
['plugin update', 'update plugin'],
|
|
191
|
-
['plugins status', 'load plugins'],
|
|
192
|
-
['providers', 'load providers'],
|
|
193
|
-
['recall', 'run recall'],
|
|
194
|
-
['resume', 'resume chat'],
|
|
195
|
-
['schedule toggle', 'toggle schedule'],
|
|
196
|
-
['setup save', 'save setup'],
|
|
197
|
-
['settings update', 'update settings'],
|
|
198
|
-
['skill add', 'add skill'],
|
|
199
|
-
['skills status', 'load skills'],
|
|
200
|
-
['tools status', 'load tool status'],
|
|
201
|
-
['usage', 'load usage'],
|
|
202
|
-
['webhook toggle', 'toggle webhook'],
|
|
203
|
-
['workflow switch', 'switch workflow'],
|
|
204
|
-
]);
|
|
205
|
-
|
|
206
|
-
function polishNoticeAction(action) {
|
|
207
|
-
const value = String(action || '').trim();
|
|
208
|
-
if (!value) return 'finish';
|
|
209
|
-
const key = value.toLowerCase();
|
|
210
|
-
for (const [candidate, replacement] of FAILED_NOTICE_ACTIONS.entries()) {
|
|
211
|
-
if (candidate.toLowerCase() === key) return replacement;
|
|
212
|
-
}
|
|
213
|
-
const suffixes = [
|
|
214
|
-
[' save', 'save'],
|
|
215
|
-
[' switch', 'switch'],
|
|
216
|
-
[' update', 'update'],
|
|
217
|
-
[' toggle', 'toggle'],
|
|
218
|
-
[' reconnect', 'reconnect'],
|
|
219
|
-
[' enable', 'enable'],
|
|
220
|
-
[' uninstall', 'uninstall'],
|
|
221
|
-
[' add', 'add'],
|
|
222
|
-
];
|
|
223
|
-
for (const [suffix, verb] of suffixes) {
|
|
224
|
-
if (!key.endsWith(suffix)) continue;
|
|
225
|
-
const subject = value.slice(0, -suffix.length).trim();
|
|
226
|
-
return subject ? `${verb} ${subject}` : verb;
|
|
227
|
-
}
|
|
228
|
-
return value;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function sentenceStart(text) {
|
|
232
|
-
const value = String(text || '').trim();
|
|
233
|
-
return value ? `${value[0].toUpperCase()}${value.slice(1)}` : value;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function polishNoticeText(text) {
|
|
237
|
-
let value = String(text ?? '').trim().replace(/^✓\s*/, '');
|
|
238
|
-
if (!value) return '';
|
|
239
|
-
const error = /^error\s*:\s*(.+)$/i.exec(value);
|
|
240
|
-
if (error?.[1]) value = error[1].trim();
|
|
241
|
-
const couldNot = /^could not\s+(.+?)(?::\s*(.+))?$/i.exec(value);
|
|
242
|
-
if (couldNot) {
|
|
243
|
-
return couldNot[2]
|
|
244
|
-
? `Couldn’t ${couldNot[1]}: ${couldNot[2]}`
|
|
245
|
-
: `Couldn’t ${couldNot[1]}.`;
|
|
246
|
-
}
|
|
247
|
-
const failed = /^(.+?)\s+failed(?::\s*(.+))?$/i.exec(value);
|
|
248
|
-
if (failed) {
|
|
249
|
-
const action = polishNoticeAction(failed[1]);
|
|
250
|
-
return failed[2] ? `Couldn’t ${action}: ${failed[2]}` : `Couldn’t ${action}.`;
|
|
251
|
-
}
|
|
252
|
-
const busy = /^(.+?)\s+already in progress\.?$/i.exec(value);
|
|
253
|
-
if (busy) return `${sentenceStart(polishNoticeAction(busy[1]))} is already running.`;
|
|
254
|
-
const required = /^(.+?)\s+is required(?:\s+for\s+(.+))?\.?$/i.exec(value);
|
|
255
|
-
if (required) {
|
|
256
|
-
const subject = required[1].trim();
|
|
257
|
-
const target = required[2]?.trim();
|
|
258
|
-
return `${subject}${target ? ` required for ${target}` : ' required'}.`;
|
|
259
|
-
}
|
|
260
|
-
return value;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function toolResultText(content) {
|
|
264
|
-
if (content == null) return '';
|
|
265
|
-
if (typeof content === 'string') return content;
|
|
266
|
-
if (Array.isArray(content)) {
|
|
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;
|
|
286
|
-
}
|
|
287
|
-
try { return JSON.stringify(content); } catch { return String(content); }
|
|
288
|
-
}
|
|
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
|
-
|
|
370
|
-
function toolErrorDisplay(value, surface = 'tool') {
|
|
371
|
-
const text = presentErrorText(value, { surface });
|
|
372
|
-
if (/^(?:Search failed|Fetch failed|No first response|The .+ went stale|(?:Web search agent|Agent|Tool) (?:stopped|was cancelled))/i.test(text)) {
|
|
373
|
-
return text;
|
|
374
|
-
}
|
|
375
|
-
return /^error\s*:/i.test(text) ? text : `Error: ${text}`;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function toolCallId(call) {
|
|
379
|
-
return call?.id ?? call?.toolCallId ?? call?.tool_call_id ?? call?.call_id;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function toolResultCallId(message) {
|
|
383
|
-
return message?.toolCallId
|
|
384
|
-
?? message?.tool_call_id
|
|
385
|
-
?? message?.tool_use_id
|
|
386
|
-
?? message?.call_id
|
|
387
|
-
?? message?.id;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
function toolCallName(call) {
|
|
391
|
-
return call?.name ?? call?.function?.name ?? call?.toolName ?? call?.tool_name ?? 'tool';
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function toolCallArgs(call) {
|
|
395
|
-
return call?.arguments ?? call?.args ?? call?.input ?? call?.function?.arguments;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function textBetweenTag(text, tag) {
|
|
399
|
-
const re = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, 'i');
|
|
400
|
-
const match = re.exec(String(text ?? ''));
|
|
401
|
-
return match ? match[1].trim() : '';
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
function stripSyntheticAgentTags(text) {
|
|
405
|
-
const value = String(text ?? '').trim();
|
|
406
|
-
const finalAnswer = textBetweenTag(value, 'final-answer');
|
|
407
|
-
if (finalAnswer) return finalAnswer;
|
|
408
|
-
const taskResult = textBetweenTag(value, 'result');
|
|
409
|
-
if (taskResult) return taskResult;
|
|
410
|
-
return value
|
|
411
|
-
.replace(/^agent result[^\n]*(?:\n|$)/i, '')
|
|
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, '')
|
|
413
|
-
.trim();
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
function splitBridgeEnvelope(text) {
|
|
417
|
-
const value = String(text ?? '').trim();
|
|
418
|
-
if (!value) return { head: '', body: '' };
|
|
419
|
-
const match = /\n\s*\n/.exec(value);
|
|
420
|
-
if (!match) return { head: value, body: '' };
|
|
421
|
-
return {
|
|
422
|
-
head: value.slice(0, match.index).trim(),
|
|
423
|
-
body: value.slice(match.index + match[0].length).trim(),
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
function agentJobStatusText(parsed) {
|
|
428
|
-
if (!parsed) return '';
|
|
429
|
-
const parts = [];
|
|
430
|
-
if (parsed.status) parts.push(`status: ${parsed.status}`);
|
|
431
|
-
if (parsed.taskId) parts.push(`task_id: ${parsed.taskId}`);
|
|
432
|
-
return parts.join(' · ');
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
function agentJobResultText(text, parsed = parseAgentJob(text)) {
|
|
436
|
-
const value = String(text ?? '').trim();
|
|
437
|
-
if (!value) return '';
|
|
438
|
-
if (parsed?.taskId) {
|
|
439
|
-
const { body } = splitBridgeEnvelope(value);
|
|
440
|
-
const cleanBody = stripSyntheticAgentTags(body);
|
|
441
|
-
if (cleanBody) return cleanBody;
|
|
442
|
-
return agentJobStatusText(parsed);
|
|
443
|
-
}
|
|
444
|
-
return stripSyntheticAgentTags(value) || value;
|
|
445
|
-
}
|
|
446
|
-
|
|
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
|
-
|
|
91
|
+
// Re-export the shared tool-result/notification helpers so importers (and tests)
|
|
92
|
+
// keep resolving them from engine.mjs unchanged.
|
|
481
93
|
export { toolResultText, toolAggregateDetailFallback, toolGroupedDisplayFallback };
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
if (match) fields[match[1].toLowerCase()] = match[2].trim();
|
|
495
|
-
}
|
|
496
|
-
const surface = String(fields.surface || fields.operation || 'task').toLowerCase();
|
|
497
|
-
const name = surface === 'explore' || surface === 'search' || surface === 'shell' || surface === 'agent' ? surface : 'task';
|
|
498
|
-
const status = String(fields.status || '').toLowerCase();
|
|
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 : '';
|
|
516
|
-
return {
|
|
517
|
-
name,
|
|
518
|
-
label: status || 'notification',
|
|
519
|
-
args: {
|
|
520
|
-
type: body ? 'result' : (fields.operation || 'status'),
|
|
521
|
-
status,
|
|
522
|
-
task_id: taskId || undefined,
|
|
523
|
-
surface,
|
|
524
|
-
operation: fields.operation || undefined,
|
|
525
|
-
label: fields.label || undefined,
|
|
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),
|
|
541
|
-
};
|
|
542
|
-
}
|
|
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
|
-
|
|
563
|
-
function bracketField(text, name) {
|
|
564
|
-
const re = new RegExp(`^\\[${name}:\\s*([^\\]]*)\\]`, 'mi');
|
|
565
|
-
return re.exec(String(text ?? ''))?.[1]?.trim() || '';
|
|
566
|
-
}
|
|
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
|
-
|
|
582
|
-
function parseSyntheticAgentMessage(text) {
|
|
583
|
-
const value = String(text ?? '').trim();
|
|
584
|
-
if (!value) return null;
|
|
585
|
-
const finalAnswer = textBetweenTag(value, 'final-answer');
|
|
586
|
-
if (finalAnswer) {
|
|
587
|
-
return {
|
|
588
|
-
name: 'agent',
|
|
589
|
-
label: 'final',
|
|
590
|
-
args: { type: 'read', description: 'agent result' },
|
|
591
|
-
result: finalAnswer,
|
|
592
|
-
};
|
|
593
|
-
}
|
|
594
|
-
const agentResult = parseAgentResultEnvelope(value);
|
|
595
|
-
if (agentResult) return agentResult;
|
|
596
|
-
const backgroundTask = parseBackgroundTaskEnvelope(value);
|
|
597
|
-
if (backgroundTask) return backgroundTask;
|
|
598
|
-
const shellTaskId = bracketField(value, 'task_id');
|
|
599
|
-
if (shellTaskId) {
|
|
600
|
-
const status = bracketField(value, 'status') || 'done';
|
|
601
|
-
const exit = bracketField(value, 'exit');
|
|
602
|
-
const command = bracketField(value, 'command');
|
|
603
|
-
return {
|
|
604
|
-
name: 'shell',
|
|
605
|
-
label: status,
|
|
606
|
-
args: { type: 'result', task_id: shellTaskId, command },
|
|
607
|
-
result: value,
|
|
608
|
-
isError: /^(failed|error|timeout|cancelled|killed)$/i.test(status) || (exit && exit !== '0' && exit !== 'n/a'),
|
|
609
|
-
};
|
|
610
|
-
}
|
|
611
|
-
const agentJob = parseAgentJob(value);
|
|
612
|
-
if (agentJob?.taskId) {
|
|
613
|
-
const label = agentJob.status || 'notification';
|
|
614
|
-
const result = agentJobResultText(value, agentJob);
|
|
615
|
-
return {
|
|
616
|
-
name: 'agent',
|
|
617
|
-
label,
|
|
618
|
-
args: agentArgsWithResultMetadata({ type: agentJob.type || 'notification', description: 'agent notification' }, agentJob),
|
|
619
|
-
result: result || agentJobStatusText(agentJob) || 'agent notification',
|
|
620
|
-
isError: /^(failed|error|timeout|cancelled|killed)$/i.test(label),
|
|
621
|
-
};
|
|
622
|
-
}
|
|
623
|
-
if (/<task-notification\b/i.test(value)) {
|
|
624
|
-
const status = textBetweenTag(value, 'status') || 'completed';
|
|
625
|
-
const summary = textBetweenTag(value, 'summary') || `Agent ${status}`;
|
|
626
|
-
const taskId = textBetweenTag(value, 'task-id');
|
|
627
|
-
const result = stripSyntheticAgentTags(value);
|
|
628
|
-
return {
|
|
629
|
-
name: 'agent',
|
|
630
|
-
label: status,
|
|
631
|
-
taskId,
|
|
632
|
-
summary,
|
|
633
|
-
result: result || summary,
|
|
634
|
-
};
|
|
635
|
-
}
|
|
636
|
-
return null;
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
function normalizeToolName(name) {
|
|
640
|
-
return String(name || 'tool')
|
|
641
|
-
.replace(/^mcp__.*__/, '')
|
|
642
|
-
.replace(/^functions\./, '')
|
|
643
|
-
.replace(/-/g, '_')
|
|
644
|
-
.toLowerCase();
|
|
645
|
-
}
|
|
646
|
-
|
|
647
|
-
function parseToolArgs(args) {
|
|
648
|
-
if (!args) return {};
|
|
649
|
-
if (typeof args === 'string') {
|
|
650
|
-
try {
|
|
651
|
-
const parsed = JSON.parse(args);
|
|
652
|
-
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
653
|
-
} catch {
|
|
654
|
-
return {};
|
|
655
|
-
}
|
|
656
|
-
}
|
|
657
|
-
return typeof args === 'object' ? args : {};
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
const yieldToRenderer = () => new Promise((resolve) => setImmediate(resolve));
|
|
661
|
-
|
|
662
|
-
function parseAgentJob(text) {
|
|
663
|
-
const value = String(text || '');
|
|
664
|
-
const idMatch = /^agent task:\s*([^\s]+)/m.exec(value) || /^task_id:\s*([^\s]+)/m.exec(value);
|
|
665
|
-
if (!idMatch) return null;
|
|
666
|
-
const statusMatch = /^status:\s*([^\s(]+)/m.exec(value);
|
|
667
|
-
const typeMatch = /^type:\s*(.+)$/m.exec(value);
|
|
668
|
-
const targetMatch = /^target:\s*(.+)$/m.exec(value);
|
|
669
|
-
const roleMatch = /^(?:agent|role):\s*(.+)$/m.exec(value);
|
|
670
|
-
const presetMatch = /^preset:\s*(.+)$/m.exec(value);
|
|
671
|
-
const modelMatch = /^model:\s*([^/\s]+)\/(.+)$/m.exec(value);
|
|
672
|
-
const effortMatch = /^effort:\s*(.+)$/m.exec(value);
|
|
673
|
-
const fastMatch = /^fast:\s*(on|off|true|false)$/m.exec(value);
|
|
674
|
-
return {
|
|
675
|
-
taskId: idMatch[1],
|
|
676
|
-
status: (statusMatch?.[1] || '').toLowerCase(),
|
|
677
|
-
type: (typeMatch?.[1] || '').trim(),
|
|
678
|
-
target: (targetMatch?.[1] || '').trim(),
|
|
679
|
-
role: (roleMatch?.[1] || '').trim(),
|
|
680
|
-
preset: (presetMatch?.[1] || '').trim(),
|
|
681
|
-
provider: (modelMatch?.[1] || '').trim(),
|
|
682
|
-
model: (modelMatch?.[2] || '').trim(),
|
|
683
|
-
effort: (effortMatch?.[1] || '').trim(),
|
|
684
|
-
fast: fastMatch ? /^(on|true)$/i.test(fastMatch[1]) : undefined,
|
|
685
|
-
};
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
const QUEUE_PRIORITY = { now: 0, next: 1, later: 2 };
|
|
689
|
-
|
|
690
|
-
function queuePriorityValue(value) {
|
|
691
|
-
return QUEUE_PRIORITY[String(value || 'next')] ?? QUEUE_PRIORITY.next;
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
function defaultQueuePriority(mode) {
|
|
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';
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
function isQueuedEntryEditable(entry) {
|
|
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('/');
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
function firstQueueLine(text) {
|
|
724
|
-
return String(text || '').split('\n').map((line) => line.trim()).find(Boolean) || '';
|
|
725
|
-
}
|
|
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
|
-
|
|
737
|
-
function notificationDisplayText(text) {
|
|
738
|
-
const parsed = parseAgentJob(text);
|
|
739
|
-
const result = agentJobResultText(text, parsed);
|
|
740
|
-
const synthetic = parseSyntheticAgentMessage(text);
|
|
741
|
-
return firstQueueLine(synthetic?.result || result || text) || 'agent notification';
|
|
742
|
-
}
|
|
743
|
-
|
|
744
|
-
function promptContentText(content) {
|
|
745
|
-
if (typeof content === 'string') return content;
|
|
746
|
-
if (Array.isArray(content)) {
|
|
747
|
-
return content.map((part) => {
|
|
748
|
-
if (typeof part === 'string') return part;
|
|
749
|
-
if (part?.type === 'text') return part.text || '';
|
|
750
|
-
if (part?.type === 'image') return '[Image]';
|
|
751
|
-
return part?.text || '';
|
|
752
|
-
}).filter(Boolean).join('\n');
|
|
753
|
-
}
|
|
754
|
-
return String(content ?? '');
|
|
755
|
-
}
|
|
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
|
-
|
|
784
|
-
function promptDisplayText(content, options = {}) {
|
|
785
|
-
if (typeof options.displayText === 'string') return options.displayText;
|
|
786
|
-
return promptContentText(content);
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
function mergePromptContents(entries) {
|
|
790
|
-
const batch = Array.isArray(entries) ? entries : [];
|
|
791
|
-
if (batch.every((entry) => typeof entry?.content === 'string')) {
|
|
792
|
-
return batch.map((entry) => entry.content).filter((text) => String(text || '').trim()).join('\n');
|
|
793
|
-
}
|
|
794
|
-
const parts = [];
|
|
795
|
-
for (const entry of batch) {
|
|
796
|
-
const content = entry?.content;
|
|
797
|
-
if (typeof content === 'string') {
|
|
798
|
-
if (content.trim()) parts.push({ type: 'text', text: content });
|
|
799
|
-
} else if (Array.isArray(content)) {
|
|
800
|
-
parts.push(...content);
|
|
801
|
-
}
|
|
802
|
-
parts.push({ type: 'text', text: '\n' });
|
|
803
|
-
}
|
|
804
|
-
while (parts.length && parts[parts.length - 1]?.type === 'text' && parts[parts.length - 1]?.text === '\n') parts.pop();
|
|
805
|
-
return parts.length === 1 && parts[0]?.type === 'text' ? parts[0].text : parts;
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
function mergePastedImages(entries) {
|
|
809
|
-
const out = {};
|
|
810
|
-
for (const entry of entries || []) {
|
|
811
|
-
const images = entry?.pastedImages;
|
|
812
|
-
if (!images || typeof images !== 'object') continue;
|
|
813
|
-
for (const [id, image] of Object.entries(images)) {
|
|
814
|
-
if (image) out[id] = image;
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
return Object.keys(out).length > 0 ? out : null;
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
function callCommitCallbacks(entries) {
|
|
821
|
-
for (const entry of entries || []) {
|
|
822
|
-
try { entry?.onCommitted?.(); } catch {}
|
|
823
|
-
}
|
|
824
|
-
}
|
|
94
|
+
export { parseBackgroundTaskEnvelope };
|
|
95
|
+
|
|
96
|
+
// Ink renders through a maxFps throttle (120fps in index.jsx, ≈8.3ms). A plain
|
|
97
|
+
// setImmediate only yields to the event loop; if Ink already painted within the
|
|
98
|
+
// current throttle window, the next paint may still be queued and our following
|
|
99
|
+
// transcript mutation can coalesce into the same visible frame. Wait just past
|
|
100
|
+
// one render window when we intentionally split transcript commits for visual
|
|
101
|
+
// stability (preamble frame → tool-card frame).
|
|
102
|
+
const RENDER_THROTTLE_FLUSH_MS = 12;
|
|
103
|
+
const yieldToRenderer = () => new Promise((resolve) => {
|
|
104
|
+
setTimeout(resolve, RENDER_THROTTLE_FLUSH_MS);
|
|
105
|
+
});
|
|
825
106
|
|
|
826
107
|
function notificationQueueKey(event, text, parsed) {
|
|
827
108
|
const meta = event?.meta && typeof event.meta === 'object' ? event.meta : {};
|
|
@@ -837,8 +118,8 @@ function notificationQueueKey(event, text, parsed) {
|
|
|
837
118
|
: tag
|
|
838
119
|
? `tag:${tag}:${shortTextFingerprint(synthetic.result || text)}`
|
|
839
120
|
: '';
|
|
840
|
-
const
|
|
841
|
-
if (resultId ||
|
|
121
|
+
const agent = String(synthetic.args?.agent || '').trim();
|
|
122
|
+
if (resultId || agent) return ['agent-result', resultId, agent].filter(Boolean).join(':');
|
|
842
123
|
}
|
|
843
124
|
const id = String(meta.execution_id || parsed?.taskId || '').trim();
|
|
844
125
|
if (!id) return '';
|
|
@@ -883,40 +164,14 @@ function isExecutionNotification(event, text, parsed) {
|
|
|
883
164
|
return Boolean(parsed?.taskId && /^(?:agent task:|task_id:)/mi.test(String(text || '')));
|
|
884
165
|
}
|
|
885
166
|
|
|
886
|
-
function agentArgsWithResultMetadata(args, parsed) {
|
|
887
|
-
if (!parsed) return args;
|
|
888
|
-
const next = { ...(args && typeof args === 'object' ? args : {}) };
|
|
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
|
-
}
|
|
898
|
-
if (parsed.status) next.status = parsed.status;
|
|
899
|
-
if (parsed.taskId) next.task_id = parsed.taskId;
|
|
900
|
-
if (parsed.role) next.role = parsed.role;
|
|
901
|
-
if (parsed.preset) next.preset = parsed.preset;
|
|
902
|
-
if (parsed.provider) next.provider = parsed.provider;
|
|
903
|
-
if (parsed.model) next.model = parsed.model;
|
|
904
|
-
if (parsed.effort) next.effort = parsed.effort;
|
|
905
|
-
if (parsed.fast !== undefined) next.fast = parsed.fast;
|
|
906
|
-
if (!next.tag && parsed.target) {
|
|
907
|
-
const target = parsed.target.split(/\s+/)[0];
|
|
908
|
-
if (target && !target.startsWith('sess_')) next.tag = target;
|
|
909
|
-
}
|
|
910
|
-
return next;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
167
|
export async function createEngineSession({
|
|
914
168
|
provider: providerName,
|
|
915
169
|
model,
|
|
916
170
|
toolMode = 'full',
|
|
171
|
+
remote = false,
|
|
917
172
|
} = {}) {
|
|
918
173
|
const startedAt = performance.now();
|
|
919
|
-
bootProfile('engine:create:start', { provider: providerName, model, toolMode });
|
|
174
|
+
bootProfile('engine:create:start', { provider: providerName, model, toolMode, remote });
|
|
920
175
|
// Silence provider/session diagnostics so they cannot tear through the
|
|
921
176
|
// alternate-screen React/ink render.
|
|
922
177
|
process.env.MIXDOG_QUIET_PROVIDER_LOG = '1';
|
|
@@ -928,7 +183,7 @@ export async function createEngineSession({
|
|
|
928
183
|
const importStartedAt = performance.now();
|
|
929
184
|
const { createMixdogSessionRuntime } = await import(SESSION_RUNTIME_MODULE);
|
|
930
185
|
bootProfile('session-runtime:imported', { ms: (performance.now() - importStartedAt).toFixed(1) });
|
|
931
|
-
const runtime = await createMixdogSessionRuntime({ provider: providerName, model, toolMode });
|
|
186
|
+
const runtime = await createMixdogSessionRuntime({ provider: providerName, model, toolMode, remote });
|
|
932
187
|
bootProfile('engine:create:runtime-ready', { ms: (performance.now() - startedAt).toFixed(1) });
|
|
933
188
|
const cwd = runtime.cwd || process.cwd();
|
|
934
189
|
const stateStartedAt = performance.now();
|
|
@@ -965,6 +220,7 @@ export async function createEngineSession({
|
|
|
965
220
|
searchRoute: runtime.getSearchRoute?.() || runtime.searchRoute || null,
|
|
966
221
|
autoClear: autoClearState(),
|
|
967
222
|
workflow: runtime.workflow || null,
|
|
223
|
+
remoteEnabled: runtime.isRemoteEnabled?.() === true,
|
|
968
224
|
});
|
|
969
225
|
|
|
970
226
|
const routeState = () => ({
|
|
@@ -997,6 +253,7 @@ export async function createEngineSession({
|
|
|
997
253
|
let state = {
|
|
998
254
|
items: [],
|
|
999
255
|
toasts: [],
|
|
256
|
+
progressHint: null,
|
|
1000
257
|
busy: false,
|
|
1001
258
|
commandBusy: false,
|
|
1002
259
|
commandStatus: null,
|
|
@@ -1006,6 +263,14 @@ export async function createEngineSession({
|
|
|
1006
263
|
toolApproval: null,
|
|
1007
264
|
lastTurn: null,
|
|
1008
265
|
stats: createSessionStats(),
|
|
266
|
+
// Incremental derivations published by the engine so App does not scan all
|
|
267
|
+
// transcript items on every change:
|
|
268
|
+
// - activeToolSummary: running Explore/Search active counts + earliest
|
|
269
|
+
// startedAt for the prompt-line status (replaces App.jsx O(n) items scan).
|
|
270
|
+
// - promptHistoryList: newest-first deduped user-prompt history, rebuilt
|
|
271
|
+
// only when a user item is appended (replaces the per-change rescan).
|
|
272
|
+
activeToolSummary: null,
|
|
273
|
+
promptHistoryList: [],
|
|
1009
274
|
...baseRouteState(),
|
|
1010
275
|
displayContextWindow: 0,
|
|
1011
276
|
compactBoundaryTokens: 0,
|
|
@@ -1119,13 +384,102 @@ export async function createEngineSession({
|
|
|
1119
384
|
const id = nextItems[i]?.id;
|
|
1120
385
|
if (id != null) itemIndexById.set(id, i);
|
|
1121
386
|
}
|
|
387
|
+
// Bulk item swap (session load / clear / compact). Derive the prompt-history
|
|
388
|
+
// list from the NEW items and stage it onto state here so App never rescans;
|
|
389
|
+
// the callers that invoke replaceItems always follow with a set({items:...,
|
|
390
|
+
// ...}) that carries fresh references, so this pre-stage does not defeat any
|
|
391
|
+
// emit (the accompanying set() diffs the full patch). A bulk swap also
|
|
392
|
+
// discards the old transcript, so drop any tracked active tool calls.
|
|
393
|
+
activeToolCalls.clear();
|
|
394
|
+
state = { ...state, items: nextItems, promptHistoryList: recomputePromptHistory(nextItems), activeToolSummary: null };
|
|
1122
395
|
return nextItems;
|
|
1123
396
|
};
|
|
397
|
+
let flushDeferredBeforeImmediatePush = null;
|
|
398
|
+
let pushingFromDeferredEntry = false;
|
|
399
|
+
// --- Prompt-history list (newest-first, deduped) maintained incrementally ---
|
|
400
|
+
// App previously rebuilt this from state.items on EVERY transcript change
|
|
401
|
+
// (App.jsx recentPromptHistory useMemo). It only changes when a user item is
|
|
402
|
+
// appended, so rebuild it there and on bulk item swaps, publishing to
|
|
403
|
+
// state.promptHistoryList.
|
|
404
|
+
const PROMPT_HISTORY_LIMIT = 50;
|
|
405
|
+
const promptHistoryKey = (value) => String(value || '').trim().replace(/\s+/g, ' ');
|
|
406
|
+
const recomputePromptHistory = (sourceItems = null) => {
|
|
407
|
+
// Pure: derive the newest-first deduped user-prompt history from the given
|
|
408
|
+
// items (defaults to state.items) and RETURN it. Callers decide whether to
|
|
409
|
+
// publish via set() so the immutable-emit contract is preserved.
|
|
410
|
+
const items = Array.isArray(sourceItems) ? sourceItems : (Array.isArray(state.items) ? state.items : []);
|
|
411
|
+
const seen = new Set();
|
|
412
|
+
const history = [];
|
|
413
|
+
for (let i = items.length - 1; i >= 0 && history.length < PROMPT_HISTORY_LIMIT; i -= 1) {
|
|
414
|
+
const item = items[i];
|
|
415
|
+
if (item?.kind !== 'user') continue;
|
|
416
|
+
const text = String(item.text || '').trim();
|
|
417
|
+
const key = promptHistoryKey(text);
|
|
418
|
+
if (!key || seen.has(key)) continue;
|
|
419
|
+
seen.add(key);
|
|
420
|
+
history.push(text);
|
|
421
|
+
}
|
|
422
|
+
return history;
|
|
423
|
+
};
|
|
424
|
+
// --- Active-tool summary (Explore/Search) maintained incrementally ---
|
|
425
|
+
// App previously scanned every transcript item on every change to derive the
|
|
426
|
+
// prompt-line "Exploring N / Searching N" status. Instead the tool lifecycle
|
|
427
|
+
// below tracks per-callId category + started-at in activeToolCalls and derives
|
|
428
|
+
// the small summary from it, publishing state.activeToolSummary only when the
|
|
429
|
+
// aggregate (counts + earliest start) actually changes.
|
|
430
|
+
const activeToolCalls = new Map(); // callKey -> { category, count, startedAt }
|
|
431
|
+
const recomputeActiveToolSummary = () => {
|
|
432
|
+
let exploreCount = 0, exploreStart = 0, searchCount = 0, searchStart = 0;
|
|
433
|
+
for (const rec of activeToolCalls.values()) {
|
|
434
|
+
if (!rec) continue;
|
|
435
|
+
const c = Math.max(1, Number(rec.count || 1));
|
|
436
|
+
const started = Number(rec.startedAt || 0);
|
|
437
|
+
if (rec.category === 'Explore') {
|
|
438
|
+
exploreCount += c;
|
|
439
|
+
if (started > 0 && (exploreStart === 0 || started < exploreStart)) exploreStart = started;
|
|
440
|
+
} else if (rec.category === 'Search') {
|
|
441
|
+
searchCount += c;
|
|
442
|
+
if (started > 0 && (searchStart === 0 || started < searchStart)) searchStart = started;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const next = (exploreCount || searchCount)
|
|
446
|
+
? `${exploreCount}:${exploreStart}:${searchCount}:${searchStart}`
|
|
447
|
+
: '';
|
|
448
|
+
const prev = state.activeToolSummary || '';
|
|
449
|
+
if (next !== prev) set({ activeToolSummary: next || null });
|
|
450
|
+
};
|
|
451
|
+
const markToolCallActive = (callKey, category, count, startedAt) => {
|
|
452
|
+
if (!callKey || (category !== 'Explore' && category !== 'Search')) return;
|
|
453
|
+
activeToolCalls.set(callKey, { category, count: Math.max(1, Number(count || 1)), startedAt: Number(startedAt || Date.now()) });
|
|
454
|
+
recomputeActiveToolSummary();
|
|
455
|
+
};
|
|
456
|
+
const markToolCallDone = (callKey) => {
|
|
457
|
+
if (!callKey || !activeToolCalls.has(callKey)) return;
|
|
458
|
+
activeToolCalls.delete(callKey);
|
|
459
|
+
recomputeActiveToolSummary();
|
|
460
|
+
};
|
|
461
|
+
const clearActiveToolSummary = () => {
|
|
462
|
+
if (activeToolCalls.size === 0 && !state.activeToolSummary) return;
|
|
463
|
+
activeToolCalls.clear();
|
|
464
|
+
if (state.activeToolSummary) set({ activeToolSummary: null });
|
|
465
|
+
};
|
|
1124
466
|
const pushItem = (item) => {
|
|
467
|
+
if (!pushingFromDeferredEntry && flushDeferredBeforeImmediatePush) {
|
|
468
|
+
flushDeferredBeforeImmediatePush();
|
|
469
|
+
}
|
|
1125
470
|
const index = state.items.length;
|
|
1126
471
|
const items = [...state.items, item];
|
|
1127
472
|
if (item?.id != null) itemIndexById.set(item.id, index);
|
|
1128
|
-
|
|
473
|
+
if (item?.kind === 'user') {
|
|
474
|
+
// Rebuild the derived history against the NEW list (not yet in state) and
|
|
475
|
+
// publish items + the fresh list in ONE set(). Do NOT pre-assign to state
|
|
476
|
+
// first — set() diffs against the current state, so a pre-assign would make
|
|
477
|
+
// the references identical and skip emit().
|
|
478
|
+
const promptHistoryList = recomputePromptHistory(items);
|
|
479
|
+
set({ items, promptHistoryList });
|
|
480
|
+
} else {
|
|
481
|
+
set({ items });
|
|
482
|
+
}
|
|
1129
483
|
};
|
|
1130
484
|
const upsertSyntheticToolItem = (text, id = nextId(), parsed = null) => {
|
|
1131
485
|
const synthetic = parseSyntheticAgentMessage(text);
|
|
@@ -1181,6 +535,16 @@ export async function createEngineSession({
|
|
|
1181
535
|
pushItem({ kind: 'notice', id, text: value, tone });
|
|
1182
536
|
return id;
|
|
1183
537
|
};
|
|
538
|
+
// Sticky (non-TTL) input-hint-line progress state, for long-running
|
|
539
|
+
// installs (e.g. voice runtime download) that would otherwise spam the
|
|
540
|
+
// 3s toast queue. Distinct from pushToast/pushNotice: it persists across
|
|
541
|
+
// renders until explicitly cleared (setProgressHint('', ...) or a falsy
|
|
542
|
+
// text), and App.jsx's inputHint falls back to it only when no promptHint
|
|
543
|
+
// and no live toast currently cover the same line.
|
|
544
|
+
const setProgressHint = (text, tone = 'info') => {
|
|
545
|
+
const value = String(text ?? '').trim();
|
|
546
|
+
set({ progressHint: value ? { text: value, tone } : null });
|
|
547
|
+
};
|
|
1184
548
|
const toolApprovalQueue = [];
|
|
1185
549
|
let activeToolApproval = null;
|
|
1186
550
|
function normalizeToolApprovalRequest(input = {}, id = nextId()) {
|
|
@@ -1384,6 +748,51 @@ export async function createEngineSession({
|
|
|
1384
748
|
});
|
|
1385
749
|
}
|
|
1386
750
|
|
|
751
|
+
const CANCELLED_RESULT_STATUS_LINE = '[status: cancelled]';
|
|
752
|
+
|
|
753
|
+
function normalizedResultStatusToken(value) {
|
|
754
|
+
const raw = String(value || '').trim().toLowerCase();
|
|
755
|
+
if (!raw) return '';
|
|
756
|
+
if (/^(running|pending|queued|in_progress|in-progress)$/.test(raw)) return 'running';
|
|
757
|
+
if (/^(completed|complete|done|success|succeeded|ok)$/.test(raw)) return 'completed';
|
|
758
|
+
if (/^(failed|fail|error|errored|timeout|timed_out|killed)$/.test(raw)) return 'failed';
|
|
759
|
+
if (/^(cancelled|canceled|cancel)$/.test(raw)) return 'cancelled';
|
|
760
|
+
return '';
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
function resultTextTerminalStatus(text) {
|
|
764
|
+
const body = String(text || '');
|
|
765
|
+
const tagged = body.match(/<status[^>]*>([\s\S]*?)<\/status>/i)?.[1]?.trim();
|
|
766
|
+
if (tagged) return normalizedResultStatusToken(tagged);
|
|
767
|
+
const bracketed = body.match(/^\[status:\s*([^\]]*)\]/mi)?.[1]?.trim();
|
|
768
|
+
if (bracketed) return normalizedResultStatusToken(bracketed);
|
|
769
|
+
const inline = body.match(/^(?:status|state):\s*([^\s·,;]+)/mi)?.[1]?.trim();
|
|
770
|
+
return normalizedResultStatusToken(inline);
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
function itemHasKnownTerminalStatus(item, texts = []) {
|
|
774
|
+
const settled = (token) => token === 'completed' || token === 'failed' || token === 'cancelled';
|
|
775
|
+
if (settled(normalizedResultStatusToken(item?.args?.status))) return true;
|
|
776
|
+
for (const text of texts) {
|
|
777
|
+
if (settled(resultTextTerminalStatus(text))) return true;
|
|
778
|
+
}
|
|
779
|
+
return false;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function withCancelledResultMarker(text, item) {
|
|
783
|
+
const body = String(text || '');
|
|
784
|
+
// Do NOT inspect item.rawResult here: aggregate rawResult is child tool
|
|
785
|
+
// output (`1. grep\n<result>…`) that can incidentally contain a `status:`
|
|
786
|
+
// line, which would false-positive as an already-terminal status and skip
|
|
787
|
+
// the cancelled marker. Only result/text/body are engine-controlled
|
|
788
|
+
// collapsed detail (empty / status word / an existing marker), so they are
|
|
789
|
+
// the trustworthy terminal-status sources.
|
|
790
|
+
const sources = [item?.result, item?.text, body];
|
|
791
|
+
if (itemHasKnownTerminalStatus(item, sources)) return body;
|
|
792
|
+
if (!body.trim()) return `${CANCELLED_RESULT_STATUS_LINE}\n`;
|
|
793
|
+
return `${CANCELLED_RESULT_STATUS_LINE}\n${body}`;
|
|
794
|
+
}
|
|
795
|
+
|
|
1387
796
|
function groupedToolResultText(group) {
|
|
1388
797
|
const completed = Math.min(group.count, group.completed);
|
|
1389
798
|
if (group.count <= 1) return group.results.at(-1)?.text ?? '';
|
|
@@ -1424,7 +833,7 @@ export async function createEngineSession({
|
|
|
1424
833
|
const chunks = [];
|
|
1425
834
|
for (const rec of calls || []) {
|
|
1426
835
|
if (rec?.resolved !== true) continue;
|
|
1427
|
-
|
|
836
|
+
let text = String(rec?.resultText || '').replace(/\s+$/, '');
|
|
1428
837
|
if (!text.trim()) continue;
|
|
1429
838
|
const label = String(rec?.name || rec?.category || 'tool').trim() || 'tool';
|
|
1430
839
|
chunks.push(`${chunks.length + 1}. ${label}\n${text}`);
|
|
@@ -1432,37 +841,15 @@ export async function createEngineSession({
|
|
|
1432
841
|
return chunks.join('\n\n');
|
|
1433
842
|
}
|
|
1434
843
|
|
|
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
|
-
|
|
1448
844
|
function aggregateBucketForCategory(category) {
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
case 'Explore':
|
|
1458
|
-
return 'explore';
|
|
1459
|
-
case 'Patch':
|
|
1460
|
-
return 'patch';
|
|
1461
|
-
default:
|
|
1462
|
-
// Shell/Agent/Channel/Setup/Other stay as their own cards so risky or
|
|
1463
|
-
// semantically distinct actions do not disappear inside a discovery log.
|
|
1464
|
-
return null;
|
|
1465
|
-
}
|
|
845
|
+
// Merge consecutive tool calls of the SAME category into one aggregate card;
|
|
846
|
+
// a different category opens a fresh card (no cross-category merge). The
|
|
847
|
+
// bucket key is the category itself, so a run of Search calls collapses into
|
|
848
|
+
// one Search card while an adjacent Read/Patch stays separate. Falls back to
|
|
849
|
+
// 'default' when a call has no resolved category. Hook/approval denials keep
|
|
850
|
+
// their dedicated ToolHookDenialCard path in App.jsx.
|
|
851
|
+
const key = String(category || '').trim();
|
|
852
|
+
return key ? `category:${key}` : 'default';
|
|
1466
853
|
}
|
|
1467
854
|
|
|
1468
855
|
function aggregateSummaries(aggregate) {
|
|
@@ -1483,6 +870,9 @@ export async function createEngineSession({
|
|
|
1483
870
|
if (!card || card.done) return false;
|
|
1484
871
|
const callId = toolResultCallId(message) || card.callId;
|
|
1485
872
|
if (callId && done.has(callId)) return false;
|
|
873
|
+
// Any resolving call clears its active-summary entry (keyed by the same
|
|
874
|
+
// callKey used at markToolCallActive; card.callId holds it for both branches).
|
|
875
|
+
markToolCallDone(card.callId);
|
|
1486
876
|
// A result for this card arrived (possibly before its deferred push delay
|
|
1487
877
|
// elapsed) — surface the card now so the patch below has a live item and the
|
|
1488
878
|
// fast tool paints a completed card directly, no pending placeholder stage.
|
|
@@ -1511,22 +901,20 @@ export async function createEngineSession({
|
|
|
1511
901
|
const allCalls = [...aggregate.calls.values()];
|
|
1512
902
|
const completed = allCalls.filter((r) => r.resolved).length;
|
|
1513
903
|
const errors = allCalls.filter((r) => r.isError).length;
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
detailText = detailText ? `${detailText} · ${errors} Failed` : `${errors} Failed`;
|
|
1523
|
-
}
|
|
1524
|
-
}
|
|
904
|
+
// Collapsed detail carries the merged per-call count summary
|
|
905
|
+
// ("512 lines, 6 matches, 3 files") so the finished card answers "how
|
|
906
|
+
// much" without ctrl+o. Failures keep a bare 'N Ok · N Failed' status so
|
|
907
|
+
// an error stays visible while collapsed.
|
|
908
|
+
const succeeded = completed - errors;
|
|
909
|
+
const detailText = errors > 0
|
|
910
|
+
? (succeeded > 0 ? `${succeeded} Ok · ${errors} Failed` : `${errors} Failed`)
|
|
911
|
+
: formatAggregateDetail(aggregateSummaries(aggregate));
|
|
1525
912
|
const currentItem = state.items.find((it) => it.id === card.itemId);
|
|
1526
913
|
const earlyCompleted = allCalls.filter((r) => r.resolved || r.completedEarly).length;
|
|
1527
914
|
const visualCompleted = Math.max(completed, earlyCompleted, Math.min(allCalls.length, Number(currentItem?.completedCount || 0)));
|
|
1528
915
|
const rawResult = aggregateRawResult(allCalls);
|
|
1529
|
-
|
|
916
|
+
// The numbered+labelled raw (rawResult) is preserved for ctrl+o expansion.
|
|
917
|
+
const displayDetail = detailText;
|
|
1530
918
|
patchItem(card.itemId, {
|
|
1531
919
|
result: displayDetail,
|
|
1532
920
|
text: displayDetail,
|
|
@@ -1575,7 +963,7 @@ export async function createEngineSession({
|
|
|
1575
963
|
return true;
|
|
1576
964
|
}
|
|
1577
965
|
|
|
1578
|
-
const flushToolResults = (messages, toolCards, cardByCallId, toolGroups, done, { finalize = false } = {}) => {
|
|
966
|
+
const flushToolResults = (messages, toolCards, cardByCallId, toolGroups, done, { finalize = false, cancelled = false } = {}) => {
|
|
1579
967
|
const results = [];
|
|
1580
968
|
for (const m of messages || []) {
|
|
1581
969
|
if (!m || m.role !== 'tool') continue;
|
|
@@ -1613,14 +1001,38 @@ export async function createEngineSession({
|
|
|
1613
1001
|
const aggregate = card.aggregate;
|
|
1614
1002
|
if (aggregate && card.itemId === aggregate.itemId) {
|
|
1615
1003
|
const allCalls = [...aggregate.calls.values()];
|
|
1004
|
+
// Never let a call that truly never resolved be presented as a real
|
|
1005
|
+
// completion. Stamp it resolved so completedCount reflects an honest
|
|
1006
|
+
// (if degenerate) accounting instead of manufacturing success out of
|
|
1007
|
+
// a call that never came back. A record already marked completedEarly
|
|
1008
|
+
// (via __earlyNotify) already carries a real isError/resultText/summary
|
|
1009
|
+
// from its actual result — preserve those; only blank-fill for calls
|
|
1010
|
+
// truly never heard from (no completedEarly, no resolved).
|
|
1011
|
+
for (const rec of allCalls) {
|
|
1012
|
+
if (rec.resolved) continue;
|
|
1013
|
+
rec.resolved = true;
|
|
1014
|
+
if (!rec.completedEarly) {
|
|
1015
|
+
rec.isError = false;
|
|
1016
|
+
rec.resultText = rec.resultText || '';
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1616
1019
|
const completed = allCalls.filter((r) => r.resolved).length;
|
|
1617
|
-
const
|
|
1618
|
-
const totalCompleted = remaining > 0 ? completed + remaining : completed;
|
|
1020
|
+
const totalCompleted = completed;
|
|
1619
1021
|
const errors = allCalls.filter((r) => r.isError).length;
|
|
1620
|
-
const
|
|
1621
|
-
const detailText = aggregateDisplayDetail(summaries, allCalls);
|
|
1022
|
+
const succeeded = completed - errors;
|
|
1622
1023
|
const rawResult = aggregateRawResult(allCalls);
|
|
1623
|
-
|
|
1024
|
+
// Collapsed detail carries the merged per-call count summary; failures
|
|
1025
|
+
// keep a bare 'N Ok · N Failed' status. Raw is kept for ctrl+o.
|
|
1026
|
+
let displayDetail = errors > 0
|
|
1027
|
+
? (succeeded > 0 ? `${succeeded} Ok · ${errors} Failed` : `${errors} Failed`)
|
|
1028
|
+
: formatAggregateDetail(aggregateSummaries(aggregate));
|
|
1029
|
+
if (cancelled) {
|
|
1030
|
+
// Cancelled aggregates MUST keep the [status: cancelled] marker on the
|
|
1031
|
+
// result so terminalStatus parsing resolves to 'cancelled'. Only normal
|
|
1032
|
+
// completions drop the summary; cancelled ones prepend the marker.
|
|
1033
|
+
const currentItem = state.items.find((it) => it.id === card.itemId);
|
|
1034
|
+
displayDetail = withCancelledResultMarker(displayDetail, currentItem);
|
|
1035
|
+
}
|
|
1624
1036
|
patchItem(card.itemId, {
|
|
1625
1037
|
result: displayDetail,
|
|
1626
1038
|
text: displayDetail,
|
|
@@ -1642,7 +1054,11 @@ export async function createEngineSession({
|
|
|
1642
1054
|
const group = toolGroups.get(card.itemId) || { count: 1, completed: 0, errors: 0, results: [] };
|
|
1643
1055
|
group.completed = Math.min(group.count, group.completed + 1);
|
|
1644
1056
|
toolGroups.set(card.itemId, group);
|
|
1645
|
-
|
|
1057
|
+
let resultText = groupedToolResultText(group);
|
|
1058
|
+
if (cancelled) {
|
|
1059
|
+
const currentItem = state.items.find((it) => it.id === card.itemId);
|
|
1060
|
+
resultText = withCancelledResultMarker(resultText, currentItem);
|
|
1061
|
+
}
|
|
1646
1062
|
patchItem(card.itemId, { result: resultText, text: resultText, isError: group.errors > 0, errorCount: group.errors, count: group.count, completedCount: group.completed, completedAt: Date.now() });
|
|
1647
1063
|
card.done = true;
|
|
1648
1064
|
if (card.callId) done.add(card.callId);
|
|
@@ -1660,6 +1076,7 @@ export async function createEngineSession({
|
|
|
1660
1076
|
activePromptRestore = {
|
|
1661
1077
|
text: String(displayText || '').trim(),
|
|
1662
1078
|
pastedImages: options.pastedImages && typeof options.pastedImages === 'object' ? options.pastedImages : null,
|
|
1079
|
+
pastedTexts: options.pastedTexts && typeof options.pastedTexts === 'object' ? options.pastedTexts : null,
|
|
1663
1080
|
onCommitted: typeof options.onCommitted === 'function' ? options.onCommitted : null,
|
|
1664
1081
|
restorable: options.restorable !== false,
|
|
1665
1082
|
submittedIds,
|
|
@@ -1688,8 +1105,7 @@ export async function createEngineSession({
|
|
|
1688
1105
|
// cards (send() still in flight). Hold those by callId until the batch lands.
|
|
1689
1106
|
const earlyResultBuffer = new Map();
|
|
1690
1107
|
const aggregateCards = []; // active aggregate cards in the current consecutive tool block
|
|
1691
|
-
const aggregateByBucket = new Map(); //
|
|
1692
|
-
let openAggregateCard = null;
|
|
1108
|
+
const aggregateByBucket = new Map(); // per-bucket reusable card for the current consecutive tool block; cleared on block seal
|
|
1693
1109
|
|
|
1694
1110
|
// ── Deferred tool-card push (scroll/text sync) ────────────────────────────
|
|
1695
1111
|
// A tool card used to enter the transcript the instant onToolCall fired,
|
|
@@ -1722,12 +1138,23 @@ export async function createEngineSession({
|
|
|
1722
1138
|
try { e.push(); } catch {}
|
|
1723
1139
|
}
|
|
1724
1140
|
};
|
|
1141
|
+
flushDeferredBeforeImmediatePush = () => {
|
|
1142
|
+
if (!deferredEntries.length) return;
|
|
1143
|
+
const last = deferredEntries[deferredEntries.length - 1];
|
|
1144
|
+
if (last) flushDeferredUpTo(last);
|
|
1145
|
+
};
|
|
1725
1146
|
const registerDeferredCard = (card) => {
|
|
1726
1147
|
const entry = {
|
|
1727
1148
|
seq: deferredSeqCounter++,
|
|
1728
1149
|
pushed: false,
|
|
1729
1150
|
timer: null,
|
|
1730
|
-
push: () => {
|
|
1151
|
+
push: () => {
|
|
1152
|
+
card.pushed = true;
|
|
1153
|
+
if (!card.spec) return;
|
|
1154
|
+
card.spec.deferredDisplayReady = true;
|
|
1155
|
+
pushingFromDeferredEntry = true;
|
|
1156
|
+
try { pushItem(card.spec); } finally { pushingFromDeferredEntry = false; }
|
|
1157
|
+
},
|
|
1731
1158
|
};
|
|
1732
1159
|
card.deferred = entry;
|
|
1733
1160
|
card.ensureVisible = () => flushDeferredUpTo(entry);
|
|
@@ -1744,7 +1171,13 @@ export async function createEngineSession({
|
|
|
1744
1171
|
seq: deferredSeqCounter++,
|
|
1745
1172
|
pushed: false,
|
|
1746
1173
|
timer: null,
|
|
1747
|
-
push: () => {
|
|
1174
|
+
push: () => {
|
|
1175
|
+
aggregate.pushed = true;
|
|
1176
|
+
if (!aggregate.pendingSpec) return;
|
|
1177
|
+
aggregate.pendingSpec.deferredDisplayReady = true;
|
|
1178
|
+
pushingFromDeferredEntry = true;
|
|
1179
|
+
try { pushItem(aggregate.pendingSpec); } finally { pushingFromDeferredEntry = false; }
|
|
1180
|
+
},
|
|
1748
1181
|
};
|
|
1749
1182
|
aggregate.deferred = entry;
|
|
1750
1183
|
aggregate.ensureVisible = () => flushDeferredUpTo(entry);
|
|
@@ -1772,6 +1205,7 @@ export async function createEngineSession({
|
|
|
1772
1205
|
activePromptRestore.committed = true;
|
|
1773
1206
|
activePromptRestore.requeueEntries = [];
|
|
1774
1207
|
activePromptRestore.pastedImages = null;
|
|
1208
|
+
activePromptRestore.pastedTexts = null;
|
|
1775
1209
|
}
|
|
1776
1210
|
};
|
|
1777
1211
|
|
|
@@ -1804,10 +1238,14 @@ export async function createEngineSession({
|
|
|
1804
1238
|
if (allCalls.length === 0) continue;
|
|
1805
1239
|
aggregate.ensureVisible?.();
|
|
1806
1240
|
const errors = allCalls.filter((r) => r.isError).length;
|
|
1807
|
-
const
|
|
1808
|
-
const
|
|
1241
|
+
const completed = allCalls.filter((r) => r.resolved).length;
|
|
1242
|
+
const succeeded = completed - errors;
|
|
1809
1243
|
const rawResult = aggregateRawResult(allCalls);
|
|
1810
|
-
|
|
1244
|
+
// Merged count summary (see patchToolCardResult); failures keep
|
|
1245
|
+
// 'N Ok · N Failed'. Raw preserved for ctrl+o expansion.
|
|
1246
|
+
const displayDetail = errors > 0
|
|
1247
|
+
? (succeeded > 0 ? `${succeeded} Ok · ${errors} Failed` : `${errors} Failed`)
|
|
1248
|
+
: formatAggregateDetail(aggregateSummaries(aggregate));
|
|
1811
1249
|
patchItem(aggregate.itemId, {
|
|
1812
1250
|
result: displayDetail,
|
|
1813
1251
|
text: displayDetail,
|
|
@@ -1824,13 +1262,11 @@ export async function createEngineSession({
|
|
|
1824
1262
|
completeAggregateVisual();
|
|
1825
1263
|
finalizeToolHeaders();
|
|
1826
1264
|
aggregateCards.length = 0;
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
const last = state.items[state.items.length - 1];
|
|
1833
|
-
return last?.kind === 'tool' && last.aggregate === true && last.id === aggregate.itemId;
|
|
1265
|
+
// Seal the block: same-bucket calls after this point must open a fresh
|
|
1266
|
+
// card, never continue one from before the seal (assistant text/turn
|
|
1267
|
+
// end boundary). Cross-block behavior is unchanged by the in-block
|
|
1268
|
+
// reuse relaxation below.
|
|
1269
|
+
aggregateByBucket.clear();
|
|
1834
1270
|
};
|
|
1835
1271
|
|
|
1836
1272
|
const rememberActiveAggregate = (aggregate) => {
|
|
@@ -1840,24 +1276,19 @@ export async function createEngineSession({
|
|
|
1840
1276
|
};
|
|
1841
1277
|
|
|
1842
1278
|
const ensureAggregateCard = (bucket) => {
|
|
1843
|
-
// Reuse
|
|
1844
|
-
//
|
|
1845
|
-
//
|
|
1846
|
-
//
|
|
1847
|
-
//
|
|
1848
|
-
//
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
//
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
const tailAggregate = aggregateByBucket.get(bucket);
|
|
1857
|
-
if (tailAggregate && (!tailAggregate.pushed || isAggregateTail(tailAggregate))) {
|
|
1858
|
-
openAggregateCard = tailAggregate;
|
|
1859
|
-
rememberActiveAggregate(tailAggregate);
|
|
1860
|
-
return tailAggregate;
|
|
1279
|
+
// Reuse any same-bucket aggregate created earlier in the SAME consecutive
|
|
1280
|
+
// tool block, even when a different-category card was interleaved after
|
|
1281
|
+
// it (Read, Search, Read → the two Reads merge into one card; the Search
|
|
1282
|
+
// stays separate). The block is only sealed — forcing a fresh card per
|
|
1283
|
+
// bucket — by clearAggregateContinuation (assistant text lands or the
|
|
1284
|
+
// turn ends), which clears aggregateByBucket. Reusing a pushed, non-tail
|
|
1285
|
+
// card is safe: the aggregate card body is a fixed header+1-detail-row
|
|
1286
|
+
// height, so patching its count/completedCount later never reflows the
|
|
1287
|
+
// cards that were pushed after it.
|
|
1288
|
+
const cached = aggregateByBucket.get(bucket);
|
|
1289
|
+
if (cached) {
|
|
1290
|
+
rememberActiveAggregate(cached);
|
|
1291
|
+
return cached;
|
|
1861
1292
|
}
|
|
1862
1293
|
const itemId = nextId();
|
|
1863
1294
|
const aggregate = {
|
|
@@ -1874,7 +1305,6 @@ export async function createEngineSession({
|
|
|
1874
1305
|
// pendingSpec current until the timer/result flushes it in call order.
|
|
1875
1306
|
registerDeferredAggregate(aggregate);
|
|
1876
1307
|
rememberActiveAggregate(aggregate);
|
|
1877
|
-
openAggregateCard = aggregate;
|
|
1878
1308
|
return aggregate;
|
|
1879
1309
|
};
|
|
1880
1310
|
|
|
@@ -1973,6 +1403,17 @@ export async function createEngineSession({
|
|
|
1973
1403
|
let _pendingThinkFlush = false; // true when a thinking update is queued
|
|
1974
1404
|
let _pendingThinkingLastEndedAt = 0;
|
|
1975
1405
|
let compactingActive = false;
|
|
1406
|
+
// Engine-local streaming scalars. Neither responseLength nor thinkingText is
|
|
1407
|
+
// rendered per-token by any consumer: App reads state.thinking only as a
|
|
1408
|
+
// boolean (App.jsx `!!(state.thinking || liveSpinner?.thinking)`) and the
|
|
1409
|
+
// Spinner takes outputTokens, not responseLength. So we keep these growing
|
|
1410
|
+
// values in engine-local vars and publish to the store only on a visible
|
|
1411
|
+
// transition (thinking on↔off), a completed visible text line, tool/usage
|
|
1412
|
+
// updates, or finalization — not on every 8ms streaming flush.
|
|
1413
|
+
let _publishedThinkingActive = false; // last thinking boolean pushed to store
|
|
1414
|
+
// responseLength is only consumed at finalize as an outputTokens fallback
|
|
1415
|
+
// (Math.round(responseLength/4)); we refresh state.spinner.responseLength on
|
|
1416
|
+
// visible-line flush and finalize so that fallback stays valid.
|
|
1976
1417
|
|
|
1977
1418
|
const flushStreamBatch = () => {
|
|
1978
1419
|
if (_batchTimer !== null) {
|
|
@@ -2017,24 +1458,51 @@ export async function createEngineSession({
|
|
|
2017
1458
|
}
|
|
2018
1459
|
}
|
|
2019
1460
|
}
|
|
1461
|
+
// Only touch the spinner when there is a real reason: a visible-line
|
|
1462
|
+
// change (patch.items set above), a thinking→responding transition, or a
|
|
1463
|
+
// pending thinking end timestamp. Refresh responseLength here so the
|
|
1464
|
+
// finalize outputTokens fallback stays valid without a per-token push.
|
|
2020
1465
|
const responseLengthVal = assistantText.length + thinkingText.length;
|
|
2021
|
-
|
|
1466
|
+
const visibleLineChanged = patch.items !== undefined;
|
|
1467
|
+
const thinkingTransition = _publishedThinkingActive === true; // was thinking, now responding
|
|
1468
|
+
if (state.spinner && (visibleLineChanged || thinkingTransition || _pendingThinkingLastEndedAt)) {
|
|
2022
1469
|
patch.spinner = { ...state.spinner, responseLength: responseLengthVal, thinking: false, thinkingLastEndedAt: _pendingThinkingLastEndedAt || state.spinner.thinkingLastEndedAt, mode: compactingActive ? 'compacting' : 'responding' };
|
|
1470
|
+
_publishedThinkingActive = false;
|
|
2023
1471
|
}
|
|
2024
1472
|
if (Object.keys(patch).length > 0) set(patch);
|
|
2025
1473
|
_pendingThinkingLastEndedAt = 0;
|
|
2026
1474
|
}
|
|
2027
1475
|
if (_pendingThinkFlush) {
|
|
2028
1476
|
_pendingThinkFlush = false;
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
1477
|
+
// App only consumes state.thinking as a boolean and the Spinner only
|
|
1478
|
+
// reads the thinking flag + timing anchors — none of them render the
|
|
1479
|
+
// growing thinkingText. So publish the thinking boolean only on the
|
|
1480
|
+
// OFF→ON transition (or when compacting toggles the flag), not on every
|
|
1481
|
+
// 8ms reasoning chunk. The full thinkingText stays engine-local and is
|
|
1482
|
+
// emitted at finalize via the normal spinner/thinking teardown.
|
|
1483
|
+
const nextThinkingActive = !compactingActive;
|
|
1484
|
+
// Skip the push when the published thinking boolean is unchanged: neither
|
|
1485
|
+
// the growing thinkingText nor responseLength is rendered per-token, and
|
|
1486
|
+
// the Spinner derives its live elapsed from the (already-published)
|
|
1487
|
+
// thinkingSegmentStartedAt anchor. Applies to both thinking and
|
|
1488
|
+
// compacting steady state.
|
|
1489
|
+
if (nextThinkingActive === _publishedThinkingActive) {
|
|
1490
|
+
// no-op: boolean unchanged
|
|
1491
|
+
} else {
|
|
1492
|
+
const responseLengthVal = assistantText.length + thinkingText.length;
|
|
1493
|
+
const thinkingElapsedMs = accumulatedThinkingMs + (thinkingSegmentStartedAt ? Math.max(0, Date.now() - thinkingSegmentStartedAt) : 0);
|
|
1494
|
+
// state.thinking stays a truthy sentinel while active; consumers read it
|
|
1495
|
+
// as a boolean. Keep the value stable (thinkingText) so a late consumer
|
|
1496
|
+
// still sees real text, but only push on transition.
|
|
1497
|
+
const patch = { thinking: compactingActive ? null : thinkingText };
|
|
1498
|
+
if (state.spinner) {
|
|
1499
|
+
patch.spinner = compactingActive
|
|
1500
|
+
? { ...state.spinner, responseLength: responseLengthVal, thinking: false, thinkingAccumulatedMs: accumulatedThinkingMs, thinkingElapsedMs, thinkingLastEndedAt: state.spinner.thinkingLastEndedAt || 0, mode: 'compacting' }
|
|
1501
|
+
: { ...state.spinner, responseLength: responseLengthVal, thinking: true, thinkingStartedAt, thinkingSegmentStartedAt, thinkingAccumulatedMs: accumulatedThinkingMs, thinkingElapsedMs, thinkingLastEndedAt: 0, mode: 'thinking' };
|
|
1502
|
+
}
|
|
1503
|
+
set(patch);
|
|
1504
|
+
_publishedThinkingActive = nextThinkingActive;
|
|
2036
1505
|
}
|
|
2037
|
-
set(patch);
|
|
2038
1506
|
}
|
|
2039
1507
|
};
|
|
2040
1508
|
|
|
@@ -2049,6 +1517,8 @@ export async function createEngineSession({
|
|
|
2049
1517
|
const markToolCardCompletedState = (callId, message) => {
|
|
2050
1518
|
const card = cardByCallId.get(callId);
|
|
2051
1519
|
if (!card) return;
|
|
1520
|
+
// Early completion also clears the active-summary entry.
|
|
1521
|
+
markToolCallDone(card.callId);
|
|
2052
1522
|
const aggregate = card.aggregate;
|
|
2053
1523
|
if (aggregate && card.itemId === aggregate.itemId) {
|
|
2054
1524
|
const callRec = aggregate.calls.get(callId);
|
|
@@ -2065,19 +1535,13 @@ export async function createEngineSession({
|
|
|
2065
1535
|
const allCalls = [...aggregate.calls.values()];
|
|
2066
1536
|
const completedCount = allCalls.filter((r) => r.resolved || r.completedEarly).length;
|
|
2067
1537
|
const errors = allCalls.filter((r) => r.isError).length;
|
|
2068
|
-
const
|
|
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
|
-
}
|
|
1538
|
+
const succeeded = completedCount - errors;
|
|
2079
1539
|
const rawResult = aggregateRawResult(allCalls);
|
|
2080
|
-
|
|
1540
|
+
// Status-only collapsed detail (no per-result summary); failures keep
|
|
1541
|
+
// 'N Failed'. Raw preserved for ctrl+o expansion.
|
|
1542
|
+
const displayDetail = errors > 0
|
|
1543
|
+
? (succeeded > 0 ? `${succeeded} Ok · ${errors} Failed` : `${errors} Failed`)
|
|
1544
|
+
: '';
|
|
2081
1545
|
const currentItem = state.items.find((it) => it.id === card.itemId);
|
|
2082
1546
|
const visualCompleted = Math.max(
|
|
2083
1547
|
completedCount,
|
|
@@ -2127,7 +1591,11 @@ export async function createEngineSession({
|
|
|
2127
1591
|
assistantText = '';
|
|
2128
1592
|
const value = String(text || '').trim();
|
|
2129
1593
|
if (value) {
|
|
2130
|
-
|
|
1594
|
+
// Any non-tool transcript item is a block boundary: seal the
|
|
1595
|
+
// aggregate continuation (not just finalize headers) so a later
|
|
1596
|
+
// same-category tool call opens a fresh card instead of reusing
|
|
1597
|
+
// one whose count would then change ABOVE this steered user item.
|
|
1598
|
+
clearAggregateContinuation();
|
|
2131
1599
|
pushUserOrSyntheticItem(value);
|
|
2132
1600
|
}
|
|
2133
1601
|
},
|
|
@@ -2141,26 +1609,41 @@ export async function createEngineSession({
|
|
|
2141
1609
|
if (thinkingText && state.thinking) {
|
|
2142
1610
|
const thinkingLastEndedAt = closeThinkingSegment();
|
|
2143
1611
|
set({ thinking: null, spinner: state.spinner ? { ...state.spinner, thinking: false, thinkingAccumulatedMs: accumulatedThinkingMs, thinkingLastEndedAt, mode: 'tool-use' } : state.spinner });
|
|
1612
|
+
_publishedThinkingActive = false;
|
|
2144
1613
|
} else if (state.spinner) {
|
|
2145
1614
|
set({ spinner: { ...state.spinner, mode: 'tool-use' } });
|
|
2146
1615
|
}
|
|
2147
1616
|
const batchCalls = (calls || []).filter(Boolean);
|
|
2148
1617
|
if (batchCalls.length === 0) return;
|
|
2149
|
-
commitAssistantSegment({ sealToolBlock: true });
|
|
1618
|
+
const committedAssistantSegment = commitAssistantSegment({ sealToolBlock: true });
|
|
1619
|
+
if (committedAssistantSegment) {
|
|
1620
|
+
// Let the pre-tool assistant preamble paint as its own frame before
|
|
1621
|
+
// the tool card reserves/pushes rows. When both enter the transcript
|
|
1622
|
+
// in the same render, the bottom-pinned viewport can appear to jump
|
|
1623
|
+
// upward by the combined height ("preamble + tool card" at once).
|
|
1624
|
+
await yieldToRenderer();
|
|
1625
|
+
}
|
|
2150
1626
|
|
|
2151
1627
|
const touchedAggregates = new Set();
|
|
2152
1628
|
for (let i = 0; i < batchCalls.length; i++) {
|
|
2153
1629
|
const c = batchCalls[i];
|
|
2154
1630
|
const name = toolCallName(c);
|
|
2155
1631
|
const args = toolCallArgs(c);
|
|
1632
|
+
// Category drives the aggregate bucket so only same-category calls
|
|
1633
|
+
// merge into one card; classify first, then bucket by it.
|
|
2156
1634
|
const category = classifyToolCategory(name, args);
|
|
2157
1635
|
const bucket = aggregateBucketForCategory(category);
|
|
2158
|
-
const categoryEntry = aggregateToolCategoryEntry(name, args, category);
|
|
2159
1636
|
const callId = toolCallId(c);
|
|
2160
1637
|
const callKey = callId || `__tool_${toolCards.length}_${i}`;
|
|
1638
|
+
// The old App scan counted multi-pattern calls via
|
|
1639
|
+
// aggregateToolCategoryEntry(...).count, not a flat 1. Derive the same
|
|
1640
|
+
// count here so the incremental Explore/Search summary matches.
|
|
1641
|
+
const activeCount = Number(aggregateToolCategoryEntry(name, args, category)?.count || 1);
|
|
1642
|
+
// Track Explore/Search calls as active for the incremental prompt-
|
|
1643
|
+
// line summary; cleared when their result lands or the turn ends.
|
|
1644
|
+
markToolCallActive(callKey, category, activeCount, Date.now());
|
|
2161
1645
|
|
|
2162
1646
|
if (!bucket) {
|
|
2163
|
-
openAggregateCard = null;
|
|
2164
1647
|
const itemId = nextId();
|
|
2165
1648
|
// Defer the visible push: hold the spec and only enter the
|
|
2166
1649
|
// transcript when the real header/detail will paint (delay
|
|
@@ -2193,6 +1676,7 @@ export async function createEngineSession({
|
|
|
2193
1676
|
continue;
|
|
2194
1677
|
}
|
|
2195
1678
|
|
|
1679
|
+
const categoryEntry = aggregateToolCategoryEntry(name, args, category);
|
|
2196
1680
|
const aggregateCard = ensureAggregateCard(bucket);
|
|
2197
1681
|
if (!aggregateCard.categories.has(categoryEntry.key)) aggregateCard.categoryOrder.push(categoryEntry.key);
|
|
2198
1682
|
const prevCategory = aggregateCard.categories.get(categoryEntry.key);
|
|
@@ -2212,6 +1696,17 @@ export async function createEngineSession({
|
|
|
2212
1696
|
for (const aggregateCard of touchedAggregates) {
|
|
2213
1697
|
syncAggregateHeader(aggregateCard);
|
|
2214
1698
|
}
|
|
1699
|
+
if (committedAssistantSegment) {
|
|
1700
|
+
// A pre-tool assistant preamble has already had one render frame to
|
|
1701
|
+
// settle. Do not let the first grouped tool card sit off-screen until
|
|
1702
|
+
// the normal 1s deferred timer: when it later inserts its real 3 rows,
|
|
1703
|
+
// the already-wrapped preamble visibly jumps. Surface the first card
|
|
1704
|
+
// now via the existing deferredDisplayReady path, so the post-
|
|
1705
|
+
// preamble frame contains the intended Running tool card immediately
|
|
1706
|
+
// (no blank placeholder, no delayed row insertion).
|
|
1707
|
+
const firstTouchedAggregate = [...touchedAggregates][0] || null;
|
|
1708
|
+
firstTouchedAggregate?.ensureVisible?.();
|
|
1709
|
+
}
|
|
2215
1710
|
for (const [bufferedCallId, bufferedMessage] of earlyResultBuffer) {
|
|
2216
1711
|
if (!cardByCallId.has(bufferedCallId)) continue;
|
|
2217
1712
|
deliverToolResultMessage(bufferedMessage);
|
|
@@ -2235,6 +1730,11 @@ export async function createEngineSession({
|
|
|
2235
1730
|
},
|
|
2236
1731
|
onCompactEvent: (event) => {
|
|
2237
1732
|
flushStreamBatch();
|
|
1733
|
+
// Non-tool transcript item — same block-boundary rule as the
|
|
1734
|
+
// steered user item above: seal any live aggregate first so a
|
|
1735
|
+
// later same-category tool call doesn't reuse a card whose count
|
|
1736
|
+
// would then change above this statusdone item.
|
|
1737
|
+
clearAggregateContinuation();
|
|
2238
1738
|
pushItem({
|
|
2239
1739
|
kind: 'statusdone',
|
|
2240
1740
|
id: nextId(),
|
|
@@ -2249,6 +1749,7 @@ export async function createEngineSession({
|
|
|
2249
1749
|
compactingActive = true;
|
|
2250
1750
|
const thinkingLastEndedAt = closeThinkingSegment();
|
|
2251
1751
|
_pendingThinkFlush = false;
|
|
1752
|
+
_publishedThinkingActive = false; // compacting cleared the thinking flag
|
|
2252
1753
|
set({
|
|
2253
1754
|
thinking: null,
|
|
2254
1755
|
spinner: {
|
|
@@ -2276,7 +1777,11 @@ export async function createEngineSession({
|
|
|
2276
1777
|
if (!textChunk) return;
|
|
2277
1778
|
markPromptCommitted();
|
|
2278
1779
|
const thinkingLastEndedAt = closeThinkingSegment();
|
|
2279
|
-
|
|
1780
|
+
// Drop any queued think-flush too: it would otherwise re-publish
|
|
1781
|
+
// spinner.thinking:true from flushStreamBatch and resurrect the
|
|
1782
|
+
// indicator after we cleared it here.
|
|
1783
|
+
_pendingThinkFlush = false;
|
|
1784
|
+
if (state.thinking) { set({ thinking: null }); _publishedThinkingActive = false; } // collapse thinking panel immediately, no batch delay
|
|
2280
1785
|
assistantText += textChunk;
|
|
2281
1786
|
currentAssistantText += textChunk;
|
|
2282
1787
|
// Accumulate text and schedule a batched flush (≤1 render per
|
|
@@ -2305,7 +1810,8 @@ export async function createEngineSession({
|
|
|
2305
1810
|
if (currentAssistantText.trim()) return;
|
|
2306
1811
|
markPromptCommitted();
|
|
2307
1812
|
closeThinkingSegment();
|
|
2308
|
-
|
|
1813
|
+
_pendingThinkFlush = false; // see onTextDelta: prevent a stale think flush resurrecting the indicator
|
|
1814
|
+
if (state.thinking) { set({ thinking: null }); _publishedThinkingActive = false; }
|
|
2309
1815
|
assistantText += full;
|
|
2310
1816
|
currentAssistantText += full;
|
|
2311
1817
|
_pendingTextFlush = true;
|
|
@@ -2367,7 +1873,7 @@ export async function createEngineSession({
|
|
|
2367
1873
|
// shows "cancelled", but in-flight tool cards remain in a perpetual
|
|
2368
1874
|
// pending/blinking state because the normal finalize path (line 992)
|
|
2369
1875
|
// was skipped when the error interrupted the try block.
|
|
2370
|
-
flushToolResults([], toolCards, cardByCallId, toolGroups, resultsDone, { finalize: true });
|
|
1876
|
+
flushToolResults([], toolCards, cardByCallId, toolGroups, resultsDone, { finalize: true, cancelled: true });
|
|
2371
1877
|
finalizeToolHeaders();
|
|
2372
1878
|
} else {
|
|
2373
1879
|
finalizeToolHeaders();
|
|
@@ -2384,13 +1890,22 @@ export async function createEngineSession({
|
|
|
2384
1890
|
if (last) flushDeferredUpTo(last);
|
|
2385
1891
|
clearDeferredTimers();
|
|
2386
1892
|
}
|
|
1893
|
+
flushDeferredBeforeImmediatePush = null;
|
|
2387
1894
|
const producedTranscriptItem = state.items.length > itemsAtTurnStart;
|
|
2388
1895
|
const reclaimed = cancelled && activePromptRestore?.reclaimed === true;
|
|
2389
1896
|
activePromptRestore = null;
|
|
2390
1897
|
closeThinkingSegment();
|
|
2391
1898
|
const elapsedMs = Date.now() - startedAt;
|
|
2392
1899
|
const thinkingElapsedMs = thinkingStartedAt ? accumulatedThinkingMs : 0;
|
|
2393
|
-
|
|
1900
|
+
// responseLength is engine-local now (not pushed per-token), so compute the
|
|
1901
|
+
// fallback from the live accumulator instead of the possibly-stale
|
|
1902
|
+
// state.spinner.responseLength. Final-only / non-streaming turns never
|
|
1903
|
+
// accumulate `assistantText` (only currentAssistantText is set at the
|
|
1904
|
+
// finalize reconcile above), so take the larger of the two text sources so
|
|
1905
|
+
// a no-usage turn still estimates tokens from the final content.
|
|
1906
|
+
const finalAssistantLen = Math.max(assistantText.length, currentAssistantText.length);
|
|
1907
|
+
const finalResponseLength = finalAssistantLen + thinkingText.length;
|
|
1908
|
+
const finalOutputTokens = Math.max(0, Number(state.spinner?.outputTokens || 0), Math.round(finalResponseLength / 4));
|
|
2394
1909
|
const turnStatus = cancelled ? 'cancelled' : 'done';
|
|
2395
1910
|
const resultContent = askResult?.content != null ? String(askResult.content).trim() : '';
|
|
2396
1911
|
const assistantOutput = (currentAssistantText || assistantText || '').trim();
|
|
@@ -2423,6 +1938,8 @@ export async function createEngineSession({
|
|
|
2423
1938
|
});
|
|
2424
1939
|
flushDeferredExecutionPendingResumeKick();
|
|
2425
1940
|
}
|
|
1941
|
+
clearActiveToolSummary();
|
|
1942
|
+
_publishedThinkingActive = false; // turn teardown cleared state.thinking
|
|
2426
1943
|
return cancelled ? 'cancelled' : 'done';
|
|
2427
1944
|
}
|
|
2428
1945
|
|
|
@@ -2439,6 +1956,7 @@ export async function createEngineSession({
|
|
|
2439
1956
|
text: displayText,
|
|
2440
1957
|
content: text,
|
|
2441
1958
|
pastedImages: options.pastedImages && typeof options.pastedImages === 'object' ? options.pastedImages : null,
|
|
1959
|
+
pastedTexts: options.pastedTexts && typeof options.pastedTexts === 'object' ? options.pastedTexts : null,
|
|
2442
1960
|
onCommitted: typeof options.onCommitted === 'function' ? options.onCommitted : null,
|
|
2443
1961
|
mode,
|
|
2444
1962
|
priority,
|
|
@@ -2452,6 +1970,7 @@ export async function createEngineSession({
|
|
|
2452
1970
|
const ids = new Set(entries.map((entry) => entry.id));
|
|
2453
1971
|
const keys = entries.map((entry) => entry.key).filter(Boolean);
|
|
2454
1972
|
for (const key of keys) pendingNotificationKeys.delete(key);
|
|
1973
|
+
for (const key of keys) displayedExecutionNotificationKeys.delete(key);
|
|
2455
1974
|
const queued = state.queued.filter((q) => !ids.has(q.id));
|
|
2456
1975
|
if (queued.length !== state.queued.length) set({ queued });
|
|
2457
1976
|
}
|
|
@@ -2481,6 +2000,7 @@ export async function createEngineSession({
|
|
|
2481
2000
|
if (pending.length === 0) return [];
|
|
2482
2001
|
const max = queuePriorityValue(maxPriority);
|
|
2483
2002
|
const predicate = typeof options.predicate === 'function' ? options.predicate : () => true;
|
|
2003
|
+
const limit = Math.max(1, Number(options.limit) || Infinity);
|
|
2484
2004
|
let bestPriority = Infinity;
|
|
2485
2005
|
let targetMode = null;
|
|
2486
2006
|
for (const entry of pending) {
|
|
@@ -2499,6 +2019,7 @@ export async function createEngineSession({
|
|
|
2499
2019
|
if (predicate(entry) && (entry.mode || 'prompt') === targetMode && queuePriorityValue(entry.priority) === bestPriority) {
|
|
2500
2020
|
batch.push(entry);
|
|
2501
2021
|
pending.splice(i, 1);
|
|
2022
|
+
if (batch.length >= limit) break;
|
|
2502
2023
|
} else {
|
|
2503
2024
|
i += 1;
|
|
2504
2025
|
}
|
|
@@ -2509,13 +2030,16 @@ export async function createEngineSession({
|
|
|
2509
2030
|
|
|
2510
2031
|
async function drain() {
|
|
2511
2032
|
if (draining) return;
|
|
2033
|
+
if (autoClearRunning) return;
|
|
2512
2034
|
draining = true;
|
|
2035
|
+
let firstBatch = true;
|
|
2513
2036
|
try {
|
|
2514
2037
|
while (pending.length > 0) {
|
|
2515
2038
|
// Drain one priority/mode bucket at a time (unified command queue):
|
|
2516
2039
|
// unified command queue semantics: prompt steering stays editable and
|
|
2517
2040
|
// task notifications stay non-editable but model-visible.
|
|
2518
|
-
const batch = dequeueQueueBatch('later');
|
|
2041
|
+
const batch = dequeueQueueBatch('later', { limit: firstBatch ? 1 : Infinity });
|
|
2042
|
+
firstBatch = false;
|
|
2519
2043
|
if (batch.length === 0) break;
|
|
2520
2044
|
const ids = new Set(batch.map((e) => e.id));
|
|
2521
2045
|
const merged = mergePromptContents(batch);
|
|
@@ -2525,9 +2049,11 @@ export async function createEngineSession({
|
|
|
2525
2049
|
}
|
|
2526
2050
|
const nonEditable = batch.filter((entry) => !isQueuedEntryEditable(entry));
|
|
2527
2051
|
const batchPastedImages = mergePastedImages(batch);
|
|
2052
|
+
const batchPastedTexts = mergePastedTexts(batch);
|
|
2528
2053
|
const turnStatus = await runTurn(merged, {
|
|
2529
2054
|
displayText: batch.map((entry) => entry.text).filter((text) => String(text || '').trim()).join('\n'),
|
|
2530
2055
|
pastedImages: batchPastedImages,
|
|
2056
|
+
pastedTexts: batchPastedTexts,
|
|
2531
2057
|
onCommitted: () => callCommitCallbacks(batch),
|
|
2532
2058
|
submittedIds: [...ids],
|
|
2533
2059
|
restorable: nonEditable.length === 0,
|
|
@@ -2559,9 +2085,12 @@ export async function createEngineSession({
|
|
|
2559
2085
|
|
|
2560
2086
|
function drainPendingSteering() {
|
|
2561
2087
|
// Mid-turn steering drain:
|
|
2562
|
-
//
|
|
2563
|
-
//
|
|
2564
|
-
//
|
|
2088
|
+
// Injects queued user prompts (steering) plus non-editable internal entries
|
|
2089
|
+
// into the CURRENT provider pre-send window so the user can redirect a turn
|
|
2090
|
+
// that is already running. Slash commands are still excluded: they must run
|
|
2091
|
+
// through the normal command processor after the turn, not be sent as plain
|
|
2092
|
+
// text. Consumed entries are spliced out of `pending` here, so the post-turn
|
|
2093
|
+
// drain() loop will not re-execute them.
|
|
2565
2094
|
const batch = dequeueQueueBatch('next', { predicate: (entry) => !isSlashQueuedEntry(entry) });
|
|
2566
2095
|
if (batch.length === 0) return [];
|
|
2567
2096
|
const out = batch
|
|
@@ -2592,7 +2121,14 @@ export async function createEngineSession({
|
|
|
2592
2121
|
const startedAt = Date.now();
|
|
2593
2122
|
set({ commandStatus: { active: true, verb: 'Auto-clearing idle conversation', startedAt, mode: 'auto-clear' } });
|
|
2594
2123
|
try {
|
|
2595
|
-
|
|
2124
|
+
// Give Ink one event-loop turn to paint the auto-clear status before the
|
|
2125
|
+
// clear/compact path starts doing synchronous session/transcript work.
|
|
2126
|
+
// Without this, long idle clears can look like a frozen prompt followed by
|
|
2127
|
+
// an already-complete status row.
|
|
2128
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
2129
|
+
const compaction = runtime.getCompactionSettings();
|
|
2130
|
+
const compactType = compaction.compactType || compaction.type;
|
|
2131
|
+
await runtime.clear({ compactType, requireCompactSuccess: !!compactType });
|
|
2596
2132
|
resetStats();
|
|
2597
2133
|
clearUiActivityBeforeContextSync();
|
|
2598
2134
|
syncContextStats({ allowEstimated: true });
|
|
@@ -2627,6 +2163,7 @@ export async function createEngineSession({
|
|
|
2627
2163
|
lastUserActivityAt = Date.now();
|
|
2628
2164
|
autoClearRunning = false;
|
|
2629
2165
|
set({ commandStatus: null });
|
|
2166
|
+
void drain();
|
|
2630
2167
|
}
|
|
2631
2168
|
}
|
|
2632
2169
|
|
|
@@ -2644,7 +2181,7 @@ export async function createEngineSession({
|
|
|
2644
2181
|
removeQueuedEntries(queued);
|
|
2645
2182
|
const queuedText = queued.map((item) => item.text).filter((text) => String(text || '').trim()).join('\n');
|
|
2646
2183
|
const combinedText = [queuedText, String(currentText || '')].filter((text) => text.trim()).join('\n');
|
|
2647
|
-
return { count: queued.length, text: combinedText, pastedImages: mergePastedImages(queued) };
|
|
2184
|
+
return { count: queued.length, text: combinedText, pastedImages: mergePastedImages(queued), pastedTexts: mergePastedTexts(queued) };
|
|
2648
2185
|
}
|
|
2649
2186
|
|
|
2650
2187
|
const resetStats = () => {
|
|
@@ -2659,6 +2196,8 @@ export async function createEngineSession({
|
|
|
2659
2196
|
state.spinner = null;
|
|
2660
2197
|
state.lastTurn = null;
|
|
2661
2198
|
state.busy = false;
|
|
2199
|
+
pendingNotificationKeys.clear();
|
|
2200
|
+
displayedExecutionNotificationKeys.clear();
|
|
2662
2201
|
};
|
|
2663
2202
|
const resetTuiForPendingSessionReset = () => {
|
|
2664
2203
|
pendingSessionReset = true;
|
|
@@ -2739,6 +2278,10 @@ export async function createEngineSession({
|
|
|
2739
2278
|
enqueue(text, queueOptions);
|
|
2740
2279
|
return true;
|
|
2741
2280
|
}
|
|
2281
|
+
if (autoClearRunning) {
|
|
2282
|
+
enqueue(text, queueOptions);
|
|
2283
|
+
return true;
|
|
2284
|
+
}
|
|
2742
2285
|
void autoClearBeforeSubmit().then(() => enqueue(text, queueOptions));
|
|
2743
2286
|
return true;
|
|
2744
2287
|
},
|
|
@@ -2747,8 +2290,10 @@ export async function createEngineSession({
|
|
|
2747
2290
|
if (state.commandBusy) return false;
|
|
2748
2291
|
set({ commandBusy: true });
|
|
2749
2292
|
try {
|
|
2750
|
-
|
|
2751
|
-
|
|
2293
|
+
// Model changes apply to the NEXT session only (default setRoute
|
|
2294
|
+
// behavior) — never rewrite the live session's provider/model, which
|
|
2295
|
+
// would force a full prompt-cache rewrite mid-conversation.
|
|
2296
|
+
await runtime.setRoute({ model: m });
|
|
2752
2297
|
set({ ...routeState(), stats: { ...state.stats } });
|
|
2753
2298
|
return true;
|
|
2754
2299
|
} finally {
|
|
@@ -2802,6 +2347,11 @@ export async function createEngineSession({
|
|
|
2802
2347
|
set({ autoClear: next });
|
|
2803
2348
|
return next;
|
|
2804
2349
|
},
|
|
2350
|
+
getUpdateSettings: () => runtime.getUpdateSettings?.() || null,
|
|
2351
|
+
setAutoUpdate: (enabled) => runtime.setAutoUpdate?.(enabled),
|
|
2352
|
+
checkForUpdate: (input = {}) => runtime.checkForUpdate?.(input),
|
|
2353
|
+
runUpdateNow: () => runtime.runUpdateNow?.(),
|
|
2354
|
+
getUpdateStatus: () => runtime.getUpdateStatus?.() || { phase: 'idle' },
|
|
2805
2355
|
getProfile: () => runtime.getProfile?.() || { title: '', language: 'system', languages: [] },
|
|
2806
2356
|
setProfile: (input = {}) => {
|
|
2807
2357
|
const next = runtime.setProfile?.(input) || runtime.getProfile?.() || null;
|
|
@@ -2815,8 +2365,15 @@ export async function createEngineSession({
|
|
|
2815
2365
|
set({ commandBusy: true });
|
|
2816
2366
|
try {
|
|
2817
2367
|
const next = runtime.setCompactionSettings?.(input) || {};
|
|
2818
|
-
syncContextStats({ allowEstimated: true });
|
|
2819
2368
|
set({ ...routeState(), stats: { ...state.stats } });
|
|
2369
|
+
// Context-stats recompute (transcript scan + per-message JSON
|
|
2370
|
+
// stringify) is the secondary hitch source on this toggle; defer it
|
|
2371
|
+
// off the key-handler tick so Ink repaints the setting change first.
|
|
2372
|
+
// Stats become eventually consistent on the next tick/repaint.
|
|
2373
|
+
setTimeout(() => {
|
|
2374
|
+
syncContextStats({ allowEstimated: true });
|
|
2375
|
+
set({ stats: { ...state.stats } });
|
|
2376
|
+
}, 0);
|
|
2820
2377
|
return next;
|
|
2821
2378
|
} finally {
|
|
2822
2379
|
set({ commandBusy: false });
|
|
@@ -2830,8 +2387,14 @@ export async function createEngineSession({
|
|
|
2830
2387
|
set({ commandBusy: true });
|
|
2831
2388
|
try {
|
|
2832
2389
|
const next = await runtime.setMemoryEnabled?.(enabled);
|
|
2833
|
-
syncContextStats({ allowEstimated: true });
|
|
2834
2390
|
set({ ...routeState(), stats: { ...state.stats } });
|
|
2391
|
+
// Deferred for the same reason as setCompactionSettings above: keep
|
|
2392
|
+
// the recompute off the key-handler tick so the toggle repaints
|
|
2393
|
+
// immediately; stats catch up right after.
|
|
2394
|
+
setTimeout(() => {
|
|
2395
|
+
syncContextStats({ allowEstimated: true });
|
|
2396
|
+
set({ stats: { ...state.stats } });
|
|
2397
|
+
}, 0);
|
|
2835
2398
|
return next;
|
|
2836
2399
|
} finally {
|
|
2837
2400
|
set({ commandBusy: false });
|
|
@@ -3157,8 +2720,24 @@ export async function createEngineSession({
|
|
|
3157
2720
|
if (!state.busy) return false;
|
|
3158
2721
|
denyAllToolApprovals('interrupted by user');
|
|
3159
2722
|
const restoreState = activePromptRestore;
|
|
3160
|
-
|
|
3161
|
-
|
|
2723
|
+
// A queued steering prompt means the user already redirected the turn:
|
|
2724
|
+
// interrupting should just cancel the running turn and let the steering
|
|
2725
|
+
// prompt run next, NOT resurrect the in-flight prompt back into the draft.
|
|
2726
|
+
const hasPendingSteering = pending.some((entry) => isQueuedEntryEditable(entry));
|
|
2727
|
+
const canRestore = restoreState?.restorable && !hasPendingSteering;
|
|
2728
|
+
const restoreText = canRestore ? restoreState.text : '';
|
|
2729
|
+
const restorePastedImages = canRestore && restoreState?.pastedImages ? restoreState.pastedImages : null;
|
|
2730
|
+
const restorePastedTexts = canRestore && restoreState?.pastedTexts ? restoreState.pastedTexts : null;
|
|
2731
|
+
// When steering suppresses the restore, the interrupted prompt's pasted
|
|
2732
|
+
// images never get committed (onCommitted won't fire) nor re-installed into
|
|
2733
|
+
// the draft, so hand them back for cleanup to avoid a stale `[Image #id]`
|
|
2734
|
+
// lingering in the paste snapshot.
|
|
2735
|
+
const discardPastedImages = restoreState?.restorable && hasPendingSteering && restoreState?.pastedImages
|
|
2736
|
+
? restoreState.pastedImages
|
|
2737
|
+
: null;
|
|
2738
|
+
const discardPastedTexts = restoreState?.restorable && hasPendingSteering && restoreState?.pastedTexts
|
|
2739
|
+
? restoreState.pastedTexts
|
|
2740
|
+
: null;
|
|
3162
2741
|
const requeueEntries = restoreState && !restoreState.committed && Array.isArray(restoreState.requeueEntries)
|
|
3163
2742
|
? restoreState.requeueEntries.slice()
|
|
3164
2743
|
: [];
|
|
@@ -3180,7 +2759,7 @@ export async function createEngineSession({
|
|
|
3180
2759
|
restoreState.restorable = false;
|
|
3181
2760
|
restoreState.requeueEntries = [];
|
|
3182
2761
|
}
|
|
3183
|
-
return { aborted, restoreText, pastedImages: restorePastedImages };
|
|
2762
|
+
return { aborted, restoreText, pastedImages: restorePastedImages, discardPastedImages, pastedTexts: restorePastedTexts, discardPastedTexts };
|
|
3184
2763
|
},
|
|
3185
2764
|
resolveToolApproval: (id, decision = {}) => {
|
|
3186
2765
|
const approved = decision === true || decision?.approved === true;
|
|
@@ -3242,8 +2821,14 @@ export async function createEngineSession({
|
|
|
3242
2821
|
set({ commandBusy: true });
|
|
3243
2822
|
try {
|
|
3244
2823
|
const result = await runtime.setOutputStyle?.(styleId);
|
|
3245
|
-
|
|
2824
|
+
resetStats();
|
|
3246
2825
|
set({ ...routeState(), stats: { ...state.stats } });
|
|
2826
|
+
// Defer the context recompute (transcript scan) off this tick so
|
|
2827
|
+
// the style change repaints immediately; stats settle right after.
|
|
2828
|
+
setTimeout(() => {
|
|
2829
|
+
syncContextStats({ allowEstimated: true });
|
|
2830
|
+
set({ stats: { ...state.stats } });
|
|
2831
|
+
}, 0);
|
|
3247
2832
|
return result;
|
|
3248
2833
|
} finally {
|
|
3249
2834
|
set({ commandBusy: false });
|
|
@@ -3260,6 +2845,18 @@ export async function createEngineSession({
|
|
|
3260
2845
|
set({ commandBusy: false });
|
|
3261
2846
|
}
|
|
3262
2847
|
},
|
|
2848
|
+
// Toggle Discord remote mode for this session. Flips the runtime's
|
|
2849
|
+
// remoteEnabled flag (booting/stopping the channel worker) and returns the
|
|
2850
|
+
// NEW enabled state so the caller can render an ON/OFF notice.
|
|
2851
|
+
toggleRemote: () => {
|
|
2852
|
+
const enabled = runtime.isRemoteEnabled?.() === true;
|
|
2853
|
+
if (enabled) runtime.stopRemote?.();
|
|
2854
|
+
else runtime.startRemote?.();
|
|
2855
|
+
const next = runtime.isRemoteEnabled?.() === true;
|
|
2856
|
+
set({ remoteEnabled: next });
|
|
2857
|
+
return next;
|
|
2858
|
+
},
|
|
2859
|
+
isRemoteEnabled: () => runtime.isRemoteEnabled?.() === true,
|
|
3263
2860
|
// Theme is a TUI-local concern (no runtime round-trip). listThemes returns
|
|
3264
2861
|
// picker metadata; getTheme reports the active id; setTheme applies the
|
|
3265
2862
|
// palette in-place + persists ui.theme and bumps a themeEpoch so the React
|
|
@@ -3274,6 +2871,9 @@ export async function createEngineSession({
|
|
|
3274
2871
|
setAgentRoute: async (agentId, opts) => {
|
|
3275
2872
|
return await runtime.setAgentRoute?.(agentId, opts);
|
|
3276
2873
|
},
|
|
2874
|
+
setDefaultProvider: async (provider) => {
|
|
2875
|
+
return await runtime.setDefaultProvider?.(provider);
|
|
2876
|
+
},
|
|
3277
2877
|
listProviders: () => {
|
|
3278
2878
|
return runtime.listProviders();
|
|
3279
2879
|
},
|
|
@@ -3286,6 +2886,10 @@ export async function createEngineSession({
|
|
|
3286
2886
|
getOnboardingStatus: () => {
|
|
3287
2887
|
return runtime.getOnboardingStatus?.() || { completed: true, workflowRoutes: {} };
|
|
3288
2888
|
},
|
|
2889
|
+
skipOnboarding: () => {
|
|
2890
|
+
// Completed-marking only; no route/agent/provider writes.
|
|
2891
|
+
return runtime.skipOnboarding?.() || null;
|
|
2892
|
+
},
|
|
3289
2893
|
completeOnboarding: async (payload = {}) => {
|
|
3290
2894
|
if (state.commandBusy) return null;
|
|
3291
2895
|
set({ commandBusy: true });
|
|
@@ -3363,6 +2967,8 @@ export async function createEngineSession({
|
|
|
3363
2967
|
getChannelSetup: () => {
|
|
3364
2968
|
return runtime.getChannelSetup();
|
|
3365
2969
|
},
|
|
2970
|
+
getChannelWorkerStatus: () => runtime.getChannelWorkerStatus?.(),
|
|
2971
|
+
setBackend: (name) => runtime.setBackend?.(name),
|
|
3366
2972
|
saveDiscordToken: (token) => {
|
|
3367
2973
|
const result = runtime.saveDiscordToken(token);
|
|
3368
2974
|
pushNotice('discord token saved', 'info');
|
|
@@ -3373,6 +2979,16 @@ export async function createEngineSession({
|
|
|
3373
2979
|
pushNotice('discord token forgotten', 'info');
|
|
3374
2980
|
return result;
|
|
3375
2981
|
},
|
|
2982
|
+
saveTelegramToken: (token) => {
|
|
2983
|
+
const result = runtime.saveTelegramToken?.(token);
|
|
2984
|
+
pushNotice('telegram token saved', 'info');
|
|
2985
|
+
return result;
|
|
2986
|
+
},
|
|
2987
|
+
forgetTelegramToken: () => {
|
|
2988
|
+
const result = runtime.forgetTelegramToken?.();
|
|
2989
|
+
pushNotice('telegram token forgotten', 'info');
|
|
2990
|
+
return result;
|
|
2991
|
+
},
|
|
3376
2992
|
saveWebhookAuthtoken: (token) => {
|
|
3377
2993
|
const result = runtime.saveWebhookAuthtoken(token);
|
|
3378
2994
|
pushNotice('webhook/ngrok authtoken saved', 'info');
|
|
@@ -3433,7 +3049,9 @@ export async function createEngineSession({
|
|
|
3433
3049
|
set({ commandBusy: true });
|
|
3434
3050
|
try {
|
|
3435
3051
|
const routeOpts = opts && typeof opts === 'object' ? opts : {};
|
|
3436
|
-
|
|
3052
|
+
// Default: apply to the NEXT session only. Only an explicit
|
|
3053
|
+
// `applyToCurrentSession: true` rewrites the live session in place.
|
|
3054
|
+
const applyToCurrentSession = routeOpts.applyToCurrentSession === true;
|
|
3437
3055
|
const { applyToCurrentSession: _drop, ...nextRoute } = routeOpts;
|
|
3438
3056
|
await runtime.setRoute(nextRoute, { applyToCurrentSession });
|
|
3439
3057
|
if (applyToCurrentSession) syncContextStats({ allowEstimated: true });
|
|
@@ -3444,10 +3062,12 @@ export async function createEngineSession({
|
|
|
3444
3062
|
}
|
|
3445
3063
|
},
|
|
3446
3064
|
pushNotice,
|
|
3065
|
+
setProgressHint,
|
|
3447
3066
|
clear: async () => {
|
|
3448
3067
|
if (state.commandBusy) return false;
|
|
3449
3068
|
set({ commandBusy: true });
|
|
3450
3069
|
clearToastTimers();
|
|
3070
|
+
resetAllStreamingMarkdownStablePrefixes();
|
|
3451
3071
|
const rollbackSnapshot = snapshotTuiBeforeSessionReset();
|
|
3452
3072
|
resetTuiForPendingSessionReset();
|
|
3453
3073
|
set({
|
|
@@ -3483,6 +3103,7 @@ export async function createEngineSession({
|
|
|
3483
3103
|
if (state.commandBusy) return false;
|
|
3484
3104
|
set({ commandBusy: true });
|
|
3485
3105
|
clearToastTimers();
|
|
3106
|
+
resetAllStreamingMarkdownStablePrefixes();
|
|
3486
3107
|
const rollbackSnapshot = snapshotTuiBeforeSessionReset();
|
|
3487
3108
|
resetTuiForPendingSessionReset();
|
|
3488
3109
|
set({
|