mixdog 0.9.2 → 0.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +8 -3
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/bench/lead-review-tasks-r3.json +20 -0
- package/scripts/bench/lead-review-tasks.json +20 -0
- package/scripts/bench/r4-mixed-tasks.json +20 -0
- package/scripts/bench/review-tasks.json +20 -0
- package/scripts/bench/round-codex.json +114 -0
- package/scripts/bench/round-mixdog-lead-r3.json +269 -0
- package/scripts/bench/round-mixdog-lead.json +269 -0
- package/scripts/bench/round-mixdog.json +126 -0
- package/scripts/bench/round-r10-bigsample.json +679 -0
- package/scripts/bench/round-r11-codexalign.json +257 -0
- package/scripts/bench/round-r4-codex.json +114 -0
- package/scripts/bench/round-r4-mixed.json +225 -0
- package/scripts/bench/round-r5-gpt-lead.json +259 -0
- package/scripts/bench/round-r6-codex.json +114 -0
- package/scripts/bench/round-r6-solo.json +257 -0
- package/scripts/bench/round-r7-full.json +254 -0
- package/scripts/bench/round-r8-fulldefault.json +255 -0
- package/scripts/bench-run.mjs +215 -29
- package/scripts/build-tui.mjs +13 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/freevar-smoke.mjs +95 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/internal-comms-bench.mjs +1 -0
- package/scripts/internal-comms-smoke.mjs +10 -9
- package/scripts/mouse-probe.mjs +45 -0
- package/scripts/output-style-bench.mjs +13 -6
- package/scripts/output-style-smoke.mjs +4 -4
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/provider-toolcall-test.mjs +7 -3
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/recall-usecase-cases.json +18 -0
- package/scripts/recall-usecase-probe.json +6 -0
- package/scripts/session-bench.mjs +152 -6
- package/scripts/tool-smoke.mjs +30 -67
- package/scripts/tui-render-smoke.mjs +90 -0
- package/scripts/webhook-smoke.mjs +208 -0
- package/src/agents/debugger/AGENT.md +5 -2
- package/src/agents/heavy-worker/AGENT.md +21 -11
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +3 -2
- package/src/agents/worker/AGENT.md +21 -11
- package/src/lib/rules-builder.cjs +4 -0
- package/src/mixdog-session-runtime.mjs +933 -3731
- package/src/output-styles/default.md +34 -9
- package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
- package/src/output-styles/minimal.md +4 -1
- package/src/output-styles/simple.md +22 -7
- package/src/repl.mjs +5 -5
- package/src/rules/agent/00-common.md +2 -0
- package/src/rules/agent/30-explorer.md +8 -11
- package/src/rules/lead/lead-brief.md +12 -0
- package/src/rules/lead/lead-tool.md +2 -9
- package/src/rules/shared/01-tool.md +11 -5
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
- package/src/runtime/agent/orchestrator/config.mjs +69 -2
- package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +63 -21
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +97 -1343
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +78 -10
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +44 -1014
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +18 -4
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
- package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +41 -1142
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +303 -2119
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +140 -995
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
- package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
- package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
- package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +274 -0
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -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 +47 -0
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -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/termination.mjs +58 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -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 +409 -1304
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +230 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +149 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +226 -2114
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
- package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +33 -42
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -87
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +78 -304
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -6
- 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 +551 -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/keyword-match.mjs +82 -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 +1080 -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-columns.mjs +45 -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 +6 -6
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
- package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
- package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -23
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +10 -74
- package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
- package/src/runtime/channels/backends/discord-access.mjs +32 -0
- package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
- package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
- package/src/runtime/channels/backends/discord.mjs +12 -292
- package/src/runtime/channels/index.mjs +241 -894
- package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
- package/src/runtime/channels/lib/event-queue.mjs +63 -4
- package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +9 -1
- package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
- package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
- package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
- package/src/runtime/channels/lib/session-discovery.mjs +56 -4
- package/src/runtime/channels/lib/telegram-format.mjs +19 -22
- package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
- package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
- package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
- package/src/runtime/channels/lib/webhook/log.mjs +42 -0
- package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
- package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
- package/src/runtime/channels/lib/webhook.mjs +36 -570
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/channels/tool-defs.mjs +11 -130
- package/src/runtime/memory/index.mjs +258 -2050
- package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
- package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
- package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +72 -837
- package/src/runtime/memory/lib/memory-embed.mjs +149 -0
- package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +20 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/query-handlers.mjs +780 -0
- package/src/runtime/memory/lib/recall-format.mjs +238 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
- package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
- package/src/runtime/memory/tool-defs.mjs +6 -14
- package/src/runtime/search/lib/http-fetch.mjs +274 -0
- package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
- package/src/runtime/search/lib/web-tools.mjs +24 -602
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +26 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/launcher-control.mjs +2 -2
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/tool-primitives.mjs +308 -0
- package/src/runtime/shared/tool-result-summary.mjs +515 -0
- package/src/runtime/shared/tool-surface.mjs +80 -898
- package/src/runtime/shared/transcript-writer.mjs +52 -2
- package/src/runtime/shared/update-checker.mjs +7 -4
- package/src/session-runtime/config-helpers.mjs +291 -0
- package/src/session-runtime/config-lifecycle.mjs +232 -0
- package/src/session-runtime/cwd-plugins.mjs +226 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/mcp-glue.mjs +177 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/model-recency.mjs +111 -0
- package/src/session-runtime/native-search.mjs +247 -0
- package/src/session-runtime/output-styles.mjs +126 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/prewarm.mjs +142 -0
- package/src/session-runtime/provider-models.mjs +278 -0
- package/src/session-runtime/provider-usage.mjs +120 -0
- package/src/session-runtime/quick-model-rows.mjs +170 -0
- package/src/session-runtime/quick-search-models.mjs +46 -0
- package/src/session-runtime/session-hooks.mjs +93 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/settings-api.mjs +319 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/tool-defs.mjs +84 -0
- package/src/session-runtime/warmup-schedulers.mjs +201 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool/helpers.mjs +237 -0
- package/src/standalone/agent-tool/notify.mjs +107 -0
- package/src/standalone/agent-tool/provider-init.mjs +143 -0
- package/src/standalone/agent-tool/render.mjs +152 -0
- package/src/standalone/agent-tool/tool-def.mjs +55 -0
- package/src/standalone/agent-tool.mjs +155 -677
- package/src/standalone/channel-worker.mjs +7 -9
- package/src/standalone/explore-tool.mjs +40 -12
- package/src/standalone/hook-bus/config.mjs +207 -0
- package/src/standalone/hook-bus/constants.mjs +90 -0
- package/src/standalone/hook-bus/handlers.mjs +481 -0
- package/src/standalone/hook-bus/payload.mjs +31 -0
- package/src/standalone/hook-bus/rules.mjs +77 -0
- package/src/standalone/hook-bus.mjs +110 -746
- package/src/standalone/memory-runtime-proxy.mjs +7 -0
- package/src/standalone/opencode-go-login.mjs +125 -0
- package/src/standalone/provider-admin.mjs +15 -19
- package/src/standalone/usage-dashboard.mjs +3 -1
- package/src/tui/App.jsx +1163 -7571
- package/src/tui/app/app-format.mjs +206 -0
- package/src/tui/app/channel-pickers.mjs +510 -0
- package/src/tui/app/clipboard.mjs +67 -0
- package/src/tui/app/core-memory-picker.mjs +210 -0
- package/src/tui/app/extension-pickers.mjs +506 -0
- package/src/tui/app/input-parsers.mjs +193 -0
- package/src/tui/app/maintenance-pickers.mjs +324 -0
- package/src/tui/app/model-options.mjs +330 -0
- package/src/tui/app/model-picker.mjs +365 -0
- package/src/tui/app/onboarding-steps.mjs +400 -0
- package/src/tui/app/project-picker.mjs +247 -0
- package/src/tui/app/provider-setup-picker.mjs +580 -0
- package/src/tui/app/resume-picker.mjs +55 -0
- package/src/tui/app/route-pickers.mjs +419 -0
- package/src/tui/app/settings-picker.mjs +490 -0
- package/src/tui/app/slash-commands.mjs +101 -0
- package/src/tui/app/slash-dispatch.mjs +427 -0
- package/src/tui/app/text-layout.mjs +46 -0
- package/src/tui/app/theme-effort-pickers.mjs +154 -0
- package/src/tui/app/transcript-window.mjs +671 -0
- package/src/tui/app/use-mouse-input.mjs +460 -0
- package/src/tui/app/use-prompt-handlers.mjs +310 -0
- package/src/tui/app/use-transcript-scroll.mjs +510 -0
- package/src/tui/app/use-transcript-window.mjs +589 -0
- package/src/tui/components/ConfirmBar.jsx +1 -1
- package/src/tui/components/Picker.jsx +32 -4
- package/src/tui/components/PromptInput.jsx +259 -80
- package/src/tui/components/SlashCommandPalette.jsx +8 -1
- package/src/tui/components/StatusLine.jsx +63 -12
- package/src/tui/components/TextEntryPanel.jsx +11 -0
- package/src/tui/components/ToolExecution.jsx +56 -588
- package/src/tui/components/TranscriptItem.jsx +105 -0
- package/src/tui/components/UsagePanel.jsx +18 -4
- package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
- package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
- package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
- package/src/tui/components/tool-execution/text-format.mjs +161 -0
- package/src/tui/components/tool-output-format.mjs +2 -2
- package/src/tui/display-width.mjs +20 -3
- package/src/tui/dist/index.mjs +18034 -17188
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/agent-job-feed.mjs +133 -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/notification-plan.mjs +76 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/render-timing.mjs +17 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-approval.mjs +94 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-card-results.mjs +234 -0
- package/src/tui/engine/tool-result-status.mjs +135 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +405 -1385
- package/src/tui/figures.mjs +5 -0
- package/src/tui/index.jsx +105 -0
- package/src/tui/input-editing.mjs +60 -10
- package/src/tui/keyboard-protocol.mjs +2 -2
- package/src/tui/lib/voice-recorder.mjs +35 -19
- package/src/tui/markdown/format-token.mjs +11 -9
- package/src/tui/paste-attachments.mjs +38 -0
- package/src/tui/statusline-ansi-bridge.mjs +11 -3
- package/src/tui/theme.mjs +6 -0
- package/src/tui/themes/base.mjs +2 -2
- package/src/tui/themes/kanagawa.mjs +4 -4
- package/src/tui/themes/teal.mjs +4 -5
- package/src/tui/themes/utils.mjs +1 -1
- package/src/ui/statusline-agents.mjs +213 -0
- package/src/ui/statusline-format.mjs +146 -0
- package/src/ui/statusline-segments.mjs +148 -0
- package/src/ui/statusline.mjs +77 -462
- package/src/ui/tool-card.mjs +0 -1
- package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
- package/src/workflows/default/WORKFLOW.md +16 -9
- package/src/workflows/sequential/WORKFLOW.md +16 -11
- package/src/workflows/solo/WORKFLOW.md +5 -1
- package/vendor/ink/build/display-width.js +19 -3
- package/vendor/ink/build/ink.js +103 -6
- package/vendor/ink/build/log-update.js +17 -3
- package/vendor/ink/build/wrap-text.js +125 -0
- package/scripts/_test-folder-dialog.mjs +0 -30
- package/scripts/fix-brief-fn.mjs +0 -35
- package/scripts/fix-format-tool-surface.mjs +0 -24
- package/scripts/fix-tool-exec-visible.mjs +0 -42
- package/scripts/patch-agent-brief.mjs +0 -48
- package/scripts/patch-app.mjs +0 -21
- package/scripts/patch-app2.mjs +0 -18
- package/scripts/patch-dist-brief.mjs +0 -96
- package/scripts/patch-tool-exec.mjs +0 -70
- package/src/examples/schedules/SCHEDULE.example.md +0 -32
- package/src/examples/webhooks/WEBHOOK.example.md +0 -40
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
- package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
- package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
- package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
- package/src/standalone/agent-task-status.test.mjs +0 -76
- package/src/tui/components/tool-output-format.test.mjs +0 -399
- package/src/tui/display-width.test.mjs +0 -35
- package/src/tui/engine-runtime-notification.test.mjs +0 -115
- package/src/tui/engine-tool-result-text.test.mjs +0 -75
- package/src/tui/markdown/format-token.test.mjs +0 -354
- package/src/tui/markdown/render-ansi.test.mjs +0 -108
- package/src/tui/markdown/stream-fence.test.mjs +0 -26
- package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
- package/src/tui/prompt-history-store.test.mjs +0 -52
- package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
- package/src/tui/transcript-tool-failures.test.mjs +0 -111
- package/src/ui/markdown.test.mjs +0 -70
- package/src/ui/statusline-context-label.test.mjs +0 -15
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
- package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
package/src/tui/engine.mjs
CHANGED
|
@@ -4,76 +4,81 @@
|
|
|
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,
|
|
17
|
+
formatAggregateDetail,
|
|
13
18
|
summarizeToolResult,
|
|
14
19
|
} from '../runtime/shared/tool-surface.mjs';
|
|
15
|
-
import { isBackgroundErrorOnlyBody, presentErrorText } from '../runtime/shared/err-text.mjs';
|
|
16
20
|
import {
|
|
17
21
|
isModelVisibleToolCompletionWrapper,
|
|
18
|
-
modelVisibleToolCompletionMessage,
|
|
19
22
|
} from '../runtime/shared/tool-execution-contract.mjs';
|
|
23
|
+
import { presentErrorText } from '../runtime/shared/err-text.mjs';
|
|
20
24
|
import { listThemes, getThemeSetting, setThemeSetting } from './theme.mjs';
|
|
21
25
|
import { resetAllStreamingMarkdownStablePrefixes } from './markdown/streaming-markdown.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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
}
|
|
26
|
+
import { bootProfile } from './engine/boot-profile.mjs';
|
|
27
|
+
import { createSessionStats, applyUsageDelta } from './engine/session-stats.mjs';
|
|
28
|
+
import {
|
|
29
|
+
pickVerb,
|
|
30
|
+
pickDoneVerb,
|
|
31
|
+
formatElapsedSeconds,
|
|
32
|
+
compactEventLabel,
|
|
33
|
+
compactEventDetail,
|
|
34
|
+
projectNameFromPath,
|
|
35
|
+
} from './engine/labels.mjs';
|
|
36
|
+
import { polishNoticeText } from './engine/notice-text.mjs';
|
|
37
|
+
import {
|
|
38
|
+
toolResultText,
|
|
39
|
+
toolAggregateDetailFallback,
|
|
40
|
+
toolGroupedDisplayFallback,
|
|
41
|
+
toolErrorDisplay,
|
|
42
|
+
} from './engine/tool-result-text.mjs';
|
|
43
|
+
import {
|
|
44
|
+
toolCallId,
|
|
45
|
+
toolResultCallId,
|
|
46
|
+
toolCallName,
|
|
47
|
+
toolCallArgs,
|
|
48
|
+
} from './engine/tool-call-fields.mjs';
|
|
49
|
+
import {
|
|
50
|
+
parseBackgroundTaskEnvelope,
|
|
51
|
+
parseSyntheticAgentMessage,
|
|
52
|
+
toolResultStatus,
|
|
53
|
+
isErrorToolStatus,
|
|
54
|
+
} from './engine/agent-envelope.mjs';
|
|
55
|
+
import {
|
|
56
|
+
queuePriorityValue,
|
|
57
|
+
defaultQueuePriority,
|
|
58
|
+
isQueuedEntryEditable,
|
|
59
|
+
isQueuedEntryVisible,
|
|
60
|
+
isSlashQueuedEntry,
|
|
61
|
+
notificationDisplayText,
|
|
62
|
+
sessionActivityTimestamp,
|
|
63
|
+
promptDisplayText,
|
|
64
|
+
mergePromptContents,
|
|
65
|
+
mergePastedImages,
|
|
66
|
+
mergePastedTexts,
|
|
67
|
+
callCommitCallbacks,
|
|
68
|
+
} from './engine/queue-helpers.mjs';
|
|
69
|
+
import {
|
|
70
|
+
resolveTuiRuntimeNotificationDelivery,
|
|
71
|
+
} from './engine/notification-plan.mjs';
|
|
72
|
+
import { yieldToRenderer } from './engine/render-timing.mjs';
|
|
73
|
+
import {
|
|
74
|
+
aggregateRawResult,
|
|
75
|
+
aggregateBucketForCategory,
|
|
76
|
+
aggregateSummaries,
|
|
77
|
+
assignAggregateSummaryOrder,
|
|
78
|
+
} from './engine/tool-result-status.mjs';
|
|
79
|
+
import { createToolApproval } from './engine/tool-approval.mjs';
|
|
80
|
+
import { createToolCardResults } from './engine/tool-card-results.mjs';
|
|
81
|
+
import { createAgentJobFeed } from './engine/agent-job-feed.mjs';
|
|
77
82
|
|
|
78
83
|
// Source tests resolve from src/tui/engine.mjs; the built bundle resolves from
|
|
79
84
|
// src/tui/dist/index.mjs.
|
|
@@ -89,836 +94,13 @@ const TOOL_APPROVAL_TIMEOUT_MS = (() => {
|
|
|
89
94
|
let _idSeq = 0;
|
|
90
95
|
const nextId = () => `it_${++_idSeq}`;
|
|
91
96
|
|
|
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
|
-
// Absolute cap for a collapsed tool detail line (the second row under the ⎿
|
|
293
|
-
// gutter). Terminal-width independent so a wide terminal never lets a long line
|
|
294
|
-
// stretch the row; lockstep with ToolExecution RESULT_LINE_HARD_MAX (80).
|
|
295
|
-
const TOOL_DETAIL_LINE_MAX = 80;
|
|
296
|
-
|
|
297
|
-
function compactToolResultObjectFallback(obj) {
|
|
298
|
-
if (obj?.type === 'tool_result') return '';
|
|
299
|
-
try {
|
|
300
|
-
const json = JSON.stringify(obj);
|
|
301
|
-
if (!json || json === '{}') return '';
|
|
302
|
-
if (json.length <= TOOL_RESULT_JSON_FALLBACK_MAX) return json;
|
|
303
|
-
return `${json.slice(0, TOOL_RESULT_JSON_FALLBACK_MAX - 1)}…`;
|
|
304
|
-
} catch {
|
|
305
|
-
return String(obj);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
function toolResultPartText(part, depth = 0) {
|
|
310
|
-
if (part == null) return '';
|
|
311
|
-
if (depth > TOOL_RESULT_PART_MAX_DEPTH) return '';
|
|
312
|
-
if (typeof part === 'string') return part;
|
|
313
|
-
if (part?.type === 'image' || part?.type === 'input_image') {
|
|
314
|
-
return `[image: ${part.mimeType || part.mediaType || part.source?.media_type || 'image'}]`;
|
|
315
|
-
}
|
|
316
|
-
if (part?.type === 'tool_result') {
|
|
317
|
-
const inner = part.content;
|
|
318
|
-
if (typeof inner === 'string') return inner;
|
|
319
|
-
if (Array.isArray(inner)) {
|
|
320
|
-
return inner.map((c) => toolResultPartText(c, depth + 1)).filter((t) => t !== '').join('\n');
|
|
321
|
-
}
|
|
322
|
-
if (inner != null && typeof inner === 'object') {
|
|
323
|
-
return toolResultPartText(inner, depth + 1);
|
|
324
|
-
}
|
|
325
|
-
return '';
|
|
326
|
-
}
|
|
327
|
-
if (part?.type === 'text' || part?.type === 'output_text' || part?.type === 'input_text') {
|
|
328
|
-
return part.text ?? '';
|
|
329
|
-
}
|
|
330
|
-
if (Array.isArray(part)) {
|
|
331
|
-
return part.map((c) => toolResultPartText(c, depth + 1)).filter((t) => t !== '').join('\n');
|
|
332
|
-
}
|
|
333
|
-
if (typeof part === 'object') {
|
|
334
|
-
if (Array.isArray(part.content)) {
|
|
335
|
-
const nested = part.content.map((c) => toolResultPartText(c, depth + 1)).filter((t) => t !== '').join('\n');
|
|
336
|
-
if (nested) return nested;
|
|
337
|
-
}
|
|
338
|
-
if (part.content != null && typeof part.content === 'object') {
|
|
339
|
-
const nested = toolResultPartText(part.content, depth + 1);
|
|
340
|
-
if (nested) return nested;
|
|
341
|
-
}
|
|
342
|
-
if (Array.isArray(part.parts)) {
|
|
343
|
-
const nested = part.parts.map((c) => toolResultPartText(c, depth + 1)).filter((t) => t !== '').join('\n');
|
|
344
|
-
if (nested) return nested;
|
|
345
|
-
}
|
|
346
|
-
if (typeof part.text === 'string' && part.text) return part.text;
|
|
347
|
-
if (typeof part.output === 'string' && part.output) return part.output;
|
|
348
|
-
if (typeof part.message === 'string' && part.message) return part.message;
|
|
349
|
-
if (typeof part.content === 'string') return part.content;
|
|
350
|
-
if (part.source?.type === 'base64' && part.source?.data) {
|
|
351
|
-
return `[image: ${part.source.media_type || part.source.mediaType || 'base64'}]`;
|
|
352
|
-
}
|
|
353
|
-
return compactToolResultObjectFallback(part);
|
|
354
|
-
}
|
|
355
|
-
return '';
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
function toolAggregateDetailFallback(detailText, rawResult) {
|
|
359
|
-
if (String(detailText || '').trim()) return detailText;
|
|
360
|
-
const raw = String(rawResult || '').replace(/\s+$/, '').trim();
|
|
361
|
-
if (!raw) return detailText;
|
|
362
|
-
const line = raw.split('\n').map((l) => l.trim()).find(Boolean) || '';
|
|
363
|
-
if (!line) return detailText;
|
|
364
|
-
return line.length > TOOL_DETAIL_LINE_MAX ? `${line.slice(0, TOOL_DETAIL_LINE_MAX - 3)}…` : line;
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
function toolGroupedDisplayFallback(resultText, text, rawText) {
|
|
368
|
-
if (String(resultText || '').trim()) return resultText;
|
|
369
|
-
const body = String(text || rawText || '').trim();
|
|
370
|
-
if (body) return text || rawText;
|
|
371
|
-
return resultText;
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
function toolErrorDisplay(value, surface = 'tool') {
|
|
375
|
-
const text = presentErrorText(value, { surface });
|
|
376
|
-
if (/^(?:Search failed|Fetch failed|No first response|The .+ went stale|(?:Web search agent|Agent|Tool) (?:stopped|was cancelled))/i.test(text)) {
|
|
377
|
-
return text;
|
|
378
|
-
}
|
|
379
|
-
return /^error\s*:/i.test(text) ? text : `Error: ${text}`;
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
function toolCallId(call) {
|
|
383
|
-
return call?.id ?? call?.toolCallId ?? call?.tool_call_id ?? call?.call_id;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
function toolResultCallId(message) {
|
|
387
|
-
return message?.toolCallId
|
|
388
|
-
?? message?.tool_call_id
|
|
389
|
-
?? message?.tool_use_id
|
|
390
|
-
?? message?.call_id
|
|
391
|
-
?? message?.id;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
function toolCallName(call) {
|
|
395
|
-
return call?.name ?? call?.function?.name ?? call?.toolName ?? call?.tool_name ?? 'tool';
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
function toolCallArgs(call) {
|
|
399
|
-
return call?.arguments ?? call?.args ?? call?.input ?? call?.function?.arguments;
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
function textBetweenTag(text, tag) {
|
|
403
|
-
const re = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)<\\/${tag}>`, 'i');
|
|
404
|
-
const match = re.exec(String(text ?? ''));
|
|
405
|
-
return match ? match[1].trim() : '';
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function stripSyntheticAgentTags(text) {
|
|
409
|
-
const value = String(text ?? '').trim();
|
|
410
|
-
const finalAnswer = textBetweenTag(value, 'final-answer');
|
|
411
|
-
if (finalAnswer) return finalAnswer;
|
|
412
|
-
const taskResult = textBetweenTag(value, 'result');
|
|
413
|
-
if (taskResult) return taskResult;
|
|
414
|
-
return value
|
|
415
|
-
.replace(/^agent result[^\n]*(?:\n|$)/i, '')
|
|
416
|
-
.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, '')
|
|
417
|
-
.trim();
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
function splitBridgeEnvelope(text) {
|
|
421
|
-
const value = String(text ?? '').trim();
|
|
422
|
-
if (!value) return { head: '', body: '' };
|
|
423
|
-
const match = /\n\s*\n/.exec(value);
|
|
424
|
-
if (!match) return { head: value, body: '' };
|
|
425
|
-
return {
|
|
426
|
-
head: value.slice(0, match.index).trim(),
|
|
427
|
-
body: value.slice(match.index + match[0].length).trim(),
|
|
428
|
-
};
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
function agentJobStatusText(parsed) {
|
|
432
|
-
if (!parsed) return '';
|
|
433
|
-
const parts = [];
|
|
434
|
-
if (parsed.status) parts.push(`status: ${parsed.status}`);
|
|
435
|
-
if (parsed.taskId) parts.push(`task_id: ${parsed.taskId}`);
|
|
436
|
-
return parts.join(' · ');
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function agentJobResultText(text, parsed = parseAgentJob(text)) {
|
|
440
|
-
const value = String(text ?? '').trim();
|
|
441
|
-
if (!value) return '';
|
|
442
|
-
if (parsed?.taskId) {
|
|
443
|
-
const { body } = splitBridgeEnvelope(value);
|
|
444
|
-
const cleanBody = stripSyntheticAgentTags(body);
|
|
445
|
-
if (cleanBody) return cleanBody;
|
|
446
|
-
return agentJobStatusText(parsed);
|
|
447
|
-
}
|
|
448
|
-
return stripSyntheticAgentTags(value) || value;
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
function parseAgentResultEnvelope(text, fallback = {}) {
|
|
452
|
-
const value = String(text ?? '').trim();
|
|
453
|
-
if (!/^agent result\b/i.test(value)) return null;
|
|
454
|
-
const [head = '', ...restLines] = value.split('\n');
|
|
455
|
-
const body = stripSyntheticAgentTags(restLines.join('\n'));
|
|
456
|
-
const attrs = {};
|
|
457
|
-
const attrRe = /([a-zA-Z][\w-]*)=("[^"]*"|'[^']*'|\S+)/g;
|
|
458
|
-
let match;
|
|
459
|
-
while ((match = attrRe.exec(head))) {
|
|
460
|
-
attrs[match[1].toLowerCase()] = String(match[2] || '').replace(/^["']|["']$/g, '');
|
|
461
|
-
}
|
|
462
|
-
const providerModel = /\s([a-zA-Z0-9_.-]+)\/([^\s]+)\s*$/i.exec(head);
|
|
463
|
-
const agent = attrs.agent || fallback.agent || '';
|
|
464
|
-
return {
|
|
465
|
-
name: 'agent',
|
|
466
|
-
label: String(fallback.status || attrs.status || 'completed').toLowerCase(),
|
|
467
|
-
args: {
|
|
468
|
-
type: 'result',
|
|
469
|
-
status: fallback.status || attrs.status || 'completed',
|
|
470
|
-
task_id: fallback.taskId || attrs.task_id || attrs.taskid || undefined,
|
|
471
|
-
tag: fallback.tag || attrs.tag || undefined,
|
|
472
|
-
agent: agent || undefined,
|
|
473
|
-
provider: fallback.provider || attrs.provider || providerModel?.[1] || undefined,
|
|
474
|
-
model: fallback.model || attrs.model || providerModel?.[2] || undefined,
|
|
475
|
-
preset: fallback.preset || attrs.preset || undefined,
|
|
476
|
-
effort: fallback.effort || attrs.effort || undefined,
|
|
477
|
-
fast: fallback.fast ?? attrs.fast,
|
|
478
|
-
},
|
|
479
|
-
result: body || agentJobStatusText({ status: fallback.status || attrs.status || 'completed', taskId: fallback.taskId || attrs.task_id || attrs.taskid || '' }),
|
|
480
|
-
isError: /^(failed|error|timeout|cancelled|canceled|killed)$/i.test(fallback.status || attrs.status || ''),
|
|
481
|
-
};
|
|
482
|
-
}
|
|
483
|
-
|
|
97
|
+
// Re-export the shared tool-result/notification helpers so importers (and tests)
|
|
98
|
+
// keep resolving them from engine.mjs unchanged.
|
|
484
99
|
export { toolResultText, toolAggregateDetailFallback, toolGroupedDisplayFallback };
|
|
485
|
-
|
|
486
|
-
export
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const allLines = value.split('\n');
|
|
490
|
-
const rest = allLines.slice(1);
|
|
491
|
-
const blank = rest.findIndex((line) => !line.trim());
|
|
492
|
-
const headLines = blank >= 0 ? rest.slice(0, blank) : rest;
|
|
493
|
-
const body = blank >= 0 ? rest.slice(blank + 1).join('\n').trim() : '';
|
|
494
|
-
const fields = {};
|
|
495
|
-
for (const line of headLines) {
|
|
496
|
-
const match = /^([a-zA-Z][\w-]*):\s*(.*)$/.exec(line.trim());
|
|
497
|
-
if (match) fields[match[1].toLowerCase()] = match[2].trim();
|
|
498
|
-
}
|
|
499
|
-
const surface = String(fields.surface || fields.operation || 'task').toLowerCase();
|
|
500
|
-
const name = surface === 'explore' || surface === 'search' || surface === 'shell' || surface === 'agent' ? surface : 'task';
|
|
501
|
-
const status = String(fields.status || '').toLowerCase();
|
|
502
|
-
const taskId = fields.task_id || fields.taskid || '';
|
|
503
|
-
const errorText = fields.error || '';
|
|
504
|
-
const agentResult = parseAgentResultEnvelope(body, {
|
|
505
|
-
status,
|
|
506
|
-
taskId,
|
|
507
|
-
tag: fields.tag || fields.label || '',
|
|
508
|
-
agent: fields.agent || '',
|
|
509
|
-
provider: fields.provider || '',
|
|
510
|
-
model: fields.model || '',
|
|
511
|
-
preset: fields.preset || '',
|
|
512
|
-
effort: fields.effort || '',
|
|
513
|
-
fast: fields.fast,
|
|
514
|
-
});
|
|
515
|
-
if (agentResult) return { ...agentResult, rawResult: value };
|
|
516
|
-
const errorOnlyBody = isBackgroundErrorOnlyBody(body, errorText);
|
|
517
|
-
const resultBody = body && !errorOnlyBody ? body : '';
|
|
518
|
-
return {
|
|
519
|
-
name,
|
|
520
|
-
label: status || 'notification',
|
|
521
|
-
args: {
|
|
522
|
-
type: body ? 'result' : (fields.operation || 'status'),
|
|
523
|
-
status,
|
|
524
|
-
task_id: taskId || undefined,
|
|
525
|
-
surface,
|
|
526
|
-
operation: fields.operation || undefined,
|
|
527
|
-
label: fields.label || undefined,
|
|
528
|
-
tag: fields.tag || undefined,
|
|
529
|
-
agent: fields.agent || undefined,
|
|
530
|
-
provider: fields.provider || undefined,
|
|
531
|
-
model: fields.model || undefined,
|
|
532
|
-
preset: fields.preset || undefined,
|
|
533
|
-
effort: fields.effort || undefined,
|
|
534
|
-
fast: fields.fast || undefined,
|
|
535
|
-
error: errorText || undefined,
|
|
536
|
-
startedAt: fields.started || fields.startedat || undefined,
|
|
537
|
-
finishedAt: fields.finished || fields.finishedat || undefined,
|
|
538
|
-
},
|
|
539
|
-
result: resultBody || (!errorText ? [status ? `status: ${status}` : '', taskId ? `task_id: ${taskId}` : ''].filter(Boolean).join(' · ') : ''),
|
|
540
|
-
rawResult: value,
|
|
541
|
-
isError: /^(failed|error|timeout|cancelled|canceled|killed)$/i.test(status) || /^error:/i.test(body) || Boolean(errorText),
|
|
542
|
-
};
|
|
543
|
-
}
|
|
544
|
-
|
|
545
|
-
function isStatusOnlyAgentCompletionNotification(text) {
|
|
546
|
-
const background = parseBackgroundTaskEnvelope(text);
|
|
547
|
-
if (background?.name === 'agent' && /^(completed|cancelled|canceled)$/i.test(background.label || '')) {
|
|
548
|
-
return !(hasAgentResponseResultText(background.result) || hasAgentResponseResultText(text));
|
|
549
|
-
}
|
|
550
|
-
const parsed = parseAgentJob(text);
|
|
551
|
-
const result = agentJobResultText(text, parsed);
|
|
552
|
-
if (!parsed?.taskId || !/^(completed|cancelled|canceled)$/i.test(parsed.status || '')) return false;
|
|
553
|
-
return !(hasAgentResponseResultText(result) || hasAgentResponseResultText(text));
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
function hasAgentResponseResultText(text) {
|
|
557
|
-
const value = String(text || '').trim();
|
|
558
|
-
if (!value) return false;
|
|
559
|
-
if (/^status:\s*(?:running|pending|queued|completed|failed|cancelled|canceled)(?:\s*·\s*task_id:\s*\S+)?$/i.test(value)) return false;
|
|
560
|
-
if (/^(?:background task\b|agent task:|task_id:)/i.test(value) && !/\n\s*\n[\s\S]*\S/.test(value)) return false;
|
|
561
|
-
return true;
|
|
562
|
-
}
|
|
563
|
-
|
|
564
|
-
function bracketField(text, name) {
|
|
565
|
-
const re = new RegExp(`^\\[${name}:\\s*([^\\]]*)\\]`, 'mi');
|
|
566
|
-
return re.exec(String(text ?? ''))?.[1]?.trim() || '';
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
function toolResultStatus(text) {
|
|
570
|
-
const value = String(text ?? '');
|
|
571
|
-
const tagged = textBetweenTag(value, 'status');
|
|
572
|
-
if (tagged) return tagged.trim();
|
|
573
|
-
const bracketed = bracketField(value, 'status');
|
|
574
|
-
if (bracketed) return bracketed.trim();
|
|
575
|
-
const inline = /^(?:status|state):\s*([^\s·,;]+)/mi.exec(value);
|
|
576
|
-
return inline ? inline[1].trim() : '';
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
function isErrorToolStatus(status) {
|
|
580
|
-
return /^(failed|error|timeout|cancelled|canceled|killed)$/i.test(String(status || '').trim());
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
function parseSyntheticAgentMessage(text) {
|
|
584
|
-
const value = String(text ?? '').trim();
|
|
585
|
-
if (!value) return null;
|
|
586
|
-
const finalAnswer = textBetweenTag(value, 'final-answer');
|
|
587
|
-
if (finalAnswer) {
|
|
588
|
-
return {
|
|
589
|
-
name: 'agent',
|
|
590
|
-
label: 'final',
|
|
591
|
-
args: { type: 'read', description: 'agent result' },
|
|
592
|
-
result: finalAnswer,
|
|
593
|
-
};
|
|
594
|
-
}
|
|
595
|
-
const agentResult = parseAgentResultEnvelope(value);
|
|
596
|
-
if (agentResult) return agentResult;
|
|
597
|
-
const backgroundTask = parseBackgroundTaskEnvelope(value);
|
|
598
|
-
if (backgroundTask) return backgroundTask;
|
|
599
|
-
const shellTaskId = bracketField(value, 'task_id');
|
|
600
|
-
if (shellTaskId) {
|
|
601
|
-
const status = bracketField(value, 'status') || 'done';
|
|
602
|
-
const exit = bracketField(value, 'exit');
|
|
603
|
-
const command = bracketField(value, 'command');
|
|
604
|
-
return {
|
|
605
|
-
name: 'shell',
|
|
606
|
-
label: status,
|
|
607
|
-
args: { type: 'result', task_id: shellTaskId, command },
|
|
608
|
-
result: value,
|
|
609
|
-
isError: /^(failed|error|timeout|cancelled|killed)$/i.test(status) || (exit && exit !== '0' && exit !== 'n/a'),
|
|
610
|
-
};
|
|
611
|
-
}
|
|
612
|
-
const agentJob = parseAgentJob(value);
|
|
613
|
-
if (agentJob?.taskId) {
|
|
614
|
-
const label = agentJob.status || 'notification';
|
|
615
|
-
const result = agentJobResultText(value, agentJob);
|
|
616
|
-
return {
|
|
617
|
-
name: 'agent',
|
|
618
|
-
label,
|
|
619
|
-
args: agentArgsWithResultMetadata({ type: agentJob.type || 'notification', description: 'agent notification' }, agentJob),
|
|
620
|
-
result: result || agentJobStatusText(agentJob) || 'agent notification',
|
|
621
|
-
isError: /^(failed|error|timeout|cancelled|killed)$/i.test(label),
|
|
622
|
-
};
|
|
623
|
-
}
|
|
624
|
-
if (/<task-notification\b/i.test(value)) {
|
|
625
|
-
const status = textBetweenTag(value, 'status') || 'completed';
|
|
626
|
-
const summary = textBetweenTag(value, 'summary') || `Agent ${status}`;
|
|
627
|
-
const taskId = textBetweenTag(value, 'task-id');
|
|
628
|
-
const result = stripSyntheticAgentTags(value);
|
|
629
|
-
return {
|
|
630
|
-
name: 'agent',
|
|
631
|
-
label: status,
|
|
632
|
-
taskId,
|
|
633
|
-
summary,
|
|
634
|
-
result: result || summary,
|
|
635
|
-
};
|
|
636
|
-
}
|
|
637
|
-
return null;
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
function normalizeToolName(name) {
|
|
641
|
-
return String(name || 'tool')
|
|
642
|
-
.replace(/^mcp__.*__/, '')
|
|
643
|
-
.replace(/^functions\./, '')
|
|
644
|
-
.replace(/-/g, '_')
|
|
645
|
-
.toLowerCase();
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
function parseToolArgs(args) {
|
|
649
|
-
if (!args) return {};
|
|
650
|
-
if (typeof args === 'string') {
|
|
651
|
-
try {
|
|
652
|
-
const parsed = JSON.parse(args);
|
|
653
|
-
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
654
|
-
} catch {
|
|
655
|
-
return {};
|
|
656
|
-
}
|
|
657
|
-
}
|
|
658
|
-
return typeof args === 'object' ? args : {};
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
// Ink renders through a maxFps throttle (120fps in index.jsx, ≈8.3ms). A plain
|
|
662
|
-
// setImmediate only yields to the event loop; if Ink already painted within the
|
|
663
|
-
// current throttle window, the next paint may still be queued and our following
|
|
664
|
-
// transcript mutation can coalesce into the same visible frame. Wait just past
|
|
665
|
-
// one render window when we intentionally split transcript commits for visual
|
|
666
|
-
// stability (preamble frame → tool-card frame).
|
|
667
|
-
const RENDER_THROTTLE_FLUSH_MS = 12;
|
|
668
|
-
const yieldToRenderer = () => new Promise((resolve) => {
|
|
669
|
-
setTimeout(resolve, RENDER_THROTTLE_FLUSH_MS);
|
|
670
|
-
});
|
|
671
|
-
|
|
672
|
-
function parseAgentJob(text) {
|
|
673
|
-
const value = String(text || '');
|
|
674
|
-
const idMatch = /^agent task:\s*([^\s]+)/m.exec(value) || /^task_id:\s*([^\s]+)/m.exec(value);
|
|
675
|
-
if (!idMatch) return null;
|
|
676
|
-
const statusMatch = /^status:\s*([^\s(]+)/m.exec(value);
|
|
677
|
-
const typeMatch = /^type:\s*(.+)$/m.exec(value);
|
|
678
|
-
const targetMatch = /^target:\s*(.+)$/m.exec(value);
|
|
679
|
-
const agentMatch = /^agent:\s*(.+)$/m.exec(value);
|
|
680
|
-
const presetMatch = /^preset:\s*(.+)$/m.exec(value);
|
|
681
|
-
const modelMatch = /^model:\s*([^/\s]+)\/(.+)$/m.exec(value);
|
|
682
|
-
const effortMatch = /^effort:\s*(.+)$/m.exec(value);
|
|
683
|
-
const fastMatch = /^fast:\s*(on|off|true|false)$/m.exec(value);
|
|
684
|
-
return {
|
|
685
|
-
taskId: idMatch[1],
|
|
686
|
-
status: (statusMatch?.[1] || '').toLowerCase(),
|
|
687
|
-
type: (typeMatch?.[1] || '').trim(),
|
|
688
|
-
target: (targetMatch?.[1] || '').trim(),
|
|
689
|
-
agent: (agentMatch?.[1] || '').trim(),
|
|
690
|
-
preset: (presetMatch?.[1] || '').trim(),
|
|
691
|
-
provider: (modelMatch?.[1] || '').trim(),
|
|
692
|
-
model: (modelMatch?.[2] || '').trim(),
|
|
693
|
-
effort: (effortMatch?.[1] || '').trim(),
|
|
694
|
-
fast: fastMatch ? /^(on|true)$/i.test(fastMatch[1]) : undefined,
|
|
695
|
-
};
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
const QUEUE_PRIORITY = { now: 0, next: 1, later: 2 };
|
|
699
|
-
|
|
700
|
-
function queuePriorityValue(value) {
|
|
701
|
-
return QUEUE_PRIORITY[String(value || 'next')] ?? QUEUE_PRIORITY.next;
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
function defaultQueuePriority(mode) {
|
|
705
|
-
// Queue priority defaults:
|
|
706
|
-
// - user/bashed prompt input defaults to `next`, so it can be attached at the
|
|
707
|
-
// next model-send boundary while a turn is active.
|
|
708
|
-
// - task notifications default to `later`, unless the caller explicitly marks
|
|
709
|
-
// them urgent (e.g. interactive shell stall/completion).
|
|
710
|
-
return mode === 'task-notification' ? 'later' : 'next';
|
|
711
|
-
}
|
|
712
|
-
|
|
713
|
-
function isQueuedEntryEditable(entry) {
|
|
714
|
-
const mode = entry?.mode || 'prompt';
|
|
715
|
-
return mode !== 'task-notification' && mode !== 'pending-resume';
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
function isQueuedEntryVisible(entry) {
|
|
719
|
-
// state.queued drives the user-command wait list above the prompt. Background
|
|
720
|
-
// task completions stay in the internal pending queue, but should never look
|
|
721
|
-
// like commands typed by the user while they wait to be drained.
|
|
722
|
-
const mode = entry?.mode || 'prompt';
|
|
723
|
-
if (mode === 'pending-resume') return false;
|
|
724
|
-
return isQueuedEntryEditable(entry);
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
function isSlashQueuedEntry(entry) {
|
|
728
|
-
if (entry?.skipSlashCommands) return false;
|
|
729
|
-
const text = promptContentText(entry?.content ?? entry?.text ?? '');
|
|
730
|
-
return text.trim().startsWith('/');
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
function firstQueueLine(text) {
|
|
734
|
-
return String(text || '').split('\n').map((line) => line.trim()).find(Boolean) || '';
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
function shortTextFingerprint(text) {
|
|
738
|
-
const value = String(text || '').trim();
|
|
739
|
-
let hash = 2166136261;
|
|
740
|
-
for (let i = 0; i < value.length; i += 1) {
|
|
741
|
-
hash ^= value.charCodeAt(i);
|
|
742
|
-
hash = Math.imul(hash, 16777619);
|
|
743
|
-
}
|
|
744
|
-
return (hash >>> 0).toString(36);
|
|
745
|
-
}
|
|
746
|
-
|
|
747
|
-
function notificationDisplayText(text) {
|
|
748
|
-
const parsed = parseAgentJob(text);
|
|
749
|
-
const result = agentJobResultText(text, parsed);
|
|
750
|
-
const synthetic = parseSyntheticAgentMessage(text);
|
|
751
|
-
return firstQueueLine(synthetic?.result || result || text) || 'agent notification';
|
|
752
|
-
}
|
|
753
|
-
|
|
754
|
-
function promptContentText(content) {
|
|
755
|
-
if (typeof content === 'string') return content;
|
|
756
|
-
if (Array.isArray(content)) {
|
|
757
|
-
return content.map((part) => {
|
|
758
|
-
if (typeof part === 'string') return part;
|
|
759
|
-
if (part?.type === 'text') return part.text || '';
|
|
760
|
-
if (part?.type === 'image') return '[Image]';
|
|
761
|
-
return part?.text || '';
|
|
762
|
-
}).filter(Boolean).join('\n');
|
|
763
|
-
}
|
|
764
|
-
return String(content ?? '');
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
function timestampMs(value) {
|
|
768
|
-
if (value == null || value === '') return 0;
|
|
769
|
-
const n = Number(value);
|
|
770
|
-
if (Number.isFinite(n) && n > 0) return n;
|
|
771
|
-
const parsed = Date.parse(String(value));
|
|
772
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
773
|
-
}
|
|
774
|
-
|
|
775
|
-
function hasModelVisibleConversation(session) {
|
|
776
|
-
const messages = Array.isArray(session?.messages) ? session.messages : [];
|
|
777
|
-
return messages.some((message) => {
|
|
778
|
-
const role = message?.role;
|
|
779
|
-
if (role !== 'user' && role !== 'assistant' && role !== 'tool') return false;
|
|
780
|
-
const text = promptContentText(message.content).trim();
|
|
781
|
-
if (role === 'user' && text.startsWith('<system-reminder>')) return false;
|
|
782
|
-
if (role === 'assistant' && text === '.' && !Array.isArray(message.toolCalls)) return false;
|
|
783
|
-
return !!text || role === 'assistant' || role === 'tool';
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
|
|
787
|
-
function sessionActivityTimestamp(session, fallback = 0) {
|
|
788
|
-
if (!hasModelVisibleConversation(session)) return 0;
|
|
789
|
-
return timestampMs(session?.lastUsedAt)
|
|
790
|
-
|| timestampMs(session?.updatedAt)
|
|
791
|
-
|| timestampMs(fallback);
|
|
792
|
-
}
|
|
793
|
-
|
|
794
|
-
function promptDisplayText(content, options = {}) {
|
|
795
|
-
if (typeof options.displayText === 'string') return options.displayText;
|
|
796
|
-
return promptContentText(content);
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
function mergePromptContents(entries) {
|
|
800
|
-
const batch = Array.isArray(entries) ? entries : [];
|
|
801
|
-
if (batch.every((entry) => typeof entry?.content === 'string')) {
|
|
802
|
-
return batch.map((entry) => entry.content).filter((text) => String(text || '').trim()).join('\n');
|
|
803
|
-
}
|
|
804
|
-
const parts = [];
|
|
805
|
-
for (const entry of batch) {
|
|
806
|
-
const content = entry?.content;
|
|
807
|
-
if (typeof content === 'string') {
|
|
808
|
-
if (content.trim()) parts.push({ type: 'text', text: content });
|
|
809
|
-
} else if (Array.isArray(content)) {
|
|
810
|
-
parts.push(...content);
|
|
811
|
-
}
|
|
812
|
-
parts.push({ type: 'text', text: '\n' });
|
|
813
|
-
}
|
|
814
|
-
while (parts.length && parts[parts.length - 1]?.type === 'text' && parts[parts.length - 1]?.text === '\n') parts.pop();
|
|
815
|
-
return parts.length === 1 && parts[0]?.type === 'text' ? parts[0].text : parts;
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
function mergePastedImages(entries) {
|
|
819
|
-
const out = {};
|
|
820
|
-
for (const entry of entries || []) {
|
|
821
|
-
const images = entry?.pastedImages;
|
|
822
|
-
if (!images || typeof images !== 'object') continue;
|
|
823
|
-
for (const [id, image] of Object.entries(images)) {
|
|
824
|
-
if (image) out[id] = image;
|
|
825
|
-
}
|
|
826
|
-
}
|
|
827
|
-
return Object.keys(out).length > 0 ? out : null;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
function callCommitCallbacks(entries) {
|
|
831
|
-
for (const entry of entries || []) {
|
|
832
|
-
try { entry?.onCommitted?.(); } catch {}
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
function notificationQueueKey(event, text, parsed) {
|
|
837
|
-
const meta = event?.meta && typeof event.meta === 'object' ? event.meta : {};
|
|
838
|
-
const synthetic = parseSyntheticAgentMessage(text);
|
|
839
|
-
if (synthetic?.name === 'agent' && String(synthetic.args?.type || '').toLowerCase() === 'result') {
|
|
840
|
-
const taskId = String(synthetic.args?.task_id || '').trim();
|
|
841
|
-
const executionId = String(meta.execution_id || '').trim();
|
|
842
|
-
const tag = String(synthetic.args?.tag || '').trim();
|
|
843
|
-
const resultId = taskId
|
|
844
|
-
? `task:${taskId}`
|
|
845
|
-
: executionId
|
|
846
|
-
? `exec:${executionId}`
|
|
847
|
-
: tag
|
|
848
|
-
? `tag:${tag}:${shortTextFingerprint(synthetic.result || text)}`
|
|
849
|
-
: '';
|
|
850
|
-
const agent = String(synthetic.args?.agent || '').trim();
|
|
851
|
-
if (resultId || agent) return ['agent-result', resultId, agent].filter(Boolean).join(':');
|
|
852
|
-
}
|
|
853
|
-
const id = String(meta.execution_id || parsed?.taskId || '').trim();
|
|
854
|
-
if (!id) return '';
|
|
855
|
-
const type = String(meta.type || '').trim();
|
|
856
|
-
const status = String(meta.status || parsed?.status || '').trim();
|
|
857
|
-
const fallbackKind = String(text || '').split('\n', 1)[0]?.trim() || 'notification';
|
|
858
|
-
// Distinguish a body-carrying completion from a header-only preview that
|
|
859
|
-
// shares the same id/type/status. An early agent preview can arrive before
|
|
860
|
-
// the session is persisted (no result body); the canonical notification that
|
|
861
|
-
// follows DOES carry the body. Without this dimension the bodyless preview
|
|
862
|
-
// would claim the dedupe key and suppress the real result. A blank-line gap
|
|
863
|
-
// separates the task header block from the result body in the envelope.
|
|
864
|
-
const hasBody = /\n\s*\n[\s\S]*\S/.test(String(text || '')) ? 'b1' : 'b0';
|
|
865
|
-
return [id, type || fallbackKind, status, hasBody].filter(Boolean).join(':');
|
|
866
|
-
}
|
|
867
|
-
|
|
868
|
-
/** Pure delivery plan for runtime.onNotification execution envelopes (tests + handler). */
|
|
869
|
-
export function resolveTuiRuntimeNotificationDelivery(event, text) {
|
|
870
|
-
const trimmed = String(text ?? '').trim();
|
|
871
|
-
if (!trimmed) return { action: 'ignore' };
|
|
872
|
-
const parsed = parseAgentJob(trimmed);
|
|
873
|
-
const meta = event?.meta && typeof event.meta === 'object' ? event.meta : {};
|
|
874
|
-
if (!isExecutionNotification(event, trimmed, parsed)) {
|
|
875
|
-
return { action: 'enqueue', displayText: trimmed, modelContent: trimmed };
|
|
876
|
-
}
|
|
877
|
-
if (isStatusOnlyAgentCompletionNotification(trimmed)) {
|
|
878
|
-
return { action: 'status-only', displayText: trimmed, modelContent: '' };
|
|
879
|
-
}
|
|
880
|
-
const modelContent = modelVisibleToolCompletionMessage(trimmed, meta);
|
|
881
|
-
return {
|
|
882
|
-
action: 'execution-ui',
|
|
883
|
-
displayText: trimmed,
|
|
884
|
-
modelContent,
|
|
885
|
-
};
|
|
886
|
-
}
|
|
887
|
-
|
|
888
|
-
function isExecutionNotification(event, text, parsed) {
|
|
889
|
-
const meta = event?.meta && typeof event.meta === 'object' ? event.meta : {};
|
|
890
|
-
if (meta.execution_id || meta.execution_surface) return true;
|
|
891
|
-
if (parseAgentResultEnvelope(text)) return true;
|
|
892
|
-
if (parseBackgroundTaskEnvelope(text)) return true;
|
|
893
|
-
return Boolean(parsed?.taskId && /^(?:agent task:|task_id:)/mi.test(String(text || '')));
|
|
894
|
-
}
|
|
895
|
-
|
|
896
|
-
function agentArgsWithResultMetadata(args, parsed) {
|
|
897
|
-
if (!parsed) return args;
|
|
898
|
-
const next = { ...(args && typeof args === 'object' ? args : {}) };
|
|
899
|
-
const requestedAction = String(next.type || next.action || next.mode || '').trim().toLowerCase();
|
|
900
|
-
if (parsed.type) {
|
|
901
|
-
// Job status envelopes report the original job type (usually "spawn").
|
|
902
|
-
// Preserve the user's current agent tool action ("status", "read", …) so
|
|
903
|
-
// manual checks render as "Reviewer status" instead of another
|
|
904
|
-
// "Spawning Reviewer" card. Keep the job type as metadata for detail.
|
|
905
|
-
if (!requestedAction || /^(notification|result|completion)$/i.test(requestedAction)) next.type = parsed.type;
|
|
906
|
-
else next.jobType = parsed.type;
|
|
907
|
-
}
|
|
908
|
-
if (parsed.status) next.status = parsed.status;
|
|
909
|
-
if (parsed.taskId) next.task_id = parsed.taskId;
|
|
910
|
-
if (parsed.agent) next.agent = parsed.agent;
|
|
911
|
-
if (parsed.preset) next.preset = parsed.preset;
|
|
912
|
-
if (parsed.provider) next.provider = parsed.provider;
|
|
913
|
-
if (parsed.model) next.model = parsed.model;
|
|
914
|
-
if (parsed.effort) next.effort = parsed.effort;
|
|
915
|
-
if (parsed.fast !== undefined) next.fast = parsed.fast;
|
|
916
|
-
if (!next.tag && parsed.target) {
|
|
917
|
-
const target = parsed.target.split(/\s+/)[0];
|
|
918
|
-
if (target && !target.startsWith('sess_')) next.tag = target;
|
|
919
|
-
}
|
|
920
|
-
return next;
|
|
921
|
-
}
|
|
100
|
+
export { parseBackgroundTaskEnvelope };
|
|
101
|
+
// Re-export the pure notification delivery plan (moved to ./engine/notification-plan.mjs)
|
|
102
|
+
// so importers/tests keep resolving resolveTuiRuntimeNotificationDelivery from engine.mjs.
|
|
103
|
+
export { resolveTuiRuntimeNotificationDelivery };
|
|
922
104
|
|
|
923
105
|
export async function createEngineSession({
|
|
924
106
|
provider: providerName,
|
|
@@ -943,7 +125,7 @@ export async function createEngineSession({
|
|
|
943
125
|
bootProfile('engine:create:runtime-ready', { ms: (performance.now() - startedAt).toFixed(1) });
|
|
944
126
|
const cwd = runtime.cwd || process.cwd();
|
|
945
127
|
const stateStartedAt = performance.now();
|
|
946
|
-
const autoClearState = () => runtime.getAutoClear?.() || runtime.autoClear || { enabled: true, idleMs: 60 * 60 * 1000 };
|
|
128
|
+
const autoClearState = () => runtime.getAutoClear?.() || runtime.autoClear || { enabled: true, idleMs: 60 * 60 * 1000, custom: false, providerDefault: 60 * 60 * 1000, provider: null };
|
|
947
129
|
const AGENT_STATUS_CACHE_MS = 250;
|
|
948
130
|
let agentStatusCache = null;
|
|
949
131
|
let agentStatusCacheAt = 0;
|
|
@@ -1009,6 +191,7 @@ export async function createEngineSession({
|
|
|
1009
191
|
let state = {
|
|
1010
192
|
items: [],
|
|
1011
193
|
toasts: [],
|
|
194
|
+
progressHint: null,
|
|
1012
195
|
busy: false,
|
|
1013
196
|
commandBusy: false,
|
|
1014
197
|
commandStatus: null,
|
|
@@ -1018,6 +201,14 @@ export async function createEngineSession({
|
|
|
1018
201
|
toolApproval: null,
|
|
1019
202
|
lastTurn: null,
|
|
1020
203
|
stats: createSessionStats(),
|
|
204
|
+
// Incremental derivations published by the engine so App does not scan all
|
|
205
|
+
// transcript items on every change:
|
|
206
|
+
// - activeToolSummary: running Explore/Search active counts + earliest
|
|
207
|
+
// startedAt for the prompt-line status (replaces App.jsx O(n) items scan).
|
|
208
|
+
// - promptHistoryList: newest-first deduped user-prompt history, rebuilt
|
|
209
|
+
// only when a user item is appended (replaces the per-change rescan).
|
|
210
|
+
activeToolSummary: null,
|
|
211
|
+
promptHistoryList: [],
|
|
1021
212
|
...baseRouteState(),
|
|
1022
213
|
displayContextWindow: 0,
|
|
1023
214
|
compactBoundaryTokens: 0,
|
|
@@ -1131,10 +322,85 @@ export async function createEngineSession({
|
|
|
1131
322
|
const id = nextItems[i]?.id;
|
|
1132
323
|
if (id != null) itemIndexById.set(id, i);
|
|
1133
324
|
}
|
|
325
|
+
// Bulk item swap (session load / clear / compact). Derive the prompt-history
|
|
326
|
+
// list from the NEW items and stage it onto state here so App never rescans;
|
|
327
|
+
// the callers that invoke replaceItems always follow with a set({items:...,
|
|
328
|
+
// ...}) that carries fresh references, so this pre-stage does not defeat any
|
|
329
|
+
// emit (the accompanying set() diffs the full patch). A bulk swap also
|
|
330
|
+
// discards the old transcript, so drop any tracked active tool calls.
|
|
331
|
+
activeToolCalls.clear();
|
|
332
|
+
state = { ...state, items: nextItems, promptHistoryList: recomputePromptHistory(nextItems), activeToolSummary: null };
|
|
1134
333
|
return nextItems;
|
|
1135
334
|
};
|
|
1136
335
|
let flushDeferredBeforeImmediatePush = null;
|
|
1137
336
|
let pushingFromDeferredEntry = false;
|
|
337
|
+
// --- Prompt-history list (newest-first, deduped) maintained incrementally ---
|
|
338
|
+
// App previously rebuilt this from state.items on EVERY transcript change
|
|
339
|
+
// (App.jsx recentPromptHistory useMemo). It only changes when a user item is
|
|
340
|
+
// appended, so rebuild it there and on bulk item swaps, publishing to
|
|
341
|
+
// state.promptHistoryList.
|
|
342
|
+
const PROMPT_HISTORY_LIMIT = 50;
|
|
343
|
+
const promptHistoryKey = (value) => String(value || '').trim().replace(/\s+/g, ' ');
|
|
344
|
+
const recomputePromptHistory = (sourceItems = null) => {
|
|
345
|
+
// Pure: derive the newest-first deduped user-prompt history from the given
|
|
346
|
+
// items (defaults to state.items) and RETURN it. Callers decide whether to
|
|
347
|
+
// publish via set() so the immutable-emit contract is preserved.
|
|
348
|
+
const items = Array.isArray(sourceItems) ? sourceItems : (Array.isArray(state.items) ? state.items : []);
|
|
349
|
+
const seen = new Set();
|
|
350
|
+
const history = [];
|
|
351
|
+
for (let i = items.length - 1; i >= 0 && history.length < PROMPT_HISTORY_LIMIT; i -= 1) {
|
|
352
|
+
const item = items[i];
|
|
353
|
+
if (item?.kind !== 'user') continue;
|
|
354
|
+
const text = String(item.text || '').trim();
|
|
355
|
+
const key = promptHistoryKey(text);
|
|
356
|
+
if (!key || seen.has(key)) continue;
|
|
357
|
+
seen.add(key);
|
|
358
|
+
history.push(text);
|
|
359
|
+
}
|
|
360
|
+
return history;
|
|
361
|
+
};
|
|
362
|
+
// --- Active-tool summary (Explore/Search) maintained incrementally ---
|
|
363
|
+
// App previously scanned every transcript item on every change to derive the
|
|
364
|
+
// prompt-line "Exploring N / Searching N" status. Instead the tool lifecycle
|
|
365
|
+
// below tracks per-callId category + started-at in activeToolCalls and derives
|
|
366
|
+
// the small summary from it, publishing state.activeToolSummary only when the
|
|
367
|
+
// aggregate (counts + earliest start) actually changes.
|
|
368
|
+
const activeToolCalls = new Map(); // callKey -> { category, count, startedAt }
|
|
369
|
+
const recomputeActiveToolSummary = () => {
|
|
370
|
+
let exploreCount = 0, exploreStart = 0, searchCount = 0, searchStart = 0;
|
|
371
|
+
for (const rec of activeToolCalls.values()) {
|
|
372
|
+
if (!rec) continue;
|
|
373
|
+
const c = Math.max(1, Number(rec.count || 1));
|
|
374
|
+
const started = Number(rec.startedAt || 0);
|
|
375
|
+
if (rec.category === 'Explore') {
|
|
376
|
+
exploreCount += c;
|
|
377
|
+
if (started > 0 && (exploreStart === 0 || started < exploreStart)) exploreStart = started;
|
|
378
|
+
} else if (rec.category === 'Search') {
|
|
379
|
+
searchCount += c;
|
|
380
|
+
if (started > 0 && (searchStart === 0 || started < searchStart)) searchStart = started;
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
const next = (exploreCount || searchCount)
|
|
384
|
+
? `${exploreCount}:${exploreStart}:${searchCount}:${searchStart}`
|
|
385
|
+
: '';
|
|
386
|
+
const prev = state.activeToolSummary || '';
|
|
387
|
+
if (next !== prev) set({ activeToolSummary: next || null });
|
|
388
|
+
};
|
|
389
|
+
const markToolCallActive = (callKey, category, count, startedAt) => {
|
|
390
|
+
if (!callKey || (category !== 'Explore' && category !== 'Search')) return;
|
|
391
|
+
activeToolCalls.set(callKey, { category, count: Math.max(1, Number(count || 1)), startedAt: Number(startedAt || Date.now()) });
|
|
392
|
+
recomputeActiveToolSummary();
|
|
393
|
+
};
|
|
394
|
+
const markToolCallDone = (callKey) => {
|
|
395
|
+
if (!callKey || !activeToolCalls.has(callKey)) return;
|
|
396
|
+
activeToolCalls.delete(callKey);
|
|
397
|
+
recomputeActiveToolSummary();
|
|
398
|
+
};
|
|
399
|
+
const clearActiveToolSummary = () => {
|
|
400
|
+
if (activeToolCalls.size === 0 && !state.activeToolSummary) return;
|
|
401
|
+
activeToolCalls.clear();
|
|
402
|
+
if (state.activeToolSummary) set({ activeToolSummary: null });
|
|
403
|
+
};
|
|
1138
404
|
const pushItem = (item) => {
|
|
1139
405
|
if (!pushingFromDeferredEntry && flushDeferredBeforeImmediatePush) {
|
|
1140
406
|
flushDeferredBeforeImmediatePush();
|
|
@@ -1142,7 +408,16 @@ export async function createEngineSession({
|
|
|
1142
408
|
const index = state.items.length;
|
|
1143
409
|
const items = [...state.items, item];
|
|
1144
410
|
if (item?.id != null) itemIndexById.set(item.id, index);
|
|
1145
|
-
|
|
411
|
+
if (item?.kind === 'user') {
|
|
412
|
+
// Rebuild the derived history against the NEW list (not yet in state) and
|
|
413
|
+
// publish items + the fresh list in ONE set(). Do NOT pre-assign to state
|
|
414
|
+
// first — set() diffs against the current state, so a pre-assign would make
|
|
415
|
+
// the references identical and skip emit().
|
|
416
|
+
const promptHistoryList = recomputePromptHistory(items);
|
|
417
|
+
set({ items, promptHistoryList });
|
|
418
|
+
} else {
|
|
419
|
+
set({ items });
|
|
420
|
+
}
|
|
1146
421
|
};
|
|
1147
422
|
const upsertSyntheticToolItem = (text, id = nextId(), parsed = null) => {
|
|
1148
423
|
const synthetic = parseSyntheticAgentMessage(text);
|
|
@@ -1198,80 +473,38 @@ export async function createEngineSession({
|
|
|
1198
473
|
pushItem({ kind: 'notice', id, text: value, tone });
|
|
1199
474
|
return id;
|
|
1200
475
|
};
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
}
|
|
1233
|
-
function finishToolApproval(id, approved, reason = '') {
|
|
1234
|
-
const targetId = String(id || '');
|
|
1235
|
-
if (activeToolApproval && activeToolApproval.id === targetId) {
|
|
1236
|
-
const entry = activeToolApproval;
|
|
1237
|
-
activeToolApproval = null;
|
|
1238
|
-
if (entry.timer) clearTimeout(entry.timer);
|
|
1239
|
-
set({ toolApproval: null });
|
|
1240
|
-
try { entry.resolve({ approved: approved === true, reason: String(reason || '') }); } catch {}
|
|
1241
|
-
presentNextToolApproval();
|
|
1242
|
-
return true;
|
|
1243
|
-
}
|
|
1244
|
-
const index = toolApprovalQueue.findIndex((entry) => entry.id === targetId);
|
|
1245
|
-
if (index >= 0) {
|
|
1246
|
-
const [entry] = toolApprovalQueue.splice(index, 1);
|
|
1247
|
-
if (entry?.timer) clearTimeout(entry.timer);
|
|
1248
|
-
try { entry.resolve({ approved: approved === true, reason: String(reason || '') }); } catch {}
|
|
1249
|
-
return true;
|
|
1250
|
-
}
|
|
1251
|
-
return false;
|
|
1252
|
-
}
|
|
1253
|
-
function denyAllToolApprovals(reason = 'approval cancelled') {
|
|
1254
|
-
if (activeToolApproval) {
|
|
1255
|
-
const entry = activeToolApproval;
|
|
1256
|
-
activeToolApproval = null;
|
|
1257
|
-
if (entry.timer) clearTimeout(entry.timer);
|
|
1258
|
-
try { entry.resolve({ approved: false, reason }); } catch {}
|
|
1259
|
-
}
|
|
1260
|
-
while (toolApprovalQueue.length > 0) {
|
|
1261
|
-
const entry = toolApprovalQueue.shift();
|
|
1262
|
-
if (entry?.timer) clearTimeout(entry.timer);
|
|
1263
|
-
try { entry.resolve({ approved: false, reason }); } catch {}
|
|
1264
|
-
}
|
|
1265
|
-
if (state.toolApproval) set({ toolApproval: null });
|
|
1266
|
-
}
|
|
1267
|
-
function requestToolApproval(input = {}) {
|
|
1268
|
-
if (disposed) return Promise.resolve({ approved: false, reason: 'runtime disposed' });
|
|
1269
|
-
return new Promise((resolve) => {
|
|
1270
|
-
const id = nextId();
|
|
1271
|
-
toolApprovalQueue.push({ id, request: normalizeToolApprovalRequest(input, id), resolve, timer: null });
|
|
1272
|
-
presentNextToolApproval();
|
|
1273
|
-
});
|
|
1274
|
-
}
|
|
476
|
+
// Remove a transcript notice previously created via pushNotice(...,
|
|
477
|
+
// {transcript:true}). Used for transient-but-persistent notices (e.g. the
|
|
478
|
+
// manual OAuth URL) that must disappear once their flow concludes.
|
|
479
|
+
const removeNotice = (id) => {
|
|
480
|
+
if (id == null) return false;
|
|
481
|
+
const items = state.items.filter((it) => !(it?.kind === 'notice' && it?.id === id));
|
|
482
|
+
if (items.length === state.items.length) return false;
|
|
483
|
+
set({ items: replaceItems(items) });
|
|
484
|
+
return true;
|
|
485
|
+
};
|
|
486
|
+
// Sticky (non-TTL) input-hint-line progress state, for long-running
|
|
487
|
+
// installs (e.g. voice runtime download) that would otherwise spam the
|
|
488
|
+
// 3s toast queue. Distinct from pushToast/pushNotice: it persists across
|
|
489
|
+
// renders until explicitly cleared (setProgressHint('', ...) or a falsy
|
|
490
|
+
// text), and App.jsx's inputHint falls back to it only when no promptHint
|
|
491
|
+
// and no live toast currently cover the same line.
|
|
492
|
+
const setProgressHint = (text, tone = 'info') => {
|
|
493
|
+
const value = String(text ?? '').trim();
|
|
494
|
+
set({ progressHint: value ? { text: value, tone } : null });
|
|
495
|
+
};
|
|
496
|
+
const {
|
|
497
|
+
presentNextToolApproval,
|
|
498
|
+
finishToolApproval,
|
|
499
|
+
denyAllToolApprovals,
|
|
500
|
+
requestToolApproval,
|
|
501
|
+
} = createToolApproval({
|
|
502
|
+
getState: () => state,
|
|
503
|
+
set,
|
|
504
|
+
nextId,
|
|
505
|
+
getDisposed: () => disposed,
|
|
506
|
+
timeoutMs: TOOL_APPROVAL_TIMEOUT_MS,
|
|
507
|
+
});
|
|
1275
508
|
const patchItem = (id, patch) => {
|
|
1276
509
|
let index = itemIndexById.get(id);
|
|
1277
510
|
if (!Number.isInteger(index) || state.items[index]?.id !== id) {
|
|
@@ -1319,396 +552,51 @@ export async function createEngineSession({
|
|
|
1319
552
|
let autoClearRunning = false;
|
|
1320
553
|
const pendingNotificationKeys = new Set();
|
|
1321
554
|
const displayedExecutionNotificationKeys = new Set();
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
}
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
const parsed = parseAgentJob(text);
|
|
1352
|
-
const current = state.items.find((it) => it.id === itemId);
|
|
1353
|
-
const rawDisplayText = agentJobResultText(text, parsed) || String(text ?? '').trim();
|
|
1354
|
-
const displayText = isError ? toolErrorDisplay(rawDisplayText, 'agent') : rawDisplayText;
|
|
1355
|
-
patchItem(itemId, {
|
|
1356
|
-
result: displayText,
|
|
1357
|
-
text: displayText,
|
|
1358
|
-
isError,
|
|
1359
|
-
errorCount: isError ? 1 : 0,
|
|
1360
|
-
...(parsed ? { args: agentArgsWithResultMetadata(current?.args, parsed) } : {}),
|
|
1361
|
-
});
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
if (typeof runtime.onNotification === 'function') {
|
|
1365
|
-
unsubscribeRuntimeNotifications = runtime.onNotification((event) => {
|
|
555
|
+
const {
|
|
556
|
+
kickExecutionPendingResume,
|
|
557
|
+
flushDeferredExecutionPendingResumeKick,
|
|
558
|
+
scheduleExecutionPendingResumeKick,
|
|
559
|
+
updateAgentJobCard,
|
|
560
|
+
subscribeRuntimeNotifications,
|
|
561
|
+
} = createAgentJobFeed({
|
|
562
|
+
runtime,
|
|
563
|
+
getState: () => state,
|
|
564
|
+
set,
|
|
565
|
+
nextId,
|
|
566
|
+
getDisposed: () => disposed,
|
|
567
|
+
patchItem,
|
|
568
|
+
enqueue: (...args) => enqueue(...args),
|
|
569
|
+
drain: (...args) => drain(...args),
|
|
570
|
+
pushUserOrSyntheticItem,
|
|
571
|
+
makeQueueEntry: (...args) => makeQueueEntry(...args),
|
|
572
|
+
getPending: () => pending,
|
|
573
|
+
agentStatusState,
|
|
574
|
+
displayedExecutionNotificationKeys,
|
|
575
|
+
});
|
|
576
|
+
unsubscribeRuntimeNotifications = subscribeRuntimeNotifications();
|
|
577
|
+
|
|
578
|
+
// Remote seat superseded by another session: runtime already stopped its
|
|
579
|
+
// worker; sync the indicator and tell the user. Non-user-initiated, so a
|
|
580
|
+
// toast (not transcript) is right.
|
|
581
|
+
let unsubscribeRemoteState = null;
|
|
582
|
+
if (typeof runtime.onRemoteStateChange === 'function') {
|
|
583
|
+
unsubscribeRemoteState = runtime.onRemoteStateChange(({ enabled, reason }) => {
|
|
1366
584
|
if (disposed) return;
|
|
1367
|
-
|
|
1368
|
-
if (
|
|
1369
|
-
|
|
1370
|
-
const notificationKey = notificationQueueKey(event, text, parsed);
|
|
1371
|
-
const delivery = resolveTuiRuntimeNotificationDelivery(event, text);
|
|
1372
|
-
if (delivery.action === 'ignore') return;
|
|
1373
|
-
if (delivery.action === 'status-only') {
|
|
1374
|
-
if (parsed?.taskId) set(agentStatusState({ force: true }));
|
|
1375
|
-
return true;
|
|
1376
|
-
}
|
|
1377
|
-
if (delivery.action === 'execution-ui') {
|
|
1378
|
-
const firstDelivery = !notificationKey || !displayedExecutionNotificationKeys.has(notificationKey);
|
|
1379
|
-
if (firstDelivery) {
|
|
1380
|
-
if (notificationKey) displayedExecutionNotificationKeys.add(notificationKey);
|
|
1381
|
-
pushUserOrSyntheticItem(delivery.displayText, nextId());
|
|
1382
|
-
}
|
|
1383
|
-
if (parsed?.taskId) set(agentStatusState({ force: true }));
|
|
1384
|
-
if (String(delivery.modelContent || '').trim()) {
|
|
1385
|
-
scheduleExecutionPendingResumeKick();
|
|
1386
|
-
}
|
|
1387
|
-
return true;
|
|
1388
|
-
}
|
|
1389
|
-
if (parsed?.taskId) {
|
|
1390
|
-
set(agentStatusState({ force: true }));
|
|
585
|
+
set({ remoteEnabled: enabled === true });
|
|
586
|
+
if (reason === 'superseded') {
|
|
587
|
+
pushNotice('Remote mode OFF — another session took over remote.', 'warn');
|
|
1391
588
|
}
|
|
1392
|
-
const modelContent = String(delivery.modelContent ?? delivery.displayText ?? text).trim();
|
|
1393
|
-
if (!modelContent) return true;
|
|
1394
|
-
enqueue(modelContent, {
|
|
1395
|
-
mode: 'task-notification',
|
|
1396
|
-
priority: 'next',
|
|
1397
|
-
key: notificationKey || undefined,
|
|
1398
|
-
displayText: delivery.displayText || text,
|
|
1399
|
-
});
|
|
1400
|
-
return true;
|
|
1401
589
|
});
|
|
1402
590
|
}
|
|
1403
591
|
|
|
1404
|
-
const
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
if (/^(cancelled|canceled|cancel)$/.test(raw)) return 'cancelled';
|
|
1413
|
-
return '';
|
|
1414
|
-
}
|
|
1415
|
-
|
|
1416
|
-
function resultTextTerminalStatus(text) {
|
|
1417
|
-
const body = String(text || '');
|
|
1418
|
-
const tagged = body.match(/<status[^>]*>([\s\S]*?)<\/status>/i)?.[1]?.trim();
|
|
1419
|
-
if (tagged) return normalizedResultStatusToken(tagged);
|
|
1420
|
-
const bracketed = body.match(/^\[status:\s*([^\]]*)\]/mi)?.[1]?.trim();
|
|
1421
|
-
if (bracketed) return normalizedResultStatusToken(bracketed);
|
|
1422
|
-
const inline = body.match(/^(?:status|state):\s*([^\s·,;]+)/mi)?.[1]?.trim();
|
|
1423
|
-
return normalizedResultStatusToken(inline);
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
|
-
function itemHasKnownTerminalStatus(item, texts = []) {
|
|
1427
|
-
const settled = (token) => token === 'completed' || token === 'failed' || token === 'cancelled';
|
|
1428
|
-
if (settled(normalizedResultStatusToken(item?.args?.status))) return true;
|
|
1429
|
-
for (const text of texts) {
|
|
1430
|
-
if (settled(resultTextTerminalStatus(text))) return true;
|
|
1431
|
-
}
|
|
1432
|
-
return false;
|
|
1433
|
-
}
|
|
1434
|
-
|
|
1435
|
-
function withCancelledResultMarker(text, item) {
|
|
1436
|
-
const body = String(text || '');
|
|
1437
|
-
// Do NOT inspect item.rawResult here: aggregate rawResult is child tool
|
|
1438
|
-
// output (`1. grep\n<result>…`) that can incidentally contain a `status:`
|
|
1439
|
-
// line, which would false-positive as an already-terminal status and skip
|
|
1440
|
-
// the cancelled marker. Only result/text/body are engine-controlled
|
|
1441
|
-
// collapsed detail (empty / status word / an existing marker), so they are
|
|
1442
|
-
// the trustworthy terminal-status sources.
|
|
1443
|
-
const sources = [item?.result, item?.text, body];
|
|
1444
|
-
if (itemHasKnownTerminalStatus(item, sources)) return body;
|
|
1445
|
-
if (!body.trim()) return `${CANCELLED_RESULT_STATUS_LINE}\n`;
|
|
1446
|
-
return `${CANCELLED_RESULT_STATUS_LINE}\n${body}`;
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
function groupedToolResultText(group) {
|
|
1450
|
-
const completed = Math.min(group.count, group.completed);
|
|
1451
|
-
if (group.count <= 1) return group.results.at(-1)?.text ?? '';
|
|
1452
|
-
if (group.errors > 0) {
|
|
1453
|
-
const succeeded = Math.max(0, completed - group.errors);
|
|
1454
|
-
const reasons = group.results
|
|
1455
|
-
.filter((result) => result?.isError)
|
|
1456
|
-
.map((result) => firstErrorLine(result?.text))
|
|
1457
|
-
.filter(Boolean);
|
|
1458
|
-
const uniqueReasons = [...new Set(reasons)].slice(0, 2);
|
|
1459
|
-
const base = succeeded > 0
|
|
1460
|
-
? `${succeeded} Ok · ${group.errors} Failed`
|
|
1461
|
-
: `${group.errors} Failed`;
|
|
1462
|
-
return [
|
|
1463
|
-
`${base}${uniqueReasons[0] ? ` · ${uniqueReasons[0]}` : ''}`,
|
|
1464
|
-
...uniqueReasons.slice(1),
|
|
1465
|
-
].join('\n');
|
|
1466
|
-
}
|
|
1467
|
-
for (const result of group.results || []) {
|
|
1468
|
-
const line = String(result?.text || '').trim();
|
|
1469
|
-
if (line) return result.text;
|
|
1470
|
-
}
|
|
1471
|
-
return '';
|
|
1472
|
-
}
|
|
1473
|
-
|
|
1474
|
-
function firstErrorLine(text) {
|
|
1475
|
-
const clean = toolErrorDisplay(text, 'tool');
|
|
1476
|
-
if (clean) return clean;
|
|
1477
|
-
for (const line of String(text || '').split('\n')) {
|
|
1478
|
-
const trimmed = line.trim();
|
|
1479
|
-
if (!trimmed) continue;
|
|
1480
|
-
if (/^(Error|\[?error|FAIL\b)/i.test(trimmed)) return trimmed;
|
|
1481
|
-
}
|
|
1482
|
-
return String(text || '').split('\n').map((line) => line.trim()).find(Boolean) || '';
|
|
1483
|
-
}
|
|
1484
|
-
|
|
1485
|
-
function aggregateRawResult(calls) {
|
|
1486
|
-
const chunks = [];
|
|
1487
|
-
for (const rec of calls || []) {
|
|
1488
|
-
if (rec?.resolved !== true) continue;
|
|
1489
|
-
let text = String(rec?.resultText || '').replace(/\s+$/, '');
|
|
1490
|
-
if (!text.trim()) continue;
|
|
1491
|
-
const label = String(rec?.name || rec?.category || 'tool').trim() || 'tool';
|
|
1492
|
-
chunks.push(`${chunks.length + 1}. ${label}\n${text}`);
|
|
1493
|
-
}
|
|
1494
|
-
return chunks.join('\n\n');
|
|
1495
|
-
}
|
|
1496
|
-
|
|
1497
|
-
function aggregateBucketForCategory(category) {
|
|
1498
|
-
// Merge consecutive tool calls of the SAME category into one aggregate card;
|
|
1499
|
-
// a different category opens a fresh card (no cross-category merge). The
|
|
1500
|
-
// bucket key is the category itself, so a run of Search calls collapses into
|
|
1501
|
-
// one Search card while an adjacent Read/Patch stays separate. Falls back to
|
|
1502
|
-
// 'default' when a call has no resolved category. Hook/approval denials keep
|
|
1503
|
-
// their dedicated ToolHookDenialCard path in App.jsx.
|
|
1504
|
-
const key = String(category || '').trim();
|
|
1505
|
-
return key ? `category:${key}` : 'default';
|
|
1506
|
-
}
|
|
1507
|
-
|
|
1508
|
-
function aggregateSummaries(aggregate) {
|
|
1509
|
-
return [...(aggregate?.calls?.values?.() || [])]
|
|
1510
|
-
.filter((r) => r.summary)
|
|
1511
|
-
.sort((a, b) => Number(a.summarySeq ?? 0) - Number(b.summarySeq ?? 0))
|
|
1512
|
-
.map((r) => r.summary);
|
|
1513
|
-
}
|
|
1514
|
-
|
|
1515
|
-
function assignAggregateSummaryOrder(aggregate, callRec) {
|
|
1516
|
-
if (!aggregate || !callRec?.summary || callRec.summarySeq != null) return;
|
|
1517
|
-
const next = Math.max(0, Number(aggregate.nextSummarySeq || 0));
|
|
1518
|
-
callRec.summarySeq = next;
|
|
1519
|
-
aggregate.nextSummarySeq = next + 1;
|
|
1520
|
-
}
|
|
1521
|
-
|
|
1522
|
-
function patchToolCardResult(card, message, toolGroups, done) {
|
|
1523
|
-
if (!card || card.done) return false;
|
|
1524
|
-
const callId = toolResultCallId(message) || card.callId;
|
|
1525
|
-
if (callId && done.has(callId)) return false;
|
|
1526
|
-
// A result for this card arrived (possibly before its deferred push delay
|
|
1527
|
-
// elapsed) — surface the card now so the patch below has a live item and the
|
|
1528
|
-
// fast tool paints a completed card directly, no pending placeholder stage.
|
|
1529
|
-
// ensureVisible flushes this card AND every earlier-created still-deferred
|
|
1530
|
-
// card in order, so transcript order always matches call order.
|
|
1531
|
-
(card.aggregate?.ensureVisible || card.ensureVisible)?.();
|
|
1532
|
-
const rawText = toolResultText(message?.content);
|
|
1533
|
-
const isError = message?.isError === true || message?.toolKind === 'error' || /^\s*\[?error/i.test(rawText) || isErrorToolStatus(toolResultStatus(rawText));
|
|
1534
|
-
const text = isError ? toolErrorDisplay(rawText, card?.name || 'tool') : rawText;
|
|
1535
|
-
|
|
1536
|
-
// Aggregate card handling — collect semantic summaries per call
|
|
1537
|
-
const aggregate = card.aggregate;
|
|
1538
|
-
if (aggregate && card.itemId === aggregate.itemId) {
|
|
1539
|
-
const callRec = callId ? aggregate.calls.get(callId) : null;
|
|
1540
|
-
if (!callRec) return false;
|
|
1541
|
-
if (callRec.resolved) {
|
|
1542
|
-
card.done = true;
|
|
1543
|
-
if (callId) done.add(callId);
|
|
1544
|
-
return false;
|
|
1545
|
-
}
|
|
1546
|
-
callRec.summary = !isError ? summarizeToolResult(callRec.name, callRec.args, rawText, isError) : null;
|
|
1547
|
-
assignAggregateSummaryOrder(aggregate, callRec);
|
|
1548
|
-
callRec.isError = isError;
|
|
1549
|
-
callRec.resultText = text;
|
|
1550
|
-
callRec.resolved = true;
|
|
1551
|
-
const allCalls = [...aggregate.calls.values()];
|
|
1552
|
-
const completed = allCalls.filter((r) => r.resolved).length;
|
|
1553
|
-
const errors = allCalls.filter((r) => r.isError).length;
|
|
1554
|
-
// Collapsed detail is status-only (no per-result summary). Failures keep a
|
|
1555
|
-
// bare 'N Failed' status so an error stays visible while collapsed.
|
|
1556
|
-
const succeeded = completed - errors;
|
|
1557
|
-
const detailText = errors > 0
|
|
1558
|
-
? (succeeded > 0 ? `${succeeded} Ok · ${errors} Failed` : `${errors} Failed`)
|
|
1559
|
-
: '';
|
|
1560
|
-
const currentItem = state.items.find((it) => it.id === card.itemId);
|
|
1561
|
-
const earlyCompleted = allCalls.filter((r) => r.resolved || r.completedEarly).length;
|
|
1562
|
-
const visualCompleted = Math.max(completed, earlyCompleted, Math.min(allCalls.length, Number(currentItem?.completedCount || 0)));
|
|
1563
|
-
const rawResult = aggregateRawResult(allCalls);
|
|
1564
|
-
// Collapsed aggregate detail carries no per-result summary — the card body
|
|
1565
|
-
// shows only a status word ('Finished') or, on failure, 'N Failed'. The
|
|
1566
|
-
// numbered+labelled raw (rawResult) is preserved for ctrl+o expansion.
|
|
1567
|
-
const displayDetail = detailText;
|
|
1568
|
-
patchItem(card.itemId, {
|
|
1569
|
-
result: displayDetail,
|
|
1570
|
-
text: displayDetail,
|
|
1571
|
-
rawResult: rawResult || null,
|
|
1572
|
-
isError: errors > 0,
|
|
1573
|
-
errorCount: errors,
|
|
1574
|
-
count: allCalls.length,
|
|
1575
|
-
completedCount: visualCompleted,
|
|
1576
|
-
completedAt: Number(currentItem?.completedAt) || Date.now(),
|
|
1577
|
-
});
|
|
1578
|
-
card.done = true;
|
|
1579
|
-
if (callId) done.add(callId);
|
|
1580
|
-
return true;
|
|
1581
|
-
}
|
|
1582
|
-
|
|
1583
|
-
// Non-aggregate (legacy agent-job cards, etc.)
|
|
1584
|
-
const group = toolGroups.get(card.itemId) || { count: 1, completed: 0, errors: 0, results: [] };
|
|
1585
|
-
group.completed = Math.min(group.count, group.completed + 1);
|
|
1586
|
-
group.errors += isError ? 1 : 0;
|
|
1587
|
-
group.results.push({ text, isError });
|
|
1588
|
-
toolGroups.set(card.itemId, group);
|
|
1589
|
-
const resultText = groupedToolResultText(group);
|
|
1590
|
-
const displayResult = toolGroupedDisplayFallback(resultText, text, rawText);
|
|
1591
|
-
const patch = {
|
|
1592
|
-
result: displayResult,
|
|
1593
|
-
text: displayResult,
|
|
1594
|
-
isError: group.errors > 0,
|
|
1595
|
-
errorCount: group.errors,
|
|
1596
|
-
count: group.count,
|
|
1597
|
-
completedCount: group.completed,
|
|
1598
|
-
completedAt: Date.now(),
|
|
1599
|
-
};
|
|
1600
|
-
if (group.count <= 1) {
|
|
1601
|
-
const body = String(text || rawText || '').trim();
|
|
1602
|
-
if (body) patch.rawResult = text || rawText;
|
|
1603
|
-
const parsedAgent = parseAgentJob(rawText);
|
|
1604
|
-
if (parsedAgent) {
|
|
1605
|
-
patch.args = agentArgsWithResultMetadata(state.items.find((it) => it.id === card.itemId)?.args, parsedAgent);
|
|
1606
|
-
set(agentStatusState({ force: true }));
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
patchItem(card.itemId, patch);
|
|
1610
|
-
if (group.count <= 1) updateAgentJobCard(card.itemId, rawText, isError);
|
|
1611
|
-
card.done = true;
|
|
1612
|
-
if (callId) done.add(callId);
|
|
1613
|
-
return true;
|
|
1614
|
-
}
|
|
1615
|
-
|
|
1616
|
-
const flushToolResults = (messages, toolCards, cardByCallId, toolGroups, done, { finalize = false, cancelled = false } = {}) => {
|
|
1617
|
-
const results = [];
|
|
1618
|
-
for (const m of messages || []) {
|
|
1619
|
-
if (!m || m.role !== 'tool') continue;
|
|
1620
|
-
const callId = toolResultCallId(m);
|
|
1621
|
-
results.push({ message: m, callId, used: false });
|
|
1622
|
-
if (!callId || done.has(callId)) continue;
|
|
1623
|
-
const card = cardByCallId.get(callId);
|
|
1624
|
-
if (patchToolCardResult(card, m, toolGroups, done)) {
|
|
1625
|
-
results[results.length - 1].used = true;
|
|
1626
|
-
}
|
|
1627
|
-
}
|
|
1628
|
-
|
|
1629
|
-
const openCards = (toolCards || []).filter((card) => !card.done);
|
|
1630
|
-
if (openCards.length === 0) return;
|
|
1631
|
-
|
|
1632
|
-
const unusedResults = results.filter((result) => !result.used);
|
|
1633
|
-
const fallbackResults = unusedResults.slice(-openCards.length);
|
|
1634
|
-
for (let i = 0; i < fallbackResults.length; i++) {
|
|
1635
|
-
const card = openCards[i];
|
|
1636
|
-
const result = fallbackResults[i];
|
|
1637
|
-
if (!card || !result || card.done) continue;
|
|
1638
|
-
if (patchToolCardResult(card, result.message, toolGroups, done)) {
|
|
1639
|
-
if (result.callId) done.add(result.callId);
|
|
1640
|
-
result.used = true;
|
|
1641
|
-
}
|
|
1642
|
-
}
|
|
1643
|
-
|
|
1644
|
-
if (!finalize) return;
|
|
1645
|
-
for (const card of toolCards || []) {
|
|
1646
|
-
if (card.done) continue;
|
|
1647
|
-
// Finalize must surface any still-deferred card before patching its result
|
|
1648
|
-
// so the completed/cancelled card is never silently dropped.
|
|
1649
|
-
(card.aggregate?.ensureVisible || card.ensureVisible)?.();
|
|
1650
|
-
// Aggregate finalize — mark any remaining calls as done
|
|
1651
|
-
const aggregate = card.aggregate;
|
|
1652
|
-
if (aggregate && card.itemId === aggregate.itemId) {
|
|
1653
|
-
const allCalls = [...aggregate.calls.values()];
|
|
1654
|
-
// Never let a call that truly never resolved be presented as a real
|
|
1655
|
-
// completion. Stamp it resolved with an empty, non-error result so
|
|
1656
|
-
// completedCount reflects an honest (if degenerate) accounting instead
|
|
1657
|
-
// of manufacturing success out of a call that never came back.
|
|
1658
|
-
for (const rec of allCalls) {
|
|
1659
|
-
if (rec.resolved) continue;
|
|
1660
|
-
rec.resolved = true;
|
|
1661
|
-
rec.isError = false;
|
|
1662
|
-
rec.resultText = rec.resultText || '';
|
|
1663
|
-
}
|
|
1664
|
-
const completed = allCalls.filter((r) => r.resolved).length;
|
|
1665
|
-
const totalCompleted = completed;
|
|
1666
|
-
const errors = allCalls.filter((r) => r.isError).length;
|
|
1667
|
-
const succeeded = completed - errors;
|
|
1668
|
-
const rawResult = aggregateRawResult(allCalls);
|
|
1669
|
-
// Collapsed detail is status-only: no per-result summary. Failures keep a
|
|
1670
|
-
// bare 'N Failed' status. The numbered+labelled raw is kept for ctrl+o.
|
|
1671
|
-
let displayDetail = errors > 0
|
|
1672
|
-
? (succeeded > 0 ? `${succeeded} Ok · ${errors} Failed` : `${errors} Failed`)
|
|
1673
|
-
: '';
|
|
1674
|
-
if (cancelled) {
|
|
1675
|
-
// Cancelled aggregates MUST keep the [status: cancelled] marker on the
|
|
1676
|
-
// result so terminalStatus parsing resolves to 'cancelled'. Only normal
|
|
1677
|
-
// completions drop the summary; cancelled ones prepend the marker.
|
|
1678
|
-
const currentItem = state.items.find((it) => it.id === card.itemId);
|
|
1679
|
-
displayDetail = withCancelledResultMarker(displayDetail, currentItem);
|
|
1680
|
-
}
|
|
1681
|
-
patchItem(card.itemId, {
|
|
1682
|
-
result: displayDetail,
|
|
1683
|
-
text: displayDetail,
|
|
1684
|
-
rawResult: rawResult || null,
|
|
1685
|
-
isError: errors > 0,
|
|
1686
|
-
errorCount: errors,
|
|
1687
|
-
count: allCalls.length,
|
|
1688
|
-
completedCount: totalCompleted,
|
|
1689
|
-
completedAt: Date.now(),
|
|
1690
|
-
});
|
|
1691
|
-
for (const sibling of toolCards || []) {
|
|
1692
|
-
if (sibling.itemId !== card.itemId) continue;
|
|
1693
|
-
sibling.done = true;
|
|
1694
|
-
if (sibling.callId) done.add(sibling.callId);
|
|
1695
|
-
}
|
|
1696
|
-
continue;
|
|
1697
|
-
}
|
|
1698
|
-
// Non-aggregate finalize
|
|
1699
|
-
const group = toolGroups.get(card.itemId) || { count: 1, completed: 0, errors: 0, results: [] };
|
|
1700
|
-
group.completed = Math.min(group.count, group.completed + 1);
|
|
1701
|
-
toolGroups.set(card.itemId, group);
|
|
1702
|
-
let resultText = groupedToolResultText(group);
|
|
1703
|
-
if (cancelled) {
|
|
1704
|
-
const currentItem = state.items.find((it) => it.id === card.itemId);
|
|
1705
|
-
resultText = withCancelledResultMarker(resultText, currentItem);
|
|
1706
|
-
}
|
|
1707
|
-
patchItem(card.itemId, { result: resultText, text: resultText, isError: group.errors > 0, errorCount: group.errors, count: group.count, completedCount: group.completed, completedAt: Date.now() });
|
|
1708
|
-
card.done = true;
|
|
1709
|
-
if (card.callId) done.add(card.callId);
|
|
1710
|
-
}
|
|
1711
|
-
};
|
|
592
|
+
const { patchToolCardResult, flushToolResults } = createToolCardResults({
|
|
593
|
+
getState: () => state,
|
|
594
|
+
set,
|
|
595
|
+
patchItem,
|
|
596
|
+
markToolCallDone,
|
|
597
|
+
updateAgentJobCard,
|
|
598
|
+
agentStatusState,
|
|
599
|
+
});
|
|
1712
600
|
|
|
1713
601
|
async function runTurn(userText, options = {}) {
|
|
1714
602
|
const turnIndex = state.stats.turns || 0;
|
|
@@ -1721,6 +609,7 @@ export async function createEngineSession({
|
|
|
1721
609
|
activePromptRestore = {
|
|
1722
610
|
text: String(displayText || '').trim(),
|
|
1723
611
|
pastedImages: options.pastedImages && typeof options.pastedImages === 'object' ? options.pastedImages : null,
|
|
612
|
+
pastedTexts: options.pastedTexts && typeof options.pastedTexts === 'object' ? options.pastedTexts : null,
|
|
1724
613
|
onCommitted: typeof options.onCommitted === 'function' ? options.onCommitted : null,
|
|
1725
614
|
restorable: options.restorable !== false,
|
|
1726
615
|
submittedIds,
|
|
@@ -1749,7 +638,7 @@ export async function createEngineSession({
|
|
|
1749
638
|
// cards (send() still in flight). Hold those by callId until the batch lands.
|
|
1750
639
|
const earlyResultBuffer = new Map();
|
|
1751
640
|
const aggregateCards = []; // active aggregate cards in the current consecutive tool block
|
|
1752
|
-
|
|
641
|
+
let tailAggregate = null; // most recently touched aggregate card; only the tail may absorb the next same-bucket call
|
|
1753
642
|
|
|
1754
643
|
// ── Deferred tool-card push (scroll/text sync) ────────────────────────────
|
|
1755
644
|
// A tool card used to enter the transcript the instant onToolCall fired,
|
|
@@ -1849,6 +738,7 @@ export async function createEngineSession({
|
|
|
1849
738
|
activePromptRestore.committed = true;
|
|
1850
739
|
activePromptRestore.requeueEntries = [];
|
|
1851
740
|
activePromptRestore.pastedImages = null;
|
|
741
|
+
activePromptRestore.pastedTexts = null;
|
|
1852
742
|
}
|
|
1853
743
|
};
|
|
1854
744
|
|
|
@@ -1884,11 +774,11 @@ export async function createEngineSession({
|
|
|
1884
774
|
const completed = allCalls.filter((r) => r.resolved).length;
|
|
1885
775
|
const succeeded = completed - errors;
|
|
1886
776
|
const rawResult = aggregateRawResult(allCalls);
|
|
1887
|
-
//
|
|
1888
|
-
//
|
|
777
|
+
// Merged count summary (see patchToolCardResult); failures keep
|
|
778
|
+
// 'N Ok · N Failed'. Raw preserved for ctrl+o expansion.
|
|
1889
779
|
const displayDetail = errors > 0
|
|
1890
780
|
? (succeeded > 0 ? `${succeeded} Ok · ${errors} Failed` : `${errors} Failed`)
|
|
1891
|
-
:
|
|
781
|
+
: formatAggregateDetail(aggregateSummaries(aggregate));
|
|
1892
782
|
patchItem(aggregate.itemId, {
|
|
1893
783
|
result: displayDetail,
|
|
1894
784
|
text: displayDetail,
|
|
@@ -1907,28 +797,25 @@ export async function createEngineSession({
|
|
|
1907
797
|
aggregateCards.length = 0;
|
|
1908
798
|
// Seal the block: same-bucket calls after this point must open a fresh
|
|
1909
799
|
// card, never continue one from before the seal (assistant text/turn
|
|
1910
|
-
// end boundary).
|
|
1911
|
-
|
|
1912
|
-
aggregateByBucket.clear();
|
|
800
|
+
// end boundary).
|
|
801
|
+
tailAggregate = null;
|
|
1913
802
|
};
|
|
1914
803
|
|
|
1915
804
|
const rememberActiveAggregate = (aggregate) => {
|
|
1916
805
|
if (!aggregate) return;
|
|
1917
806
|
if (!aggregateCards.includes(aggregate)) aggregateCards.push(aggregate);
|
|
1918
|
-
|
|
807
|
+
tailAggregate = aggregate;
|
|
1919
808
|
};
|
|
1920
809
|
|
|
1921
810
|
const ensureAggregateCard = (bucket) => {
|
|
1922
|
-
//
|
|
1923
|
-
//
|
|
1924
|
-
//
|
|
1925
|
-
//
|
|
1926
|
-
//
|
|
1927
|
-
//
|
|
1928
|
-
//
|
|
1929
|
-
|
|
1930
|
-
// cards that were pushed after it.
|
|
1931
|
-
const cached = aggregateByBucket.get(bucket);
|
|
811
|
+
// Only the TAIL aggregate (most recent card) may absorb the next call,
|
|
812
|
+
// and only when the bucket matches. Any different-bucket aggregate or
|
|
813
|
+
// standalone card in between breaks the run, so Search, Memory, Search
|
|
814
|
+
// renders as three cards in call order — a new call never merges into
|
|
815
|
+
// an earlier card above the current tail (which read as out-of-order
|
|
816
|
+
// count changes in the transcript). clearAggregateContinuation seals
|
|
817
|
+
// the block at assistant-text/turn boundaries.
|
|
818
|
+
const cached = tailAggregate && tailAggregate.bucket === bucket ? tailAggregate : null;
|
|
1932
819
|
if (cached) {
|
|
1933
820
|
rememberActiveAggregate(cached);
|
|
1934
821
|
return cached;
|
|
@@ -2046,6 +933,17 @@ export async function createEngineSession({
|
|
|
2046
933
|
let _pendingThinkFlush = false; // true when a thinking update is queued
|
|
2047
934
|
let _pendingThinkingLastEndedAt = 0;
|
|
2048
935
|
let compactingActive = false;
|
|
936
|
+
// Engine-local streaming scalars. Neither responseLength nor thinkingText is
|
|
937
|
+
// rendered per-token by any consumer: App reads state.thinking only as a
|
|
938
|
+
// boolean (App.jsx `!!(state.thinking || liveSpinner?.thinking)`) and the
|
|
939
|
+
// Spinner takes outputTokens, not responseLength. So we keep these growing
|
|
940
|
+
// values in engine-local vars and publish to the store only on a visible
|
|
941
|
+
// transition (thinking on↔off), a completed visible text line, tool/usage
|
|
942
|
+
// updates, or finalization — not on every 8ms streaming flush.
|
|
943
|
+
let _publishedThinkingActive = false; // last thinking boolean pushed to store
|
|
944
|
+
// responseLength is only consumed at finalize as an outputTokens fallback
|
|
945
|
+
// (Math.round(responseLength/4)); we refresh state.spinner.responseLength on
|
|
946
|
+
// visible-line flush and finalize so that fallback stays valid.
|
|
2049
947
|
|
|
2050
948
|
const flushStreamBatch = () => {
|
|
2051
949
|
if (_batchTimer !== null) {
|
|
@@ -2090,24 +988,51 @@ export async function createEngineSession({
|
|
|
2090
988
|
}
|
|
2091
989
|
}
|
|
2092
990
|
}
|
|
991
|
+
// Only touch the spinner when there is a real reason: a visible-line
|
|
992
|
+
// change (patch.items set above), a thinking→responding transition, or a
|
|
993
|
+
// pending thinking end timestamp. Refresh responseLength here so the
|
|
994
|
+
// finalize outputTokens fallback stays valid without a per-token push.
|
|
2093
995
|
const responseLengthVal = assistantText.length + thinkingText.length;
|
|
2094
|
-
|
|
996
|
+
const visibleLineChanged = patch.items !== undefined;
|
|
997
|
+
const thinkingTransition = _publishedThinkingActive === true; // was thinking, now responding
|
|
998
|
+
if (state.spinner && (visibleLineChanged || thinkingTransition || _pendingThinkingLastEndedAt)) {
|
|
2095
999
|
patch.spinner = { ...state.spinner, responseLength: responseLengthVal, thinking: false, thinkingLastEndedAt: _pendingThinkingLastEndedAt || state.spinner.thinkingLastEndedAt, mode: compactingActive ? 'compacting' : 'responding' };
|
|
1000
|
+
_publishedThinkingActive = false;
|
|
2096
1001
|
}
|
|
2097
1002
|
if (Object.keys(patch).length > 0) set(patch);
|
|
2098
1003
|
_pendingThinkingLastEndedAt = 0;
|
|
2099
1004
|
}
|
|
2100
1005
|
if (_pendingThinkFlush) {
|
|
2101
1006
|
_pendingThinkFlush = false;
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
1007
|
+
// App only consumes state.thinking as a boolean and the Spinner only
|
|
1008
|
+
// reads the thinking flag + timing anchors — none of them render the
|
|
1009
|
+
// growing thinkingText. So publish the thinking boolean only on the
|
|
1010
|
+
// OFF→ON transition (or when compacting toggles the flag), not on every
|
|
1011
|
+
// 8ms reasoning chunk. The full thinkingText stays engine-local and is
|
|
1012
|
+
// emitted at finalize via the normal spinner/thinking teardown.
|
|
1013
|
+
const nextThinkingActive = !compactingActive;
|
|
1014
|
+
// Skip the push when the published thinking boolean is unchanged: neither
|
|
1015
|
+
// the growing thinkingText nor responseLength is rendered per-token, and
|
|
1016
|
+
// the Spinner derives its live elapsed from the (already-published)
|
|
1017
|
+
// thinkingSegmentStartedAt anchor. Applies to both thinking and
|
|
1018
|
+
// compacting steady state.
|
|
1019
|
+
if (nextThinkingActive === _publishedThinkingActive) {
|
|
1020
|
+
// no-op: boolean unchanged
|
|
1021
|
+
} else {
|
|
1022
|
+
const responseLengthVal = assistantText.length + thinkingText.length;
|
|
1023
|
+
const thinkingElapsedMs = accumulatedThinkingMs + (thinkingSegmentStartedAt ? Math.max(0, Date.now() - thinkingSegmentStartedAt) : 0);
|
|
1024
|
+
// state.thinking stays a truthy sentinel while active; consumers read it
|
|
1025
|
+
// as a boolean. Keep the value stable (thinkingText) so a late consumer
|
|
1026
|
+
// still sees real text, but only push on transition.
|
|
1027
|
+
const patch = { thinking: compactingActive ? null : thinkingText };
|
|
1028
|
+
if (state.spinner) {
|
|
1029
|
+
patch.spinner = compactingActive
|
|
1030
|
+
? { ...state.spinner, responseLength: responseLengthVal, thinking: false, thinkingAccumulatedMs: accumulatedThinkingMs, thinkingElapsedMs, thinkingLastEndedAt: state.spinner.thinkingLastEndedAt || 0, mode: 'compacting' }
|
|
1031
|
+
: { ...state.spinner, responseLength: responseLengthVal, thinking: true, thinkingStartedAt, thinkingSegmentStartedAt, thinkingAccumulatedMs: accumulatedThinkingMs, thinkingElapsedMs, thinkingLastEndedAt: 0, mode: 'thinking' };
|
|
1032
|
+
}
|
|
1033
|
+
set(patch);
|
|
1034
|
+
_publishedThinkingActive = nextThinkingActive;
|
|
2109
1035
|
}
|
|
2110
|
-
set(patch);
|
|
2111
1036
|
}
|
|
2112
1037
|
};
|
|
2113
1038
|
|
|
@@ -2122,6 +1047,8 @@ export async function createEngineSession({
|
|
|
2122
1047
|
const markToolCardCompletedState = (callId, message) => {
|
|
2123
1048
|
const card = cardByCallId.get(callId);
|
|
2124
1049
|
if (!card) return;
|
|
1050
|
+
// Early completion also clears the active-summary entry.
|
|
1051
|
+
markToolCallDone(card.callId);
|
|
2125
1052
|
const aggregate = card.aggregate;
|
|
2126
1053
|
if (aggregate && card.itemId === aggregate.itemId) {
|
|
2127
1054
|
const callRec = aggregate.calls.get(callId);
|
|
@@ -2194,7 +1121,11 @@ export async function createEngineSession({
|
|
|
2194
1121
|
assistantText = '';
|
|
2195
1122
|
const value = String(text || '').trim();
|
|
2196
1123
|
if (value) {
|
|
2197
|
-
|
|
1124
|
+
// Any non-tool transcript item is a block boundary: seal the
|
|
1125
|
+
// aggregate continuation (not just finalize headers) so a later
|
|
1126
|
+
// same-category tool call opens a fresh card instead of reusing
|
|
1127
|
+
// one whose count would then change ABOVE this steered user item.
|
|
1128
|
+
clearAggregateContinuation();
|
|
2198
1129
|
pushUserOrSyntheticItem(value);
|
|
2199
1130
|
}
|
|
2200
1131
|
},
|
|
@@ -2208,6 +1139,7 @@ export async function createEngineSession({
|
|
|
2208
1139
|
if (thinkingText && state.thinking) {
|
|
2209
1140
|
const thinkingLastEndedAt = closeThinkingSegment();
|
|
2210
1141
|
set({ thinking: null, spinner: state.spinner ? { ...state.spinner, thinking: false, thinkingAccumulatedMs: accumulatedThinkingMs, thinkingLastEndedAt, mode: 'tool-use' } : state.spinner });
|
|
1142
|
+
_publishedThinkingActive = false;
|
|
2211
1143
|
} else if (state.spinner) {
|
|
2212
1144
|
set({ spinner: { ...state.spinner, mode: 'tool-use' } });
|
|
2213
1145
|
}
|
|
@@ -2230,9 +1162,20 @@ export async function createEngineSession({
|
|
|
2230
1162
|
// Category drives the aggregate bucket so only same-category calls
|
|
2231
1163
|
// merge into one card; classify first, then bucket by it.
|
|
2232
1164
|
const category = classifyToolCategory(name, args);
|
|
2233
|
-
|
|
1165
|
+
// Agent/bridge calls always get their own standalone card so
|
|
1166
|
+
// ToolExecution's isAgentTool/agentActionTitle path renders a
|
|
1167
|
+
// "Spawn <Agent> (model, tag)" header instead of being folded
|
|
1168
|
+
// into the "Called N agent(s)" category-aggregate card.
|
|
1169
|
+
const bucket = category === 'Agent' ? null : aggregateBucketForCategory(category);
|
|
2234
1170
|
const callId = toolCallId(c);
|
|
2235
1171
|
const callKey = callId || `__tool_${toolCards.length}_${i}`;
|
|
1172
|
+
// The old App scan counted multi-pattern calls via
|
|
1173
|
+
// aggregateToolCategoryEntry(...).count, not a flat 1. Derive the same
|
|
1174
|
+
// count here so the incremental Explore/Search summary matches.
|
|
1175
|
+
const activeCount = Number(aggregateToolCategoryEntry(name, args, category)?.count || 1);
|
|
1176
|
+
// Track Explore/Search calls as active for the incremental prompt-
|
|
1177
|
+
// line summary; cleared when their result lands or the turn ends.
|
|
1178
|
+
markToolCallActive(callKey, category, activeCount, Date.now());
|
|
2236
1179
|
|
|
2237
1180
|
if (!bucket) {
|
|
2238
1181
|
const itemId = nextId();
|
|
@@ -2264,6 +1207,10 @@ export async function createEngineSession({
|
|
|
2264
1207
|
cardByCallId.set(callId, card);
|
|
2265
1208
|
}
|
|
2266
1209
|
toolCards.push(card);
|
|
1210
|
+
// A standalone card (Agent) breaks the consecutive run too: a
|
|
1211
|
+
// later same-bucket call must open a fresh card BELOW it, not
|
|
1212
|
+
// merge into an aggregate above it.
|
|
1213
|
+
tailAggregate = null;
|
|
2267
1214
|
continue;
|
|
2268
1215
|
}
|
|
2269
1216
|
|
|
@@ -2321,6 +1268,11 @@ export async function createEngineSession({
|
|
|
2321
1268
|
},
|
|
2322
1269
|
onCompactEvent: (event) => {
|
|
2323
1270
|
flushStreamBatch();
|
|
1271
|
+
// Non-tool transcript item — same block-boundary rule as the
|
|
1272
|
+
// steered user item above: seal any live aggregate first so a
|
|
1273
|
+
// later same-category tool call doesn't reuse a card whose count
|
|
1274
|
+
// would then change above this statusdone item.
|
|
1275
|
+
clearAggregateContinuation();
|
|
2324
1276
|
pushItem({
|
|
2325
1277
|
kind: 'statusdone',
|
|
2326
1278
|
id: nextId(),
|
|
@@ -2335,6 +1287,7 @@ export async function createEngineSession({
|
|
|
2335
1287
|
compactingActive = true;
|
|
2336
1288
|
const thinkingLastEndedAt = closeThinkingSegment();
|
|
2337
1289
|
_pendingThinkFlush = false;
|
|
1290
|
+
_publishedThinkingActive = false; // compacting cleared the thinking flag
|
|
2338
1291
|
set({
|
|
2339
1292
|
thinking: null,
|
|
2340
1293
|
spinner: {
|
|
@@ -2362,7 +1315,11 @@ export async function createEngineSession({
|
|
|
2362
1315
|
if (!textChunk) return;
|
|
2363
1316
|
markPromptCommitted();
|
|
2364
1317
|
const thinkingLastEndedAt = closeThinkingSegment();
|
|
2365
|
-
|
|
1318
|
+
// Drop any queued think-flush too: it would otherwise re-publish
|
|
1319
|
+
// spinner.thinking:true from flushStreamBatch and resurrect the
|
|
1320
|
+
// indicator after we cleared it here.
|
|
1321
|
+
_pendingThinkFlush = false;
|
|
1322
|
+
if (state.thinking) { set({ thinking: null }); _publishedThinkingActive = false; } // collapse thinking panel immediately, no batch delay
|
|
2366
1323
|
assistantText += textChunk;
|
|
2367
1324
|
currentAssistantText += textChunk;
|
|
2368
1325
|
// Accumulate text and schedule a batched flush (≤1 render per
|
|
@@ -2391,7 +1348,8 @@ export async function createEngineSession({
|
|
|
2391
1348
|
if (currentAssistantText.trim()) return;
|
|
2392
1349
|
markPromptCommitted();
|
|
2393
1350
|
closeThinkingSegment();
|
|
2394
|
-
|
|
1351
|
+
_pendingThinkFlush = false; // see onTextDelta: prevent a stale think flush resurrecting the indicator
|
|
1352
|
+
if (state.thinking) { set({ thinking: null }); _publishedThinkingActive = false; }
|
|
2395
1353
|
assistantText += full;
|
|
2396
1354
|
currentAssistantText += full;
|
|
2397
1355
|
_pendingTextFlush = true;
|
|
@@ -2477,7 +1435,15 @@ export async function createEngineSession({
|
|
|
2477
1435
|
closeThinkingSegment();
|
|
2478
1436
|
const elapsedMs = Date.now() - startedAt;
|
|
2479
1437
|
const thinkingElapsedMs = thinkingStartedAt ? accumulatedThinkingMs : 0;
|
|
2480
|
-
|
|
1438
|
+
// responseLength is engine-local now (not pushed per-token), so compute the
|
|
1439
|
+
// fallback from the live accumulator instead of the possibly-stale
|
|
1440
|
+
// state.spinner.responseLength. Final-only / non-streaming turns never
|
|
1441
|
+
// accumulate `assistantText` (only currentAssistantText is set at the
|
|
1442
|
+
// finalize reconcile above), so take the larger of the two text sources so
|
|
1443
|
+
// a no-usage turn still estimates tokens from the final content.
|
|
1444
|
+
const finalAssistantLen = Math.max(assistantText.length, currentAssistantText.length);
|
|
1445
|
+
const finalResponseLength = finalAssistantLen + thinkingText.length;
|
|
1446
|
+
const finalOutputTokens = Math.max(0, Number(state.spinner?.outputTokens || 0), Math.round(finalResponseLength / 4));
|
|
2481
1447
|
const turnStatus = cancelled ? 'cancelled' : 'done';
|
|
2482
1448
|
const resultContent = askResult?.content != null ? String(askResult.content).trim() : '';
|
|
2483
1449
|
const assistantOutput = (currentAssistantText || assistantText || '').trim();
|
|
@@ -2510,6 +1476,8 @@ export async function createEngineSession({
|
|
|
2510
1476
|
});
|
|
2511
1477
|
flushDeferredExecutionPendingResumeKick();
|
|
2512
1478
|
}
|
|
1479
|
+
clearActiveToolSummary();
|
|
1480
|
+
_publishedThinkingActive = false; // turn teardown cleared state.thinking
|
|
2513
1481
|
return cancelled ? 'cancelled' : 'done';
|
|
2514
1482
|
}
|
|
2515
1483
|
|
|
@@ -2526,6 +1494,7 @@ export async function createEngineSession({
|
|
|
2526
1494
|
text: displayText,
|
|
2527
1495
|
content: text,
|
|
2528
1496
|
pastedImages: options.pastedImages && typeof options.pastedImages === 'object' ? options.pastedImages : null,
|
|
1497
|
+
pastedTexts: options.pastedTexts && typeof options.pastedTexts === 'object' ? options.pastedTexts : null,
|
|
2529
1498
|
onCommitted: typeof options.onCommitted === 'function' ? options.onCommitted : null,
|
|
2530
1499
|
mode,
|
|
2531
1500
|
priority,
|
|
@@ -2539,6 +1508,7 @@ export async function createEngineSession({
|
|
|
2539
1508
|
const ids = new Set(entries.map((entry) => entry.id));
|
|
2540
1509
|
const keys = entries.map((entry) => entry.key).filter(Boolean);
|
|
2541
1510
|
for (const key of keys) pendingNotificationKeys.delete(key);
|
|
1511
|
+
for (const key of keys) displayedExecutionNotificationKeys.delete(key);
|
|
2542
1512
|
const queued = state.queued.filter((q) => !ids.has(q.id));
|
|
2543
1513
|
if (queued.length !== state.queued.length) set({ queued });
|
|
2544
1514
|
}
|
|
@@ -2617,9 +1587,11 @@ export async function createEngineSession({
|
|
|
2617
1587
|
}
|
|
2618
1588
|
const nonEditable = batch.filter((entry) => !isQueuedEntryEditable(entry));
|
|
2619
1589
|
const batchPastedImages = mergePastedImages(batch);
|
|
1590
|
+
const batchPastedTexts = mergePastedTexts(batch);
|
|
2620
1591
|
const turnStatus = await runTurn(merged, {
|
|
2621
1592
|
displayText: batch.map((entry) => entry.text).filter((text) => String(text || '').trim()).join('\n'),
|
|
2622
1593
|
pastedImages: batchPastedImages,
|
|
1594
|
+
pastedTexts: batchPastedTexts,
|
|
2623
1595
|
onCommitted: () => callCommitCallbacks(batch),
|
|
2624
1596
|
submittedIds: [...ids],
|
|
2625
1597
|
restorable: nonEditable.length === 0,
|
|
@@ -2747,7 +1719,7 @@ export async function createEngineSession({
|
|
|
2747
1719
|
removeQueuedEntries(queued);
|
|
2748
1720
|
const queuedText = queued.map((item) => item.text).filter((text) => String(text || '').trim()).join('\n');
|
|
2749
1721
|
const combinedText = [queuedText, String(currentText || '')].filter((text) => text.trim()).join('\n');
|
|
2750
|
-
return { count: queued.length, text: combinedText, pastedImages: mergePastedImages(queued) };
|
|
1722
|
+
return { count: queued.length, text: combinedText, pastedImages: mergePastedImages(queued), pastedTexts: mergePastedTexts(queued) };
|
|
2751
1723
|
}
|
|
2752
1724
|
|
|
2753
1725
|
const resetStats = () => {
|
|
@@ -2762,6 +1734,8 @@ export async function createEngineSession({
|
|
|
2762
1734
|
state.spinner = null;
|
|
2763
1735
|
state.lastTurn = null;
|
|
2764
1736
|
state.busy = false;
|
|
1737
|
+
pendingNotificationKeys.clear();
|
|
1738
|
+
displayedExecutionNotificationKeys.clear();
|
|
2765
1739
|
};
|
|
2766
1740
|
const resetTuiForPendingSessionReset = () => {
|
|
2767
1741
|
pendingSessionReset = true;
|
|
@@ -2929,8 +1903,15 @@ export async function createEngineSession({
|
|
|
2929
1903
|
set({ commandBusy: true });
|
|
2930
1904
|
try {
|
|
2931
1905
|
const next = runtime.setCompactionSettings?.(input) || {};
|
|
2932
|
-
syncContextStats({ allowEstimated: true });
|
|
2933
1906
|
set({ ...routeState(), stats: { ...state.stats } });
|
|
1907
|
+
// Context-stats recompute (transcript scan + per-message JSON
|
|
1908
|
+
// stringify) is the secondary hitch source on this toggle; defer it
|
|
1909
|
+
// off the key-handler tick so Ink repaints the setting change first.
|
|
1910
|
+
// Stats become eventually consistent on the next tick/repaint.
|
|
1911
|
+
setTimeout(() => {
|
|
1912
|
+
syncContextStats({ allowEstimated: true });
|
|
1913
|
+
set({ stats: { ...state.stats } });
|
|
1914
|
+
}, 0);
|
|
2934
1915
|
return next;
|
|
2935
1916
|
} finally {
|
|
2936
1917
|
set({ commandBusy: false });
|
|
@@ -2944,8 +1925,14 @@ export async function createEngineSession({
|
|
|
2944
1925
|
set({ commandBusy: true });
|
|
2945
1926
|
try {
|
|
2946
1927
|
const next = await runtime.setMemoryEnabled?.(enabled);
|
|
2947
|
-
syncContextStats({ allowEstimated: true });
|
|
2948
1928
|
set({ ...routeState(), stats: { ...state.stats } });
|
|
1929
|
+
// Deferred for the same reason as setCompactionSettings above: keep
|
|
1930
|
+
// the recompute off the key-handler tick so the toggle repaints
|
|
1931
|
+
// immediately; stats catch up right after.
|
|
1932
|
+
setTimeout(() => {
|
|
1933
|
+
syncContextStats({ allowEstimated: true });
|
|
1934
|
+
set({ stats: { ...state.stats } });
|
|
1935
|
+
}, 0);
|
|
2949
1936
|
return next;
|
|
2950
1937
|
} finally {
|
|
2951
1938
|
set({ commandBusy: false });
|
|
@@ -3278,6 +2265,7 @@ export async function createEngineSession({
|
|
|
3278
2265
|
const canRestore = restoreState?.restorable && !hasPendingSteering;
|
|
3279
2266
|
const restoreText = canRestore ? restoreState.text : '';
|
|
3280
2267
|
const restorePastedImages = canRestore && restoreState?.pastedImages ? restoreState.pastedImages : null;
|
|
2268
|
+
const restorePastedTexts = canRestore && restoreState?.pastedTexts ? restoreState.pastedTexts : null;
|
|
3281
2269
|
// When steering suppresses the restore, the interrupted prompt's pasted
|
|
3282
2270
|
// images never get committed (onCommitted won't fire) nor re-installed into
|
|
3283
2271
|
// the draft, so hand them back for cleanup to avoid a stale `[Image #id]`
|
|
@@ -3285,6 +2273,9 @@ export async function createEngineSession({
|
|
|
3285
2273
|
const discardPastedImages = restoreState?.restorable && hasPendingSteering && restoreState?.pastedImages
|
|
3286
2274
|
? restoreState.pastedImages
|
|
3287
2275
|
: null;
|
|
2276
|
+
const discardPastedTexts = restoreState?.restorable && hasPendingSteering && restoreState?.pastedTexts
|
|
2277
|
+
? restoreState.pastedTexts
|
|
2278
|
+
: null;
|
|
3288
2279
|
const requeueEntries = restoreState && !restoreState.committed && Array.isArray(restoreState.requeueEntries)
|
|
3289
2280
|
? restoreState.requeueEntries.slice()
|
|
3290
2281
|
: [];
|
|
@@ -3306,7 +2297,7 @@ export async function createEngineSession({
|
|
|
3306
2297
|
restoreState.restorable = false;
|
|
3307
2298
|
restoreState.requeueEntries = [];
|
|
3308
2299
|
}
|
|
3309
|
-
return { aborted, restoreText, pastedImages: restorePastedImages, discardPastedImages };
|
|
2300
|
+
return { aborted, restoreText, pastedImages: restorePastedImages, discardPastedImages, pastedTexts: restorePastedTexts, discardPastedTexts };
|
|
3310
2301
|
},
|
|
3311
2302
|
resolveToolApproval: (id, decision = {}) => {
|
|
3312
2303
|
const approved = decision === true || decision?.approved === true;
|
|
@@ -3368,8 +2359,14 @@ export async function createEngineSession({
|
|
|
3368
2359
|
set({ commandBusy: true });
|
|
3369
2360
|
try {
|
|
3370
2361
|
const result = await runtime.setOutputStyle?.(styleId);
|
|
3371
|
-
|
|
2362
|
+
resetStats();
|
|
3372
2363
|
set({ ...routeState(), stats: { ...state.stats } });
|
|
2364
|
+
// Defer the context recompute (transcript scan) off this tick so
|
|
2365
|
+
// the style change repaints immediately; stats settle right after.
|
|
2366
|
+
setTimeout(() => {
|
|
2367
|
+
syncContextStats({ allowEstimated: true });
|
|
2368
|
+
set({ stats: { ...state.stats } });
|
|
2369
|
+
}, 0);
|
|
3373
2370
|
return result;
|
|
3374
2371
|
} finally {
|
|
3375
2372
|
set({ commandBusy: false });
|
|
@@ -3397,6 +2394,16 @@ export async function createEngineSession({
|
|
|
3397
2394
|
set({ remoteEnabled: next });
|
|
3398
2395
|
return next;
|
|
3399
2396
|
},
|
|
2397
|
+
// Force-claim remote for this session (single-holder, last-wins). Always
|
|
2398
|
+
// turns remote ON here and steals the bridge seat; the previous holder is
|
|
2399
|
+
// superseded and flips itself OFF via onRemoteStateChange. Used by the
|
|
2400
|
+
// `/remote` slash command — repeated /remote just re-claims (idempotent).
|
|
2401
|
+
claimRemote: () => {
|
|
2402
|
+
runtime.startRemote?.();
|
|
2403
|
+
const next = runtime.isRemoteEnabled?.() === true;
|
|
2404
|
+
set({ remoteEnabled: next });
|
|
2405
|
+
return next;
|
|
2406
|
+
},
|
|
3400
2407
|
isRemoteEnabled: () => runtime.isRemoteEnabled?.() === true,
|
|
3401
2408
|
// Theme is a TUI-local concern (no runtime round-trip). listThemes returns
|
|
3402
2409
|
// picker metadata; getTheme reports the active id; setTheme applies the
|
|
@@ -3479,6 +2486,15 @@ export async function createEngineSession({
|
|
|
3479
2486
|
'info');
|
|
3480
2487
|
return true;
|
|
3481
2488
|
},
|
|
2489
|
+
loginOpenCodeGoUsage: async () => {
|
|
2490
|
+
if (state.commandBusy) throw new Error('command busy');
|
|
2491
|
+
set({ commandBusy: true });
|
|
2492
|
+
try {
|
|
2493
|
+
return await runtime.loginOpenCodeGoUsage();
|
|
2494
|
+
} finally {
|
|
2495
|
+
set({ commandBusy: false });
|
|
2496
|
+
}
|
|
2497
|
+
},
|
|
3482
2498
|
saveOpenAIUsageSessionKey: (secret) => {
|
|
3483
2499
|
runtime.saveOpenAIUsageSessionKey(secret);
|
|
3484
2500
|
pushNotice('OpenAI usage auth saved', 'info');
|
|
@@ -3603,6 +2619,8 @@ export async function createEngineSession({
|
|
|
3603
2619
|
}
|
|
3604
2620
|
},
|
|
3605
2621
|
pushNotice,
|
|
2622
|
+
removeNotice,
|
|
2623
|
+
setProgressHint,
|
|
3606
2624
|
clear: async () => {
|
|
3607
2625
|
if (state.commandBusy) return false;
|
|
3608
2626
|
set({ commandBusy: true });
|
|
@@ -3739,6 +2757,8 @@ export async function createEngineSession({
|
|
|
3739
2757
|
try { clearInterval(runtimePulseTimer); } catch {}
|
|
3740
2758
|
try { unsubscribeRuntimeNotifications?.(); } catch {}
|
|
3741
2759
|
unsubscribeRuntimeNotifications = null;
|
|
2760
|
+
try { unsubscribeRemoteState?.(); } catch {}
|
|
2761
|
+
unsubscribeRemoteState = null;
|
|
3742
2762
|
denyAllToolApprovals('runtime closing');
|
|
3743
2763
|
await runtime.close(reason, options);
|
|
3744
2764
|
listeners.clear();
|