mixdog 0.9.65 → 0.9.67
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 +1 -1
- package/src/app.mjs +2 -2
- package/src/cli.mjs +1 -1
- package/src/repl.mjs +72 -6
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +76 -21
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +45 -4
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +11 -1
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +36 -13
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +83 -18
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +37 -2
- package/src/runtime/channels/lib/config.mjs +7 -0
- package/src/runtime/channels/lib/status-snapshot.mjs +6 -30
- package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +203 -0
- package/src/runtime/channels/lib/webhook.mjs +23 -201
- package/src/runtime/shared/config.mjs +0 -9
- package/src/runtime/shared/time-format.mjs +56 -0
- package/src/runtime/shared/tool-card-model.mjs +740 -0
- package/src/session-runtime/channel-config-api.mjs +5 -0
- package/src/session-runtime/context-status.mjs +2 -1
- package/src/session-runtime/lifecycle-api.mjs +5 -0
- package/src/session-runtime/prewarm.mjs +13 -7
- package/src/session-runtime/provider-request-tools.mjs +6 -0
- package/src/session-runtime/runtime-core.mjs +28 -9
- package/src/session-runtime/session-text.mjs +6 -0
- package/src/session-runtime/settings-api.mjs +0 -12
- package/src/session-runtime/tool-catalog-schema.mjs +5 -1
- package/src/standalone/channel-admin.mjs +35 -21
- package/src/tui/App.jsx +0 -15
- package/src/tui/app/channel-pickers.mjs +18 -42
- package/src/tui/app/doctor.mjs +0 -1
- package/src/tui/app/transcript-window.mjs +16 -0
- package/src/tui/app/use-transcript-window.mjs +36 -1
- package/src/tui/components/Markdown.jsx +15 -1
- package/src/tui/components/Message.jsx +1 -1
- package/src/tui/components/PromptInput.jsx +7 -0
- package/src/tui/components/ToolExecution.jsx +47 -196
- package/src/tui/components/tool-execution/surface-detail.mjs +33 -394
- package/src/tui/components/tool-execution/text-format.mjs +27 -104
- package/src/tui/dist/index.mjs +613 -264
- package/src/tui/engine/frame-batched-store.mjs +5 -3
- package/src/tui/engine/live-share.mjs +9 -0
- package/src/tui/engine/render-timing.mjs +28 -0
- package/src/tui/engine/session-api-ext.mjs +7 -10
- package/src/tui/engine/tui-steering-persist.mjs +25 -4
- package/src/tui/engine.mjs +9 -1
- package/src/tui/index.jsx +11 -3
- package/src/tui/markdown/measure-rendered-rows.mjs +28 -16
- package/src/tui/markdown/render-ansi.mjs +34 -1
- package/src/tui/markdown/stream-fence.mjs +44 -7
- package/src/tui/markdown/streaming-markdown.mjs +95 -27
- package/src/tui/time-format.mjs +4 -51
- package/src/ui/stream-finalize.mjs +45 -0
- package/src/runtime/channels/lib/webhook/ngrok.mjs +0 -181
|
@@ -0,0 +1,740 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* runtime/shared/tool-card-model.mjs — the ONE collapsed tool-card text model.
|
|
3
|
+
*
|
|
4
|
+
* Every surface (TUI ToolExecution, desktop ToolCard) derives its tool-card
|
|
5
|
+
* header label, parenthesized arg summary, and collapsed `└ detail` row from
|
|
6
|
+
* deriveToolCardModel() so per-tool formats, casing, status merging
|
|
7
|
+
* ("Failed · cause", "Exit 1", "Running · 12s") can never drift between
|
|
8
|
+
* clients. The pure surface/status helpers here were moved verbatim from
|
|
9
|
+
* src/tui/components/tool-execution/{text-format,surface-detail}.mjs — those
|
|
10
|
+
* TUI modules now re-export from this file; width fitting and theme colors
|
|
11
|
+
* stay surface-side.
|
|
12
|
+
*/
|
|
13
|
+
import stripAnsi from 'strip-ansi';
|
|
14
|
+
import {
|
|
15
|
+
AGENT_SURFACE_BRIEF_MAX,
|
|
16
|
+
displayModelName,
|
|
17
|
+
formatAggregateHeader,
|
|
18
|
+
formatToolActionHeader,
|
|
19
|
+
formatToolSurface,
|
|
20
|
+
summarizeAgentSurfaceBrief,
|
|
21
|
+
summarizeToolResult,
|
|
22
|
+
} from './tool-surface.mjs';
|
|
23
|
+
import { backgroundTaskFailureStatusLabel, isBackgroundErrorOnlyBody } from './err-text.mjs';
|
|
24
|
+
import { normalizeToolTerminalStatus, toolResultTerminalStatus } from './tool-status.mjs';
|
|
25
|
+
import { formatElapsed } from './time-format.mjs';
|
|
26
|
+
|
|
27
|
+
export const MIN_RESULT_LINE_CHARS = 24;
|
|
28
|
+
// Hard cap for the collapsed result detail row (the second line under the ⎿
|
|
29
|
+
// gutter). Independent of terminal width so a wide terminal never lets a long
|
|
30
|
+
// line (e.g. an agent response brief) stretch the whole row — anything past
|
|
31
|
+
// this is truncated with an ellipsis. Expanding still shows the full body.
|
|
32
|
+
export const RESULT_LINE_HARD_MAX = 80;
|
|
33
|
+
// Hard cap for the parenthesized header arg summary so a long path/query does
|
|
34
|
+
// not eat the whole header line; anything longer is truncated with an ellipsis.
|
|
35
|
+
export const SUMMARY_MAX_CHARS = 48;
|
|
36
|
+
export const HEADER_FAILURE_STATUS_MAX = 32;
|
|
37
|
+
|
|
38
|
+
// Collapsed tool headers/details are laid out as single rows. Never let raw
|
|
39
|
+
// C0/control bytes (CR, tabs, cursor escapes, etc.) reach those rows.
|
|
40
|
+
const INLINE_CONTROL_RE = /[\u0000-\u001F\u007F]/g;
|
|
41
|
+
|
|
42
|
+
export function safeInlineText(value) {
|
|
43
|
+
return stripAnsi(String(value ?? ''))
|
|
44
|
+
.replace(/\r\n?/g, '\n')
|
|
45
|
+
.replace(/\t/g, ' ')
|
|
46
|
+
.replace(/\n+/g, ' ')
|
|
47
|
+
.replace(INLINE_CONTROL_RE, ' ')
|
|
48
|
+
.replace(/\s+/g, ' ')
|
|
49
|
+
.trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function normalizeCount(value) {
|
|
53
|
+
const n = Number(value || 0);
|
|
54
|
+
return Number.isFinite(n) ? Math.max(0, Math.floor(n)) : 0;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function normalizeCountMap(value = {}) {
|
|
58
|
+
const out = {};
|
|
59
|
+
for (const [key, raw] of Object.entries(value || {})) {
|
|
60
|
+
if (raw && typeof raw === 'object') {
|
|
61
|
+
out[key] = { ...raw, count: normalizeCount(raw.count) };
|
|
62
|
+
} else {
|
|
63
|
+
out[key] = normalizeCount(raw);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function plural(count, singular, pluralText = `${singular}s`) {
|
|
70
|
+
return count === 1 ? singular : pluralText;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function shellResultStatus(value) {
|
|
74
|
+
const match = String(value || '').match(/(?:^|\b)status:\s*(running|pending|queued|completed|failed|cancelled|canceled)\b/im);
|
|
75
|
+
return match ? String(match[1] || '').toLowerCase() : '';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function normalizeTerminalStatus(value) {
|
|
79
|
+
return normalizeToolTerminalStatus(value);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function displayTerminalStatus(value) {
|
|
83
|
+
// 'exit' is a shell-only pseudo-status (command RAN but exited non-zero); it
|
|
84
|
+
// is intentionally NOT a normalized terminal status so it never colors red.
|
|
85
|
+
if (String(value || '').trim().toLowerCase() === 'exit') return 'Exit';
|
|
86
|
+
const status = normalizeTerminalStatus(value);
|
|
87
|
+
if (status === 'running') return 'Running';
|
|
88
|
+
if (status === 'completed') return 'Finished';
|
|
89
|
+
if (status === 'failed') return 'Failed';
|
|
90
|
+
if (status === 'cancelled') return 'Cancelled';
|
|
91
|
+
if (status === 'denied') return 'Denied';
|
|
92
|
+
return '';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function resultTerminalStatus(value) {
|
|
96
|
+
return toolResultTerminalStatus(value);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const LEADING_STATUS_MARKER_LINE_RE = /^\[status:\s*[^\]]*\]\s*$/i;
|
|
100
|
+
|
|
101
|
+
export function stripLeadingStatusMarkerLines(lines) {
|
|
102
|
+
const out = Array.isArray(lines) ? lines.slice() : [];
|
|
103
|
+
if (out.length > 0 && LEADING_STATUS_MARKER_LINE_RE.test(String(out[0] ?? '').trim())) out.shift();
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function stripLeadingStatusMarkerFromText(text) {
|
|
108
|
+
return stripLeadingStatusMarkerLines(String(text || '').split('\n')).join('\n');
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function shellResultElapsed(value) {
|
|
112
|
+
const match = String(value || '').match(/^\[elapsed:\s*(\d+)\s*ms\]/mi);
|
|
113
|
+
if (!match) return '';
|
|
114
|
+
const elapsedMs = Number(match[1]);
|
|
115
|
+
return Number.isFinite(elapsedMs) && elapsedMs >= 1000 ? formatElapsed(elapsedMs) : '';
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// `+N`/`-N` line-delta tokens inside a detail row ("+177 lines"). Surfaces
|
|
119
|
+
// color the token (TUI theme / desktop CSS); the SPLIT lives here so both
|
|
120
|
+
// recognize the same grammar.
|
|
121
|
+
const LINE_DELTA_RE = /(^|[\s([,{·])([+-]\s*\d+)(?=\s+Lines?\b)/gi;
|
|
122
|
+
|
|
123
|
+
export function splitLineDeltaTokens(text) {
|
|
124
|
+
const value = String(text ?? '');
|
|
125
|
+
const parts = [];
|
|
126
|
+
let last = 0;
|
|
127
|
+
let match;
|
|
128
|
+
LINE_DELTA_RE.lastIndex = 0;
|
|
129
|
+
while ((match = LINE_DELTA_RE.exec(value))) {
|
|
130
|
+
const prefix = match[1] || '';
|
|
131
|
+
const token = (match[2] || '').replace(/\s+/g, '');
|
|
132
|
+
const tokenStart = match.index + prefix.length;
|
|
133
|
+
if (match.index > last) parts.push({ text: value.slice(last, match.index) });
|
|
134
|
+
if (prefix) parts.push({ text: prefix });
|
|
135
|
+
if (token) parts.push({ text: token, delta: token.startsWith('+') ? '+' : '-' });
|
|
136
|
+
last = tokenStart + (match[2] || '').length;
|
|
137
|
+
}
|
|
138
|
+
if (last < value.length) parts.push({ text: value.slice(last) });
|
|
139
|
+
return parts;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function isShellTool(normalizedName, label = '') {
|
|
143
|
+
const n = String(normalizedName || '').toLowerCase();
|
|
144
|
+
const l = String(label || '').toLowerCase();
|
|
145
|
+
return n === 'shell' || n === 'bash' || n === 'bash_session' || n === 'shell_command' || n === 'job_wait' || l === 'run';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function shellDisplayStatus({ pending = false, failedCount = 0, exitFailedCount = 0, isError = false, result = '' } = {}) {
|
|
149
|
+
const status = shellResultStatus(result);
|
|
150
|
+
if (pending || /^(running|pending|queued)$/.test(status)) return 'running';
|
|
151
|
+
if (/^cancel/.test(status)) return 'cancelled';
|
|
152
|
+
if (/^(failed|error|killed|timeout)$/.test(status)) return 'failed';
|
|
153
|
+
// A command that RAN but exited non-zero is a command-exit, not a real
|
|
154
|
+
// failure: render the neutral "Exit" state unless there is ALSO a real
|
|
155
|
+
// tool-call/result failure in the group.
|
|
156
|
+
const realFailed = Math.max(0, Number(failedCount) - Number(exitFailedCount));
|
|
157
|
+
if (realFailed > 0) return 'failed';
|
|
158
|
+
if (Number(exitFailedCount) > 0) return 'exit';
|
|
159
|
+
if (isError || failedCount > 0) return 'failed';
|
|
160
|
+
return 'completed';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function shellHeader(status, count = 1) {
|
|
164
|
+
const n = Math.max(1, Number(count) || 1);
|
|
165
|
+
const object = `${n} ${plural(n, 'command')}`;
|
|
166
|
+
if (status === 'running') return `Running ${object}`;
|
|
167
|
+
return `Ran ${object}`;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function isAgentTool(normalizedName) {
|
|
171
|
+
return normalizedName === 'agent';
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
export const SKILL_SURFACE_NAMES = new Set([
|
|
175
|
+
'skill', 'skill_execute', 'skill_view', 'skills_list', 'use_skill',
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
export function isBackgroundTaskTool(normalizedName) {
|
|
179
|
+
return new Set(['explore', 'search', 'shell', 'bash', 'bash_session', 'shell_command', 'task']).has(String(normalizedName || '').toLowerCase());
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const AGENT_DISPLAY_NAMES = new Map([
|
|
183
|
+
['explore', 'Explore'],
|
|
184
|
+
['maintainer', 'Maintainer'],
|
|
185
|
+
['worker', 'Worker'],
|
|
186
|
+
['heavy-worker', 'Heavy Worker'],
|
|
187
|
+
['reviewer', 'Reviewer'],
|
|
188
|
+
['debugger', 'Debugger'],
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
function titleizeAgentName(value) {
|
|
192
|
+
const text = String(value || '').trim();
|
|
193
|
+
if (!text) return '';
|
|
194
|
+
const key = text.toLowerCase().replace(/[\s_]+/g, '-');
|
|
195
|
+
if (AGENT_DISPLAY_NAMES.has(key)) return AGENT_DISPLAY_NAMES.get(key);
|
|
196
|
+
return text
|
|
197
|
+
.replace(/[_-]+/g, ' ')
|
|
198
|
+
.split(/\s+/)
|
|
199
|
+
.filter(Boolean)
|
|
200
|
+
.map((part) => `${part.slice(0, 1).toUpperCase()}${part.slice(1).toLowerCase()}`)
|
|
201
|
+
.join(' ');
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function agentModelLabel(args) {
|
|
205
|
+
const a = args && typeof args === 'object' ? args : {};
|
|
206
|
+
const provider = String(a.provider || a.providerId || a.provider_id || '').trim();
|
|
207
|
+
const model = String(a.model || '').trim();
|
|
208
|
+
const displayHint = String(a.modelDisplay || a.model_display || a.displayModel || '').trim();
|
|
209
|
+
return displayModelName(model, provider, displayHint);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function agentTagLabel(args) {
|
|
213
|
+
// The real spawn tag (engine fills parsedArgs.tag from the envelope target).
|
|
214
|
+
// Never fall back to task_id — only the human-meaningful spawn tag belongs in
|
|
215
|
+
// the header parentheses.
|
|
216
|
+
return String(args?.tag || '').trim();
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function withModelAndTag(label, args) {
|
|
220
|
+
const model = agentModelLabel(args);
|
|
221
|
+
const tag = agentTagLabel(args);
|
|
222
|
+
const inner = [model, tag].filter(Boolean).join(', ');
|
|
223
|
+
return inner ? `${label} (${inner})` : label;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Append an agent name to a base action word without leaving a trailing space
|
|
227
|
+
// when the agent is unknown (no generic "Agent" fallback).
|
|
228
|
+
function joinActionAgent(action, agent) {
|
|
229
|
+
return agent ? `${action} ${agent}` : action;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function agentResponseTitle(args, count = 1) {
|
|
233
|
+
const total = Math.max(1, Number(count) || 1);
|
|
234
|
+
if (total > 1) return `Responses ${total} agents`;
|
|
235
|
+
const name = titleizeAgentName(args?.agent || args?.subagent_type || args?.name || '');
|
|
236
|
+
// The agent + model identify the responder; the response summary itself
|
|
237
|
+
// is hidden in the collapsed card (expanding still shows the full body).
|
|
238
|
+
// No generic "Agent" fallback — render just "Response" when the agent is empty.
|
|
239
|
+
return withModelAndTag(joinActionAgent('Response', name), args);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
export function agentActionTitle(args) {
|
|
243
|
+
const name = titleizeAgentName(args?.agent || args?.subagent_type || args?.name || '');
|
|
244
|
+
// Runtime treats an omitted type/action as "spawn" (see agent-tool.mjs default),
|
|
245
|
+
// so mirror that contract here instead of falling through to the generic
|
|
246
|
+
// "Called agent" status copy.
|
|
247
|
+
const action = String(args?.type || args?.action || 'spawn').toLowerCase();
|
|
248
|
+
// Fixed action verbs regardless of running/completed status. No generic
|
|
249
|
+
// "Agent" fallback for the agent: when the agent is unknown render the action
|
|
250
|
+
// word alone ("Spawn") instead of "Spawn Agent".
|
|
251
|
+
if (action === 'spawn') return withModelAndTag(joinActionAgent('Spawn', name), args);
|
|
252
|
+
if (action === 'send') return withModelAndTag(joinActionAgent('Send', name), args);
|
|
253
|
+
if (action === 'list') return 'Agent status';
|
|
254
|
+
if (action === 'cancel') return withModelAndTag(joinActionAgent('Cancel', name), args);
|
|
255
|
+
if (action === 'close') return withModelAndTag(joinActionAgent('Close', name), args);
|
|
256
|
+
if (action === 'cleanup') return withModelAndTag(joinActionAgent('Cleanup', name), args);
|
|
257
|
+
if (action === 'read' || action === 'status') return withModelAndTag(joinActionAgent('Status', name), args);
|
|
258
|
+
return '';
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
export function agentActionSummary(args, summary) {
|
|
262
|
+
const text = String(summary || '').trim();
|
|
263
|
+
if (!text) return '';
|
|
264
|
+
const name = titleizeAgentName(args?.agent || args?.subagent_type || args?.name || '');
|
|
265
|
+
if (name && text === name) return '';
|
|
266
|
+
let rest = name && text.startsWith(`${name} · `) ? text.slice(name.length + 3).trim() : text;
|
|
267
|
+
// The agent/model/tag surface summary ("Heavy Worker · Opus 4.8") is now folded
|
|
268
|
+
// into the header label itself ("Spawn Heavy Worker (Opus 4.8, tag)"), so drop
|
|
269
|
+
// the model and tag tokens from the parenthesized summary to avoid showing
|
|
270
|
+
// them twice.
|
|
271
|
+
const model = agentModelLabel(args);
|
|
272
|
+
if (model && rest === model) return '';
|
|
273
|
+
const tag = agentTagLabel(args);
|
|
274
|
+
if (tag && rest === tag) return '';
|
|
275
|
+
return rest;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function hasAgentResponseResult(value) {
|
|
279
|
+
const text = String(value || '').trim();
|
|
280
|
+
if (!text) return false;
|
|
281
|
+
if (/^(?:undefined|null)$/i.test(text)) return false;
|
|
282
|
+
if (/^status:\s*(?:running|pending|queued|completed|failed|cancelled|canceled)(?:\s*·\s*task_id:\s*\S+)?$/i.test(text)) return false;
|
|
283
|
+
const isBridgeEnvelope = /^(?:agent task:|background task\b|agent message queued\b|agent close:)/i.test(text)
|
|
284
|
+
|| /^(?:agents|tasks):\s*\d/i.test(text)
|
|
285
|
+
|| /^\(no agents or tasks\)$/i.test(text)
|
|
286
|
+
|| (/^task_id:\s*\S+/mi.test(text) && /^(?:surface|operation|status):\s*/mi.test(text));
|
|
287
|
+
if (!isBridgeEnvelope) return true;
|
|
288
|
+
let sawBlank = false;
|
|
289
|
+
for (const line of text.split('\n')) {
|
|
290
|
+
const trimmed = line.trim();
|
|
291
|
+
if (!trimmed) {
|
|
292
|
+
sawBlank = true;
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
295
|
+
if (/^agent result\b/i.test(trimmed)) continue;
|
|
296
|
+
if (/^(?:undefined|null)$/i.test(trimmed)) continue;
|
|
297
|
+
if (/^<\/?(?:final-answer|task-notification|task-id|tool-use-id|output-file|result|status|summary|usage|total_tokens|tool_uses|duration_ms|worktree|worktreePath|worktreeBranch)[^>]*>$/i.test(trimmed)) continue;
|
|
298
|
+
if (!sawBlank && /^(?:agent task|background task|agent message queued\b|agent close:|task_id|surface|operation|label|status|type|target|agent|preset|model|effort|fast|limits|started|finished|error|notification|queueDepth):?\s*/i.test(trimmed)) continue;
|
|
299
|
+
if (!sawBlank && /^(?:agents|tasks):\s*/i.test(trimmed)) continue;
|
|
300
|
+
if (/^\(no agents or tasks\)$/i.test(trimmed)) continue;
|
|
301
|
+
if (!sawBlank && /^-\s+\S+/i.test(trimmed)) continue;
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function parseBackgroundTaskResult(value) {
|
|
308
|
+
const text = String(value || '').trim();
|
|
309
|
+
if (!text) return null;
|
|
310
|
+
const allLines = text.split('\n');
|
|
311
|
+
const start = allLines.findIndex((line) => line.trim() === 'background task');
|
|
312
|
+
if (start < 0) return null;
|
|
313
|
+
const rest = allLines.slice(start + 1);
|
|
314
|
+
const blank = rest.findIndex((line) => !line.trim());
|
|
315
|
+
const headLines = blank >= 0 ? rest.slice(0, blank) : rest;
|
|
316
|
+
const body = blank >= 0 ? rest.slice(blank + 1).join('\n').trim() : '';
|
|
317
|
+
const fields = {};
|
|
318
|
+
for (const line of headLines) {
|
|
319
|
+
const match = /^([a-zA-Z][\w-]*):\s*(.*)$/.exec(line.trim());
|
|
320
|
+
if (match) fields[match[1].toLowerCase()] = match[2].trim();
|
|
321
|
+
}
|
|
322
|
+
const status = String(fields.status || '').toLowerCase();
|
|
323
|
+
const error = fields.error || '';
|
|
324
|
+
return {
|
|
325
|
+
taskId: fields.task_id || fields.taskid || '',
|
|
326
|
+
surface: fields.surface || '',
|
|
327
|
+
operation: fields.operation || '',
|
|
328
|
+
label: fields.label || '',
|
|
329
|
+
status,
|
|
330
|
+
startedAt: fields.started || fields.startedat || '',
|
|
331
|
+
finishedAt: fields.finished || fields.finishedat || '',
|
|
332
|
+
body,
|
|
333
|
+
error,
|
|
334
|
+
hasResponse: Boolean(body) && !isBackgroundErrorOnlyBody(body, error) && !/^(running|pending|queued)$/i.test(status),
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function backgroundTaskMetaFromArgs(args = {}) {
|
|
339
|
+
const taskId = String(args.task_id || args.taskId || '').trim();
|
|
340
|
+
if (!taskId) return null;
|
|
341
|
+
return {
|
|
342
|
+
taskId,
|
|
343
|
+
surface: args.surface || '',
|
|
344
|
+
operation: args.operation || '',
|
|
345
|
+
label: args.label || '',
|
|
346
|
+
status: String(args.status || '').toLowerCase(),
|
|
347
|
+
startedAt: args.startedAt || args.started || '',
|
|
348
|
+
finishedAt: args.finishedAt || args.finished || '',
|
|
349
|
+
error: args.error || '',
|
|
350
|
+
body: '',
|
|
351
|
+
hasResponse: false,
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
export function resolveBackgroundTaskMeta(parsedArgs = {}, resultText = '') {
|
|
356
|
+
const parsed = parseBackgroundTaskResult(resultText);
|
|
357
|
+
if (parsed) {
|
|
358
|
+
if (!parsed.error && parsedArgs?.error) parsed.error = parsedArgs.error;
|
|
359
|
+
if (!parsed.status && parsedArgs?.status) parsed.status = String(parsedArgs.status).toLowerCase();
|
|
360
|
+
if (!parsed.surface && parsedArgs?.surface) parsed.surface = parsedArgs.surface;
|
|
361
|
+
return parsed;
|
|
362
|
+
}
|
|
363
|
+
return backgroundTaskMetaFromArgs(parsedArgs);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export function backgroundTaskElapsed(meta = {}, fallback = '') {
|
|
367
|
+
const startedMs = Date.parse(meta.startedAt || '');
|
|
368
|
+
const finishedMs = Date.parse(meta.finishedAt || '');
|
|
369
|
+
if (Number.isFinite(startedMs) && Number.isFinite(finishedMs) && finishedMs >= startedMs) {
|
|
370
|
+
const elapsedMs = finishedMs - startedMs;
|
|
371
|
+
return elapsedMs >= 1000 ? formatElapsed(elapsedMs) : '';
|
|
372
|
+
}
|
|
373
|
+
return fallback || '';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
export function prefixElapsed(detail, elapsed = '') {
|
|
377
|
+
const text = String(detail || '').trim();
|
|
378
|
+
const time = String(elapsed || '').trim();
|
|
379
|
+
if (!time) return text;
|
|
380
|
+
// Unified convention: the elapsed time ALWAYS goes at the END, ` · ` separated.
|
|
381
|
+
// Guard against a double-append when the text already ends with the same time.
|
|
382
|
+
if (text && text.endsWith(`· ${time}`)) return text;
|
|
383
|
+
return text ? `${text} · ${time}` : time;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
export function mergeTerminalDetail(status, detail = '') {
|
|
387
|
+
const label = displayTerminalStatus(status);
|
|
388
|
+
const text = String(detail || '').trim();
|
|
389
|
+
if (!label) return text;
|
|
390
|
+
if (label === 'Finished' && text) return text;
|
|
391
|
+
if (!text) return label;
|
|
392
|
+
if (text.toLowerCase().startsWith(label.toLowerCase())) return text;
|
|
393
|
+
return `${label} · ${text}`;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export function shouldPrefixSyncElapsed(normalizedName, label) {
|
|
397
|
+
const n = String(normalizedName || '').toLowerCase();
|
|
398
|
+
const l = String(label || '').toLowerCase();
|
|
399
|
+
return n === 'explore' || l === 'explore' || n === 'search' || l === 'search' || l === 'web search';
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function backgroundTaskDisplayName(normalizedName, meta = {}) {
|
|
403
|
+
const surface = String(meta.surface || normalizedName || '').toLowerCase();
|
|
404
|
+
if (surface === 'explore') return 'Explore';
|
|
405
|
+
if (surface === 'search') return 'Search';
|
|
406
|
+
if (surface === 'shell' || surface === 'bash' || surface === 'bash_session' || surface === 'shell_command' || surface === 'task') return 'Shell';
|
|
407
|
+
return titleizeAgentName(surface || normalizedName || 'Task');
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function backgroundTaskResultTitle(normalizedName, meta = {}) {
|
|
411
|
+
const display = backgroundTaskDisplayName(normalizedName, meta);
|
|
412
|
+
if (display === 'Shell') return 'Shell output';
|
|
413
|
+
if (display === 'Search') return 'Search results';
|
|
414
|
+
return `${display} response`;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export function backgroundTaskActionTitle(normalizedName, meta = {}) {
|
|
418
|
+
const display = backgroundTaskDisplayName(normalizedName, meta);
|
|
419
|
+
if (/^(running|pending|queued)$/i.test(meta.status || '')) return `Started ${display}`;
|
|
420
|
+
if (meta.hasResponse) return backgroundTaskResultTitle(normalizedName, meta);
|
|
421
|
+
return `${display} status`;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
export function backgroundTaskFailureDetail(meta = {}, parsedArgs = {}) {
|
|
425
|
+
const status = meta.status || parsedArgs?.status;
|
|
426
|
+
const error = meta.error || parsedArgs?.error;
|
|
427
|
+
if (!error) return '';
|
|
428
|
+
const surface = meta.surface || parsedArgs?.surface || '';
|
|
429
|
+
return backgroundTaskFailureStatusLabel(status, error, { surface });
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
export function backgroundTaskDetail(meta = {}, elapsed = '', parsedArgs = {}) {
|
|
433
|
+
const parts = [];
|
|
434
|
+
const status = displayTerminalStatus(meta.status);
|
|
435
|
+
if (status) parts.push(status);
|
|
436
|
+
if (meta.taskId) parts.push(`task_id: ${meta.taskId}`);
|
|
437
|
+
const firstBodyLine = String(meta.body || '').split('\n').map((line) => line.trim()).find(Boolean) || '';
|
|
438
|
+
if (firstBodyLine && /^(running|pending|queued)$/i.test(meta.status || '')) parts.push(firstBodyLine);
|
|
439
|
+
return prefixElapsed(parts.join(' · '), elapsed);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
export function isBackgroundTaskResponseArgs(normalizedName, args = {}) {
|
|
443
|
+
if (!isBackgroundTaskTool(normalizedName)) return false;
|
|
444
|
+
const type = String(args?.type || args?.action || '').toLowerCase();
|
|
445
|
+
const status = String(args?.status || '').toLowerCase();
|
|
446
|
+
return type === 'result' || type === 'completion' || (/^(completed|cancelled|canceled)$/i.test(status) && Boolean(args?.task_id));
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function isOutputDetailTool(normalizedName, label) {
|
|
450
|
+
const n = String(normalizedName || '').toLowerCase();
|
|
451
|
+
const l = String(label || '').toLowerCase();
|
|
452
|
+
return new Set([
|
|
453
|
+
'shell', 'bash', 'bash_session', 'shell_command', 'job_wait',
|
|
454
|
+
'read', 'view_image', 'read_mcp_resource',
|
|
455
|
+
'grep', 'glob', 'search', 'search_query', 'image_query', 'web_search', 'web_search_call', 'explore', 'web_fetch', 'fetch',
|
|
456
|
+
'list', 'ls', 'code_graph',
|
|
457
|
+
'recall', 'recall_memory', 'search_memories', 'remember', 'save_memory', 'update_memory',
|
|
458
|
+
]).has(n) || l === 'read' || l === 'search' || l === 'web search' || l === 'run';
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
export function genericCompletedDetail({ normalizedName, label, hasResult, firstResultLine, isError }) {
|
|
462
|
+
const n = String(normalizedName || '').toLowerCase();
|
|
463
|
+
const l = String(label || '').toLowerCase();
|
|
464
|
+
if (isError) return hasResult ? firstResultLine : 'Failed';
|
|
465
|
+
if (n === 'shell' || n === 'bash' || n === 'bash_session' || n === 'shell_command' || n === 'job_wait') {
|
|
466
|
+
return '';
|
|
467
|
+
}
|
|
468
|
+
if (isOutputDetailTool(n, l)) {
|
|
469
|
+
return hasResult ? firstResultLine : '';
|
|
470
|
+
}
|
|
471
|
+
return '';
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
export function toolSearchLoadedSummary(resultText) {
|
|
475
|
+
let parsed;
|
|
476
|
+
try {
|
|
477
|
+
parsed = JSON.parse(String(resultText || ''));
|
|
478
|
+
} catch {
|
|
479
|
+
const text = String(resultText || '');
|
|
480
|
+
const loaded = /^Loaded deferred tools:\s*(.+)$/m.exec(text)?.[1] || '';
|
|
481
|
+
const already = /^Already active:\s*(.+)$/m.exec(text)?.[1] || '';
|
|
482
|
+
return [
|
|
483
|
+
...(loaded ? [`Loaded: ${loaded}`] : []),
|
|
484
|
+
...(already ? [`Already active: ${already}`] : []),
|
|
485
|
+
].join(' · ');
|
|
486
|
+
}
|
|
487
|
+
const tools = parsed?.selected?.tools;
|
|
488
|
+
if (!tools || typeof tools !== 'object') return '';
|
|
489
|
+
const uniqueNames = (names) => [...new Set((Array.isArray(names) ? names : [])
|
|
490
|
+
.map((name) => String(name || '').trim())
|
|
491
|
+
.filter(Boolean))];
|
|
492
|
+
const loaded = uniqueNames(tools.added);
|
|
493
|
+
const already = uniqueNames(tools.already);
|
|
494
|
+
return [
|
|
495
|
+
...(loaded.length ? [`Loaded: ${loaded.join(', ')}`] : []),
|
|
496
|
+
...(already.length ? [`Already active: ${already.join(', ')}`] : []),
|
|
497
|
+
].join(' · ');
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
export function agentTerminalDetail(status, isError, elapsed, error = '') {
|
|
501
|
+
const failureDetail = isError && error
|
|
502
|
+
? backgroundTaskFailureStatusLabel(status, error, { surface: 'agent' })
|
|
503
|
+
: '';
|
|
504
|
+
if (failureDetail) return failureDetail;
|
|
505
|
+
const s = String(status || '').toLowerCase();
|
|
506
|
+
const word = /cancel/.test(s)
|
|
507
|
+
? 'Cancelled'
|
|
508
|
+
: /error|fail|killed|timeout/.test(s) || isError
|
|
509
|
+
? 'Failed'
|
|
510
|
+
: /done|success|complete|closed/.test(s)
|
|
511
|
+
? 'Finished'
|
|
512
|
+
: '';
|
|
513
|
+
// Unified ` · <time>` convention (previously "Finished after 12s").
|
|
514
|
+
return word ? `${word}${elapsed ? ` · ${elapsed}` : ''}` : '';
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
export function clampFailureCount(errorCount, groupCount, isError) {
|
|
518
|
+
const explicit = Number(errorCount);
|
|
519
|
+
if (Number.isFinite(explicit)) return Math.max(0, Math.min(groupCount, Math.floor(explicit)));
|
|
520
|
+
return isError ? groupCount : 0;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
function clipPlain(text, maxChars) {
|
|
524
|
+
const value = safeInlineText(text);
|
|
525
|
+
const max = Math.max(1, Number(maxChars) || 1);
|
|
526
|
+
if (value.length <= max) return value;
|
|
527
|
+
return `${value.slice(0, Math.max(0, max - 1))}…`;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Derive the collapsed tool-card text model shared by every surface.
|
|
532
|
+
*
|
|
533
|
+
* Mirrors ToolExecution's derivation exactly; the TUI consumes this and adds
|
|
534
|
+
* width fitting/colors, the desktop consumes this and adds CSS/ellipsis.
|
|
535
|
+
*
|
|
536
|
+
* @param {object} input Tool item fields (name/args/result/rawResult/isError/
|
|
537
|
+
* errorCount/callErrorCount/exitErrorCount/count/completedCount/startedAt/
|
|
538
|
+
* completedAt/aggregate/categories/doneCategories/headerFinalized/nowMs).
|
|
539
|
+
* @param {object} options { truncate(text,width), maxResultChars }.
|
|
540
|
+
*/
|
|
541
|
+
export function deriveToolCardModel(input = {}, options = {}) {
|
|
542
|
+
const {
|
|
543
|
+
name = '', args = {}, result = null, rawResult = null,
|
|
544
|
+
isError = false, errorCount, callErrorCount, exitErrorCount,
|
|
545
|
+
count = 1, completedCount, startedAt = 0, completedAt = 0,
|
|
546
|
+
aggregate = false, categories = {}, doneCategories = null,
|
|
547
|
+
headerFinalized = true,
|
|
548
|
+
} = input;
|
|
549
|
+
const nowMs = Number(input.nowMs || Date.now());
|
|
550
|
+
const truncate = typeof options.truncate === 'function' ? options.truncate : clipPlain;
|
|
551
|
+
const maxResultChars = Math.max(MIN_RESULT_LINE_CHARS,
|
|
552
|
+
Math.min(RESULT_LINE_HARD_MAX, Number(options.maxResultChars ?? RESULT_LINE_HARD_MAX)));
|
|
553
|
+
|
|
554
|
+
const groupCount = Math.max(1, Number(count || 1));
|
|
555
|
+
const doneCount = Math.max(0, Math.min(groupCount, Number(completedCount ?? (result == null ? 0 : groupCount))));
|
|
556
|
+
const rt = result == null ? null : String(result).replace(/\s+$/, '');
|
|
557
|
+
const rawRt = rawResult == null ? null : String(rawResult).replace(/\s+$/, '');
|
|
558
|
+
const pending = doneCount < groupCount;
|
|
559
|
+
const headerPending = pending || headerFinalized === false;
|
|
560
|
+
const hasResult = result != null && Boolean(String(rt || '').trim());
|
|
561
|
+
const hasRawResult = rawResult != null && Boolean(String(rawRt || '').trim());
|
|
562
|
+
const startedAtMs = Number(startedAt || 0);
|
|
563
|
+
const completedAtMs = Number(completedAt || 0);
|
|
564
|
+
const elapsedMs = startedAtMs ? Math.max(0, (pending ? nowMs : (completedAtMs || nowMs)) - startedAtMs) : 0;
|
|
565
|
+
const elapsed = elapsedMs >= 1000 ? formatElapsed(elapsedMs) : '';
|
|
566
|
+
const failedCount = clampFailureCount(errorCount, groupCount, isError);
|
|
567
|
+
const callFailedCount = clampFailureCount(callErrorCount, groupCount, false);
|
|
568
|
+
const exitFailedCount = clampFailureCount(exitErrorCount, groupCount, false);
|
|
569
|
+
|
|
570
|
+
if (aggregate) {
|
|
571
|
+
const displayCategories = normalizeCountMap(categories || {});
|
|
572
|
+
const normalizedDone = doneCategories ? normalizeCountMap(doneCategories) : displayCategories;
|
|
573
|
+
const hasDoneCounts = Object.values(normalizedDone || {}).some(
|
|
574
|
+
(v) => (v && typeof v === 'object' ? Number(v.count || 0) : Number(v || 0)) > 0,
|
|
575
|
+
);
|
|
576
|
+
const displayDone = hasDoneCounts ? normalizedDone : displayCategories;
|
|
577
|
+
const headerOrder = Array.isArray(args?.categoryOrder) ? args.categoryOrder : null;
|
|
578
|
+
const labelText = safeInlineText(formatAggregateHeader(
|
|
579
|
+
(headerPending ? displayCategories : displayDone) || {},
|
|
580
|
+
{ pending: headerPending, order: headerOrder },
|
|
581
|
+
));
|
|
582
|
+
const detailText = hasResult ? safeInlineText(rt) : '';
|
|
583
|
+
const terminalStatus = pending
|
|
584
|
+
? 'running'
|
|
585
|
+
: (resultTerminalStatus(rt) || (isError || failedCount > 0 ? 'failed' : 'completed'));
|
|
586
|
+
return {
|
|
587
|
+
aggregate: true, pending, headerPending, groupCount, doneCount, elapsed,
|
|
588
|
+
failedCount, callFailedCount, exitFailedCount, terminalStatus,
|
|
589
|
+
labelText, summaryText: '', headerFailureText: '',
|
|
590
|
+
detailLine: detailText || (pending ? 'Running' : 'Finished'),
|
|
591
|
+
detailIsPlaceholder: !detailText,
|
|
592
|
+
hasResult, hasRawResult,
|
|
593
|
+
displayedResultBodyText: rt || '',
|
|
594
|
+
firstResultLine: detailText, totalLines: detailText ? 1 : 0,
|
|
595
|
+
resultSummary: detailText || null, toolArgPath: '',
|
|
596
|
+
normalizedName: '', label: '', parsedArgs: args,
|
|
597
|
+
isShellSurface: false, isSkillSurface: false, isAgentSurfaceCard: false,
|
|
598
|
+
isAgentResponse: false, isBackgroundResponse: false, isBackgroundMetadataResult: false,
|
|
599
|
+
backgroundMeta: null,
|
|
600
|
+
};
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const { label, summary, normalizedName, args: parsedArgs } = formatToolSurface(name, args);
|
|
604
|
+
const isShellSurface = isShellTool(normalizedName, label);
|
|
605
|
+
const isSkillSurface = SKILL_SURFACE_NAMES.has(String(normalizedName || '').toLowerCase());
|
|
606
|
+
const backgroundMeta = !pending && isBackgroundTaskTool(normalizedName)
|
|
607
|
+
? resolveBackgroundTaskMeta(parsedArgs, rt || '')
|
|
608
|
+
: null;
|
|
609
|
+
const backgroundError = backgroundMeta?.error || parsedArgs?.error || '';
|
|
610
|
+
const errorOnlyResult = Boolean(rt) && isBackgroundErrorOnlyBody(rt, backgroundError);
|
|
611
|
+
const backgroundResultText = backgroundMeta?.hasResponse ? backgroundMeta.body : '';
|
|
612
|
+
const displayedResultText = backgroundResultText || (errorOnlyResult ? '' : (rt || ''));
|
|
613
|
+
const hasDisplayResult = Boolean(String(displayedResultText || '').trim());
|
|
614
|
+
const displayedResultBodyText = stripLeadingStatusMarkerFromText(displayedResultText);
|
|
615
|
+
const hasDisplayBody = Boolean(String(displayedResultBodyText || '').trim());
|
|
616
|
+
const lines = displayedResultBodyText ? displayedResultBodyText.split('\n') : [];
|
|
617
|
+
const totalLines = lines.length;
|
|
618
|
+
const resultSummary = !pending && hasDisplayBody
|
|
619
|
+
? summarizeToolResult(name, args, displayedResultBodyText, isError)
|
|
620
|
+
: null;
|
|
621
|
+
const firstResultLine = hasDisplayResult ? String(lines[0] ?? '') : '';
|
|
622
|
+
const shellStatus = isShellSurface
|
|
623
|
+
? shellDisplayStatus({ pending, failedCount, exitFailedCount, isError, result: displayedResultText })
|
|
624
|
+
: '';
|
|
625
|
+
const shellElapsed = isShellSurface ? (shellResultElapsed(displayedResultText) || elapsed) : '';
|
|
626
|
+
const backgroundElapsed = backgroundMeta
|
|
627
|
+
? backgroundTaskElapsed(backgroundMeta, elapsed)
|
|
628
|
+
: (isBackgroundTaskTool(normalizedName) ? backgroundTaskElapsed(parsedArgs, elapsed) : '');
|
|
629
|
+
const toolArgPath = parsedArgs?.path ?? parsedArgs?.file_path ?? parsedArgs?.file ?? '';
|
|
630
|
+
const imageDetail = normalizedName === 'view_image' && toolArgPath && !isError ? String(toolArgPath) : '';
|
|
631
|
+
const isBackgroundResult = !pending && isBackgroundTaskTool(normalizedName) && Boolean(backgroundMeta);
|
|
632
|
+
const isBackgroundResponse = isBackgroundResult
|
|
633
|
+
&& (backgroundMeta?.hasResponse || isBackgroundTaskResponseArgs(normalizedName, parsedArgs));
|
|
634
|
+
const isBackgroundMetadataResult = isBackgroundResult && !isBackgroundResponse && Boolean(backgroundMeta);
|
|
635
|
+
const backgroundMetadataFailureLabel = isBackgroundMetadataResult
|
|
636
|
+
? backgroundTaskFailureDetail(backgroundMeta, parsedArgs)
|
|
637
|
+
: '';
|
|
638
|
+
const backgroundMetadataHeaderFailure = Boolean(backgroundMetadataFailureLabel) && !hasDisplayResult
|
|
639
|
+
? backgroundMetadataFailureLabel
|
|
640
|
+
: '';
|
|
641
|
+
const agentHeaderFailure = !pending && isAgentTool(normalizedName) && isError && parsedArgs?.error && !hasDisplayResult
|
|
642
|
+
? backgroundTaskFailureStatusLabel(parsedArgs?.status, parsedArgs?.error, { surface: 'agent' })
|
|
643
|
+
: '';
|
|
644
|
+
const headerFailureText = backgroundMetadataHeaderFailure || agentHeaderFailure || '';
|
|
645
|
+
const agentCompletionDetail = !pending && isAgentTool(normalizedName) && !agentHeaderFailure
|
|
646
|
+
? agentTerminalDetail(parsedArgs?.status, isError, elapsed, parsedArgs?.error)
|
|
647
|
+
: '';
|
|
648
|
+
const agentDetail = !pending && isAgentTool(normalizedName) && !hasDisplayResult
|
|
649
|
+
? agentCompletionDetail
|
|
650
|
+
: '';
|
|
651
|
+
const genericDetail = !pending && !isShellSurface && !agentDetail && !imageDetail && !resultSummary
|
|
652
|
+
? genericCompletedDetail({ normalizedName, label, hasResult, firstResultLine, isError })
|
|
653
|
+
: '';
|
|
654
|
+
const terminalStatus = pending
|
|
655
|
+
? 'running'
|
|
656
|
+
: (shellStatus || normalizeTerminalStatus(backgroundMeta?.status) || normalizeTerminalStatus(parsedArgs?.status) || resultTerminalStatus(displayedResultText) || (isError || failedCount > 0 ? 'failed' : 'completed'));
|
|
657
|
+
const backgroundMetadataDetail = isBackgroundMetadataResult && !backgroundMetadataHeaderFailure
|
|
658
|
+
? backgroundTaskDetail(backgroundMeta, backgroundElapsed, parsedArgs)
|
|
659
|
+
: '';
|
|
660
|
+
const backgroundResponseDetail = isBackgroundResponse && resultSummary
|
|
661
|
+
? prefixElapsed(resultSummary, backgroundElapsed)
|
|
662
|
+
: resultSummary;
|
|
663
|
+
const syncElapsedDetail = !isBackgroundResponse && shouldPrefixSyncElapsed(normalizedName, label)
|
|
664
|
+
? prefixElapsed(backgroundResponseDetail, elapsed)
|
|
665
|
+
: backgroundResponseDetail;
|
|
666
|
+
const nonShellDetail = backgroundMetadataDetail || (/^(Cancelled|Failed|Finished)$/i.test(resultSummary || '') && agentCompletionDetail
|
|
667
|
+
? agentCompletionDetail
|
|
668
|
+
: syncElapsedDetail) || agentDetail || imageDetail || genericDetail;
|
|
669
|
+
const pendingDetailPlaceholder = pending && !isSkillSurface
|
|
670
|
+
? (elapsed ? `Running · ${elapsed}` : 'Running')
|
|
671
|
+
: '';
|
|
672
|
+
const shellCollapsedSummary = isShellSurface && !pending && hasDisplayResult
|
|
673
|
+
? (resultSummary || truncate(firstResultLine, Math.min(120, maxResultChars)))
|
|
674
|
+
: resultSummary;
|
|
675
|
+
const collapsedDetail = pending
|
|
676
|
+
? pendingDetailPlaceholder
|
|
677
|
+
: isShellSurface
|
|
678
|
+
? prefixElapsed(mergeTerminalDetail(shellStatus, shellCollapsedSummary), shellElapsed)
|
|
679
|
+
: mergeTerminalDetail(terminalStatus, nonShellDetail);
|
|
680
|
+
|
|
681
|
+
const isAgentResult = !isBackgroundResult && !pending && isAgentTool(normalizedName) && hasDisplayResult;
|
|
682
|
+
const isAgentResponse = isAgentResult && hasAgentResponseResult(rt);
|
|
683
|
+
const isAgentSurfaceCard = isAgentTool(normalizedName);
|
|
684
|
+
const agentSurfaceBriefRaw = isAgentSurfaceCard
|
|
685
|
+
? summarizeAgentSurfaceBrief(name, parsedArgs, displayedResultText || '', { isError, isResponse: isAgentResponse })
|
|
686
|
+
: '';
|
|
687
|
+
const agentSurfaceBrief = agentSurfaceBriefRaw
|
|
688
|
+
? truncate(agentSurfaceBriefRaw, Math.min(AGENT_SURFACE_BRIEF_MAX, maxResultChars))
|
|
689
|
+
: '';
|
|
690
|
+
|
|
691
|
+
// Collapsed visible detail line (skill/agent gating identical to the TUI).
|
|
692
|
+
let detailLine = collapsedDetail;
|
|
693
|
+
if (isSkillSurface) {
|
|
694
|
+
detailLine = isError && collapsedDetail ? collapsedDetail : '';
|
|
695
|
+
} else if (isBackgroundMetadataResult && backgroundMetadataHeaderFailure) {
|
|
696
|
+
detailLine = '';
|
|
697
|
+
} else if (isAgentSurfaceCard) {
|
|
698
|
+
const agentDetailFallback = collapsedDetail
|
|
699
|
+
|| (pending ? (pendingDetailPlaceholder || 'Running') : 'Finished');
|
|
700
|
+
const agentDetailLine = agentSurfaceBrief
|
|
701
|
+
|| truncate(String(agentDetailFallback), Math.min(AGENT_SURFACE_BRIEF_MAX, maxResultChars));
|
|
702
|
+
const agentFailureText = /\b(Cancelled|Canceled|Failed)\b/i.test(agentSurfaceBrief || collapsedDetail || '');
|
|
703
|
+
const keepAgentDetail = (isError || agentFailureText) && !(agentHeaderFailure && !agentSurfaceBrief);
|
|
704
|
+
detailLine = keepAgentDetail ? agentDetailLine : '';
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
let labelText;
|
|
708
|
+
if (isAgentResponse) labelText = agentResponseTitle(parsedArgs, groupCount);
|
|
709
|
+
else if (isBackgroundResponse) labelText = backgroundTaskResultTitle(normalizedName, backgroundMeta || parsedArgs);
|
|
710
|
+
else if (isBackgroundMetadataResult) labelText = backgroundTaskActionTitle(normalizedName, backgroundMeta);
|
|
711
|
+
else if (isShellSurface) labelText = shellHeader(shellStatus, groupCount);
|
|
712
|
+
else labelText = (isAgentTool(normalizedName) ? agentActionTitle(parsedArgs) : '')
|
|
713
|
+
|| formatToolActionHeader(name, args, { pending: headerPending, count: groupCount });
|
|
714
|
+
labelText = safeInlineText(labelText);
|
|
715
|
+
const toolSearchSummary = !pending && normalizedName === 'load_tool' && hasResult
|
|
716
|
+
? toolSearchLoadedSummary(displayedResultText)
|
|
717
|
+
: '';
|
|
718
|
+
const rawSummaryText = safeInlineText(isAgentResponse || isBackgroundResponse
|
|
719
|
+
? ''
|
|
720
|
+
: toolSearchSummary || (isAgentTool(normalizedName) ? agentActionSummary(parsedArgs, summary) : summary));
|
|
721
|
+
// Drop the parenthesized arg summary when it is a bare "<n> <unit>" count
|
|
722
|
+
// that the header verb already spells out.
|
|
723
|
+
const summaryIsHeaderCount = rawSummaryText
|
|
724
|
+
&& /^\d+\s+\S+$/.test(rawSummaryText)
|
|
725
|
+
&& labelText.endsWith(rawSummaryText);
|
|
726
|
+
const summaryText = summaryIsHeaderCount ? '' : rawSummaryText;
|
|
727
|
+
|
|
728
|
+
return {
|
|
729
|
+
aggregate: false, pending, headerPending, groupCount, doneCount, elapsed,
|
|
730
|
+
failedCount, callFailedCount, exitFailedCount, terminalStatus,
|
|
731
|
+
labelText, summaryText, headerFailureText,
|
|
732
|
+
detailLine, detailIsPlaceholder: Boolean(pendingDetailPlaceholder) && detailLine === pendingDetailPlaceholder,
|
|
733
|
+
hasResult, hasRawResult, hasDisplayResult, hasDisplayBody,
|
|
734
|
+
displayedResultBodyText, firstResultLine, totalLines, resultSummary,
|
|
735
|
+
shellCollapsedSummary, agentSurfaceBrief, toolArgPath: String(toolArgPath || ''),
|
|
736
|
+
normalizedName, label, parsedArgs,
|
|
737
|
+
isShellSurface, isSkillSurface, isAgentSurfaceCard, isAgentResponse,
|
|
738
|
+
isBackgroundResponse, isBackgroundMetadataResult, backgroundMeta,
|
|
739
|
+
};
|
|
740
|
+
}
|