mixdog 0.9.55 → 0.9.59
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 +3 -1
- package/scripts/_smoke_wd.mjs +7 -0
- package/scripts/agent-terminal-reap-test.mjs +127 -2
- package/scripts/compact-file-reattach-test.mjs +88 -0
- package/scripts/compact-pressure-test.mjs +45 -21
- package/scripts/compact-recall-digest-test.mjs +57 -0
- package/scripts/compact-smoke.mjs +35 -39
- package/scripts/desktop-session-bridge-test.mjs +271 -9
- package/scripts/generate-oc-icons.mjs +11 -0
- package/scripts/live-share-test.mjs +148 -0
- package/scripts/live-worker-smoke.mjs +1 -1
- package/scripts/max-output-recovery-test.mjs +12 -1
- package/scripts/mouse-tracking-restore-smoke.mjs +107 -0
- package/scripts/provider-admission-scheduler-test.mjs +100 -0
- package/scripts/provider-contract-test.mjs +49 -0
- package/scripts/resource-admission-test.mjs +5 -2
- package/scripts/session-ingest-compaction-smoke.mjs +38 -0
- package/scripts/steering-drain-buckets-test.mjs +15 -0
- package/scripts/submit-commandbusy-race-test.mjs +54 -3
- package/scripts/tool-smoke.mjs +2 -3
- package/scripts/tui-ambiguous-width-test.mjs +113 -0
- package/scripts/tui-runtime-load-bench.mjs +5 -0
- package/scripts/tui-transcript-perf-test.mjs +167 -0
- package/src/app.mjs +23 -0
- package/src/cli.mjs +6 -0
- package/src/lib/keychain-cjs.cjs +70 -20
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +4 -2
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +18 -17
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +111 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +61 -2
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +13 -4
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +43 -2
- package/src/runtime/agent/orchestrator/session/compact/file-reattach.mjs +112 -0
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +203 -49
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +6 -2
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +110 -15
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +7 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +109 -2
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -7
- package/src/runtime/agent/orchestrator/session/manager/idle-cleanup.mjs +39 -39
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +108 -4
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +19 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +22 -11
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +132 -24
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager.mjs +16 -1
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +39 -21
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +18 -0
- package/src/runtime/agent/orchestrator/session/store/paths-heartbeat.mjs +91 -1
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +14 -33
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +169 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +474 -42
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +17 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +36 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +249 -11
- package/src/runtime/channels/data/voice-runtime-manifest.json +21 -5
- package/src/runtime/channels/lib/scheduler.mjs +8 -6
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +71 -8
- package/src/runtime/channels/lib/voice-transcription.mjs +2 -2
- package/src/runtime/channels/lib/whisper-language.mjs +4 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +7 -0
- package/src/runtime/memory/lib/embedding-worker.mjs +20 -0
- package/src/runtime/memory/lib/query-handlers.mjs +10 -4
- package/src/runtime/memory/lib/recall-format.mjs +43 -2
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +90 -64
- package/src/runtime/shared/err-text.mjs +4 -1
- package/src/runtime/shared/resource-admission.mjs +11 -0
- package/src/runtime/shared/schedule-model-ref.mjs +28 -0
- package/src/runtime/shared/schedule-session-run.mjs +63 -0
- package/src/runtime/shared/schedules-db.mjs +8 -3
- package/src/runtime/shared/tool-result-summary.mjs +4 -1
- package/src/runtime/shared/tool-surface.mjs +7 -7
- package/src/runtime/shared/transcript-writer.mjs +17 -0
- package/src/session-runtime/channel-config-api.mjs +11 -0
- package/src/session-runtime/config-helpers.mjs +9 -6
- package/src/session-runtime/context-status.mjs +80 -3
- package/src/session-runtime/lifecycle-api.mjs +154 -40
- package/src/session-runtime/provider-auth-api.mjs +21 -2
- package/src/session-runtime/provider-usage.mjs +4 -1
- package/src/session-runtime/resource-api.mjs +12 -11
- package/src/session-runtime/runtime-core.mjs +45 -11
- package/src/session-runtime/session-text.mjs +56 -6
- package/src/session-runtime/session-turn-api.mjs +70 -2
- package/src/session-runtime/tool-catalog.mjs +2 -1
- package/src/session-runtime/workflow-agents-api.mjs +2 -2
- package/src/standalone/agent-tool/render.mjs +5 -1
- package/src/standalone/agent-tool.mjs +63 -3
- package/src/standalone/channel-admin.mjs +19 -3
- package/src/standalone/channel-daemon.mjs +40 -0
- package/src/tui/app/resume-picker.mjs +5 -1
- package/src/tui/app/settings-picker.mjs +19 -0
- package/src/tui/app/transcript-window.mjs +45 -8
- package/src/tui/app/use-mouse-input.mjs +101 -31
- package/src/tui/app/use-transcript-window.mjs +53 -9
- package/src/tui/components/ToolExecution.jsx +3 -4
- package/src/tui/components/TranscriptItem.jsx +3 -0
- package/src/tui/dist/index.mjs +17391 -14920
- package/src/tui/engine/context-state.mjs +10 -1
- package/src/tui/engine/live-share.mjs +390 -0
- package/src/tui/engine/queue-helpers.mjs +23 -0
- package/src/tui/engine/session-api-ext.mjs +439 -40
- package/src/tui/engine/session-api.mjs +7 -1
- package/src/tui/engine/session-flow.mjs +21 -7
- package/src/tui/engine/tool-card-results.mjs +3 -1
- package/src/tui/engine/turn.mjs +57 -4
- package/src/tui/engine.mjs +211 -7
- package/src/tui/index.jsx +2 -0
- package/src/tui/lib/voice-setup.mjs +10 -4
- package/src/tui/paste-attachments.mjs +4 -1
- package/src/vendor/statusline/src/gateway/session-routes.mjs +24 -1
- package/src/workflows/default/WORKFLOW.md +3 -0
- package/vendor/ink/build/display-width.js +14 -0
- package/vendor/ink/build/output.js +24 -3
- package/vendor/ink/build/wrap-text.d.ts +2 -0
- package/vendor/ink/build/wrap-text.js +45 -4
|
@@ -7,10 +7,313 @@ import { toolResultText } from './tool-result-text.mjs';
|
|
|
7
7
|
import { parseSyntheticAgentMessage } from './agent-envelope.mjs';
|
|
8
8
|
import { flushTuiSteeringPersist } from './tui-steering-persist.mjs';
|
|
9
9
|
import { getVoiceStatus, toggleVoice } from '../lib/voice-setup.mjs';
|
|
10
|
+
import { aggregateToolCategoryEntry, aggregateDoneCategories, classifyToolCategory, formatAggregateDetail, summarizeToolResult } from '../../runtime/shared/tool-surface.mjs';
|
|
11
|
+
import { aggregateBucketForCategory, aggregateRawResult, failureDetailText, shellCommandExitCode } from './tool-result-status.mjs';
|
|
12
|
+
|
|
13
|
+
export function restoredTranscriptMetadata(message) {
|
|
14
|
+
const value = message?.meta?.transcript;
|
|
15
|
+
if (!value || typeof value !== 'object') return {};
|
|
16
|
+
const completionValue = value.completion && typeof value.completion === 'object'
|
|
17
|
+
? value.completion
|
|
18
|
+
: null;
|
|
19
|
+
const completionStatus = typeof completionValue?.status === 'string'
|
|
20
|
+
? completionValue.status
|
|
21
|
+
: '';
|
|
22
|
+
const completionElapsedMs = Number(completionValue?.elapsedMs);
|
|
23
|
+
const completion = completionValue && completionStatus && Number.isFinite(completionElapsedMs)
|
|
24
|
+
? {
|
|
25
|
+
status: completionStatus,
|
|
26
|
+
elapsedMs: Math.max(0, completionElapsedMs),
|
|
27
|
+
...(typeof completionValue.verb === 'string' && completionValue.verb
|
|
28
|
+
? { verb: completionValue.verb }
|
|
29
|
+
: {}),
|
|
30
|
+
}
|
|
31
|
+
: null;
|
|
32
|
+
return {
|
|
33
|
+
...(Number.isFinite(Number(value.at)) ? { at: Number(value.at) } : {}),
|
|
34
|
+
...(typeof value.model === 'string' && value.model ? { model: value.model } : {}),
|
|
35
|
+
...(typeof value.provider === 'string' && value.provider ? { provider: value.provider } : {}),
|
|
36
|
+
...(typeof value.agent === 'string' && value.agent ? { agent: value.agent } : {}),
|
|
37
|
+
...(completion ? { completion } : {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function restoredAssistantTranscriptItems(message, nextId) {
|
|
42
|
+
const text = (typeof message?.content === 'string' ? message.content : toolResultText(message?.content)).trim();
|
|
43
|
+
if (!text) return [];
|
|
44
|
+
const { completion, ...metadata } = restoredTranscriptMetadata(message);
|
|
45
|
+
const items = [{ kind: 'assistant', id: nextId(), text, ...metadata }];
|
|
46
|
+
if (completion) {
|
|
47
|
+
items.push({
|
|
48
|
+
kind: 'turndone',
|
|
49
|
+
id: nextId(),
|
|
50
|
+
...completion,
|
|
51
|
+
...(metadata.at ? { at: metadata.at } : {}),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return items;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Restored tool cards: stored assistant messages keep their (compacted)
|
|
58
|
+
// tool_calls and the follow-up role:'tool' results, but resume used to drop
|
|
59
|
+
// both — a reopened session lost every tool marker (user bug). Rebuild one
|
|
60
|
+
// transcript tool item per call and attach its result by tool_call_id.
|
|
61
|
+
export function restoredToolCallItems(message, nextId, pendingByCallId) {
|
|
62
|
+
const calls = Array.isArray(message?.tool_calls) ? message.tool_calls
|
|
63
|
+
: Array.isArray(message?.toolCalls) ? message.toolCalls : [];
|
|
64
|
+
const at = Number(message?.meta?.transcript?.at);
|
|
65
|
+
const items = [];
|
|
66
|
+
for (const call of calls) {
|
|
67
|
+
const name = String(call?.function?.name || call?.name || 'tool').trim() || 'tool';
|
|
68
|
+
let args = call?.function?.arguments ?? call?.arguments;
|
|
69
|
+
if (typeof args === 'string') {
|
|
70
|
+
try { args = JSON.parse(args); } catch { /* keep the raw string args */ }
|
|
71
|
+
}
|
|
72
|
+
const item = {
|
|
73
|
+
kind: 'tool',
|
|
74
|
+
id: nextId(),
|
|
75
|
+
name,
|
|
76
|
+
...(args !== undefined && args !== '' ? { args } : {}),
|
|
77
|
+
expanded: false,
|
|
78
|
+
count: 1,
|
|
79
|
+
completedCount: 1,
|
|
80
|
+
...(Number.isFinite(at) ? { at, startedAt: at, completedAt: at } : {}),
|
|
81
|
+
};
|
|
82
|
+
const callId = typeof call?.id === 'string' ? call.id : '';
|
|
83
|
+
if (callId) pendingByCallId.set(callId, item);
|
|
84
|
+
items.push(item);
|
|
85
|
+
}
|
|
86
|
+
return items;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function attachRestoredToolResult(message, pendingByCallId) {
|
|
90
|
+
const callId = typeof message?.tool_call_id === 'string' && message.tool_call_id
|
|
91
|
+
? message.tool_call_id
|
|
92
|
+
: typeof message?.toolCallId === 'string' ? message.toolCallId : '';
|
|
93
|
+
const target = callId ? pendingByCallId.get(callId) : null;
|
|
94
|
+
if (!target) return;
|
|
95
|
+
pendingByCallId.delete(callId);
|
|
96
|
+
const text = (typeof message?.content === 'string' ? message.content : toolResultText(message?.content)) || '';
|
|
97
|
+
target.result = text;
|
|
98
|
+
if (/^\s*(?:error|\[error|failed\b)/i.test(text)) target.isError = true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Collapse a consecutive run (≥2) of restored per-call tool items into ONE
|
|
102
|
+
// done aggregate item shaped exactly like the live turn's '__aggregate__'
|
|
103
|
+
// card (turn.mjs completeAggregateVisual): merged category header counts,
|
|
104
|
+
// per-call result summaries as the collapsed detail, raw bodies preserved
|
|
105
|
+
// for expansion, failures/exits surfaced as 'N Ok · N Failed'/'Exit N'.
|
|
106
|
+
function buildRestoredAggregateItem(members) {
|
|
107
|
+
const categories = new Map();
|
|
108
|
+
const categoryOrder = [];
|
|
109
|
+
const calls = [];
|
|
110
|
+
for (const { item, category } of members) {
|
|
111
|
+
const entry = aggregateToolCategoryEntry(item.name, item.args, category);
|
|
112
|
+
if (!categories.has(entry.key)) categoryOrder.push(entry.key);
|
|
113
|
+
const prev = categories.get(entry.key);
|
|
114
|
+
categories.set(entry.key, { ...entry, count: Number(prev?.count || 0) + Number(entry.count || 1) });
|
|
115
|
+
const resultText = String(item.result ?? '');
|
|
116
|
+
// Mirror live outcome semantics: a plain non-zero shell exit is the
|
|
117
|
+
// neutral 'Exit N' state, not a red failure; only real call errors count.
|
|
118
|
+
const exitCode = shellCommandExitCode(resultText);
|
|
119
|
+
const isExitError = exitCode != null;
|
|
120
|
+
const isCallError = !isExitError && item.isError === true;
|
|
121
|
+
calls.push({
|
|
122
|
+
name: item.name,
|
|
123
|
+
args: item.args,
|
|
124
|
+
category,
|
|
125
|
+
isError: isCallError,
|
|
126
|
+
isCallError,
|
|
127
|
+
isExitError,
|
|
128
|
+
exitCode,
|
|
129
|
+
resultText,
|
|
130
|
+
resolved: true,
|
|
131
|
+
summary: !isCallError && resultText.trim()
|
|
132
|
+
? summarizeToolResult(item.name, item.args, resultText, false)
|
|
133
|
+
: null,
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
const errors = calls.filter((r) => r.isError).length;
|
|
137
|
+
const callErrors = calls.filter((r) => r.isCallError).length;
|
|
138
|
+
const exitErrors = calls.filter((r) => r.isExitError).length;
|
|
139
|
+
const succeeded = Math.max(0, calls.length - errors - exitErrors);
|
|
140
|
+
const displayDetail = errors > 0 || exitErrors > 0
|
|
141
|
+
? failureDetailText({ succeeded, realErrors: callErrors, exitErrors, exitCode: calls.find((r) => r.isExitError)?.exitCode })
|
|
142
|
+
: formatAggregateDetail(calls.filter((r) => r.summary).map((r) => r.summary));
|
|
143
|
+
const rawResult = aggregateRawResult(calls);
|
|
144
|
+
const first = members[0].item;
|
|
145
|
+
const last = members[members.length - 1].item;
|
|
146
|
+
return {
|
|
147
|
+
kind: 'tool',
|
|
148
|
+
id: first.id,
|
|
149
|
+
name: '__aggregate__',
|
|
150
|
+
args: { categoryOrder },
|
|
151
|
+
aggregate: true,
|
|
152
|
+
categories: Object.fromEntries(categories),
|
|
153
|
+
doneCategories: aggregateDoneCategories(calls),
|
|
154
|
+
count: calls.length,
|
|
155
|
+
completedCount: calls.length,
|
|
156
|
+
isError: errors > 0,
|
|
157
|
+
errorCount: errors,
|
|
158
|
+
callErrorCount: callErrors,
|
|
159
|
+
exitErrorCount: exitErrors,
|
|
160
|
+
result: displayDetail,
|
|
161
|
+
text: displayDetail,
|
|
162
|
+
rawResult: rawResult || null,
|
|
163
|
+
expanded: false,
|
|
164
|
+
headerFinalized: true,
|
|
165
|
+
...(first.at != null ? { at: first.at } : {}),
|
|
166
|
+
...(first.startedAt != null ? { startedAt: first.startedAt } : {}),
|
|
167
|
+
...(last.completedAt != null ? { completedAt: last.completedAt } : {}),
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Restored transcripts must mirror the live turn's category merging: the live
|
|
172
|
+
// engine (turn.mjs) collapses consecutive same-bucket tool calls into one
|
|
173
|
+
// aggregate card, but resume rebuilt one card per call, so a reopened session
|
|
174
|
+
// un-merged every run (user bug). Walk the restored items and merge adjacent
|
|
175
|
+
// tool cards whose aggregateBucketForCategory matches; any non-tool item
|
|
176
|
+
// (user/assistant/turndone) is a block boundary, same as the live seal rule.
|
|
177
|
+
// Runs of 1 keep the plain per-call card (its argument summary stays visible).
|
|
178
|
+
// Agent cards never merge on restore: the live rule scopes Agent grouping to
|
|
179
|
+
// a single provider batch, a boundary the stored history no longer carries.
|
|
180
|
+
export function mergeRestoredToolItems(items) {
|
|
181
|
+
const merged = [];
|
|
182
|
+
let run = null; // { bucket, members: [{ item, category }] }
|
|
183
|
+
const flushRun = () => {
|
|
184
|
+
if (!run) return;
|
|
185
|
+
if (run.members.length >= 2) merged.push(buildRestoredAggregateItem(run.members));
|
|
186
|
+
else for (const member of run.members) merged.push(member.item);
|
|
187
|
+
run = null;
|
|
188
|
+
};
|
|
189
|
+
for (const item of items || []) {
|
|
190
|
+
const mergeable = item?.kind === 'tool' && item.aggregate !== true;
|
|
191
|
+
if (!mergeable) { flushRun(); merged.push(item); continue; }
|
|
192
|
+
const category = classifyToolCategory(item.name, item.args);
|
|
193
|
+
const bucket = category === 'Agent' ? '' : aggregateBucketForCategory(category);
|
|
194
|
+
if (!bucket) { flushRun(); merged.push(item); continue; }
|
|
195
|
+
if (run && run.bucket === bucket) { run.members.push({ item, category }); continue; }
|
|
196
|
+
flushRun();
|
|
197
|
+
run = { bucket, members: [{ item, category }] };
|
|
198
|
+
}
|
|
199
|
+
flushRun();
|
|
200
|
+
return merged;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function restoredMessageItemUpperBound(message) {
|
|
204
|
+
if (message?.role === 'user') return 1;
|
|
205
|
+
if (message?.role !== 'assistant') return 0;
|
|
206
|
+
const calls = Array.isArray(message?.tool_calls) ? message.tool_calls
|
|
207
|
+
: Array.isArray(message?.toolCalls) ? message.toolCalls : [];
|
|
208
|
+
const content = message?.content;
|
|
209
|
+
const hasContent = typeof content === 'string'
|
|
210
|
+
? content.length > 0
|
|
211
|
+
: Array.isArray(content) && content.length > 0;
|
|
212
|
+
const hasCompletion = Boolean(message?.meta?.transcript?.completion);
|
|
213
|
+
return Number(hasContent) + Number(hasCompletion) + calls.length;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function restoredUserTranscriptItems(message, nextId) {
|
|
217
|
+
// Injected model-context payloads are model-visible but never user-authored:
|
|
218
|
+
// skill bodies (meta:'skill'), hook/system reminders, and tag-wrapped context
|
|
219
|
+
// blocks must not restore as user bubbles in any client.
|
|
220
|
+
if (message?.meta === 'skill' || message?.meta === 'hook') return [];
|
|
221
|
+
const text = (typeof message?.content === 'string'
|
|
222
|
+
? message.content
|
|
223
|
+
: toolResultText(message?.content)).trim();
|
|
224
|
+
if (/^<(?:system-reminder|skill|memory-context|mcp-instructions|available-deferred-tools|event)\b/i.test(text)) {
|
|
225
|
+
return [];
|
|
226
|
+
}
|
|
227
|
+
if (!text) return [];
|
|
228
|
+
const synthetic = parseSyntheticAgentMessage(text);
|
|
229
|
+
if (!synthetic) {
|
|
230
|
+
return [{ kind: 'user', id: nextId(), text, ...restoredTranscriptMetadata(message) }];
|
|
231
|
+
}
|
|
232
|
+
const label = synthetic.label || 'notification';
|
|
233
|
+
const syntheticAt = Number(message?.meta?.transcript?.at);
|
|
234
|
+
return [{
|
|
235
|
+
kind: 'tool',
|
|
236
|
+
id: nextId(),
|
|
237
|
+
name: synthetic.name || 'agent',
|
|
238
|
+
args: synthetic.args || {
|
|
239
|
+
type: label,
|
|
240
|
+
task_id: synthetic.taskId || undefined,
|
|
241
|
+
description: synthetic.summary || 'agent notification',
|
|
242
|
+
},
|
|
243
|
+
result: synthetic.result,
|
|
244
|
+
rawResult: synthetic.rawResult ?? text,
|
|
245
|
+
isError: synthetic.isError ?? /^(failed|error|killed|cancelled)$/i.test(label),
|
|
246
|
+
expanded: false,
|
|
247
|
+
count: 1,
|
|
248
|
+
completedCount: 1,
|
|
249
|
+
...(Number.isFinite(syntheticAt)
|
|
250
|
+
? { at: syntheticAt, startedAt: syntheticAt, completedAt: syntheticAt }
|
|
251
|
+
: {}),
|
|
252
|
+
}];
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function restoreTranscriptRange(messages, start, sessionId) {
|
|
256
|
+
const items = [];
|
|
257
|
+
const pendingToolCalls = new Map();
|
|
258
|
+
for (let index = start; index < messages.length; index += 1) {
|
|
259
|
+
const message = messages[index];
|
|
260
|
+
let part = 0;
|
|
261
|
+
// Message-position ids remain stable when newer messages are appended and
|
|
262
|
+
// let a tail-only restore skip the prefix without first counting every old
|
|
263
|
+
// projected item.
|
|
264
|
+
const restoredId = () => `hist_${sessionId}_${index}_${++part}`;
|
|
265
|
+
if (message?.role === 'user') {
|
|
266
|
+
items.push(...restoredUserTranscriptItems(message, restoredId));
|
|
267
|
+
} else if (message?.role === 'assistant') {
|
|
268
|
+
items.push(...restoredAssistantTranscriptItems(message, restoredId));
|
|
269
|
+
items.push(...restoredToolCallItems(message, restoredId, pendingToolCalls));
|
|
270
|
+
} else if (message?.role === 'tool') {
|
|
271
|
+
attachRestoredToolResult(message, pendingToolCalls);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
return mergeRestoredToolItems(items);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export function restoreTranscriptItems(messages, {
|
|
278
|
+
sessionId = 'session',
|
|
279
|
+
itemLimit = Number.POSITIVE_INFINITY,
|
|
280
|
+
} = {}) {
|
|
281
|
+
const source = Array.isArray(messages) ? messages : [];
|
|
282
|
+
const numericLimit = Number(itemLimit);
|
|
283
|
+
const limited = Number.isFinite(numericLimit) && numericLimit > 0;
|
|
284
|
+
if (!limited) return restoreTranscriptRange(source, 0, sessionId);
|
|
285
|
+
|
|
286
|
+
const limit = Math.max(1, Math.floor(numericLimit));
|
|
287
|
+
// Restore beyond the visible cap so a boundary tool run can merge exactly
|
|
288
|
+
// as it did in the full transcript. Selection is incremental from the tail:
|
|
289
|
+
// large cold sessions never read or project their old message bodies.
|
|
290
|
+
const target = limit + Math.min(128, limit);
|
|
291
|
+
let start = source.length;
|
|
292
|
+
let upperBound = 0;
|
|
293
|
+
while (start > 0 && upperBound < target) {
|
|
294
|
+
start -= 1;
|
|
295
|
+
upperBound += restoredMessageItemUpperBound(source[start]);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let restored = restoreTranscriptRange(source, start, sessionId);
|
|
299
|
+
// Hidden/context messages and aggregate tool runs can make the cheap upper
|
|
300
|
+
// bound optimistic. Expand backward exponentially only when necessary.
|
|
301
|
+
let expansionTarget = Math.max(64, limit - restored.length);
|
|
302
|
+
while (start > 0 && restored.length < limit) {
|
|
303
|
+
let expansion = 0;
|
|
304
|
+
while (start > 0 && expansion < expansionTarget) {
|
|
305
|
+
start -= 1;
|
|
306
|
+
expansion += restoredMessageItemUpperBound(source[start]);
|
|
307
|
+
}
|
|
308
|
+
restored = restoreTranscriptRange(source, start, sessionId);
|
|
309
|
+
expansionTarget *= 2;
|
|
310
|
+
}
|
|
311
|
+
return restored.length > limit ? restored.slice(-limit) : restored;
|
|
312
|
+
}
|
|
10
313
|
|
|
11
314
|
export function createEngineApiB(bag) {
|
|
12
315
|
const {
|
|
13
|
-
runtime, nextId, flags, lifecycle, listeners, getState, set, disposeEmit, replaceItems, pushNotice, removeNotice, setProgressHint, clearToastTimers, disposeTranscriptSpill, routeState, syncContextStats, finishToolApproval, denyAllToolApprovals, restoreLeadSteeringFromDisk, resetStats, clearUiActivityBeforeContextSync, resetTuiForPendingSessionReset, snapshotTuiBeforeSessionReset, restoreTuiAfterFailedSessionReset, commitTuiSessionReset, resetStatsAndSyncContext,
|
|
316
|
+
runtime, nextId, flags, lifecycle, listeners, getState, set, flushEmitImmediate, disposeEmit, replaceItems, pushNotice, removeNotice, setProgressHint, clearToastTimers, disposeTranscriptSpill, routeState, syncContextStats, finishToolApproval, denyAllToolApprovals, restoreLeadSteeringFromDisk, resetStats, clearUiActivityBeforeContextSync, resetTuiForPendingSessionReset, snapshotTuiBeforeSessionReset, restoreTuiAfterFailedSessionReset, commitTuiSessionReset, resetStatsAndSyncContext,
|
|
14
317
|
} = bag;
|
|
15
318
|
return {
|
|
16
319
|
resolveToolApproval: (id, decision = {}) => {
|
|
@@ -23,6 +326,9 @@ export function createEngineApiB(bag) {
|
|
|
23
326
|
listProviderModels: (options = {}) => {
|
|
24
327
|
return runtime.listProviderModels(options);
|
|
25
328
|
},
|
|
329
|
+
prefetchSession: (id) => {
|
|
330
|
+
return runtime.prefetchSession?.(id) === true;
|
|
331
|
+
},
|
|
26
332
|
getSearchRoute: () => {
|
|
27
333
|
return runtime.getSearchRoute?.() || runtime.searchRoute || null;
|
|
28
334
|
},
|
|
@@ -120,6 +426,60 @@ export function createEngineApiB(bag) {
|
|
|
120
426
|
},
|
|
121
427
|
isRemoteEnabled: () => runtime.isRemoteEnabled?.() === true,
|
|
122
428
|
getVoiceStatus: () => getVoiceStatus(),
|
|
429
|
+
// Desktop push-to-talk dictation: accept a recorded audio payload
|
|
430
|
+
// (base64), stage it as a temp file, and run it through the SAME managed
|
|
431
|
+
// whisper.cpp pipeline the channels use (ffmpeg convert -> whisper server,
|
|
432
|
+
// model selected per voice.model/system language). Returns the transcript
|
|
433
|
+
// text or throws a user-actionable error (e.g. runtime not installed).
|
|
434
|
+
transcribeAudio: async ({ data, mimeType = 'audio/webm' } = {}) => {
|
|
435
|
+
const base64 = String(data || '');
|
|
436
|
+
if (!base64) throw new Error('transcribeAudio: audio payload is required');
|
|
437
|
+
if (base64.length > 40_000_000) throw new Error('transcribeAudio: recording too large');
|
|
438
|
+
const [{ createVoiceTranscription }, { resolvePluginData }, { readSection }, os, path, fsp, crypto] = await Promise.all([
|
|
439
|
+
import('../../runtime/channels/lib/voice-transcription.mjs'),
|
|
440
|
+
import('../../runtime/shared/plugin-paths.mjs'),
|
|
441
|
+
import('../../runtime/shared/config.mjs'),
|
|
442
|
+
import('node:os'),
|
|
443
|
+
import('node:path'),
|
|
444
|
+
import('node:fs/promises'),
|
|
445
|
+
import('node:crypto'),
|
|
446
|
+
]);
|
|
447
|
+
const extension = /ogg/i.test(mimeType) ? 'ogg' : /wav/i.test(mimeType) ? 'wav' : /mp4|m4a/i.test(mimeType) ? 'm4a' : 'webm';
|
|
448
|
+
const audioPath = path.join(os.tmpdir(), `mixdog-dictation-${process.pid}-${Date.now()}.${extension}`);
|
|
449
|
+
await fsp.writeFile(audioPath, Buffer.from(base64, 'base64'));
|
|
450
|
+
try {
|
|
451
|
+
const { transcribeVoice } = createVoiceTranscription({
|
|
452
|
+
getConfig: () => ({ voice: readSection('voice') || {} }),
|
|
453
|
+
dataDir: resolvePluginData(),
|
|
454
|
+
});
|
|
455
|
+
const text = await transcribeVoice(audioPath, {
|
|
456
|
+
attachmentId: `dictation-${crypto.randomUUID()}`,
|
|
457
|
+
});
|
|
458
|
+
return typeof text === 'string' ? text : '';
|
|
459
|
+
} finally {
|
|
460
|
+
fsp.rm(audioPath, { force: true }).catch(() => undefined);
|
|
461
|
+
}
|
|
462
|
+
},
|
|
463
|
+
// Desktop composer image attach: run the SAME optional-sharp resize
|
|
464
|
+
// pipeline the TUI paste path uses (<=2000x2000, 5MB-base64 token budget,
|
|
465
|
+
// identical no-sharp size errors) so both clients send identical image
|
|
466
|
+
// payloads to the provider.
|
|
467
|
+
resizeImage: async ({ data, mimeType = 'image/png', filename = '' } = {}) => {
|
|
468
|
+
const base64 = String(data || '');
|
|
469
|
+
if (!base64) throw new Error('resizeImage: image payload is required');
|
|
470
|
+
if (base64.length > 40_000_000) throw new Error('resizeImage: image too large');
|
|
471
|
+
const { imageAttachmentFromBuffer } = await import('../paste-attachments.mjs');
|
|
472
|
+
const attachment = await imageAttachmentFromBuffer(
|
|
473
|
+
Buffer.from(base64, 'base64'),
|
|
474
|
+
String(mimeType || 'image/png'),
|
|
475
|
+
{ filename: String(filename || 'Pasted image') },
|
|
476
|
+
);
|
|
477
|
+
return {
|
|
478
|
+
data: attachment.content,
|
|
479
|
+
mimeType: attachment.mediaType,
|
|
480
|
+
metadataText: attachment.metadataText || '',
|
|
481
|
+
};
|
|
482
|
+
},
|
|
123
483
|
toggleVoice: async () => {
|
|
124
484
|
const result = await toggleVoice({ pushNotice, setProgressHint });
|
|
125
485
|
return {
|
|
@@ -305,6 +665,11 @@ export function createEngineApiB(bag) {
|
|
|
305
665
|
pushNotice(`schedule ${enabled ? 'enabled' : 'disabled'}: ${name}`, 'info');
|
|
306
666
|
return result;
|
|
307
667
|
},
|
|
668
|
+
runScheduleNow: async (name) => {
|
|
669
|
+
const result = await runtime.runScheduleNow(name);
|
|
670
|
+
pushNotice(`schedule ran: ${name}`, 'info');
|
|
671
|
+
return result;
|
|
672
|
+
},
|
|
308
673
|
saveWebhook: async (entry) => {
|
|
309
674
|
const result = await runtime.saveWebhook(entry);
|
|
310
675
|
pushNotice(`webhook saved: ${result.name}`, 'info');
|
|
@@ -377,6 +742,46 @@ export function createEngineApiB(bag) {
|
|
|
377
742
|
listSessions: (options) => {
|
|
378
743
|
return runtime.listSessions(options);
|
|
379
744
|
},
|
|
745
|
+
deleteSession: async (id) => {
|
|
746
|
+
if (getState().commandBusy) return false;
|
|
747
|
+
const deletingCurrent = String(runtime.session?.id || getState().sessionId || '') === String(id || '');
|
|
748
|
+
set({ commandBusy: true });
|
|
749
|
+
clearToastTimers();
|
|
750
|
+
resetAllStreamingMarkdownStablePrefixes();
|
|
751
|
+
const rollbackSnapshot = deletingCurrent ? snapshotTuiBeforeSessionReset() : null;
|
|
752
|
+
if (deletingCurrent) resetTuiForPendingSessionReset();
|
|
753
|
+
try {
|
|
754
|
+
if (await runtime.deleteSession(id) !== true) {
|
|
755
|
+
if (rollbackSnapshot) restoreTuiAfterFailedSessionReset(rollbackSnapshot);
|
|
756
|
+
return false;
|
|
757
|
+
}
|
|
758
|
+
if (deletingCurrent) {
|
|
759
|
+
clearUiActivityBeforeContextSync();
|
|
760
|
+
flags.pendingSessionReset = false;
|
|
761
|
+
resetStatsAndSyncContext();
|
|
762
|
+
set({
|
|
763
|
+
items: replaceItems([]),
|
|
764
|
+
toasts: [],
|
|
765
|
+
queued: [],
|
|
766
|
+
thinking: null,
|
|
767
|
+
spinner: null,
|
|
768
|
+
lastTurn: null,
|
|
769
|
+
sessionId: null,
|
|
770
|
+
cwd: runtime.cwd,
|
|
771
|
+
...routeState(),
|
|
772
|
+
stats: { ...getState().stats },
|
|
773
|
+
});
|
|
774
|
+
commitTuiSessionReset(rollbackSnapshot);
|
|
775
|
+
}
|
|
776
|
+
return true;
|
|
777
|
+
} catch (error) {
|
|
778
|
+
if (rollbackSnapshot) restoreTuiAfterFailedSessionReset(rollbackSnapshot);
|
|
779
|
+
throw error;
|
|
780
|
+
} finally {
|
|
781
|
+
flags.pendingSessionReset = false;
|
|
782
|
+
set({ commandBusy: false });
|
|
783
|
+
}
|
|
784
|
+
},
|
|
380
785
|
switchContext: async (options) => {
|
|
381
786
|
if (getState().commandBusy) return false;
|
|
382
787
|
set({ commandBusy: true });
|
|
@@ -444,52 +849,32 @@ export function createEngineApiB(bag) {
|
|
|
444
849
|
set({ commandBusy: false });
|
|
445
850
|
}
|
|
446
851
|
},
|
|
447
|
-
resume: async (id) => {
|
|
852
|
+
resume: async (id, options = {}) => {
|
|
448
853
|
if (getState().commandBusy) return false;
|
|
449
|
-
|
|
854
|
+
// quiet: viewer-follow refreshes (engine share tick) re-resume on every
|
|
855
|
+
// owner turn — they must not flash the "Resuming conversation" status.
|
|
856
|
+
set({
|
|
857
|
+
commandBusy: true,
|
|
858
|
+
...(options.quiet === true
|
|
859
|
+
? {}
|
|
860
|
+
: { commandStatus: { active: true, verb: 'Resuming conversation', startedAt: Date.now(), mode: 'resuming' } }),
|
|
861
|
+
});
|
|
450
862
|
clearToastTimers();
|
|
451
863
|
try {
|
|
452
864
|
const r = await runtime.resume(id);
|
|
453
865
|
if (!r) return false;
|
|
454
866
|
resetStatsAndSyncContext();
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
// content may be a string OR an array of parts (text/tool-call
|
|
459
|
-
// interleaving) — toolResultText coerces both to readable text so
|
|
460
|
-
// array-content messages aren't silently dropped.
|
|
461
|
-
const text = (typeof m.content === 'string' ? m.content : toolResultText(m.content)).trim();
|
|
462
|
-
if (text) {
|
|
463
|
-
const synthetic = parseSyntheticAgentMessage(text);
|
|
464
|
-
if (synthetic) {
|
|
465
|
-
const label = synthetic.label || 'notification';
|
|
466
|
-
items.push({
|
|
467
|
-
kind: 'tool',
|
|
468
|
-
id: nextId(),
|
|
469
|
-
name: synthetic.name || 'agent',
|
|
470
|
-
args: synthetic.args || {
|
|
471
|
-
type: label,
|
|
472
|
-
task_id: synthetic.taskId || undefined,
|
|
473
|
-
description: synthetic.summary || 'agent notification',
|
|
474
|
-
},
|
|
475
|
-
result: synthetic.result,
|
|
476
|
-
rawResult: synthetic.rawResult ?? text,
|
|
477
|
-
isError: synthetic.isError ?? /^(failed|error|killed|cancelled)$/i.test(label),
|
|
478
|
-
expanded: false,
|
|
479
|
-
count: 1,
|
|
480
|
-
completedCount: 1,
|
|
481
|
-
startedAt: Date.now(),
|
|
482
|
-
completedAt: Date.now(),
|
|
483
|
-
});
|
|
484
|
-
} else {
|
|
485
|
-
items.push({ kind: 'user', id: nextId(), text });
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
} else if (m.role === 'assistant') {
|
|
489
|
-
const text = (typeof m.content === 'string' ? m.content : toolResultText(m.content)).trim();
|
|
490
|
-
if (text) items.push({ kind: 'assistant', id: nextId(), text });
|
|
491
|
-
}
|
|
867
|
+
const requestedLimit = Number(options.transcriptItemLimit);
|
|
868
|
+
if (Number.isFinite(requestedLimit) && requestedLimit > 0) {
|
|
869
|
+
flags.resumeTranscriptItemLimit = Math.max(1, Math.floor(requestedLimit));
|
|
492
870
|
}
|
|
871
|
+
const itemLimit = Number(flags.resumeTranscriptItemLimit);
|
|
872
|
+
const items = restoreTranscriptItems(r.messages, {
|
|
873
|
+
sessionId: String(r.id || id),
|
|
874
|
+
itemLimit: Number.isFinite(itemLimit) && itemLimit > 0
|
|
875
|
+
? itemLimit
|
|
876
|
+
: Number.POSITIVE_INFINITY,
|
|
877
|
+
});
|
|
493
878
|
set({
|
|
494
879
|
items: replaceItems(items),
|
|
495
880
|
toasts: [],
|
|
@@ -500,10 +885,20 @@ export function createEngineApiB(bag) {
|
|
|
500
885
|
...routeState(),
|
|
501
886
|
stats: { ...getState().stats },
|
|
502
887
|
});
|
|
888
|
+
// Reconcile the live-share legs NOW (viewer pipe attach / owner pipe
|
|
889
|
+
// start). The 3s share tick otherwise leaves a live-owned session on
|
|
890
|
+
// the stale disk snapshot and then full-swaps it seconds after entry —
|
|
891
|
+
// the visible transcript lurch. Connecting here makes the owner's
|
|
892
|
+
// full frame land at the resume boundary, so entry paints settled.
|
|
893
|
+
bag.ensureLiveShare?.();
|
|
503
894
|
void restoreLeadSteeringFromDisk();
|
|
504
895
|
return true;
|
|
505
896
|
} finally {
|
|
506
897
|
set({ commandBusy: false, commandStatus: null });
|
|
898
|
+
// Desktop resume returns a snapshot immediately after this promise.
|
|
899
|
+
// Publish the completed route/transcript boundary now so callers never
|
|
900
|
+
// observe the previous frame's session id and title.
|
|
901
|
+
flushEmitImmediate();
|
|
507
902
|
}
|
|
508
903
|
},
|
|
509
904
|
|
|
@@ -511,6 +906,10 @@ export function createEngineApiB(bag) {
|
|
|
511
906
|
if (flags.disposed) return;
|
|
512
907
|
disposeEmit?.();
|
|
513
908
|
flags.disposed = true;
|
|
909
|
+
// Release the interactive-presence beacon so a cross-open after this
|
|
910
|
+
// surface exits takes normal ownership instead of viewer-attaching to a
|
|
911
|
+
// dead owner (crash paths fall back to the 2min staleness window).
|
|
912
|
+
try { runtime.clearSessionPresence?.(); } catch { /* best-effort */ }
|
|
514
913
|
clearToastTimers();
|
|
515
914
|
disposeTranscriptSpill?.();
|
|
516
915
|
try { clearInterval(lifecycle.runtimePulseTimer); } catch {}
|
|
@@ -206,7 +206,13 @@ export function createEngineApiA(bag) {
|
|
|
206
206
|
set({ commandBusy: false });
|
|
207
207
|
}
|
|
208
208
|
},
|
|
209
|
-
agentControl: async (args = {}) => {
|
|
209
|
+
agentControl: async (args = {}, options = {}) => {
|
|
210
|
+
// Silent reads (desktop dock agent viewer) go straight to the runtime:
|
|
211
|
+
// no transcript card, no commandBusy — mirrors memoryControl's silent
|
|
212
|
+
// read path.
|
|
213
|
+
if (options.silent === true) {
|
|
214
|
+
return runtime.agentControl(args);
|
|
215
|
+
}
|
|
210
216
|
if (getState().commandBusy) return null;
|
|
211
217
|
set({ commandBusy: true });
|
|
212
218
|
try {
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { presentErrorText } from '../../runtime/shared/err-text.mjs';
|
|
5
5
|
import { resetAllStreamingMarkdownStablePrefixes } from '../markdown/streaming-markdown.mjs';
|
|
6
6
|
import { createSessionStats } from './session-stats.mjs';
|
|
7
|
-
import { queuePriorityValue, defaultQueuePriority, isQueuedEntryEditable, isQueuedEntryVisible, isSlashQueuedEntry, notificationDisplayText, sessionActivityTimestamp, promptDisplayText, mergePromptContents, mergePastedImages, mergePastedTexts, callCommitCallbacks, STEERING_SUPPRESSED_DISPLAY } from './queue-helpers.mjs';
|
|
7
|
+
import { queuePriorityValue, defaultQueuePriority, isQueuedEntryEditable, isQueuedEntryVisible, isSlashQueuedEntry, notificationDisplayText, sessionActivityTimestamp, promptDisplayText, promptContentImageMeta, mergePromptContents, mergePastedImages, mergePastedTexts, callCommitCallbacks, STEERING_SUPPRESSED_DISPLAY } from './queue-helpers.mjs';
|
|
8
8
|
import { appendTuiSteeringPersist, dropTuiSteeringPersist, drainTuiSteeringPersist } from './tui-steering-persist.mjs';
|
|
9
9
|
|
|
10
10
|
export function createSessionFlow(bag) {
|
|
@@ -49,6 +49,7 @@ export function createSessionFlow(bag) {
|
|
|
49
49
|
content: text,
|
|
50
50
|
pastedImages: options.pastedImages && typeof options.pastedImages === 'object' ? options.pastedImages : null,
|
|
51
51
|
pastedTexts: options.pastedTexts && typeof options.pastedTexts === 'object' ? options.pastedTexts : null,
|
|
52
|
+
images: promptContentImageMeta(text, options.pastedImages),
|
|
52
53
|
onCommitted: typeof options.onCommitted === 'function' ? options.onCommitted : null,
|
|
53
54
|
mode,
|
|
54
55
|
priority,
|
|
@@ -200,7 +201,12 @@ export function createSessionFlow(bag) {
|
|
|
200
201
|
// at delivery time; the queued twin is model-visible only and must
|
|
201
202
|
// NOT render a second transcript card here (no fall-back to content).
|
|
202
203
|
if (entry.suppressDisplay) continue;
|
|
203
|
-
pushUserOrSyntheticItem(
|
|
204
|
+
pushUserOrSyntheticItem(
|
|
205
|
+
entry.text,
|
|
206
|
+
entry.id,
|
|
207
|
+
isQueuedEntryEditable(entry) ? 'user' : 'injected',
|
|
208
|
+
Array.isArray(entry.images) && entry.images.length ? { images: entry.images } : null,
|
|
209
|
+
);
|
|
204
210
|
}
|
|
205
211
|
const nonEditable = batch.filter((entry) => !isQueuedEntryEditable(entry));
|
|
206
212
|
// A completion resume is owned by the completion that woke it. Esc
|
|
@@ -357,15 +363,22 @@ export function createSessionFlow(bag) {
|
|
|
357
363
|
const minContextPercent = Number(cfg.minContextPercent ?? 10);
|
|
358
364
|
if (minContextPercent > 0) {
|
|
359
365
|
const status = runtime.contextStatus?.() || null;
|
|
360
|
-
|
|
361
|
-
|
|
366
|
+
// A zero is often an unavailable/stale meter field, not an authoritative
|
|
367
|
+
// measurement. Do not let it mask the sibling live estimate.
|
|
368
|
+
const usedTokens = Math.max(
|
|
369
|
+
0,
|
|
370
|
+
Number(status?.usedTokens) || 0,
|
|
371
|
+
Number(status?.currentEstimatedTokens) || 0,
|
|
372
|
+
Number(status?.compaction?.currentEstimatedTokens) || 0,
|
|
373
|
+
);
|
|
362
374
|
const triggerTokens = Number(
|
|
363
375
|
status?.compaction?.triggerTokens
|
|
364
376
|
|| status?.compaction?.autoCompactTokenLimit
|
|
365
377
|
|| runtime.session?.autoCompactTokenLimit
|
|
366
378
|
|| 0,
|
|
367
379
|
);
|
|
368
|
-
if (!(usedTokens
|
|
380
|
+
if (!Number.isFinite(usedTokens) || !Number.isFinite(triggerTokens)
|
|
381
|
+
|| !(usedTokens > 0 && triggerTokens > 0)) {
|
|
369
382
|
if (!activityAt) flags.lastUserActivityAt = now;
|
|
370
383
|
return false;
|
|
371
384
|
}
|
|
@@ -481,11 +494,12 @@ export function createSessionFlow(bag) {
|
|
|
481
494
|
}
|
|
482
495
|
}
|
|
483
496
|
|
|
484
|
-
function restoreQueued(currentText = '') {
|
|
497
|
+
function restoreQueued(currentText = '', selectedId = '') {
|
|
498
|
+
const targetId = String(selectedId || '').trim();
|
|
485
499
|
const queued = [];
|
|
486
500
|
for (let i = 0; i < pending.length;) {
|
|
487
501
|
const entry = pending[i];
|
|
488
|
-
if (isQueuedEntryEditable(entry)) {
|
|
502
|
+
if (isQueuedEntryEditable(entry) && (!targetId || String(entry.id) === targetId)) {
|
|
489
503
|
queued.push(entry);
|
|
490
504
|
pending.splice(i, 1);
|
|
491
505
|
} else {
|
|
@@ -159,6 +159,7 @@ export function createToolCardResults({
|
|
|
159
159
|
count: group.count,
|
|
160
160
|
completedCount: group.completed,
|
|
161
161
|
completedAt: Date.now(),
|
|
162
|
+
liveOutput: null,
|
|
162
163
|
};
|
|
163
164
|
if (group.count <= 1) {
|
|
164
165
|
const body = String(text || rawText || '').trim();
|
|
@@ -292,7 +293,8 @@ export function createToolCardResults({
|
|
|
292
293
|
const currentItem = itemById(card.itemId);
|
|
293
294
|
resultText = withCancelledResultMarker(resultText, currentItem);
|
|
294
295
|
}
|
|
295
|
-
|
|
296
|
+
// liveOutput: null — the settled result supersedes any streamed tail.
|
|
297
|
+
patchToolItem(card.itemId, { result: resultText, text: resultText, isError: group.errors > 0, errorCount: group.errors, callErrorCount: group.callErrors || 0, exitErrorCount: group.exitErrors || 0, count: group.count, completedCount: group.completed, completedAt: Date.now(), liveOutput: null });
|
|
296
298
|
card.done = true;
|
|
297
299
|
if (card.callId) done.add(card.callId);
|
|
298
300
|
}
|