mixdog 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -3
- package/scripts/_bench-cwc.json +20 -0
- package/scripts/agent-loop-policy-test.mjs +37 -0
- package/scripts/agent-parallel-smoke.mjs +54 -10
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
- package/scripts/internal-comms-bench.mjs +727 -0
- package/scripts/internal-comms-smoke.mjs +75 -0
- package/scripts/lead-workflow-smoke.mjs +4 -4
- package/scripts/live-worker-smoke.mjs +9 -9
- package/scripts/output-style-bench.mjs +285 -0
- package/scripts/output-style-smoke.mjs +13 -10
- package/scripts/patch-replay.mjs +90 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/session-ingest-smoke.mjs +2 -2
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +306 -66
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +4 -2
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +4 -2
- package/src/app.mjs +10 -6
- package/src/defaults/{hidden-roles.json → agents.json} +7 -7
- package/src/examples/schedules/SCHEDULE.example.md +32 -0
- package/src/examples/webhooks/WEBHOOK.example.md +40 -0
- package/src/headless-role.mjs +14 -14
- package/src/help.mjs +1 -0
- package/src/lib/mixdog-debug.cjs +0 -22
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +34 -56
- package/src/mixdog-session-runtime.mjs +710 -319
- package/src/output-styles/default.md +12 -7
- package/src/output-styles/minimal.md +25 -0
- package/src/output-styles/oneline.md +21 -0
- package/src/output-styles/simple.md +10 -9
- package/src/repl.mjs +12 -4
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +7 -8
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +7 -0
- package/src/rules/shared/01-tool.md +17 -12
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
- package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
- package/src/runtime/agent/orchestrator/config.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
- package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
- package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
- package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +359 -106
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +86 -30
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +265 -1
- package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
- package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
- package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
- package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +441 -1254
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +54 -3
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/session-discovery.mjs +0 -4
- package/src/runtime/channels/lib/telegram-format.mjs +283 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +187 -43
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
- package/src/runtime/memory/lib/memory.mjs +101 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
- package/src/runtime/memory/lib/session-ingest.mjs +116 -7
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +6 -3
- package/src/runtime/search/index.mjs +2 -7
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +0 -1
- package/src/runtime/shared/channel-notification-routing.mjs +12 -0
- package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
- package/src/runtime/shared/child-spawn-gate.mjs +0 -6
- package/src/runtime/shared/config.mjs +9 -0
- package/src/runtime/shared/llm/http-agent.mjs +12 -5
- package/src/runtime/shared/schedules-store.mjs +21 -19
- package/src/runtime/shared/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +129 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/standalone/agent-tool.mjs +255 -109
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +8 -291
- package/src/standalone/explore-tool.mjs +2 -2
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/provider-admin.mjs +11 -0
- package/src/standalone/seeds.mjs +1 -11
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2137 -750
- package/src/tui/components/ConfirmBar.jsx +47 -0
- package/src/tui/components/ContextPanel.jsx +5 -3
- package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
- package/src/tui/components/Markdown.jsx +22 -98
- package/src/tui/components/Message.jsx +14 -35
- package/src/tui/components/Picker.jsx +87 -12
- package/src/tui/components/PromptInput.jsx +146 -9
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +8 -5
- package/src/tui/components/Spinner.jsx +7 -7
- package/src/tui/components/StatusLine.jsx +40 -21
- package/src/tui/components/TextEntryPanel.jsx +51 -7
- package/src/tui/components/ToolExecution.jsx +177 -100
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +312 -40
- package/src/tui/components/tool-output-format.test.mjs +180 -1
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +7324 -2393
- package/src/tui/engine.mjs +287 -126
- package/src/tui/index.jsx +117 -7
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +453 -0
- package/src/tui/markdown/format-token.mjs +354 -142
- package/src/tui/markdown/format-token.test.mjs +155 -17
- package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
- package/src/tui/markdown/render-ansi.test.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +167 -0
- package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
- package/src/tui/markdown/table-layout.mjs +9 -9
- package/src/tui/paste-attachments.mjs +0 -11
- package/src/tui/prompt-history-store.mjs +129 -0
- package/src/tui/prompt-history-store.test.mjs +52 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
- package/src/tui/theme.mjs +41 -647
- package/src/tui/themes/base.mjs +86 -0
- package/src/tui/themes/basic.mjs +85 -0
- package/src/tui/themes/catppuccin.mjs +72 -0
- package/src/tui/themes/dracula.mjs +70 -0
- package/src/tui/themes/everforest.mjs +71 -0
- package/src/tui/themes/gruvbox.mjs +71 -0
- package/src/tui/themes/index.mjs +71 -0
- package/src/tui/themes/indigo.mjs +78 -0
- package/src/tui/themes/kanagawa.mjs +80 -0
- package/src/tui/themes/light.mjs +81 -0
- package/src/tui/themes/nord.mjs +72 -0
- package/src/tui/themes/onedark.mjs +16 -0
- package/src/tui/themes/rosepine.mjs +70 -0
- package/src/tui/themes/teal.mjs +81 -0
- package/src/tui/themes/tokyonight.mjs +79 -0
- package/src/tui/themes/utils.mjs +106 -0
- package/src/tui/themes/warm.mjs +79 -0
- package/src/tui/transcript-tool-failures.mjs +13 -2
- package/src/ui/markdown.mjs +1 -1
- package/src/ui/model-display.mjs +2 -2
- package/src/ui/statusline.mjs +26 -27
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
- package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
- package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
- package/src/workflows/default/WORKFLOW.md +39 -12
- package/src/workflows/sequential/WORKFLOW.md +46 -0
- package/src/workflows/solo/WORKFLOW.md +7 -0
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +154 -20
- package/vendor/ink/build/measure-text.js +4 -1
- package/vendor/ink/build/output.js +115 -9
- package/vendor/ink/build/render-node-to-output.js +4 -1
- package/vendor/ink/build/render.js +4 -0
- package/src/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
package/src/tui/engine.mjs
CHANGED
|
@@ -10,7 +10,6 @@ import { SPINNER_VERBS } from './spinner-verbs.mjs';
|
|
|
10
10
|
import {
|
|
11
11
|
aggregateToolCategoryEntry,
|
|
12
12
|
classifyToolCategory,
|
|
13
|
-
formatAggregateDetail,
|
|
14
13
|
summarizeToolResult,
|
|
15
14
|
} from '../runtime/shared/tool-surface.mjs';
|
|
16
15
|
import { isBackgroundErrorOnlyBody, presentErrorText } from '../runtime/shared/err-text.mjs';
|
|
@@ -19,6 +18,7 @@ import {
|
|
|
19
18
|
modelVisibleToolCompletionMessage,
|
|
20
19
|
} from '../runtime/shared/tool-execution-contract.mjs';
|
|
21
20
|
import { listThemes, getThemeSetting, setThemeSetting } from './theme.mjs';
|
|
21
|
+
import { resetAllStreamingMarkdownStablePrefixes } from './markdown/streaming-markdown.mjs';
|
|
22
22
|
|
|
23
23
|
const BOOT_PROFILE_ENABLED = /^(1|true|yes|on)$/i.test(String(process.env.MIXDOG_BOOT_PROFILE || ''));
|
|
24
24
|
const BOOT_PROFILE_START = globalThis.__mixdogBootProfileStart || (globalThis.__mixdogBootProfileStart = performance.now());
|
|
@@ -289,6 +289,10 @@ function toolResultText(content) {
|
|
|
289
289
|
|
|
290
290
|
const TOOL_RESULT_PART_MAX_DEPTH = 12;
|
|
291
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;
|
|
292
296
|
|
|
293
297
|
function compactToolResultObjectFallback(obj) {
|
|
294
298
|
if (obj?.type === 'tool_result') return '';
|
|
@@ -357,7 +361,7 @@ function toolAggregateDetailFallback(detailText, rawResult) {
|
|
|
357
361
|
if (!raw) return detailText;
|
|
358
362
|
const line = raw.split('\n').map((l) => l.trim()).find(Boolean) || '';
|
|
359
363
|
if (!line) return detailText;
|
|
360
|
-
return line.length >
|
|
364
|
+
return line.length > TOOL_DETAIL_LINE_MAX ? `${line.slice(0, TOOL_DETAIL_LINE_MAX - 3)}…` : line;
|
|
361
365
|
}
|
|
362
366
|
|
|
363
367
|
function toolGroupedDisplayFallback(resultText, text, rawText) {
|
|
@@ -456,7 +460,7 @@ function parseAgentResultEnvelope(text, fallback = {}) {
|
|
|
456
460
|
attrs[match[1].toLowerCase()] = String(match[2] || '').replace(/^["']|["']$/g, '');
|
|
457
461
|
}
|
|
458
462
|
const providerModel = /\s([a-zA-Z0-9_.-]+)\/([^\s]+)\s*$/i.exec(head);
|
|
459
|
-
const
|
|
463
|
+
const agent = attrs.agent || fallback.agent || '';
|
|
460
464
|
return {
|
|
461
465
|
name: 'agent',
|
|
462
466
|
label: String(fallback.status || attrs.status || 'completed').toLowerCase(),
|
|
@@ -465,8 +469,7 @@ function parseAgentResultEnvelope(text, fallback = {}) {
|
|
|
465
469
|
status: fallback.status || attrs.status || 'completed',
|
|
466
470
|
task_id: fallback.taskId || attrs.task_id || attrs.taskid || undefined,
|
|
467
471
|
tag: fallback.tag || attrs.tag || undefined,
|
|
468
|
-
agent:
|
|
469
|
-
role: role || undefined,
|
|
472
|
+
agent: agent || undefined,
|
|
470
473
|
provider: fallback.provider || attrs.provider || providerModel?.[1] || undefined,
|
|
471
474
|
model: fallback.model || attrs.model || providerModel?.[2] || undefined,
|
|
472
475
|
preset: fallback.preset || attrs.preset || undefined,
|
|
@@ -502,7 +505,6 @@ export function parseBackgroundTaskEnvelope(text) {
|
|
|
502
505
|
status,
|
|
503
506
|
taskId,
|
|
504
507
|
tag: fields.tag || fields.label || '',
|
|
505
|
-
role: fields.role || fields.agent || '',
|
|
506
508
|
agent: fields.agent || '',
|
|
507
509
|
provider: fields.provider || '',
|
|
508
510
|
model: fields.model || '',
|
|
@@ -524,8 +526,7 @@ export function parseBackgroundTaskEnvelope(text) {
|
|
|
524
526
|
operation: fields.operation || undefined,
|
|
525
527
|
label: fields.label || undefined,
|
|
526
528
|
tag: fields.tag || undefined,
|
|
527
|
-
agent: fields.agent ||
|
|
528
|
-
role: fields.role || fields.agent || undefined,
|
|
529
|
+
agent: fields.agent || undefined,
|
|
529
530
|
provider: fields.provider || undefined,
|
|
530
531
|
model: fields.model || undefined,
|
|
531
532
|
preset: fields.preset || undefined,
|
|
@@ -657,7 +658,16 @@ function parseToolArgs(args) {
|
|
|
657
658
|
return typeof args === 'object' ? args : {};
|
|
658
659
|
}
|
|
659
660
|
|
|
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
|
+
});
|
|
661
671
|
|
|
662
672
|
function parseAgentJob(text) {
|
|
663
673
|
const value = String(text || '');
|
|
@@ -666,7 +676,7 @@ function parseAgentJob(text) {
|
|
|
666
676
|
const statusMatch = /^status:\s*([^\s(]+)/m.exec(value);
|
|
667
677
|
const typeMatch = /^type:\s*(.+)$/m.exec(value);
|
|
668
678
|
const targetMatch = /^target:\s*(.+)$/m.exec(value);
|
|
669
|
-
const
|
|
679
|
+
const agentMatch = /^agent:\s*(.+)$/m.exec(value);
|
|
670
680
|
const presetMatch = /^preset:\s*(.+)$/m.exec(value);
|
|
671
681
|
const modelMatch = /^model:\s*([^/\s]+)\/(.+)$/m.exec(value);
|
|
672
682
|
const effortMatch = /^effort:\s*(.+)$/m.exec(value);
|
|
@@ -676,7 +686,7 @@ function parseAgentJob(text) {
|
|
|
676
686
|
status: (statusMatch?.[1] || '').toLowerCase(),
|
|
677
687
|
type: (typeMatch?.[1] || '').trim(),
|
|
678
688
|
target: (targetMatch?.[1] || '').trim(),
|
|
679
|
-
|
|
689
|
+
agent: (agentMatch?.[1] || '').trim(),
|
|
680
690
|
preset: (presetMatch?.[1] || '').trim(),
|
|
681
691
|
provider: (modelMatch?.[1] || '').trim(),
|
|
682
692
|
model: (modelMatch?.[2] || '').trim(),
|
|
@@ -837,8 +847,8 @@ function notificationQueueKey(event, text, parsed) {
|
|
|
837
847
|
: tag
|
|
838
848
|
? `tag:${tag}:${shortTextFingerprint(synthetic.result || text)}`
|
|
839
849
|
: '';
|
|
840
|
-
const
|
|
841
|
-
if (resultId ||
|
|
850
|
+
const agent = String(synthetic.args?.agent || '').trim();
|
|
851
|
+
if (resultId || agent) return ['agent-result', resultId, agent].filter(Boolean).join(':');
|
|
842
852
|
}
|
|
843
853
|
const id = String(meta.execution_id || parsed?.taskId || '').trim();
|
|
844
854
|
if (!id) return '';
|
|
@@ -897,7 +907,7 @@ function agentArgsWithResultMetadata(args, parsed) {
|
|
|
897
907
|
}
|
|
898
908
|
if (parsed.status) next.status = parsed.status;
|
|
899
909
|
if (parsed.taskId) next.task_id = parsed.taskId;
|
|
900
|
-
if (parsed.
|
|
910
|
+
if (parsed.agent) next.agent = parsed.agent;
|
|
901
911
|
if (parsed.preset) next.preset = parsed.preset;
|
|
902
912
|
if (parsed.provider) next.provider = parsed.provider;
|
|
903
913
|
if (parsed.model) next.model = parsed.model;
|
|
@@ -914,9 +924,10 @@ export async function createEngineSession({
|
|
|
914
924
|
provider: providerName,
|
|
915
925
|
model,
|
|
916
926
|
toolMode = 'full',
|
|
927
|
+
remote = false,
|
|
917
928
|
} = {}) {
|
|
918
929
|
const startedAt = performance.now();
|
|
919
|
-
bootProfile('engine:create:start', { provider: providerName, model, toolMode });
|
|
930
|
+
bootProfile('engine:create:start', { provider: providerName, model, toolMode, remote });
|
|
920
931
|
// Silence provider/session diagnostics so they cannot tear through the
|
|
921
932
|
// alternate-screen React/ink render.
|
|
922
933
|
process.env.MIXDOG_QUIET_PROVIDER_LOG = '1';
|
|
@@ -928,7 +939,7 @@ export async function createEngineSession({
|
|
|
928
939
|
const importStartedAt = performance.now();
|
|
929
940
|
const { createMixdogSessionRuntime } = await import(SESSION_RUNTIME_MODULE);
|
|
930
941
|
bootProfile('session-runtime:imported', { ms: (performance.now() - importStartedAt).toFixed(1) });
|
|
931
|
-
const runtime = await createMixdogSessionRuntime({ provider: providerName, model, toolMode });
|
|
942
|
+
const runtime = await createMixdogSessionRuntime({ provider: providerName, model, toolMode, remote });
|
|
932
943
|
bootProfile('engine:create:runtime-ready', { ms: (performance.now() - startedAt).toFixed(1) });
|
|
933
944
|
const cwd = runtime.cwd || process.cwd();
|
|
934
945
|
const stateStartedAt = performance.now();
|
|
@@ -965,6 +976,7 @@ export async function createEngineSession({
|
|
|
965
976
|
searchRoute: runtime.getSearchRoute?.() || runtime.searchRoute || null,
|
|
966
977
|
autoClear: autoClearState(),
|
|
967
978
|
workflow: runtime.workflow || null,
|
|
979
|
+
remoteEnabled: runtime.isRemoteEnabled?.() === true,
|
|
968
980
|
});
|
|
969
981
|
|
|
970
982
|
const routeState = () => ({
|
|
@@ -1121,7 +1133,12 @@ export async function createEngineSession({
|
|
|
1121
1133
|
}
|
|
1122
1134
|
return nextItems;
|
|
1123
1135
|
};
|
|
1136
|
+
let flushDeferredBeforeImmediatePush = null;
|
|
1137
|
+
let pushingFromDeferredEntry = false;
|
|
1124
1138
|
const pushItem = (item) => {
|
|
1139
|
+
if (!pushingFromDeferredEntry && flushDeferredBeforeImmediatePush) {
|
|
1140
|
+
flushDeferredBeforeImmediatePush();
|
|
1141
|
+
}
|
|
1125
1142
|
const index = state.items.length;
|
|
1126
1143
|
const items = [...state.items, item];
|
|
1127
1144
|
if (item?.id != null) itemIndexById.set(item.id, index);
|
|
@@ -1384,6 +1401,51 @@ export async function createEngineSession({
|
|
|
1384
1401
|
});
|
|
1385
1402
|
}
|
|
1386
1403
|
|
|
1404
|
+
const CANCELLED_RESULT_STATUS_LINE = '[status: cancelled]';
|
|
1405
|
+
|
|
1406
|
+
function normalizedResultStatusToken(value) {
|
|
1407
|
+
const raw = String(value || '').trim().toLowerCase();
|
|
1408
|
+
if (!raw) return '';
|
|
1409
|
+
if (/^(running|pending|queued|in_progress|in-progress)$/.test(raw)) return 'running';
|
|
1410
|
+
if (/^(completed|complete|done|success|succeeded|ok)$/.test(raw)) return 'completed';
|
|
1411
|
+
if (/^(failed|fail|error|errored|timeout|timed_out|killed)$/.test(raw)) return 'failed';
|
|
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
|
+
|
|
1387
1449
|
function groupedToolResultText(group) {
|
|
1388
1450
|
const completed = Math.min(group.count, group.completed);
|
|
1389
1451
|
if (group.count <= 1) return group.results.at(-1)?.text ?? '';
|
|
@@ -1424,7 +1486,7 @@ export async function createEngineSession({
|
|
|
1424
1486
|
const chunks = [];
|
|
1425
1487
|
for (const rec of calls || []) {
|
|
1426
1488
|
if (rec?.resolved !== true) continue;
|
|
1427
|
-
|
|
1489
|
+
let text = String(rec?.resultText || '').replace(/\s+$/, '');
|
|
1428
1490
|
if (!text.trim()) continue;
|
|
1429
1491
|
const label = String(rec?.name || rec?.category || 'tool').trim() || 'tool';
|
|
1430
1492
|
chunks.push(`${chunks.length + 1}. ${label}\n${text}`);
|
|
@@ -1432,37 +1494,15 @@ export async function createEngineSession({
|
|
|
1432
1494
|
return chunks.join('\n\n');
|
|
1433
1495
|
}
|
|
1434
1496
|
|
|
1435
|
-
function aggregateDisplayDetail(summaries, allCalls) {
|
|
1436
|
-
const fromSummaries = formatAggregateDetail(summaries);
|
|
1437
|
-
if (String(fromSummaries || '').trim()) return fromSummaries;
|
|
1438
|
-
let line = '';
|
|
1439
|
-
for (const rec of allCalls || []) {
|
|
1440
|
-
const text = String(rec?.resultText || '').replace(/\s+$/, '');
|
|
1441
|
-
line = text.split('\n').map((l) => l.trim()).find(Boolean) || '';
|
|
1442
|
-
if (line) break;
|
|
1443
|
-
}
|
|
1444
|
-
if (!line) return '';
|
|
1445
|
-
return line.length > 160 ? `${line.slice(0, 157)}…` : line;
|
|
1446
|
-
}
|
|
1447
|
-
|
|
1448
1497
|
function aggregateBucketForCategory(category) {
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
case 'Explore':
|
|
1458
|
-
return 'explore';
|
|
1459
|
-
case 'Patch':
|
|
1460
|
-
return 'patch';
|
|
1461
|
-
default:
|
|
1462
|
-
// Shell/Agent/Channel/Setup/Other stay as their own cards so risky or
|
|
1463
|
-
// semantically distinct actions do not disappear inside a discovery log.
|
|
1464
|
-
return null;
|
|
1465
|
-
}
|
|
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';
|
|
1466
1506
|
}
|
|
1467
1507
|
|
|
1468
1508
|
function aggregateSummaries(aggregate) {
|
|
@@ -1511,22 +1551,20 @@ export async function createEngineSession({
|
|
|
1511
1551
|
const allCalls = [...aggregate.calls.values()];
|
|
1512
1552
|
const completed = allCalls.filter((r) => r.resolved).length;
|
|
1513
1553
|
const errors = allCalls.filter((r) => r.isError).length;
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
detailText = aggregateDisplayDetail(summaries, allCalls);
|
|
1521
|
-
if (errors > 0) {
|
|
1522
|
-
detailText = detailText ? `${detailText} · ${errors} Failed` : `${errors} Failed`;
|
|
1523
|
-
}
|
|
1524
|
-
}
|
|
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
|
+
: '';
|
|
1525
1560
|
const currentItem = state.items.find((it) => it.id === card.itemId);
|
|
1526
1561
|
const earlyCompleted = allCalls.filter((r) => r.resolved || r.completedEarly).length;
|
|
1527
1562
|
const visualCompleted = Math.max(completed, earlyCompleted, Math.min(allCalls.length, Number(currentItem?.completedCount || 0)));
|
|
1528
1563
|
const rawResult = aggregateRawResult(allCalls);
|
|
1529
|
-
|
|
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;
|
|
1530
1568
|
patchItem(card.itemId, {
|
|
1531
1569
|
result: displayDetail,
|
|
1532
1570
|
text: displayDetail,
|
|
@@ -1575,7 +1613,7 @@ export async function createEngineSession({
|
|
|
1575
1613
|
return true;
|
|
1576
1614
|
}
|
|
1577
1615
|
|
|
1578
|
-
const flushToolResults = (messages, toolCards, cardByCallId, toolGroups, done, { finalize = false } = {}) => {
|
|
1616
|
+
const flushToolResults = (messages, toolCards, cardByCallId, toolGroups, done, { finalize = false, cancelled = false } = {}) => {
|
|
1579
1617
|
const results = [];
|
|
1580
1618
|
for (const m of messages || []) {
|
|
1581
1619
|
if (!m || m.role !== 'tool') continue;
|
|
@@ -1613,14 +1651,33 @@ export async function createEngineSession({
|
|
|
1613
1651
|
const aggregate = card.aggregate;
|
|
1614
1652
|
if (aggregate && card.itemId === aggregate.itemId) {
|
|
1615
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
|
+
}
|
|
1616
1664
|
const completed = allCalls.filter((r) => r.resolved).length;
|
|
1617
|
-
const
|
|
1618
|
-
const totalCompleted = remaining > 0 ? completed + remaining : completed;
|
|
1665
|
+
const totalCompleted = completed;
|
|
1619
1666
|
const errors = allCalls.filter((r) => r.isError).length;
|
|
1620
|
-
const
|
|
1621
|
-
const detailText = aggregateDisplayDetail(summaries, allCalls);
|
|
1667
|
+
const succeeded = completed - errors;
|
|
1622
1668
|
const rawResult = aggregateRawResult(allCalls);
|
|
1623
|
-
|
|
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
|
+
}
|
|
1624
1681
|
patchItem(card.itemId, {
|
|
1625
1682
|
result: displayDetail,
|
|
1626
1683
|
text: displayDetail,
|
|
@@ -1642,7 +1699,11 @@ export async function createEngineSession({
|
|
|
1642
1699
|
const group = toolGroups.get(card.itemId) || { count: 1, completed: 0, errors: 0, results: [] };
|
|
1643
1700
|
group.completed = Math.min(group.count, group.completed + 1);
|
|
1644
1701
|
toolGroups.set(card.itemId, group);
|
|
1645
|
-
|
|
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
|
+
}
|
|
1646
1707
|
patchItem(card.itemId, { result: resultText, text: resultText, isError: group.errors > 0, errorCount: group.errors, count: group.count, completedCount: group.completed, completedAt: Date.now() });
|
|
1647
1708
|
card.done = true;
|
|
1648
1709
|
if (card.callId) done.add(card.callId);
|
|
@@ -1688,8 +1749,7 @@ export async function createEngineSession({
|
|
|
1688
1749
|
// cards (send() still in flight). Hold those by callId until the batch lands.
|
|
1689
1750
|
const earlyResultBuffer = new Map();
|
|
1690
1751
|
const aggregateCards = []; // active aggregate cards in the current consecutive tool block
|
|
1691
|
-
const aggregateByBucket = new Map(); //
|
|
1692
|
-
let openAggregateCard = null;
|
|
1752
|
+
const aggregateByBucket = new Map(); // per-bucket reusable card for the current consecutive tool block; cleared on block seal
|
|
1693
1753
|
|
|
1694
1754
|
// ── Deferred tool-card push (scroll/text sync) ────────────────────────────
|
|
1695
1755
|
// A tool card used to enter the transcript the instant onToolCall fired,
|
|
@@ -1722,12 +1782,23 @@ export async function createEngineSession({
|
|
|
1722
1782
|
try { e.push(); } catch {}
|
|
1723
1783
|
}
|
|
1724
1784
|
};
|
|
1785
|
+
flushDeferredBeforeImmediatePush = () => {
|
|
1786
|
+
if (!deferredEntries.length) return;
|
|
1787
|
+
const last = deferredEntries[deferredEntries.length - 1];
|
|
1788
|
+
if (last) flushDeferredUpTo(last);
|
|
1789
|
+
};
|
|
1725
1790
|
const registerDeferredCard = (card) => {
|
|
1726
1791
|
const entry = {
|
|
1727
1792
|
seq: deferredSeqCounter++,
|
|
1728
1793
|
pushed: false,
|
|
1729
1794
|
timer: null,
|
|
1730
|
-
push: () => {
|
|
1795
|
+
push: () => {
|
|
1796
|
+
card.pushed = true;
|
|
1797
|
+
if (!card.spec) return;
|
|
1798
|
+
card.spec.deferredDisplayReady = true;
|
|
1799
|
+
pushingFromDeferredEntry = true;
|
|
1800
|
+
try { pushItem(card.spec); } finally { pushingFromDeferredEntry = false; }
|
|
1801
|
+
},
|
|
1731
1802
|
};
|
|
1732
1803
|
card.deferred = entry;
|
|
1733
1804
|
card.ensureVisible = () => flushDeferredUpTo(entry);
|
|
@@ -1744,7 +1815,13 @@ export async function createEngineSession({
|
|
|
1744
1815
|
seq: deferredSeqCounter++,
|
|
1745
1816
|
pushed: false,
|
|
1746
1817
|
timer: null,
|
|
1747
|
-
push: () => {
|
|
1818
|
+
push: () => {
|
|
1819
|
+
aggregate.pushed = true;
|
|
1820
|
+
if (!aggregate.pendingSpec) return;
|
|
1821
|
+
aggregate.pendingSpec.deferredDisplayReady = true;
|
|
1822
|
+
pushingFromDeferredEntry = true;
|
|
1823
|
+
try { pushItem(aggregate.pendingSpec); } finally { pushingFromDeferredEntry = false; }
|
|
1824
|
+
},
|
|
1748
1825
|
};
|
|
1749
1826
|
aggregate.deferred = entry;
|
|
1750
1827
|
aggregate.ensureVisible = () => flushDeferredUpTo(entry);
|
|
@@ -1804,10 +1881,14 @@ export async function createEngineSession({
|
|
|
1804
1881
|
if (allCalls.length === 0) continue;
|
|
1805
1882
|
aggregate.ensureVisible?.();
|
|
1806
1883
|
const errors = allCalls.filter((r) => r.isError).length;
|
|
1807
|
-
const
|
|
1808
|
-
const
|
|
1884
|
+
const completed = allCalls.filter((r) => r.resolved).length;
|
|
1885
|
+
const succeeded = completed - errors;
|
|
1809
1886
|
const rawResult = aggregateRawResult(allCalls);
|
|
1810
|
-
|
|
1887
|
+
// Status-only collapsed detail (see patchToolCardResult): no per-result
|
|
1888
|
+
// summary; failures keep 'N Failed'. Raw preserved for ctrl+o expansion.
|
|
1889
|
+
const displayDetail = errors > 0
|
|
1890
|
+
? (succeeded > 0 ? `${succeeded} Ok · ${errors} Failed` : `${errors} Failed`)
|
|
1891
|
+
: '';
|
|
1811
1892
|
patchItem(aggregate.itemId, {
|
|
1812
1893
|
result: displayDetail,
|
|
1813
1894
|
text: displayDetail,
|
|
@@ -1824,13 +1905,11 @@ export async function createEngineSession({
|
|
|
1824
1905
|
completeAggregateVisual();
|
|
1825
1906
|
finalizeToolHeaders();
|
|
1826
1907
|
aggregateCards.length = 0;
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
const last = state.items[state.items.length - 1];
|
|
1833
|
-
return last?.kind === 'tool' && last.aggregate === true && last.id === aggregate.itemId;
|
|
1908
|
+
// Seal the block: same-bucket calls after this point must open a fresh
|
|
1909
|
+
// card, never continue one from before the seal (assistant text/turn
|
|
1910
|
+
// end boundary). Cross-block behavior is unchanged by the in-block
|
|
1911
|
+
// reuse relaxation below.
|
|
1912
|
+
aggregateByBucket.clear();
|
|
1834
1913
|
};
|
|
1835
1914
|
|
|
1836
1915
|
const rememberActiveAggregate = (aggregate) => {
|
|
@@ -1840,24 +1919,19 @@ export async function createEngineSession({
|
|
|
1840
1919
|
};
|
|
1841
1920
|
|
|
1842
1921
|
const ensureAggregateCard = (bucket) => {
|
|
1843
|
-
// Reuse
|
|
1844
|
-
//
|
|
1845
|
-
//
|
|
1846
|
-
//
|
|
1847
|
-
//
|
|
1848
|
-
//
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
//
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
const tailAggregate = aggregateByBucket.get(bucket);
|
|
1857
|
-
if (tailAggregate && (!tailAggregate.pushed || isAggregateTail(tailAggregate))) {
|
|
1858
|
-
openAggregateCard = tailAggregate;
|
|
1859
|
-
rememberActiveAggregate(tailAggregate);
|
|
1860
|
-
return tailAggregate;
|
|
1922
|
+
// Reuse any same-bucket aggregate created earlier in the SAME consecutive
|
|
1923
|
+
// tool block, even when a different-category card was interleaved after
|
|
1924
|
+
// it (Read, Search, Read → the two Reads merge into one card; the Search
|
|
1925
|
+
// stays separate). The block is only sealed — forcing a fresh card per
|
|
1926
|
+
// bucket — by clearAggregateContinuation (assistant text lands or the
|
|
1927
|
+
// turn ends), which clears aggregateByBucket. Reusing a pushed, non-tail
|
|
1928
|
+
// card is safe: the aggregate card body is a fixed header+1-detail-row
|
|
1929
|
+
// height, so patching its count/completedCount later never reflows the
|
|
1930
|
+
// cards that were pushed after it.
|
|
1931
|
+
const cached = aggregateByBucket.get(bucket);
|
|
1932
|
+
if (cached) {
|
|
1933
|
+
rememberActiveAggregate(cached);
|
|
1934
|
+
return cached;
|
|
1861
1935
|
}
|
|
1862
1936
|
const itemId = nextId();
|
|
1863
1937
|
const aggregate = {
|
|
@@ -1874,7 +1948,6 @@ export async function createEngineSession({
|
|
|
1874
1948
|
// pendingSpec current until the timer/result flushes it in call order.
|
|
1875
1949
|
registerDeferredAggregate(aggregate);
|
|
1876
1950
|
rememberActiveAggregate(aggregate);
|
|
1877
|
-
openAggregateCard = aggregate;
|
|
1878
1951
|
return aggregate;
|
|
1879
1952
|
};
|
|
1880
1953
|
|
|
@@ -2065,19 +2138,13 @@ export async function createEngineSession({
|
|
|
2065
2138
|
const allCalls = [...aggregate.calls.values()];
|
|
2066
2139
|
const completedCount = allCalls.filter((r) => r.resolved || r.completedEarly).length;
|
|
2067
2140
|
const errors = allCalls.filter((r) => r.isError).length;
|
|
2068
|
-
const
|
|
2069
|
-
let detailText;
|
|
2070
|
-
if (errors > 0 && summaries.length === 0) {
|
|
2071
|
-
const succeeded = completedCount - errors;
|
|
2072
|
-
detailText = succeeded > 0 ? `${succeeded} Ok · ${errors} Failed` : `${errors} Failed`;
|
|
2073
|
-
} else {
|
|
2074
|
-
detailText = aggregateDisplayDetail(summaries, allCalls);
|
|
2075
|
-
if (errors > 0) {
|
|
2076
|
-
detailText = detailText ? `${detailText} · ${errors} Failed` : `${errors} Failed`;
|
|
2077
|
-
}
|
|
2078
|
-
}
|
|
2141
|
+
const succeeded = completedCount - errors;
|
|
2079
2142
|
const rawResult = aggregateRawResult(allCalls);
|
|
2080
|
-
|
|
2143
|
+
// Status-only collapsed detail (no per-result summary); failures keep
|
|
2144
|
+
// 'N Failed'. Raw preserved for ctrl+o expansion.
|
|
2145
|
+
const displayDetail = errors > 0
|
|
2146
|
+
? (succeeded > 0 ? `${succeeded} Ok · ${errors} Failed` : `${errors} Failed`)
|
|
2147
|
+
: '';
|
|
2081
2148
|
const currentItem = state.items.find((it) => it.id === card.itemId);
|
|
2082
2149
|
const visualCompleted = Math.max(
|
|
2083
2150
|
completedCount,
|
|
@@ -2146,21 +2213,28 @@ export async function createEngineSession({
|
|
|
2146
2213
|
}
|
|
2147
2214
|
const batchCalls = (calls || []).filter(Boolean);
|
|
2148
2215
|
if (batchCalls.length === 0) return;
|
|
2149
|
-
commitAssistantSegment({ sealToolBlock: true });
|
|
2216
|
+
const committedAssistantSegment = commitAssistantSegment({ sealToolBlock: true });
|
|
2217
|
+
if (committedAssistantSegment) {
|
|
2218
|
+
// Let the pre-tool assistant preamble paint as its own frame before
|
|
2219
|
+
// the tool card reserves/pushes rows. When both enter the transcript
|
|
2220
|
+
// in the same render, the bottom-pinned viewport can appear to jump
|
|
2221
|
+
// upward by the combined height ("preamble + tool card" at once).
|
|
2222
|
+
await yieldToRenderer();
|
|
2223
|
+
}
|
|
2150
2224
|
|
|
2151
2225
|
const touchedAggregates = new Set();
|
|
2152
2226
|
for (let i = 0; i < batchCalls.length; i++) {
|
|
2153
2227
|
const c = batchCalls[i];
|
|
2154
2228
|
const name = toolCallName(c);
|
|
2155
2229
|
const args = toolCallArgs(c);
|
|
2230
|
+
// Category drives the aggregate bucket so only same-category calls
|
|
2231
|
+
// merge into one card; classify first, then bucket by it.
|
|
2156
2232
|
const category = classifyToolCategory(name, args);
|
|
2157
2233
|
const bucket = aggregateBucketForCategory(category);
|
|
2158
|
-
const categoryEntry = aggregateToolCategoryEntry(name, args, category);
|
|
2159
2234
|
const callId = toolCallId(c);
|
|
2160
2235
|
const callKey = callId || `__tool_${toolCards.length}_${i}`;
|
|
2161
2236
|
|
|
2162
2237
|
if (!bucket) {
|
|
2163
|
-
openAggregateCard = null;
|
|
2164
2238
|
const itemId = nextId();
|
|
2165
2239
|
// Defer the visible push: hold the spec and only enter the
|
|
2166
2240
|
// transcript when the real header/detail will paint (delay
|
|
@@ -2193,6 +2267,7 @@ export async function createEngineSession({
|
|
|
2193
2267
|
continue;
|
|
2194
2268
|
}
|
|
2195
2269
|
|
|
2270
|
+
const categoryEntry = aggregateToolCategoryEntry(name, args, category);
|
|
2196
2271
|
const aggregateCard = ensureAggregateCard(bucket);
|
|
2197
2272
|
if (!aggregateCard.categories.has(categoryEntry.key)) aggregateCard.categoryOrder.push(categoryEntry.key);
|
|
2198
2273
|
const prevCategory = aggregateCard.categories.get(categoryEntry.key);
|
|
@@ -2212,6 +2287,17 @@ export async function createEngineSession({
|
|
|
2212
2287
|
for (const aggregateCard of touchedAggregates) {
|
|
2213
2288
|
syncAggregateHeader(aggregateCard);
|
|
2214
2289
|
}
|
|
2290
|
+
if (committedAssistantSegment) {
|
|
2291
|
+
// A pre-tool assistant preamble has already had one render frame to
|
|
2292
|
+
// settle. Do not let the first grouped tool card sit off-screen until
|
|
2293
|
+
// the normal 1s deferred timer: when it later inserts its real 3 rows,
|
|
2294
|
+
// the already-wrapped preamble visibly jumps. Surface the first card
|
|
2295
|
+
// now via the existing deferredDisplayReady path, so the post-
|
|
2296
|
+
// preamble frame contains the intended Running tool card immediately
|
|
2297
|
+
// (no blank placeholder, no delayed row insertion).
|
|
2298
|
+
const firstTouchedAggregate = [...touchedAggregates][0] || null;
|
|
2299
|
+
firstTouchedAggregate?.ensureVisible?.();
|
|
2300
|
+
}
|
|
2215
2301
|
for (const [bufferedCallId, bufferedMessage] of earlyResultBuffer) {
|
|
2216
2302
|
if (!cardByCallId.has(bufferedCallId)) continue;
|
|
2217
2303
|
deliverToolResultMessage(bufferedMessage);
|
|
@@ -2367,7 +2453,7 @@ export async function createEngineSession({
|
|
|
2367
2453
|
// shows "cancelled", but in-flight tool cards remain in a perpetual
|
|
2368
2454
|
// pending/blinking state because the normal finalize path (line 992)
|
|
2369
2455
|
// was skipped when the error interrupted the try block.
|
|
2370
|
-
flushToolResults([], toolCards, cardByCallId, toolGroups, resultsDone, { finalize: true });
|
|
2456
|
+
flushToolResults([], toolCards, cardByCallId, toolGroups, resultsDone, { finalize: true, cancelled: true });
|
|
2371
2457
|
finalizeToolHeaders();
|
|
2372
2458
|
} else {
|
|
2373
2459
|
finalizeToolHeaders();
|
|
@@ -2384,6 +2470,7 @@ export async function createEngineSession({
|
|
|
2384
2470
|
if (last) flushDeferredUpTo(last);
|
|
2385
2471
|
clearDeferredTimers();
|
|
2386
2472
|
}
|
|
2473
|
+
flushDeferredBeforeImmediatePush = null;
|
|
2387
2474
|
const producedTranscriptItem = state.items.length > itemsAtTurnStart;
|
|
2388
2475
|
const reclaimed = cancelled && activePromptRestore?.reclaimed === true;
|
|
2389
2476
|
activePromptRestore = null;
|
|
@@ -2481,6 +2568,7 @@ export async function createEngineSession({
|
|
|
2481
2568
|
if (pending.length === 0) return [];
|
|
2482
2569
|
const max = queuePriorityValue(maxPriority);
|
|
2483
2570
|
const predicate = typeof options.predicate === 'function' ? options.predicate : () => true;
|
|
2571
|
+
const limit = Math.max(1, Number(options.limit) || Infinity);
|
|
2484
2572
|
let bestPriority = Infinity;
|
|
2485
2573
|
let targetMode = null;
|
|
2486
2574
|
for (const entry of pending) {
|
|
@@ -2499,6 +2587,7 @@ export async function createEngineSession({
|
|
|
2499
2587
|
if (predicate(entry) && (entry.mode || 'prompt') === targetMode && queuePriorityValue(entry.priority) === bestPriority) {
|
|
2500
2588
|
batch.push(entry);
|
|
2501
2589
|
pending.splice(i, 1);
|
|
2590
|
+
if (batch.length >= limit) break;
|
|
2502
2591
|
} else {
|
|
2503
2592
|
i += 1;
|
|
2504
2593
|
}
|
|
@@ -2509,13 +2598,16 @@ export async function createEngineSession({
|
|
|
2509
2598
|
|
|
2510
2599
|
async function drain() {
|
|
2511
2600
|
if (draining) return;
|
|
2601
|
+
if (autoClearRunning) return;
|
|
2512
2602
|
draining = true;
|
|
2603
|
+
let firstBatch = true;
|
|
2513
2604
|
try {
|
|
2514
2605
|
while (pending.length > 0) {
|
|
2515
2606
|
// Drain one priority/mode bucket at a time (unified command queue):
|
|
2516
2607
|
// unified command queue semantics: prompt steering stays editable and
|
|
2517
2608
|
// task notifications stay non-editable but model-visible.
|
|
2518
|
-
const batch = dequeueQueueBatch('later');
|
|
2609
|
+
const batch = dequeueQueueBatch('later', { limit: firstBatch ? 1 : Infinity });
|
|
2610
|
+
firstBatch = false;
|
|
2519
2611
|
if (batch.length === 0) break;
|
|
2520
2612
|
const ids = new Set(batch.map((e) => e.id));
|
|
2521
2613
|
const merged = mergePromptContents(batch);
|
|
@@ -2559,9 +2651,12 @@ export async function createEngineSession({
|
|
|
2559
2651
|
|
|
2560
2652
|
function drainPendingSteering() {
|
|
2561
2653
|
// Mid-turn steering drain:
|
|
2562
|
-
//
|
|
2563
|
-
//
|
|
2564
|
-
//
|
|
2654
|
+
// Injects queued user prompts (steering) plus non-editable internal entries
|
|
2655
|
+
// into the CURRENT provider pre-send window so the user can redirect a turn
|
|
2656
|
+
// that is already running. Slash commands are still excluded: they must run
|
|
2657
|
+
// through the normal command processor after the turn, not be sent as plain
|
|
2658
|
+
// text. Consumed entries are spliced out of `pending` here, so the post-turn
|
|
2659
|
+
// drain() loop will not re-execute them.
|
|
2565
2660
|
const batch = dequeueQueueBatch('next', { predicate: (entry) => !isSlashQueuedEntry(entry) });
|
|
2566
2661
|
if (batch.length === 0) return [];
|
|
2567
2662
|
const out = batch
|
|
@@ -2592,7 +2687,14 @@ export async function createEngineSession({
|
|
|
2592
2687
|
const startedAt = Date.now();
|
|
2593
2688
|
set({ commandStatus: { active: true, verb: 'Auto-clearing idle conversation', startedAt, mode: 'auto-clear' } });
|
|
2594
2689
|
try {
|
|
2595
|
-
|
|
2690
|
+
// Give Ink one event-loop turn to paint the auto-clear status before the
|
|
2691
|
+
// clear/compact path starts doing synchronous session/transcript work.
|
|
2692
|
+
// Without this, long idle clears can look like a frozen prompt followed by
|
|
2693
|
+
// an already-complete status row.
|
|
2694
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
2695
|
+
const compaction = runtime.getCompactionSettings();
|
|
2696
|
+
const compactType = compaction.compactType || compaction.type;
|
|
2697
|
+
await runtime.clear({ compactType, requireCompactSuccess: !!compactType });
|
|
2596
2698
|
resetStats();
|
|
2597
2699
|
clearUiActivityBeforeContextSync();
|
|
2598
2700
|
syncContextStats({ allowEstimated: true });
|
|
@@ -2627,6 +2729,7 @@ export async function createEngineSession({
|
|
|
2627
2729
|
lastUserActivityAt = Date.now();
|
|
2628
2730
|
autoClearRunning = false;
|
|
2629
2731
|
set({ commandStatus: null });
|
|
2732
|
+
void drain();
|
|
2630
2733
|
}
|
|
2631
2734
|
}
|
|
2632
2735
|
|
|
@@ -2739,6 +2842,10 @@ export async function createEngineSession({
|
|
|
2739
2842
|
enqueue(text, queueOptions);
|
|
2740
2843
|
return true;
|
|
2741
2844
|
}
|
|
2845
|
+
if (autoClearRunning) {
|
|
2846
|
+
enqueue(text, queueOptions);
|
|
2847
|
+
return true;
|
|
2848
|
+
}
|
|
2742
2849
|
void autoClearBeforeSubmit().then(() => enqueue(text, queueOptions));
|
|
2743
2850
|
return true;
|
|
2744
2851
|
},
|
|
@@ -2747,8 +2854,10 @@ export async function createEngineSession({
|
|
|
2747
2854
|
if (state.commandBusy) return false;
|
|
2748
2855
|
set({ commandBusy: true });
|
|
2749
2856
|
try {
|
|
2750
|
-
|
|
2751
|
-
|
|
2857
|
+
// Model changes apply to the NEXT session only (default setRoute
|
|
2858
|
+
// behavior) — never rewrite the live session's provider/model, which
|
|
2859
|
+
// would force a full prompt-cache rewrite mid-conversation.
|
|
2860
|
+
await runtime.setRoute({ model: m });
|
|
2752
2861
|
set({ ...routeState(), stats: { ...state.stats } });
|
|
2753
2862
|
return true;
|
|
2754
2863
|
} finally {
|
|
@@ -2802,6 +2911,11 @@ export async function createEngineSession({
|
|
|
2802
2911
|
set({ autoClear: next });
|
|
2803
2912
|
return next;
|
|
2804
2913
|
},
|
|
2914
|
+
getUpdateSettings: () => runtime.getUpdateSettings?.() || null,
|
|
2915
|
+
setAutoUpdate: (enabled) => runtime.setAutoUpdate?.(enabled),
|
|
2916
|
+
checkForUpdate: (input = {}) => runtime.checkForUpdate?.(input),
|
|
2917
|
+
runUpdateNow: () => runtime.runUpdateNow?.(),
|
|
2918
|
+
getUpdateStatus: () => runtime.getUpdateStatus?.() || { phase: 'idle' },
|
|
2805
2919
|
getProfile: () => runtime.getProfile?.() || { title: '', language: 'system', languages: [] },
|
|
2806
2920
|
setProfile: (input = {}) => {
|
|
2807
2921
|
const next = runtime.setProfile?.(input) || runtime.getProfile?.() || null;
|
|
@@ -3157,8 +3271,20 @@ export async function createEngineSession({
|
|
|
3157
3271
|
if (!state.busy) return false;
|
|
3158
3272
|
denyAllToolApprovals('interrupted by user');
|
|
3159
3273
|
const restoreState = activePromptRestore;
|
|
3160
|
-
|
|
3161
|
-
|
|
3274
|
+
// A queued steering prompt means the user already redirected the turn:
|
|
3275
|
+
// interrupting should just cancel the running turn and let the steering
|
|
3276
|
+
// prompt run next, NOT resurrect the in-flight prompt back into the draft.
|
|
3277
|
+
const hasPendingSteering = pending.some((entry) => isQueuedEntryEditable(entry));
|
|
3278
|
+
const canRestore = restoreState?.restorable && !hasPendingSteering;
|
|
3279
|
+
const restoreText = canRestore ? restoreState.text : '';
|
|
3280
|
+
const restorePastedImages = canRestore && restoreState?.pastedImages ? restoreState.pastedImages : null;
|
|
3281
|
+
// When steering suppresses the restore, the interrupted prompt's pasted
|
|
3282
|
+
// images never get committed (onCommitted won't fire) nor re-installed into
|
|
3283
|
+
// the draft, so hand them back for cleanup to avoid a stale `[Image #id]`
|
|
3284
|
+
// lingering in the paste snapshot.
|
|
3285
|
+
const discardPastedImages = restoreState?.restorable && hasPendingSteering && restoreState?.pastedImages
|
|
3286
|
+
? restoreState.pastedImages
|
|
3287
|
+
: null;
|
|
3162
3288
|
const requeueEntries = restoreState && !restoreState.committed && Array.isArray(restoreState.requeueEntries)
|
|
3163
3289
|
? restoreState.requeueEntries.slice()
|
|
3164
3290
|
: [];
|
|
@@ -3180,7 +3306,7 @@ export async function createEngineSession({
|
|
|
3180
3306
|
restoreState.restorable = false;
|
|
3181
3307
|
restoreState.requeueEntries = [];
|
|
3182
3308
|
}
|
|
3183
|
-
return { aborted, restoreText, pastedImages: restorePastedImages };
|
|
3309
|
+
return { aborted, restoreText, pastedImages: restorePastedImages, discardPastedImages };
|
|
3184
3310
|
},
|
|
3185
3311
|
resolveToolApproval: (id, decision = {}) => {
|
|
3186
3312
|
const approved = decision === true || decision?.approved === true;
|
|
@@ -3260,6 +3386,18 @@ export async function createEngineSession({
|
|
|
3260
3386
|
set({ commandBusy: false });
|
|
3261
3387
|
}
|
|
3262
3388
|
},
|
|
3389
|
+
// Toggle Discord remote mode for this session. Flips the runtime's
|
|
3390
|
+
// remoteEnabled flag (booting/stopping the channel worker) and returns the
|
|
3391
|
+
// NEW enabled state so the caller can render an ON/OFF notice.
|
|
3392
|
+
toggleRemote: () => {
|
|
3393
|
+
const enabled = runtime.isRemoteEnabled?.() === true;
|
|
3394
|
+
if (enabled) runtime.stopRemote?.();
|
|
3395
|
+
else runtime.startRemote?.();
|
|
3396
|
+
const next = runtime.isRemoteEnabled?.() === true;
|
|
3397
|
+
set({ remoteEnabled: next });
|
|
3398
|
+
return next;
|
|
3399
|
+
},
|
|
3400
|
+
isRemoteEnabled: () => runtime.isRemoteEnabled?.() === true,
|
|
3263
3401
|
// Theme is a TUI-local concern (no runtime round-trip). listThemes returns
|
|
3264
3402
|
// picker metadata; getTheme reports the active id; setTheme applies the
|
|
3265
3403
|
// palette in-place + persists ui.theme and bumps a themeEpoch so the React
|
|
@@ -3274,6 +3412,9 @@ export async function createEngineSession({
|
|
|
3274
3412
|
setAgentRoute: async (agentId, opts) => {
|
|
3275
3413
|
return await runtime.setAgentRoute?.(agentId, opts);
|
|
3276
3414
|
},
|
|
3415
|
+
setDefaultProvider: async (provider) => {
|
|
3416
|
+
return await runtime.setDefaultProvider?.(provider);
|
|
3417
|
+
},
|
|
3277
3418
|
listProviders: () => {
|
|
3278
3419
|
return runtime.listProviders();
|
|
3279
3420
|
},
|
|
@@ -3286,6 +3427,10 @@ export async function createEngineSession({
|
|
|
3286
3427
|
getOnboardingStatus: () => {
|
|
3287
3428
|
return runtime.getOnboardingStatus?.() || { completed: true, workflowRoutes: {} };
|
|
3288
3429
|
},
|
|
3430
|
+
skipOnboarding: () => {
|
|
3431
|
+
// Completed-marking only; no route/agent/provider writes.
|
|
3432
|
+
return runtime.skipOnboarding?.() || null;
|
|
3433
|
+
},
|
|
3289
3434
|
completeOnboarding: async (payload = {}) => {
|
|
3290
3435
|
if (state.commandBusy) return null;
|
|
3291
3436
|
set({ commandBusy: true });
|
|
@@ -3363,6 +3508,8 @@ export async function createEngineSession({
|
|
|
3363
3508
|
getChannelSetup: () => {
|
|
3364
3509
|
return runtime.getChannelSetup();
|
|
3365
3510
|
},
|
|
3511
|
+
getChannelWorkerStatus: () => runtime.getChannelWorkerStatus?.(),
|
|
3512
|
+
setBackend: (name) => runtime.setBackend?.(name),
|
|
3366
3513
|
saveDiscordToken: (token) => {
|
|
3367
3514
|
const result = runtime.saveDiscordToken(token);
|
|
3368
3515
|
pushNotice('discord token saved', 'info');
|
|
@@ -3373,6 +3520,16 @@ export async function createEngineSession({
|
|
|
3373
3520
|
pushNotice('discord token forgotten', 'info');
|
|
3374
3521
|
return result;
|
|
3375
3522
|
},
|
|
3523
|
+
saveTelegramToken: (token) => {
|
|
3524
|
+
const result = runtime.saveTelegramToken?.(token);
|
|
3525
|
+
pushNotice('telegram token saved', 'info');
|
|
3526
|
+
return result;
|
|
3527
|
+
},
|
|
3528
|
+
forgetTelegramToken: () => {
|
|
3529
|
+
const result = runtime.forgetTelegramToken?.();
|
|
3530
|
+
pushNotice('telegram token forgotten', 'info');
|
|
3531
|
+
return result;
|
|
3532
|
+
},
|
|
3376
3533
|
saveWebhookAuthtoken: (token) => {
|
|
3377
3534
|
const result = runtime.saveWebhookAuthtoken(token);
|
|
3378
3535
|
pushNotice('webhook/ngrok authtoken saved', 'info');
|
|
@@ -3433,7 +3590,9 @@ export async function createEngineSession({
|
|
|
3433
3590
|
set({ commandBusy: true });
|
|
3434
3591
|
try {
|
|
3435
3592
|
const routeOpts = opts && typeof opts === 'object' ? opts : {};
|
|
3436
|
-
|
|
3593
|
+
// Default: apply to the NEXT session only. Only an explicit
|
|
3594
|
+
// `applyToCurrentSession: true` rewrites the live session in place.
|
|
3595
|
+
const applyToCurrentSession = routeOpts.applyToCurrentSession === true;
|
|
3437
3596
|
const { applyToCurrentSession: _drop, ...nextRoute } = routeOpts;
|
|
3438
3597
|
await runtime.setRoute(nextRoute, { applyToCurrentSession });
|
|
3439
3598
|
if (applyToCurrentSession) syncContextStats({ allowEstimated: true });
|
|
@@ -3448,6 +3607,7 @@ export async function createEngineSession({
|
|
|
3448
3607
|
if (state.commandBusy) return false;
|
|
3449
3608
|
set({ commandBusy: true });
|
|
3450
3609
|
clearToastTimers();
|
|
3610
|
+
resetAllStreamingMarkdownStablePrefixes();
|
|
3451
3611
|
const rollbackSnapshot = snapshotTuiBeforeSessionReset();
|
|
3452
3612
|
resetTuiForPendingSessionReset();
|
|
3453
3613
|
set({
|
|
@@ -3483,6 +3643,7 @@ export async function createEngineSession({
|
|
|
3483
3643
|
if (state.commandBusy) return false;
|
|
3484
3644
|
set({ commandBusy: true });
|
|
3485
3645
|
clearToastTimers();
|
|
3646
|
+
resetAllStreamingMarkdownStablePrefixes();
|
|
3486
3647
|
const rollbackSnapshot = snapshotTuiBeforeSessionReset();
|
|
3487
3648
|
resetTuiForPendingSessionReset();
|
|
3488
3649
|
set({
|