mixdog 0.9.2 → 0.9.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -1
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/tool-smoke.mjs +7 -4
- package/src/agents/debugger/AGENT.md +2 -2
- package/src/agents/heavy-worker/AGENT.md +20 -11
- package/src/agents/reviewer/AGENT.md +2 -2
- package/src/agents/worker/AGENT.md +17 -11
- package/src/mixdog-session-runtime.mjs +424 -1812
- package/src/repl.mjs +5 -5
- package/src/rules/agent/30-explorer.md +8 -11
- package/src/rules/lead/lead-tool.md +9 -5
- package/src/rules/shared/01-tool.md +11 -5
- 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 +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +22 -68
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +46 -7
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +7 -5
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +33 -23
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +31 -14
- 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/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +169 -918
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +65 -1032
- package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/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/search-tool.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
- package/src/runtime/agent/orchestrator/tools/patch.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/channels/index.mjs +14 -233
- 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/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +8 -0
- package/src/runtime/channels/lib/telegram-format.mjs +19 -22
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/memory/index.mjs +314 -359
- package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +56 -3
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +20 -0
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/tool-defs.mjs +4 -4
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/transcript-writer.mjs +29 -2
- package/src/session-runtime/config-helpers.mjs +209 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/output-styles.mjs +124 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool.mjs +49 -10
- package/src/standalone/channel-worker.mjs +3 -2
- package/src/standalone/explore-tool.mjs +10 -3
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +14 -3
- package/src/tui/App.jsx +884 -143
- package/src/tui/components/PromptInput.jsx +272 -15
- package/src/tui/components/ToolExecution.jsx +20 -10
- package/src/tui/components/tool-output-format.mjs +2 -2
- package/src/tui/dist/index.mjs +1771 -1947
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +311 -851
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +2 -2
- package/src/tui/lib/voice-recorder.mjs +35 -19
- package/src/tui/markdown/format-token.mjs +7 -8
- package/src/tui/markdown/format-token.test.mjs +3 -3
- package/src/tui/paste-attachments.mjs +38 -0
- package/src/tui/paste-fix.test.mjs +119 -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.mjs +49 -0
- 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/src/tui/engine.mjs
CHANGED
|
@@ -4,76 +4,75 @@
|
|
|
4
4
|
* Runs mixdog's session manager outside React and exposes a tiny subscribable
|
|
5
5
|
* store. The React/ink layer consumes it via useSyncExternalStore
|
|
6
6
|
* (see hooks/useEngine.mjs).
|
|
7
|
+
*
|
|
8
|
+
* Pure/stateless helpers live in ./engine/* (boot-profile, session-stats,
|
|
9
|
+
* labels, notice-text, tool-result-text, tool-call-fields, agent-envelope,
|
|
10
|
+
* queue-helpers) and are re-exported here so the public surface is unchanged.
|
|
11
|
+
* This file keeps the stateful createEngineSession store + notification plan.
|
|
7
12
|
*/
|
|
8
13
|
import { performance } from 'node:perf_hooks';
|
|
9
|
-
import { SPINNER_VERBS } from './spinner-verbs.mjs';
|
|
10
14
|
import {
|
|
11
15
|
aggregateToolCategoryEntry,
|
|
12
16
|
classifyToolCategory,
|
|
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
22
|
modelVisibleToolCompletionMessage,
|
|
19
23
|
} from '../runtime/shared/tool-execution-contract.mjs';
|
|
24
|
+
import { presentErrorText } from '../runtime/shared/err-text.mjs';
|
|
20
25
|
import { listThemes, getThemeSetting, setThemeSetting } from './theme.mjs';
|
|
21
26
|
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
|
-
stats.latestCachedTokens = cachedTokens;
|
|
72
|
-
stats.latestCacheWriteTokens = cacheWriteTokens;
|
|
73
|
-
stats.latestPromptTokens = promptTokens;
|
|
74
|
-
stats.costUsd += num(delta.costUsd);
|
|
75
|
-
return stats;
|
|
76
|
-
}
|
|
27
|
+
import { bootProfile } from './engine/boot-profile.mjs';
|
|
28
|
+
import { createSessionStats, applyUsageDelta } from './engine/session-stats.mjs';
|
|
29
|
+
import {
|
|
30
|
+
pickVerb,
|
|
31
|
+
pickDoneVerb,
|
|
32
|
+
formatElapsedSeconds,
|
|
33
|
+
compactEventLabel,
|
|
34
|
+
compactEventDetail,
|
|
35
|
+
projectNameFromPath,
|
|
36
|
+
} from './engine/labels.mjs';
|
|
37
|
+
import { polishNoticeText } from './engine/notice-text.mjs';
|
|
38
|
+
import {
|
|
39
|
+
toolResultText,
|
|
40
|
+
toolAggregateDetailFallback,
|
|
41
|
+
toolGroupedDisplayFallback,
|
|
42
|
+
toolErrorDisplay,
|
|
43
|
+
} from './engine/tool-result-text.mjs';
|
|
44
|
+
import {
|
|
45
|
+
toolCallId,
|
|
46
|
+
toolResultCallId,
|
|
47
|
+
toolCallName,
|
|
48
|
+
toolCallArgs,
|
|
49
|
+
} from './engine/tool-call-fields.mjs';
|
|
50
|
+
import {
|
|
51
|
+
parseAgentJob,
|
|
52
|
+
parseAgentResultEnvelope,
|
|
53
|
+
parseBackgroundTaskEnvelope,
|
|
54
|
+
parseSyntheticAgentMessage,
|
|
55
|
+
isStatusOnlyAgentCompletionNotification,
|
|
56
|
+
agentJobResultText,
|
|
57
|
+
agentArgsWithResultMetadata,
|
|
58
|
+
toolResultStatus,
|
|
59
|
+
isErrorToolStatus,
|
|
60
|
+
} from './engine/agent-envelope.mjs';
|
|
61
|
+
import {
|
|
62
|
+
queuePriorityValue,
|
|
63
|
+
defaultQueuePriority,
|
|
64
|
+
isQueuedEntryEditable,
|
|
65
|
+
isQueuedEntryVisible,
|
|
66
|
+
isSlashQueuedEntry,
|
|
67
|
+
shortTextFingerprint,
|
|
68
|
+
notificationDisplayText,
|
|
69
|
+
sessionActivityTimestamp,
|
|
70
|
+
promptDisplayText,
|
|
71
|
+
mergePromptContents,
|
|
72
|
+
mergePastedImages,
|
|
73
|
+
mergePastedTexts,
|
|
74
|
+
callCommitCallbacks,
|
|
75
|
+
} from './engine/queue-helpers.mjs';
|
|
77
76
|
|
|
78
77
|
// Source tests resolve from src/tui/engine.mjs; the built bundle resolves from
|
|
79
78
|
// src/tui/dist/index.mjs.
|
|
@@ -89,574 +88,10 @@ const TOOL_APPROVAL_TIMEOUT_MS = (() => {
|
|
|
89
88
|
let _idSeq = 0;
|
|
90
89
|
const nextId = () => `it_${++_idSeq}`;
|
|
91
90
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
const TURN_DONE_VERBS = [
|
|
97
|
-
'Thought',
|
|
98
|
-
'Reasoned',
|
|
99
|
-
'Mapped',
|
|
100
|
-
'Checked',
|
|
101
|
-
'Solved',
|
|
102
|
-
'Composed',
|
|
103
|
-
'Synthesized',
|
|
104
|
-
'Wrapped',
|
|
105
|
-
];
|
|
106
|
-
|
|
107
|
-
function pickDoneVerb(turn) {
|
|
108
|
-
return TURN_DONE_VERBS[(turn * 5 + 2) % TURN_DONE_VERBS.length];
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function formatElapsedSeconds(ms) {
|
|
112
|
-
const value = Math.max(0, Number(ms) || 0);
|
|
113
|
-
if (value <= 0) return '0s';
|
|
114
|
-
return `${Math.max(1, Math.ceil(value / 1000))}s`;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function compactEventLabel(event = {}) {
|
|
118
|
-
const status = String(event.status || '').toLowerCase();
|
|
119
|
-
const reactive = String(event.trigger || '').toLowerCase() === 'reactive';
|
|
120
|
-
if (status === 'failed') return reactive ? 'Compact failed (overflow retry)' : 'Compact failed';
|
|
121
|
-
if (status === 'skipped') return 'Compact skipped';
|
|
122
|
-
if (status === 'no_change') return 'Compact checked';
|
|
123
|
-
return reactive ? 'Compact complete (overflow recovery)' : 'Compact complete';
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function compactEventDetail(event = {}) {
|
|
127
|
-
// Keep the elapsed time as the lead detail, but no longer discard the rest of
|
|
128
|
-
// the compact metadata. Surface type/trigger and the boundary/pressure so the
|
|
129
|
-
// statusdone marker reflects what actually fired.
|
|
130
|
-
const parts = [];
|
|
131
|
-
const elapsed = formatElapsedSeconds(Number(event.durationMs ?? event.elapsedMs ?? 0));
|
|
132
|
-
if (elapsed) parts.push(elapsed);
|
|
133
|
-
const type = String(event.compactType || event.type || '').trim();
|
|
134
|
-
if (type && type !== 'semantic') parts.push(type);
|
|
135
|
-
const trigger = String(event.trigger || '').toLowerCase();
|
|
136
|
-
if (trigger === 'reactive') parts.push('reactive');
|
|
137
|
-
else if (trigger === 'manual') parts.push('manual');
|
|
138
|
-
const before = Number(event.beforeTokens ?? event.pressureTokens ?? 0);
|
|
139
|
-
const after = Number(event.afterTokens ?? 0);
|
|
140
|
-
const fmtTok = (n) => {
|
|
141
|
-
const v = Number(n) || 0;
|
|
142
|
-
if (v >= 1000) return `${(v / 1000).toFixed(v >= 10_000 ? 0 : 1)}k`;
|
|
143
|
-
return `${Math.round(v)}`;
|
|
144
|
-
};
|
|
145
|
-
if (before > 0 && after > 0 && after !== before) parts.push(`${fmtTok(before)}→${fmtTok(after)}`);
|
|
146
|
-
return parts.join(' · ');
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function projectNameFromPath(value) {
|
|
150
|
-
const text = String(value || '').replace(/[\\/]+$/, '');
|
|
151
|
-
return text.split(/[\\/]/).pop() || text || '(current)';
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const FAILED_NOTICE_ACTIONS = new Map([
|
|
155
|
-
['api key save', 'save API key'],
|
|
156
|
-
['auth-forget', 'forget auth'],
|
|
157
|
-
['auto-clear', 'update auto-clear'],
|
|
158
|
-
['autoclear', 'update auto-clear'],
|
|
159
|
-
['agent', 'run agent command'],
|
|
160
|
-
['channels', 'load channels'],
|
|
161
|
-
['channels update', 'update channels'],
|
|
162
|
-
['clear', 'clear chat'],
|
|
163
|
-
['compact', 'compact context'],
|
|
164
|
-
['copy', 'copy'],
|
|
165
|
-
['core memory', 'load core memory'],
|
|
166
|
-
['cwd', 'update working directory'],
|
|
167
|
-
['effort switch', 'switch effort'],
|
|
168
|
-
['fast', 'update fast mode'],
|
|
169
|
-
['hook rule update', 'update hook rule'],
|
|
170
|
-
['hook toggle', 'toggle hook'],
|
|
171
|
-
['hook update', 'update hook'],
|
|
172
|
-
['hooks status', 'load hooks'],
|
|
173
|
-
['local provider update', 'update local provider'],
|
|
174
|
-
['mcp add', 'add MCP server'],
|
|
175
|
-
['mcp reconnect', 'reconnect MCP server'],
|
|
176
|
-
['mcp status', 'load MCP status'],
|
|
177
|
-
['mcp toggle', 'toggle MCP server'],
|
|
178
|
-
['memory', 'run memory command'],
|
|
179
|
-
['memory status', 'load memory status'],
|
|
180
|
-
['model save', 'save model'],
|
|
181
|
-
['model switch', 'switch model'],
|
|
182
|
-
['oauth code', 'finish OAuth login'],
|
|
183
|
-
['oauth login', 'start OAuth login'],
|
|
184
|
-
['output style switch', 'switch output style'],
|
|
185
|
-
['OpenAI usage auth save', 'save OpenAI usage auth'],
|
|
186
|
-
['OpenCode Go usage auth save', 'save OpenCode Go usage auth'],
|
|
187
|
-
['plugin add', 'add plugin'],
|
|
188
|
-
['plugin MCP enable', 'enable plugin MCP'],
|
|
189
|
-
['plugin uninstall', 'uninstall plugin'],
|
|
190
|
-
['plugin update', 'update plugin'],
|
|
191
|
-
['plugins status', 'load plugins'],
|
|
192
|
-
['providers', 'load providers'],
|
|
193
|
-
['recall', 'run recall'],
|
|
194
|
-
['resume', 'resume chat'],
|
|
195
|
-
['schedule toggle', 'toggle schedule'],
|
|
196
|
-
['setup save', 'save setup'],
|
|
197
|
-
['settings update', 'update settings'],
|
|
198
|
-
['skill add', 'add skill'],
|
|
199
|
-
['skills status', 'load skills'],
|
|
200
|
-
['tools status', 'load tool status'],
|
|
201
|
-
['usage', 'load usage'],
|
|
202
|
-
['webhook toggle', 'toggle webhook'],
|
|
203
|
-
['workflow switch', 'switch workflow'],
|
|
204
|
-
]);
|
|
205
|
-
|
|
206
|
-
function polishNoticeAction(action) {
|
|
207
|
-
const value = String(action || '').trim();
|
|
208
|
-
if (!value) return 'finish';
|
|
209
|
-
const key = value.toLowerCase();
|
|
210
|
-
for (const [candidate, replacement] of FAILED_NOTICE_ACTIONS.entries()) {
|
|
211
|
-
if (candidate.toLowerCase() === key) return replacement;
|
|
212
|
-
}
|
|
213
|
-
const suffixes = [
|
|
214
|
-
[' save', 'save'],
|
|
215
|
-
[' switch', 'switch'],
|
|
216
|
-
[' update', 'update'],
|
|
217
|
-
[' toggle', 'toggle'],
|
|
218
|
-
[' reconnect', 'reconnect'],
|
|
219
|
-
[' enable', 'enable'],
|
|
220
|
-
[' uninstall', 'uninstall'],
|
|
221
|
-
[' add', 'add'],
|
|
222
|
-
];
|
|
223
|
-
for (const [suffix, verb] of suffixes) {
|
|
224
|
-
if (!key.endsWith(suffix)) continue;
|
|
225
|
-
const subject = value.slice(0, -suffix.length).trim();
|
|
226
|
-
return subject ? `${verb} ${subject}` : verb;
|
|
227
|
-
}
|
|
228
|
-
return value;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function sentenceStart(text) {
|
|
232
|
-
const value = String(text || '').trim();
|
|
233
|
-
return value ? `${value[0].toUpperCase()}${value.slice(1)}` : value;
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function polishNoticeText(text) {
|
|
237
|
-
let value = String(text ?? '').trim().replace(/^✓\s*/, '');
|
|
238
|
-
if (!value) return '';
|
|
239
|
-
const error = /^error\s*:\s*(.+)$/i.exec(value);
|
|
240
|
-
if (error?.[1]) value = error[1].trim();
|
|
241
|
-
const couldNot = /^could not\s+(.+?)(?::\s*(.+))?$/i.exec(value);
|
|
242
|
-
if (couldNot) {
|
|
243
|
-
return couldNot[2]
|
|
244
|
-
? `Couldn’t ${couldNot[1]}: ${couldNot[2]}`
|
|
245
|
-
: `Couldn’t ${couldNot[1]}.`;
|
|
246
|
-
}
|
|
247
|
-
const failed = /^(.+?)\s+failed(?::\s*(.+))?$/i.exec(value);
|
|
248
|
-
if (failed) {
|
|
249
|
-
const action = polishNoticeAction(failed[1]);
|
|
250
|
-
return failed[2] ? `Couldn’t ${action}: ${failed[2]}` : `Couldn’t ${action}.`;
|
|
251
|
-
}
|
|
252
|
-
const busy = /^(.+?)\s+already in progress\.?$/i.exec(value);
|
|
253
|
-
if (busy) return `${sentenceStart(polishNoticeAction(busy[1]))} is already running.`;
|
|
254
|
-
const required = /^(.+?)\s+is required(?:\s+for\s+(.+))?\.?$/i.exec(value);
|
|
255
|
-
if (required) {
|
|
256
|
-
const subject = required[1].trim();
|
|
257
|
-
const target = required[2]?.trim();
|
|
258
|
-
return `${subject}${target ? ` required for ${target}` : ' required'}.`;
|
|
259
|
-
}
|
|
260
|
-
return value;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function toolResultText(content) {
|
|
264
|
-
if (content == null) return '';
|
|
265
|
-
if (typeof content === 'string') return content;
|
|
266
|
-
if (Array.isArray(content)) {
|
|
267
|
-
return content.map((c) => toolResultPartText(c)).filter((t) => t !== '').join('\n');
|
|
268
|
-
}
|
|
269
|
-
if (typeof content === 'object') {
|
|
270
|
-
if (Array.isArray(content.content)) {
|
|
271
|
-
const nested = content.content.map((c) => toolResultPartText(c)).filter((t) => t !== '').join('\n');
|
|
272
|
-
if (nested) return nested;
|
|
273
|
-
} else if (content.content != null && typeof content.content === 'object') {
|
|
274
|
-
const nested = toolResultPartText(content.content);
|
|
275
|
-
if (nested) return nested;
|
|
276
|
-
}
|
|
277
|
-
if (Array.isArray(content.parts)) {
|
|
278
|
-
const nested = content.parts.map((c) => toolResultPartText(c)).filter((t) => t !== '').join('\n');
|
|
279
|
-
if (nested) return nested;
|
|
280
|
-
}
|
|
281
|
-
const fromPart = toolResultPartText(content);
|
|
282
|
-
if (fromPart) return fromPart;
|
|
283
|
-
if (content?.type === 'tool_result') return '';
|
|
284
|
-
if (typeof content.text === 'string') return content.text;
|
|
285
|
-
if (typeof content.content === 'string') return content.content;
|
|
286
|
-
}
|
|
287
|
-
try { return JSON.stringify(content); } catch { return String(content); }
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
const TOOL_RESULT_PART_MAX_DEPTH = 12;
|
|
291
|
-
const TOOL_RESULT_JSON_FALLBACK_MAX = 480;
|
|
292
|
-
// 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
|
-
|
|
91
|
+
// Re-export the shared tool-result/notification helpers so importers (and tests)
|
|
92
|
+
// keep resolving them from engine.mjs unchanged.
|
|
484
93
|
export { toolResultText, toolAggregateDetailFallback, toolGroupedDisplayFallback };
|
|
485
|
-
|
|
486
|
-
export function parseBackgroundTaskEnvelope(text) {
|
|
487
|
-
const value = String(text ?? '').trim();
|
|
488
|
-
if (!/^background task\b/i.test(value)) return null;
|
|
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
|
-
}
|
|
94
|
+
export { parseBackgroundTaskEnvelope };
|
|
660
95
|
|
|
661
96
|
// Ink renders through a maxFps throttle (120fps in index.jsx, ≈8.3ms). A plain
|
|
662
97
|
// setImmediate only yields to the event loop; if Ink already painted within the
|
|
@@ -669,170 +104,6 @@ const yieldToRenderer = () => new Promise((resolve) => {
|
|
|
669
104
|
setTimeout(resolve, RENDER_THROTTLE_FLUSH_MS);
|
|
670
105
|
});
|
|
671
106
|
|
|
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
107
|
function notificationQueueKey(event, text, parsed) {
|
|
837
108
|
const meta = event?.meta && typeof event.meta === 'object' ? event.meta : {};
|
|
838
109
|
const synthetic = parseSyntheticAgentMessage(text);
|
|
@@ -893,33 +164,6 @@ function isExecutionNotification(event, text, parsed) {
|
|
|
893
164
|
return Boolean(parsed?.taskId && /^(?:agent task:|task_id:)/mi.test(String(text || '')));
|
|
894
165
|
}
|
|
895
166
|
|
|
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
|
-
}
|
|
922
|
-
|
|
923
167
|
export async function createEngineSession({
|
|
924
168
|
provider: providerName,
|
|
925
169
|
model,
|
|
@@ -1009,6 +253,7 @@ export async function createEngineSession({
|
|
|
1009
253
|
let state = {
|
|
1010
254
|
items: [],
|
|
1011
255
|
toasts: [],
|
|
256
|
+
progressHint: null,
|
|
1012
257
|
busy: false,
|
|
1013
258
|
commandBusy: false,
|
|
1014
259
|
commandStatus: null,
|
|
@@ -1018,6 +263,14 @@ export async function createEngineSession({
|
|
|
1018
263
|
toolApproval: null,
|
|
1019
264
|
lastTurn: null,
|
|
1020
265
|
stats: createSessionStats(),
|
|
266
|
+
// Incremental derivations published by the engine so App does not scan all
|
|
267
|
+
// transcript items on every change:
|
|
268
|
+
// - activeToolSummary: running Explore/Search active counts + earliest
|
|
269
|
+
// startedAt for the prompt-line status (replaces App.jsx O(n) items scan).
|
|
270
|
+
// - promptHistoryList: newest-first deduped user-prompt history, rebuilt
|
|
271
|
+
// only when a user item is appended (replaces the per-change rescan).
|
|
272
|
+
activeToolSummary: null,
|
|
273
|
+
promptHistoryList: [],
|
|
1021
274
|
...baseRouteState(),
|
|
1022
275
|
displayContextWindow: 0,
|
|
1023
276
|
compactBoundaryTokens: 0,
|
|
@@ -1131,10 +384,85 @@ export async function createEngineSession({
|
|
|
1131
384
|
const id = nextItems[i]?.id;
|
|
1132
385
|
if (id != null) itemIndexById.set(id, i);
|
|
1133
386
|
}
|
|
387
|
+
// Bulk item swap (session load / clear / compact). Derive the prompt-history
|
|
388
|
+
// list from the NEW items and stage it onto state here so App never rescans;
|
|
389
|
+
// the callers that invoke replaceItems always follow with a set({items:...,
|
|
390
|
+
// ...}) that carries fresh references, so this pre-stage does not defeat any
|
|
391
|
+
// emit (the accompanying set() diffs the full patch). A bulk swap also
|
|
392
|
+
// discards the old transcript, so drop any tracked active tool calls.
|
|
393
|
+
activeToolCalls.clear();
|
|
394
|
+
state = { ...state, items: nextItems, promptHistoryList: recomputePromptHistory(nextItems), activeToolSummary: null };
|
|
1134
395
|
return nextItems;
|
|
1135
396
|
};
|
|
1136
397
|
let flushDeferredBeforeImmediatePush = null;
|
|
1137
398
|
let pushingFromDeferredEntry = false;
|
|
399
|
+
// --- Prompt-history list (newest-first, deduped) maintained incrementally ---
|
|
400
|
+
// App previously rebuilt this from state.items on EVERY transcript change
|
|
401
|
+
// (App.jsx recentPromptHistory useMemo). It only changes when a user item is
|
|
402
|
+
// appended, so rebuild it there and on bulk item swaps, publishing to
|
|
403
|
+
// state.promptHistoryList.
|
|
404
|
+
const PROMPT_HISTORY_LIMIT = 50;
|
|
405
|
+
const promptHistoryKey = (value) => String(value || '').trim().replace(/\s+/g, ' ');
|
|
406
|
+
const recomputePromptHistory = (sourceItems = null) => {
|
|
407
|
+
// Pure: derive the newest-first deduped user-prompt history from the given
|
|
408
|
+
// items (defaults to state.items) and RETURN it. Callers decide whether to
|
|
409
|
+
// publish via set() so the immutable-emit contract is preserved.
|
|
410
|
+
const items = Array.isArray(sourceItems) ? sourceItems : (Array.isArray(state.items) ? state.items : []);
|
|
411
|
+
const seen = new Set();
|
|
412
|
+
const history = [];
|
|
413
|
+
for (let i = items.length - 1; i >= 0 && history.length < PROMPT_HISTORY_LIMIT; i -= 1) {
|
|
414
|
+
const item = items[i];
|
|
415
|
+
if (item?.kind !== 'user') continue;
|
|
416
|
+
const text = String(item.text || '').trim();
|
|
417
|
+
const key = promptHistoryKey(text);
|
|
418
|
+
if (!key || seen.has(key)) continue;
|
|
419
|
+
seen.add(key);
|
|
420
|
+
history.push(text);
|
|
421
|
+
}
|
|
422
|
+
return history;
|
|
423
|
+
};
|
|
424
|
+
// --- Active-tool summary (Explore/Search) maintained incrementally ---
|
|
425
|
+
// App previously scanned every transcript item on every change to derive the
|
|
426
|
+
// prompt-line "Exploring N / Searching N" status. Instead the tool lifecycle
|
|
427
|
+
// below tracks per-callId category + started-at in activeToolCalls and derives
|
|
428
|
+
// the small summary from it, publishing state.activeToolSummary only when the
|
|
429
|
+
// aggregate (counts + earliest start) actually changes.
|
|
430
|
+
const activeToolCalls = new Map(); // callKey -> { category, count, startedAt }
|
|
431
|
+
const recomputeActiveToolSummary = () => {
|
|
432
|
+
let exploreCount = 0, exploreStart = 0, searchCount = 0, searchStart = 0;
|
|
433
|
+
for (const rec of activeToolCalls.values()) {
|
|
434
|
+
if (!rec) continue;
|
|
435
|
+
const c = Math.max(1, Number(rec.count || 1));
|
|
436
|
+
const started = Number(rec.startedAt || 0);
|
|
437
|
+
if (rec.category === 'Explore') {
|
|
438
|
+
exploreCount += c;
|
|
439
|
+
if (started > 0 && (exploreStart === 0 || started < exploreStart)) exploreStart = started;
|
|
440
|
+
} else if (rec.category === 'Search') {
|
|
441
|
+
searchCount += c;
|
|
442
|
+
if (started > 0 && (searchStart === 0 || started < searchStart)) searchStart = started;
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
const next = (exploreCount || searchCount)
|
|
446
|
+
? `${exploreCount}:${exploreStart}:${searchCount}:${searchStart}`
|
|
447
|
+
: '';
|
|
448
|
+
const prev = state.activeToolSummary || '';
|
|
449
|
+
if (next !== prev) set({ activeToolSummary: next || null });
|
|
450
|
+
};
|
|
451
|
+
const markToolCallActive = (callKey, category, count, startedAt) => {
|
|
452
|
+
if (!callKey || (category !== 'Explore' && category !== 'Search')) return;
|
|
453
|
+
activeToolCalls.set(callKey, { category, count: Math.max(1, Number(count || 1)), startedAt: Number(startedAt || Date.now()) });
|
|
454
|
+
recomputeActiveToolSummary();
|
|
455
|
+
};
|
|
456
|
+
const markToolCallDone = (callKey) => {
|
|
457
|
+
if (!callKey || !activeToolCalls.has(callKey)) return;
|
|
458
|
+
activeToolCalls.delete(callKey);
|
|
459
|
+
recomputeActiveToolSummary();
|
|
460
|
+
};
|
|
461
|
+
const clearActiveToolSummary = () => {
|
|
462
|
+
if (activeToolCalls.size === 0 && !state.activeToolSummary) return;
|
|
463
|
+
activeToolCalls.clear();
|
|
464
|
+
if (state.activeToolSummary) set({ activeToolSummary: null });
|
|
465
|
+
};
|
|
1138
466
|
const pushItem = (item) => {
|
|
1139
467
|
if (!pushingFromDeferredEntry && flushDeferredBeforeImmediatePush) {
|
|
1140
468
|
flushDeferredBeforeImmediatePush();
|
|
@@ -1142,7 +470,16 @@ export async function createEngineSession({
|
|
|
1142
470
|
const index = state.items.length;
|
|
1143
471
|
const items = [...state.items, item];
|
|
1144
472
|
if (item?.id != null) itemIndexById.set(item.id, index);
|
|
1145
|
-
|
|
473
|
+
if (item?.kind === 'user') {
|
|
474
|
+
// Rebuild the derived history against the NEW list (not yet in state) and
|
|
475
|
+
// publish items + the fresh list in ONE set(). Do NOT pre-assign to state
|
|
476
|
+
// first — set() diffs against the current state, so a pre-assign would make
|
|
477
|
+
// the references identical and skip emit().
|
|
478
|
+
const promptHistoryList = recomputePromptHistory(items);
|
|
479
|
+
set({ items, promptHistoryList });
|
|
480
|
+
} else {
|
|
481
|
+
set({ items });
|
|
482
|
+
}
|
|
1146
483
|
};
|
|
1147
484
|
const upsertSyntheticToolItem = (text, id = nextId(), parsed = null) => {
|
|
1148
485
|
const synthetic = parseSyntheticAgentMessage(text);
|
|
@@ -1198,6 +535,16 @@ export async function createEngineSession({
|
|
|
1198
535
|
pushItem({ kind: 'notice', id, text: value, tone });
|
|
1199
536
|
return id;
|
|
1200
537
|
};
|
|
538
|
+
// Sticky (non-TTL) input-hint-line progress state, for long-running
|
|
539
|
+
// installs (e.g. voice runtime download) that would otherwise spam the
|
|
540
|
+
// 3s toast queue. Distinct from pushToast/pushNotice: it persists across
|
|
541
|
+
// renders until explicitly cleared (setProgressHint('', ...) or a falsy
|
|
542
|
+
// text), and App.jsx's inputHint falls back to it only when no promptHint
|
|
543
|
+
// and no live toast currently cover the same line.
|
|
544
|
+
const setProgressHint = (text, tone = 'info') => {
|
|
545
|
+
const value = String(text ?? '').trim();
|
|
546
|
+
set({ progressHint: value ? { text: value, tone } : null });
|
|
547
|
+
};
|
|
1201
548
|
const toolApprovalQueue = [];
|
|
1202
549
|
let activeToolApproval = null;
|
|
1203
550
|
function normalizeToolApprovalRequest(input = {}, id = nextId()) {
|
|
@@ -1523,6 +870,9 @@ export async function createEngineSession({
|
|
|
1523
870
|
if (!card || card.done) return false;
|
|
1524
871
|
const callId = toolResultCallId(message) || card.callId;
|
|
1525
872
|
if (callId && done.has(callId)) return false;
|
|
873
|
+
// Any resolving call clears its active-summary entry (keyed by the same
|
|
874
|
+
// callKey used at markToolCallActive; card.callId holds it for both branches).
|
|
875
|
+
markToolCallDone(card.callId);
|
|
1526
876
|
// A result for this card arrived (possibly before its deferred push delay
|
|
1527
877
|
// elapsed) — surface the card now so the patch below has a live item and the
|
|
1528
878
|
// fast tool paints a completed card directly, no pending placeholder stage.
|
|
@@ -1551,19 +901,19 @@ export async function createEngineSession({
|
|
|
1551
901
|
const allCalls = [...aggregate.calls.values()];
|
|
1552
902
|
const completed = allCalls.filter((r) => r.resolved).length;
|
|
1553
903
|
const errors = allCalls.filter((r) => r.isError).length;
|
|
1554
|
-
// Collapsed detail
|
|
1555
|
-
//
|
|
904
|
+
// Collapsed detail carries the merged per-call count summary
|
|
905
|
+
// ("512 lines, 6 matches, 3 files") so the finished card answers "how
|
|
906
|
+
// much" without ctrl+o. Failures keep a bare 'N Ok · N Failed' status so
|
|
907
|
+
// an error stays visible while collapsed.
|
|
1556
908
|
const succeeded = completed - errors;
|
|
1557
909
|
const detailText = errors > 0
|
|
1558
910
|
? (succeeded > 0 ? `${succeeded} Ok · ${errors} Failed` : `${errors} Failed`)
|
|
1559
|
-
:
|
|
911
|
+
: formatAggregateDetail(aggregateSummaries(aggregate));
|
|
1560
912
|
const currentItem = state.items.find((it) => it.id === card.itemId);
|
|
1561
913
|
const earlyCompleted = allCalls.filter((r) => r.resolved || r.completedEarly).length;
|
|
1562
914
|
const visualCompleted = Math.max(completed, earlyCompleted, Math.min(allCalls.length, Number(currentItem?.completedCount || 0)));
|
|
1563
915
|
const rawResult = aggregateRawResult(allCalls);
|
|
1564
|
-
//
|
|
1565
|
-
// shows only a status word ('Finished') or, on failure, 'N Failed'. The
|
|
1566
|
-
// numbered+labelled raw (rawResult) is preserved for ctrl+o expansion.
|
|
916
|
+
// The numbered+labelled raw (rawResult) is preserved for ctrl+o expansion.
|
|
1567
917
|
const displayDetail = detailText;
|
|
1568
918
|
patchItem(card.itemId, {
|
|
1569
919
|
result: displayDetail,
|
|
@@ -1652,25 +1002,30 @@ export async function createEngineSession({
|
|
|
1652
1002
|
if (aggregate && card.itemId === aggregate.itemId) {
|
|
1653
1003
|
const allCalls = [...aggregate.calls.values()];
|
|
1654
1004
|
// Never let a call that truly never resolved be presented as a real
|
|
1655
|
-
// completion. Stamp it resolved
|
|
1656
|
-
//
|
|
1657
|
-
//
|
|
1005
|
+
// completion. Stamp it resolved so completedCount reflects an honest
|
|
1006
|
+
// (if degenerate) accounting instead of manufacturing success out of
|
|
1007
|
+
// a call that never came back. A record already marked completedEarly
|
|
1008
|
+
// (via __earlyNotify) already carries a real isError/resultText/summary
|
|
1009
|
+
// from its actual result — preserve those; only blank-fill for calls
|
|
1010
|
+
// truly never heard from (no completedEarly, no resolved).
|
|
1658
1011
|
for (const rec of allCalls) {
|
|
1659
1012
|
if (rec.resolved) continue;
|
|
1660
1013
|
rec.resolved = true;
|
|
1661
|
-
rec.
|
|
1662
|
-
|
|
1014
|
+
if (!rec.completedEarly) {
|
|
1015
|
+
rec.isError = false;
|
|
1016
|
+
rec.resultText = rec.resultText || '';
|
|
1017
|
+
}
|
|
1663
1018
|
}
|
|
1664
1019
|
const completed = allCalls.filter((r) => r.resolved).length;
|
|
1665
1020
|
const totalCompleted = completed;
|
|
1666
1021
|
const errors = allCalls.filter((r) => r.isError).length;
|
|
1667
1022
|
const succeeded = completed - errors;
|
|
1668
1023
|
const rawResult = aggregateRawResult(allCalls);
|
|
1669
|
-
// Collapsed detail
|
|
1670
|
-
// bare 'N Failed' status.
|
|
1024
|
+
// Collapsed detail carries the merged per-call count summary; failures
|
|
1025
|
+
// keep a bare 'N Ok · N Failed' status. Raw is kept for ctrl+o.
|
|
1671
1026
|
let displayDetail = errors > 0
|
|
1672
1027
|
? (succeeded > 0 ? `${succeeded} Ok · ${errors} Failed` : `${errors} Failed`)
|
|
1673
|
-
:
|
|
1028
|
+
: formatAggregateDetail(aggregateSummaries(aggregate));
|
|
1674
1029
|
if (cancelled) {
|
|
1675
1030
|
// Cancelled aggregates MUST keep the [status: cancelled] marker on the
|
|
1676
1031
|
// result so terminalStatus parsing resolves to 'cancelled'. Only normal
|
|
@@ -1721,6 +1076,7 @@ export async function createEngineSession({
|
|
|
1721
1076
|
activePromptRestore = {
|
|
1722
1077
|
text: String(displayText || '').trim(),
|
|
1723
1078
|
pastedImages: options.pastedImages && typeof options.pastedImages === 'object' ? options.pastedImages : null,
|
|
1079
|
+
pastedTexts: options.pastedTexts && typeof options.pastedTexts === 'object' ? options.pastedTexts : null,
|
|
1724
1080
|
onCommitted: typeof options.onCommitted === 'function' ? options.onCommitted : null,
|
|
1725
1081
|
restorable: options.restorable !== false,
|
|
1726
1082
|
submittedIds,
|
|
@@ -1849,6 +1205,7 @@ export async function createEngineSession({
|
|
|
1849
1205
|
activePromptRestore.committed = true;
|
|
1850
1206
|
activePromptRestore.requeueEntries = [];
|
|
1851
1207
|
activePromptRestore.pastedImages = null;
|
|
1208
|
+
activePromptRestore.pastedTexts = null;
|
|
1852
1209
|
}
|
|
1853
1210
|
};
|
|
1854
1211
|
|
|
@@ -1884,11 +1241,11 @@ export async function createEngineSession({
|
|
|
1884
1241
|
const completed = allCalls.filter((r) => r.resolved).length;
|
|
1885
1242
|
const succeeded = completed - errors;
|
|
1886
1243
|
const rawResult = aggregateRawResult(allCalls);
|
|
1887
|
-
//
|
|
1888
|
-
//
|
|
1244
|
+
// Merged count summary (see patchToolCardResult); failures keep
|
|
1245
|
+
// 'N Ok · N Failed'. Raw preserved for ctrl+o expansion.
|
|
1889
1246
|
const displayDetail = errors > 0
|
|
1890
1247
|
? (succeeded > 0 ? `${succeeded} Ok · ${errors} Failed` : `${errors} Failed`)
|
|
1891
|
-
:
|
|
1248
|
+
: formatAggregateDetail(aggregateSummaries(aggregate));
|
|
1892
1249
|
patchItem(aggregate.itemId, {
|
|
1893
1250
|
result: displayDetail,
|
|
1894
1251
|
text: displayDetail,
|
|
@@ -2046,6 +1403,17 @@ export async function createEngineSession({
|
|
|
2046
1403
|
let _pendingThinkFlush = false; // true when a thinking update is queued
|
|
2047
1404
|
let _pendingThinkingLastEndedAt = 0;
|
|
2048
1405
|
let compactingActive = false;
|
|
1406
|
+
// Engine-local streaming scalars. Neither responseLength nor thinkingText is
|
|
1407
|
+
// rendered per-token by any consumer: App reads state.thinking only as a
|
|
1408
|
+
// boolean (App.jsx `!!(state.thinking || liveSpinner?.thinking)`) and the
|
|
1409
|
+
// Spinner takes outputTokens, not responseLength. So we keep these growing
|
|
1410
|
+
// values in engine-local vars and publish to the store only on a visible
|
|
1411
|
+
// transition (thinking on↔off), a completed visible text line, tool/usage
|
|
1412
|
+
// updates, or finalization — not on every 8ms streaming flush.
|
|
1413
|
+
let _publishedThinkingActive = false; // last thinking boolean pushed to store
|
|
1414
|
+
// responseLength is only consumed at finalize as an outputTokens fallback
|
|
1415
|
+
// (Math.round(responseLength/4)); we refresh state.spinner.responseLength on
|
|
1416
|
+
// visible-line flush and finalize so that fallback stays valid.
|
|
2049
1417
|
|
|
2050
1418
|
const flushStreamBatch = () => {
|
|
2051
1419
|
if (_batchTimer !== null) {
|
|
@@ -2090,24 +1458,51 @@ export async function createEngineSession({
|
|
|
2090
1458
|
}
|
|
2091
1459
|
}
|
|
2092
1460
|
}
|
|
1461
|
+
// Only touch the spinner when there is a real reason: a visible-line
|
|
1462
|
+
// change (patch.items set above), a thinking→responding transition, or a
|
|
1463
|
+
// pending thinking end timestamp. Refresh responseLength here so the
|
|
1464
|
+
// finalize outputTokens fallback stays valid without a per-token push.
|
|
2093
1465
|
const responseLengthVal = assistantText.length + thinkingText.length;
|
|
2094
|
-
|
|
1466
|
+
const visibleLineChanged = patch.items !== undefined;
|
|
1467
|
+
const thinkingTransition = _publishedThinkingActive === true; // was thinking, now responding
|
|
1468
|
+
if (state.spinner && (visibleLineChanged || thinkingTransition || _pendingThinkingLastEndedAt)) {
|
|
2095
1469
|
patch.spinner = { ...state.spinner, responseLength: responseLengthVal, thinking: false, thinkingLastEndedAt: _pendingThinkingLastEndedAt || state.spinner.thinkingLastEndedAt, mode: compactingActive ? 'compacting' : 'responding' };
|
|
1470
|
+
_publishedThinkingActive = false;
|
|
2096
1471
|
}
|
|
2097
1472
|
if (Object.keys(patch).length > 0) set(patch);
|
|
2098
1473
|
_pendingThinkingLastEndedAt = 0;
|
|
2099
1474
|
}
|
|
2100
1475
|
if (_pendingThinkFlush) {
|
|
2101
1476
|
_pendingThinkFlush = false;
|
|
2102
|
-
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
1477
|
+
// App only consumes state.thinking as a boolean and the Spinner only
|
|
1478
|
+
// reads the thinking flag + timing anchors — none of them render the
|
|
1479
|
+
// growing thinkingText. So publish the thinking boolean only on the
|
|
1480
|
+
// OFF→ON transition (or when compacting toggles the flag), not on every
|
|
1481
|
+
// 8ms reasoning chunk. The full thinkingText stays engine-local and is
|
|
1482
|
+
// emitted at finalize via the normal spinner/thinking teardown.
|
|
1483
|
+
const nextThinkingActive = !compactingActive;
|
|
1484
|
+
// Skip the push when the published thinking boolean is unchanged: neither
|
|
1485
|
+
// the growing thinkingText nor responseLength is rendered per-token, and
|
|
1486
|
+
// the Spinner derives its live elapsed from the (already-published)
|
|
1487
|
+
// thinkingSegmentStartedAt anchor. Applies to both thinking and
|
|
1488
|
+
// compacting steady state.
|
|
1489
|
+
if (nextThinkingActive === _publishedThinkingActive) {
|
|
1490
|
+
// no-op: boolean unchanged
|
|
1491
|
+
} else {
|
|
1492
|
+
const responseLengthVal = assistantText.length + thinkingText.length;
|
|
1493
|
+
const thinkingElapsedMs = accumulatedThinkingMs + (thinkingSegmentStartedAt ? Math.max(0, Date.now() - thinkingSegmentStartedAt) : 0);
|
|
1494
|
+
// state.thinking stays a truthy sentinel while active; consumers read it
|
|
1495
|
+
// as a boolean. Keep the value stable (thinkingText) so a late consumer
|
|
1496
|
+
// still sees real text, but only push on transition.
|
|
1497
|
+
const patch = { thinking: compactingActive ? null : thinkingText };
|
|
1498
|
+
if (state.spinner) {
|
|
1499
|
+
patch.spinner = compactingActive
|
|
1500
|
+
? { ...state.spinner, responseLength: responseLengthVal, thinking: false, thinkingAccumulatedMs: accumulatedThinkingMs, thinkingElapsedMs, thinkingLastEndedAt: state.spinner.thinkingLastEndedAt || 0, mode: 'compacting' }
|
|
1501
|
+
: { ...state.spinner, responseLength: responseLengthVal, thinking: true, thinkingStartedAt, thinkingSegmentStartedAt, thinkingAccumulatedMs: accumulatedThinkingMs, thinkingElapsedMs, thinkingLastEndedAt: 0, mode: 'thinking' };
|
|
1502
|
+
}
|
|
1503
|
+
set(patch);
|
|
1504
|
+
_publishedThinkingActive = nextThinkingActive;
|
|
2109
1505
|
}
|
|
2110
|
-
set(patch);
|
|
2111
1506
|
}
|
|
2112
1507
|
};
|
|
2113
1508
|
|
|
@@ -2122,6 +1517,8 @@ export async function createEngineSession({
|
|
|
2122
1517
|
const markToolCardCompletedState = (callId, message) => {
|
|
2123
1518
|
const card = cardByCallId.get(callId);
|
|
2124
1519
|
if (!card) return;
|
|
1520
|
+
// Early completion also clears the active-summary entry.
|
|
1521
|
+
markToolCallDone(card.callId);
|
|
2125
1522
|
const aggregate = card.aggregate;
|
|
2126
1523
|
if (aggregate && card.itemId === aggregate.itemId) {
|
|
2127
1524
|
const callRec = aggregate.calls.get(callId);
|
|
@@ -2194,7 +1591,11 @@ export async function createEngineSession({
|
|
|
2194
1591
|
assistantText = '';
|
|
2195
1592
|
const value = String(text || '').trim();
|
|
2196
1593
|
if (value) {
|
|
2197
|
-
|
|
1594
|
+
// Any non-tool transcript item is a block boundary: seal the
|
|
1595
|
+
// aggregate continuation (not just finalize headers) so a later
|
|
1596
|
+
// same-category tool call opens a fresh card instead of reusing
|
|
1597
|
+
// one whose count would then change ABOVE this steered user item.
|
|
1598
|
+
clearAggregateContinuation();
|
|
2198
1599
|
pushUserOrSyntheticItem(value);
|
|
2199
1600
|
}
|
|
2200
1601
|
},
|
|
@@ -2208,6 +1609,7 @@ export async function createEngineSession({
|
|
|
2208
1609
|
if (thinkingText && state.thinking) {
|
|
2209
1610
|
const thinkingLastEndedAt = closeThinkingSegment();
|
|
2210
1611
|
set({ thinking: null, spinner: state.spinner ? { ...state.spinner, thinking: false, thinkingAccumulatedMs: accumulatedThinkingMs, thinkingLastEndedAt, mode: 'tool-use' } : state.spinner });
|
|
1612
|
+
_publishedThinkingActive = false;
|
|
2211
1613
|
} else if (state.spinner) {
|
|
2212
1614
|
set({ spinner: { ...state.spinner, mode: 'tool-use' } });
|
|
2213
1615
|
}
|
|
@@ -2233,6 +1635,13 @@ export async function createEngineSession({
|
|
|
2233
1635
|
const bucket = aggregateBucketForCategory(category);
|
|
2234
1636
|
const callId = toolCallId(c);
|
|
2235
1637
|
const callKey = callId || `__tool_${toolCards.length}_${i}`;
|
|
1638
|
+
// The old App scan counted multi-pattern calls via
|
|
1639
|
+
// aggregateToolCategoryEntry(...).count, not a flat 1. Derive the same
|
|
1640
|
+
// count here so the incremental Explore/Search summary matches.
|
|
1641
|
+
const activeCount = Number(aggregateToolCategoryEntry(name, args, category)?.count || 1);
|
|
1642
|
+
// Track Explore/Search calls as active for the incremental prompt-
|
|
1643
|
+
// line summary; cleared when their result lands or the turn ends.
|
|
1644
|
+
markToolCallActive(callKey, category, activeCount, Date.now());
|
|
2236
1645
|
|
|
2237
1646
|
if (!bucket) {
|
|
2238
1647
|
const itemId = nextId();
|
|
@@ -2321,6 +1730,11 @@ export async function createEngineSession({
|
|
|
2321
1730
|
},
|
|
2322
1731
|
onCompactEvent: (event) => {
|
|
2323
1732
|
flushStreamBatch();
|
|
1733
|
+
// Non-tool transcript item — same block-boundary rule as the
|
|
1734
|
+
// steered user item above: seal any live aggregate first so a
|
|
1735
|
+
// later same-category tool call doesn't reuse a card whose count
|
|
1736
|
+
// would then change above this statusdone item.
|
|
1737
|
+
clearAggregateContinuation();
|
|
2324
1738
|
pushItem({
|
|
2325
1739
|
kind: 'statusdone',
|
|
2326
1740
|
id: nextId(),
|
|
@@ -2335,6 +1749,7 @@ export async function createEngineSession({
|
|
|
2335
1749
|
compactingActive = true;
|
|
2336
1750
|
const thinkingLastEndedAt = closeThinkingSegment();
|
|
2337
1751
|
_pendingThinkFlush = false;
|
|
1752
|
+
_publishedThinkingActive = false; // compacting cleared the thinking flag
|
|
2338
1753
|
set({
|
|
2339
1754
|
thinking: null,
|
|
2340
1755
|
spinner: {
|
|
@@ -2362,7 +1777,11 @@ export async function createEngineSession({
|
|
|
2362
1777
|
if (!textChunk) return;
|
|
2363
1778
|
markPromptCommitted();
|
|
2364
1779
|
const thinkingLastEndedAt = closeThinkingSegment();
|
|
2365
|
-
|
|
1780
|
+
// Drop any queued think-flush too: it would otherwise re-publish
|
|
1781
|
+
// spinner.thinking:true from flushStreamBatch and resurrect the
|
|
1782
|
+
// indicator after we cleared it here.
|
|
1783
|
+
_pendingThinkFlush = false;
|
|
1784
|
+
if (state.thinking) { set({ thinking: null }); _publishedThinkingActive = false; } // collapse thinking panel immediately, no batch delay
|
|
2366
1785
|
assistantText += textChunk;
|
|
2367
1786
|
currentAssistantText += textChunk;
|
|
2368
1787
|
// Accumulate text and schedule a batched flush (≤1 render per
|
|
@@ -2391,7 +1810,8 @@ export async function createEngineSession({
|
|
|
2391
1810
|
if (currentAssistantText.trim()) return;
|
|
2392
1811
|
markPromptCommitted();
|
|
2393
1812
|
closeThinkingSegment();
|
|
2394
|
-
|
|
1813
|
+
_pendingThinkFlush = false; // see onTextDelta: prevent a stale think flush resurrecting the indicator
|
|
1814
|
+
if (state.thinking) { set({ thinking: null }); _publishedThinkingActive = false; }
|
|
2395
1815
|
assistantText += full;
|
|
2396
1816
|
currentAssistantText += full;
|
|
2397
1817
|
_pendingTextFlush = true;
|
|
@@ -2477,7 +1897,15 @@ export async function createEngineSession({
|
|
|
2477
1897
|
closeThinkingSegment();
|
|
2478
1898
|
const elapsedMs = Date.now() - startedAt;
|
|
2479
1899
|
const thinkingElapsedMs = thinkingStartedAt ? accumulatedThinkingMs : 0;
|
|
2480
|
-
|
|
1900
|
+
// responseLength is engine-local now (not pushed per-token), so compute the
|
|
1901
|
+
// fallback from the live accumulator instead of the possibly-stale
|
|
1902
|
+
// state.spinner.responseLength. Final-only / non-streaming turns never
|
|
1903
|
+
// accumulate `assistantText` (only currentAssistantText is set at the
|
|
1904
|
+
// finalize reconcile above), so take the larger of the two text sources so
|
|
1905
|
+
// a no-usage turn still estimates tokens from the final content.
|
|
1906
|
+
const finalAssistantLen = Math.max(assistantText.length, currentAssistantText.length);
|
|
1907
|
+
const finalResponseLength = finalAssistantLen + thinkingText.length;
|
|
1908
|
+
const finalOutputTokens = Math.max(0, Number(state.spinner?.outputTokens || 0), Math.round(finalResponseLength / 4));
|
|
2481
1909
|
const turnStatus = cancelled ? 'cancelled' : 'done';
|
|
2482
1910
|
const resultContent = askResult?.content != null ? String(askResult.content).trim() : '';
|
|
2483
1911
|
const assistantOutput = (currentAssistantText || assistantText || '').trim();
|
|
@@ -2510,6 +1938,8 @@ export async function createEngineSession({
|
|
|
2510
1938
|
});
|
|
2511
1939
|
flushDeferredExecutionPendingResumeKick();
|
|
2512
1940
|
}
|
|
1941
|
+
clearActiveToolSummary();
|
|
1942
|
+
_publishedThinkingActive = false; // turn teardown cleared state.thinking
|
|
2513
1943
|
return cancelled ? 'cancelled' : 'done';
|
|
2514
1944
|
}
|
|
2515
1945
|
|
|
@@ -2526,6 +1956,7 @@ export async function createEngineSession({
|
|
|
2526
1956
|
text: displayText,
|
|
2527
1957
|
content: text,
|
|
2528
1958
|
pastedImages: options.pastedImages && typeof options.pastedImages === 'object' ? options.pastedImages : null,
|
|
1959
|
+
pastedTexts: options.pastedTexts && typeof options.pastedTexts === 'object' ? options.pastedTexts : null,
|
|
2529
1960
|
onCommitted: typeof options.onCommitted === 'function' ? options.onCommitted : null,
|
|
2530
1961
|
mode,
|
|
2531
1962
|
priority,
|
|
@@ -2539,6 +1970,7 @@ export async function createEngineSession({
|
|
|
2539
1970
|
const ids = new Set(entries.map((entry) => entry.id));
|
|
2540
1971
|
const keys = entries.map((entry) => entry.key).filter(Boolean);
|
|
2541
1972
|
for (const key of keys) pendingNotificationKeys.delete(key);
|
|
1973
|
+
for (const key of keys) displayedExecutionNotificationKeys.delete(key);
|
|
2542
1974
|
const queued = state.queued.filter((q) => !ids.has(q.id));
|
|
2543
1975
|
if (queued.length !== state.queued.length) set({ queued });
|
|
2544
1976
|
}
|
|
@@ -2617,9 +2049,11 @@ export async function createEngineSession({
|
|
|
2617
2049
|
}
|
|
2618
2050
|
const nonEditable = batch.filter((entry) => !isQueuedEntryEditable(entry));
|
|
2619
2051
|
const batchPastedImages = mergePastedImages(batch);
|
|
2052
|
+
const batchPastedTexts = mergePastedTexts(batch);
|
|
2620
2053
|
const turnStatus = await runTurn(merged, {
|
|
2621
2054
|
displayText: batch.map((entry) => entry.text).filter((text) => String(text || '').trim()).join('\n'),
|
|
2622
2055
|
pastedImages: batchPastedImages,
|
|
2056
|
+
pastedTexts: batchPastedTexts,
|
|
2623
2057
|
onCommitted: () => callCommitCallbacks(batch),
|
|
2624
2058
|
submittedIds: [...ids],
|
|
2625
2059
|
restorable: nonEditable.length === 0,
|
|
@@ -2747,7 +2181,7 @@ export async function createEngineSession({
|
|
|
2747
2181
|
removeQueuedEntries(queued);
|
|
2748
2182
|
const queuedText = queued.map((item) => item.text).filter((text) => String(text || '').trim()).join('\n');
|
|
2749
2183
|
const combinedText = [queuedText, String(currentText || '')].filter((text) => text.trim()).join('\n');
|
|
2750
|
-
return { count: queued.length, text: combinedText, pastedImages: mergePastedImages(queued) };
|
|
2184
|
+
return { count: queued.length, text: combinedText, pastedImages: mergePastedImages(queued), pastedTexts: mergePastedTexts(queued) };
|
|
2751
2185
|
}
|
|
2752
2186
|
|
|
2753
2187
|
const resetStats = () => {
|
|
@@ -2762,6 +2196,8 @@ export async function createEngineSession({
|
|
|
2762
2196
|
state.spinner = null;
|
|
2763
2197
|
state.lastTurn = null;
|
|
2764
2198
|
state.busy = false;
|
|
2199
|
+
pendingNotificationKeys.clear();
|
|
2200
|
+
displayedExecutionNotificationKeys.clear();
|
|
2765
2201
|
};
|
|
2766
2202
|
const resetTuiForPendingSessionReset = () => {
|
|
2767
2203
|
pendingSessionReset = true;
|
|
@@ -2929,8 +2365,15 @@ export async function createEngineSession({
|
|
|
2929
2365
|
set({ commandBusy: true });
|
|
2930
2366
|
try {
|
|
2931
2367
|
const next = runtime.setCompactionSettings?.(input) || {};
|
|
2932
|
-
syncContextStats({ allowEstimated: true });
|
|
2933
2368
|
set({ ...routeState(), stats: { ...state.stats } });
|
|
2369
|
+
// Context-stats recompute (transcript scan + per-message JSON
|
|
2370
|
+
// stringify) is the secondary hitch source on this toggle; defer it
|
|
2371
|
+
// off the key-handler tick so Ink repaints the setting change first.
|
|
2372
|
+
// Stats become eventually consistent on the next tick/repaint.
|
|
2373
|
+
setTimeout(() => {
|
|
2374
|
+
syncContextStats({ allowEstimated: true });
|
|
2375
|
+
set({ stats: { ...state.stats } });
|
|
2376
|
+
}, 0);
|
|
2934
2377
|
return next;
|
|
2935
2378
|
} finally {
|
|
2936
2379
|
set({ commandBusy: false });
|
|
@@ -2944,8 +2387,14 @@ export async function createEngineSession({
|
|
|
2944
2387
|
set({ commandBusy: true });
|
|
2945
2388
|
try {
|
|
2946
2389
|
const next = await runtime.setMemoryEnabled?.(enabled);
|
|
2947
|
-
syncContextStats({ allowEstimated: true });
|
|
2948
2390
|
set({ ...routeState(), stats: { ...state.stats } });
|
|
2391
|
+
// Deferred for the same reason as setCompactionSettings above: keep
|
|
2392
|
+
// the recompute off the key-handler tick so the toggle repaints
|
|
2393
|
+
// immediately; stats catch up right after.
|
|
2394
|
+
setTimeout(() => {
|
|
2395
|
+
syncContextStats({ allowEstimated: true });
|
|
2396
|
+
set({ stats: { ...state.stats } });
|
|
2397
|
+
}, 0);
|
|
2949
2398
|
return next;
|
|
2950
2399
|
} finally {
|
|
2951
2400
|
set({ commandBusy: false });
|
|
@@ -3278,6 +2727,7 @@ export async function createEngineSession({
|
|
|
3278
2727
|
const canRestore = restoreState?.restorable && !hasPendingSteering;
|
|
3279
2728
|
const restoreText = canRestore ? restoreState.text : '';
|
|
3280
2729
|
const restorePastedImages = canRestore && restoreState?.pastedImages ? restoreState.pastedImages : null;
|
|
2730
|
+
const restorePastedTexts = canRestore && restoreState?.pastedTexts ? restoreState.pastedTexts : null;
|
|
3281
2731
|
// When steering suppresses the restore, the interrupted prompt's pasted
|
|
3282
2732
|
// images never get committed (onCommitted won't fire) nor re-installed into
|
|
3283
2733
|
// the draft, so hand them back for cleanup to avoid a stale `[Image #id]`
|
|
@@ -3285,6 +2735,9 @@ export async function createEngineSession({
|
|
|
3285
2735
|
const discardPastedImages = restoreState?.restorable && hasPendingSteering && restoreState?.pastedImages
|
|
3286
2736
|
? restoreState.pastedImages
|
|
3287
2737
|
: null;
|
|
2738
|
+
const discardPastedTexts = restoreState?.restorable && hasPendingSteering && restoreState?.pastedTexts
|
|
2739
|
+
? restoreState.pastedTexts
|
|
2740
|
+
: null;
|
|
3288
2741
|
const requeueEntries = restoreState && !restoreState.committed && Array.isArray(restoreState.requeueEntries)
|
|
3289
2742
|
? restoreState.requeueEntries.slice()
|
|
3290
2743
|
: [];
|
|
@@ -3306,7 +2759,7 @@ export async function createEngineSession({
|
|
|
3306
2759
|
restoreState.restorable = false;
|
|
3307
2760
|
restoreState.requeueEntries = [];
|
|
3308
2761
|
}
|
|
3309
|
-
return { aborted, restoreText, pastedImages: restorePastedImages, discardPastedImages };
|
|
2762
|
+
return { aborted, restoreText, pastedImages: restorePastedImages, discardPastedImages, pastedTexts: restorePastedTexts, discardPastedTexts };
|
|
3310
2763
|
},
|
|
3311
2764
|
resolveToolApproval: (id, decision = {}) => {
|
|
3312
2765
|
const approved = decision === true || decision?.approved === true;
|
|
@@ -3368,8 +2821,14 @@ export async function createEngineSession({
|
|
|
3368
2821
|
set({ commandBusy: true });
|
|
3369
2822
|
try {
|
|
3370
2823
|
const result = await runtime.setOutputStyle?.(styleId);
|
|
3371
|
-
|
|
2824
|
+
resetStats();
|
|
3372
2825
|
set({ ...routeState(), stats: { ...state.stats } });
|
|
2826
|
+
// Defer the context recompute (transcript scan) off this tick so
|
|
2827
|
+
// the style change repaints immediately; stats settle right after.
|
|
2828
|
+
setTimeout(() => {
|
|
2829
|
+
syncContextStats({ allowEstimated: true });
|
|
2830
|
+
set({ stats: { ...state.stats } });
|
|
2831
|
+
}, 0);
|
|
3373
2832
|
return result;
|
|
3374
2833
|
} finally {
|
|
3375
2834
|
set({ commandBusy: false });
|
|
@@ -3603,6 +3062,7 @@ export async function createEngineSession({
|
|
|
3603
3062
|
}
|
|
3604
3063
|
},
|
|
3605
3064
|
pushNotice,
|
|
3065
|
+
setProgressHint,
|
|
3606
3066
|
clear: async () => {
|
|
3607
3067
|
if (state.commandBusy) return false;
|
|
3608
3068
|
set({ commandBusy: true });
|