mixdog 0.9.66 → 0.9.68
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/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +20 -2
- package/src/runtime/channels/lib/config.mjs +13 -5
- package/src/runtime/channels/lib/owned-runtime.mjs +65 -5
- package/src/runtime/channels/lib/scheduler.mjs +7 -15
- package/src/runtime/channels/lib/status-snapshot.mjs +6 -30
- package/src/runtime/channels/lib/tool-dispatch.mjs +6 -1
- package/src/runtime/channels/lib/webhook/relay-tunnel.mjs +203 -0
- package/src/runtime/channels/lib/webhook.mjs +46 -246
- package/src/runtime/channels/lib/worker-main.mjs +45 -92
- package/src/runtime/shared/automation-attachments.mjs +72 -0
- package/src/runtime/shared/automation-workflow.mjs +41 -0
- package/src/runtime/shared/config.mjs +0 -9
- package/src/runtime/shared/schedule-session-run.mjs +10 -1
- package/src/runtime/shared/schedules-db.mjs +18 -3
- package/src/runtime/shared/time-format.mjs +56 -0
- package/src/runtime/shared/tool-card-model.mjs +740 -0
- package/src/runtime/shared/webhook-session-run.mjs +57 -0
- package/src/runtime/shared/webhooks-db.mjs +19 -3
- package/src/session-runtime/channel-config-api.mjs +13 -1
- package/src/session-runtime/lifecycle-api.mjs +12 -0
- package/src/session-runtime/prewarm.mjs +13 -7
- package/src/session-runtime/runtime-core.mjs +127 -22
- package/src/session-runtime/session-text.mjs +6 -0
- package/src/session-runtime/settings-api.mjs +0 -12
- package/src/standalone/channel-admin.mjs +71 -22
- package/src/standalone/channel-daemon-client.mjs +5 -1
- package/src/standalone/channel-daemon-transport.mjs +112 -20
- package/src/standalone/channel-daemon.mjs +6 -4
- package/src/tui/App.jsx +2 -47
- package/src/tui/app/channel-pickers.mjs +8 -173
- package/src/tui/app/doctor.mjs +0 -1
- package/src/tui/app/slash-commands.mjs +1 -3
- package/src/tui/app/slash-dispatch.mjs +3 -3
- 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 +340 -346
- package/src/tui/engine/live-share.mjs +9 -0
- package/src/tui/engine/session-api-ext.mjs +32 -16
- package/src/tui/engine.mjs +14 -1
- package/src/tui/time-format.mjs +4 -51
- package/src/runtime/channels/lib/webhook/ngrok.mjs +0 -181
|
@@ -215,165 +215,8 @@ export function createChannelPickers({
|
|
|
215
215
|
setChannelPrompt(prompt);
|
|
216
216
|
};
|
|
217
217
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (focus === 'schedules') {
|
|
221
|
-
const schedules = setup.schedules || [];
|
|
222
|
-
const items = [
|
|
223
|
-
...(schedules.length ? schedules.map((schedule) => {
|
|
224
|
-
const enabled = schedule.enabled !== false;
|
|
225
|
-
return {
|
|
226
|
-
value: `schedule:${schedule.name}`,
|
|
227
|
-
label: schedule.name,
|
|
228
|
-
marker: enabled ? '●' : '○',
|
|
229
|
-
markerColor: channelRemoteEnabled ? (enabled ? theme.success : theme.inactive) : theme.inactive,
|
|
230
|
-
description: `${schedule.time || '(no cron)'} · ${schedule.route}${schedule.model ? ` · ${schedule.model}` : ''}${channelRemoteEnabled ? '' : ' · channel off'}`,
|
|
231
|
-
_action: 'schedule-toggle',
|
|
232
|
-
_name: schedule.name,
|
|
233
|
-
_enabled: enabled,
|
|
234
|
-
};
|
|
235
|
-
}) : [{
|
|
236
|
-
value: 'empty',
|
|
237
|
-
label: 'No schedules',
|
|
238
|
-
description: 'no schedules configured',
|
|
239
|
-
_action: 'noop',
|
|
240
|
-
}]),
|
|
241
|
-
];
|
|
242
|
-
const toggleSchedule = async (item) => {
|
|
243
|
-
if (item._action !== 'schedule-toggle') return;
|
|
244
|
-
if (!channelRemoteEnabled) {
|
|
245
|
-
store.pushNotice('enable channel first', 'warn');
|
|
246
|
-
return;
|
|
247
|
-
}
|
|
248
|
-
try {
|
|
249
|
-
await store.setScheduleEnabled?.(item._name, !item._enabled);
|
|
250
|
-
void openChannelSetupPicker('schedules', { highlightValue: `schedule:${item._name}` });
|
|
251
|
-
} catch (e) {
|
|
252
|
-
store.pushNotice(`schedule toggle failed: ${e?.message || e}`, 'error');
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
setPicker({
|
|
256
|
-
title: 'Schedules',
|
|
257
|
-
description: channelRemoteEnabled ? 'Enable or disable cron schedules.' : 'Enable channel to toggle schedules.',
|
|
258
|
-
initialIndex: Math.max(0, items.findIndex((entry) => entry.value === options.highlightValue)),
|
|
259
|
-
items,
|
|
260
|
-
onSelect: (_value, item) => toggleSchedule(item),
|
|
261
|
-
onLeft: (item) => toggleSchedule(item),
|
|
262
|
-
onRight: (item) => toggleSchedule(item),
|
|
263
|
-
onCancel: () => {
|
|
264
|
-
setPicker(null);
|
|
265
|
-
},
|
|
266
|
-
});
|
|
267
|
-
return;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
if (focus === 'webhook-endpoint') {
|
|
271
|
-
const returnTo = typeof options.returnTo === 'function'
|
|
272
|
-
? options.returnTo
|
|
273
|
-
: () => setPicker(null);
|
|
274
|
-
const domain = setup.webhook?.ngrokDomain || setup.webhook?.domain || '';
|
|
275
|
-
const items = [
|
|
276
|
-
{
|
|
277
|
-
value: 'endpoint-domain',
|
|
278
|
-
label: 'ngrok domain',
|
|
279
|
-
description: domain ? domain : 'Not set · Enter ngrok domain',
|
|
280
|
-
_action: 'endpoint-domain',
|
|
281
|
-
},
|
|
282
|
-
{
|
|
283
|
-
value: 'endpoint-authtoken',
|
|
284
|
-
label: 'authtoken',
|
|
285
|
-
description: setup.webhook?.authenticated === true ? 'Set' : 'Not set · Enter authtoken',
|
|
286
|
-
_action: 'endpoint-authtoken',
|
|
287
|
-
},
|
|
288
|
-
];
|
|
289
|
-
setPicker({
|
|
290
|
-
title: 'Webhook endpoint',
|
|
291
|
-
description: 'ngrok domain and authtoken. Toggle individual webhooks in /webhooks.',
|
|
292
|
-
help: '↑/↓ Select · Enter Edit · Esc Back',
|
|
293
|
-
indexMode: 'always',
|
|
294
|
-
labelWidth: 18,
|
|
295
|
-
items,
|
|
296
|
-
onSelect: (_value, item) => {
|
|
297
|
-
try {
|
|
298
|
-
if (item._action === 'endpoint-domain') {
|
|
299
|
-
openChannelPrompt({
|
|
300
|
-
kind: 'webhook-domain',
|
|
301
|
-
label: 'ngrok domain',
|
|
302
|
-
hint: 'Paste the reserved ngrok domain (e.g. my-app.ngrok-free.app).',
|
|
303
|
-
afterSave: () => void openChannelSetupPicker('webhook-endpoint', options),
|
|
304
|
-
});
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
if (item._action === 'endpoint-authtoken') {
|
|
308
|
-
openChannelPrompt({
|
|
309
|
-
kind: 'webhook-token',
|
|
310
|
-
label: 'Webhook/ngrok authtoken',
|
|
311
|
-
hint: 'Paste the webhook/ngrok authtoken. It is stored in the OS keychain.',
|
|
312
|
-
afterSave: () => void openChannelSetupPicker('webhook-endpoint', options),
|
|
313
|
-
});
|
|
314
|
-
}
|
|
315
|
-
} catch (e) {
|
|
316
|
-
store.pushNotice(`webhook endpoint failed: ${e?.message || e}`, 'error');
|
|
317
|
-
}
|
|
318
|
-
},
|
|
319
|
-
onCancel: () => {
|
|
320
|
-
setPicker(null);
|
|
321
|
-
returnTo();
|
|
322
|
-
},
|
|
323
|
-
});
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
if (focus === 'webhooks') {
|
|
328
|
-
const hooks = setup.webhooks || [];
|
|
329
|
-
const items = [
|
|
330
|
-
...(hooks.length ? hooks.map((hook) => {
|
|
331
|
-
const enabled = hook.enabled !== false;
|
|
332
|
-
return {
|
|
333
|
-
value: `webhook:${hook.name}`,
|
|
334
|
-
label: hook.name,
|
|
335
|
-
marker: enabled ? '●' : '○',
|
|
336
|
-
markerColor: channelRemoteEnabled ? (enabled ? theme.success : theme.inactive) : theme.inactive,
|
|
337
|
-
description: `${hook.parser || 'github'} · ${hook.route} · secret:${hook.secretSet ? 'set' : 'missing'}${channelRemoteEnabled ? '' : ' · channel off'}`,
|
|
338
|
-
_action: 'webhook-toggle',
|
|
339
|
-
_name: hook.name,
|
|
340
|
-
_enabled: enabled,
|
|
341
|
-
};
|
|
342
|
-
}) : [{
|
|
343
|
-
value: 'empty',
|
|
344
|
-
label: 'No webhooks',
|
|
345
|
-
description: 'no webhook endpoints configured',
|
|
346
|
-
_action: 'noop',
|
|
347
|
-
}]),
|
|
348
|
-
];
|
|
349
|
-
const toggleWebhook = async (item) => {
|
|
350
|
-
if (item._action !== 'webhook-toggle') return;
|
|
351
|
-
if (!channelRemoteEnabled) {
|
|
352
|
-
store.pushNotice('enable channel first', 'warn');
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
try {
|
|
356
|
-
await store.setWebhookEnabled?.(item._name, !item._enabled);
|
|
357
|
-
void openChannelSetupPicker('webhooks', { highlightValue: `webhook:${item._name}` });
|
|
358
|
-
} catch (e) {
|
|
359
|
-
store.pushNotice(`webhook toggle failed: ${e?.message || e}`, 'error');
|
|
360
|
-
}
|
|
361
|
-
};
|
|
362
|
-
setPicker({
|
|
363
|
-
title: 'Webhooks',
|
|
364
|
-
description: channelRemoteEnabled ? 'Enable or disable inbound webhook endpoints.' : 'Enable channel to toggle webhooks.',
|
|
365
|
-
initialIndex: Math.max(0, items.findIndex((entry) => entry.value === options.highlightValue)),
|
|
366
|
-
items,
|
|
367
|
-
onSelect: (_value, item) => toggleWebhook(item),
|
|
368
|
-
onLeft: (item) => toggleWebhook(item),
|
|
369
|
-
onRight: (item) => toggleWebhook(item),
|
|
370
|
-
onCancel: () => {
|
|
371
|
-
setPicker(null);
|
|
372
|
-
},
|
|
373
|
-
});
|
|
374
|
-
return;
|
|
375
|
-
}
|
|
376
|
-
|
|
218
|
+
// Schedules/webhooks management is desktop-only (user decision): the TUI
|
|
219
|
+
// no longer carries their pickers or the webhook-endpoint info page.
|
|
377
220
|
const worker = store.getChannelWorkerStatus?.();
|
|
378
221
|
const activeBackend = setup.backend === 'telegram' ? 'telegram' : 'discord';
|
|
379
222
|
const backendLabel = activeBackend === 'telegram' ? 'Telegram' : 'Discord';
|
|
@@ -465,20 +308,12 @@ export function createChannelPickers({
|
|
|
465
308
|
{
|
|
466
309
|
value: 'webhook-endpoint',
|
|
467
310
|
label: 'Webhook endpoint',
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const hasDomain = Boolean(setup.webhook?.ngrokDomain || setup.webhook?.domain);
|
|
475
|
-
const hasAuth = setup.webhook?.authenticated === true;
|
|
476
|
-
const needs = [
|
|
477
|
-
...(hasDomain ? [] : ['domain']),
|
|
478
|
-
...(hasAuth ? [] : ['authtoken']),
|
|
479
|
-
];
|
|
480
|
-
return needs.length ? `Needs ${needs.join(' + ')}` : 'ngrok domain and authtoken set';
|
|
481
|
-
})(),
|
|
311
|
+
// Relay tunnel: public exposure is automatic, so the endpoint is
|
|
312
|
+
// "On" whenever the webhook server itself is enabled.
|
|
313
|
+
meta: setup.webhook?.enabled === false ? 'Off' : 'On',
|
|
314
|
+
description: setup.webhook?.publicUrl
|
|
315
|
+
? setup.webhook.publicUrl
|
|
316
|
+
: 'Mixdog relay tunnel — URL assigned on first run',
|
|
482
317
|
_action: 'webhook-endpoint',
|
|
483
318
|
},
|
|
484
319
|
];
|
package/src/tui/app/doctor.mjs
CHANGED
|
@@ -165,7 +165,6 @@ export async function buildDoctorReport(runtime = {}, getState = () => ({})) {
|
|
|
165
165
|
const tokens = [];
|
|
166
166
|
if (setup.discord?.authenticated) tokens.push('discord');
|
|
167
167
|
if (setup.telegram?.authenticated) tokens.push('telegram');
|
|
168
|
-
if (setup.webhook?.authenticated) tokens.push('webhook');
|
|
169
168
|
const running = worker.running === true;
|
|
170
169
|
const detail = `enabled · worker ${running ? 'running' : 'stopped'} · tokens: ${tokens.length ? tokens.join(', ') : 'none'}`;
|
|
171
170
|
row(running ? 'ok' : 'warn', detail);
|
|
@@ -25,10 +25,8 @@ export const SLASH_COMMANDS = [
|
|
|
25
25
|
{ name: 'plugins', usage: '/plugins', description: 'Manage local plugin integrations' },
|
|
26
26
|
{ name: 'hooks', usage: '/hooks', description: 'Manage before-tool hook rules and events' },
|
|
27
27
|
{ name: 'providers', usage: '/providers', description: 'Manage auth, API keys, OAuth, and local endpoints' },
|
|
28
|
-
{ name: 'channels', usage: '/channels', description: 'Manage Discord,
|
|
28
|
+
{ name: 'channels', usage: '/channels', description: 'Manage Discord, Telegram, and voice' },
|
|
29
29
|
{ name: 'remote', usage: '/remote', description: 'Claim remote for this session (takes over from any other session)' },
|
|
30
|
-
{ name: 'schedules', usage: '/schedules', description: 'Manage schedules' },
|
|
31
|
-
{ name: 'webhooks', usage: '/webhooks', description: 'Manage inbound webhooks' },
|
|
32
30
|
{ name: 'settings', usage: '/setting', aliases: ['setting', 'config'], aliasUsage: ['settings', 'config'], showAliasUsage: false, description: 'Open runtime settings' },
|
|
33
31
|
{ name: 'profile', usage: '/profile', description: 'Set your title and response language' },
|
|
34
32
|
{ name: 'update', usage: '/update', description: 'Check version and update mixdog' },
|
|
@@ -265,10 +265,10 @@ export function createSlashDispatch({
|
|
|
265
265
|
void openChannelSetupPicker('all');
|
|
266
266
|
return true;
|
|
267
267
|
case 'schedules':
|
|
268
|
-
void openChannelSetupPicker('schedules');
|
|
269
|
-
return true;
|
|
270
268
|
case 'webhooks':
|
|
271
|
-
|
|
269
|
+
// Management surface is desktop-only (user decision): hidden from the
|
|
270
|
+
// palette, and a typed command answers instead of opening a picker.
|
|
271
|
+
store.pushNotice('Schedules and webhooks are managed in the Mixdog desktop app', 'info');
|
|
272
272
|
return true;
|
|
273
273
|
case 'auth':
|
|
274
274
|
store.pushNotice('/auth moved to /providers', 'info');
|
|
@@ -18,13 +18,9 @@ import { BULLET_OPERATOR } from '../figures.mjs';
|
|
|
18
18
|
import {
|
|
19
19
|
displayToolName as surfaceDisplayToolName,
|
|
20
20
|
formatToolSurface,
|
|
21
|
-
summarizeToolResult as surfaceSummarizeToolResult,
|
|
22
21
|
formatAggregateHeader,
|
|
23
|
-
formatToolActionHeader,
|
|
24
|
-
summarizeAgentSurfaceBrief,
|
|
25
|
-
AGENT_SURFACE_BRIEF_MAX,
|
|
26
22
|
} from '../../runtime/shared/tool-surface.mjs';
|
|
27
|
-
import {
|
|
23
|
+
import { deriveToolCardModel } from '../../runtime/shared/tool-card-model.mjs';
|
|
28
24
|
import {
|
|
29
25
|
MIN_RESULT_LINE_CHARS,
|
|
30
26
|
RESULT_LINE_HARD_MAX,
|
|
@@ -33,36 +29,13 @@ import {
|
|
|
33
29
|
safeInlineText,
|
|
34
30
|
normalizeCountMap,
|
|
35
31
|
truncateToWidth,
|
|
36
|
-
shellResultElapsed,
|
|
37
|
-
normalizeTerminalStatus,
|
|
38
32
|
resultTerminalStatus,
|
|
39
33
|
stripLeadingStatusMarkerLines,
|
|
40
34
|
stripLeadingStatusMarkerFromText,
|
|
41
35
|
} from './tool-execution/text-format.mjs';
|
|
42
36
|
import {
|
|
43
37
|
SKILL_SURFACE_NAMES,
|
|
44
|
-
isShellTool,
|
|
45
|
-
shellDisplayStatus,
|
|
46
|
-
shellHeader,
|
|
47
38
|
isAgentTool,
|
|
48
|
-
isBackgroundTaskTool,
|
|
49
|
-
agentResponseTitle,
|
|
50
|
-
agentActionTitle,
|
|
51
|
-
agentActionSummary,
|
|
52
|
-
hasAgentResponseResult,
|
|
53
|
-
resolveBackgroundTaskMeta,
|
|
54
|
-
backgroundTaskElapsed,
|
|
55
|
-
prefixElapsed,
|
|
56
|
-
mergeTerminalDetail,
|
|
57
|
-
shouldPrefixSyncElapsed,
|
|
58
|
-
backgroundTaskResultTitle,
|
|
59
|
-
backgroundTaskActionTitle,
|
|
60
|
-
backgroundTaskFailureDetail,
|
|
61
|
-
backgroundTaskDetail,
|
|
62
|
-
isBackgroundTaskResponseArgs,
|
|
63
|
-
genericCompletedDetail,
|
|
64
|
-
toolSearchLoadedSummary,
|
|
65
|
-
agentTerminalDetail,
|
|
66
39
|
clampFailureCount,
|
|
67
40
|
toolStatusColor,
|
|
68
41
|
} from './tool-execution/surface-detail.mjs';
|
|
@@ -77,15 +50,6 @@ const TOOL_PENDING_SHOW_DELAY_MS = 1000;
|
|
|
77
50
|
// One shared-tick cadence covers both the 500ms blink and per-second elapsed;
|
|
78
51
|
// finer than either boundary so both stay crisp off a single timer.
|
|
79
52
|
const TOOL_ANIM_TICK_MS = TOOL_BLINK_MS;
|
|
80
|
-
function statusCopy(name, label, count, doneCount, pending, isError, args = {}) {
|
|
81
|
-
// No stableVerbWidth padding: it padded the done verb to the active ("-ing")
|
|
82
|
-
// width, which Ink trims at the line END (vendor output trimEnd) so it never
|
|
83
|
-
// stabilized the pending→done flip — it only left an UGLY mid-header gap
|
|
84
|
-
// ("Searched 1 pattern", "Read 1 file"). The header is wrap="truncate"
|
|
85
|
-
// behind a fixed gutter and the fullscreen full-clear repaints the row, so
|
|
86
|
-
// dropping the pad just normalizes the spacing.
|
|
87
|
-
return formatToolActionHeader(name, args, { pending, count });
|
|
88
|
-
}
|
|
89
53
|
export function ToolExecution({ name, args, result, rawResult, isError, errorCount, callErrorCount, exitErrorCount, expanded, columns = 80, attached = false, count = 1, completedCount = 0, startedAt = 0, completedAt = 0, aggregate = false, categories = {}, doneCategories = null, headerFinalized = true, deferredDisplayReady = false, agentResponseAggregate = false }) {
|
|
90
54
|
const rowWidth = Math.max(1, Number(columns || 80));
|
|
91
55
|
const groupCount = Math.max(1, Number(count || 1));
|
|
@@ -284,104 +248,52 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
284
248
|
}
|
|
285
249
|
|
|
286
250
|
// ── Normal (non-aggregate) tool card ────────────────────────────
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
? resolveBackgroundTaskMeta(parsedArgs, rt || '')
|
|
292
|
-
: null;
|
|
293
|
-
const backgroundError = backgroundMeta?.error || parsedArgs?.error || '';
|
|
294
|
-
const errorOnlyResult = Boolean(rt) && isBackgroundErrorOnlyBody(rt, backgroundError);
|
|
295
|
-
const backgroundResultText = backgroundMeta?.hasResponse ? backgroundMeta.body : '';
|
|
296
|
-
const displayedResultText = backgroundResultText || (errorOnlyResult ? '' : (rt || ''));
|
|
297
|
-
const hasDisplayResult = Boolean(String(displayedResultText || '').trim());
|
|
298
|
-
const displayedResultBodyText = stripLeadingStatusMarkerFromText(displayedResultText);
|
|
299
|
-
const hasDisplayBody = Boolean(String(displayedResultBodyText || '').trim());
|
|
300
|
-
const lines = displayedResultBodyText ? displayedResultBodyText.split('\n') : [];
|
|
301
|
-
const totalLines = lines.length;
|
|
302
|
-
// Semantic one-line summary derived purely from name/args/result text.
|
|
303
|
-
// Shown in the collapsed, non-error view in place of the raw result block.
|
|
304
|
-
// Grouped cards ("Searched N files" / "Read N files") get the same treatment
|
|
305
|
-
// as single calls: a one-line semantic summary stands in for the raw block.
|
|
306
|
-
const resultSummary = !pending && hasDisplayBody
|
|
307
|
-
? surfaceSummarizeToolResult(name, args, displayedResultBodyText, isError)
|
|
308
|
-
: null;
|
|
309
|
-
// Same fit budget fitResultLine() uses, to detect a line that will be clipped.
|
|
251
|
+
// Single source: the shared collapsed-card derivation (labels, casing,
|
|
252
|
+
// status merging, detail row) consumed by BOTH the TUI and the desktop
|
|
253
|
+
// renderer (apps/desktop TranscriptView ToolCard). Width fitting, theme
|
|
254
|
+
// colors, blink, and expansion handling stay TUI-side below.
|
|
310
255
|
const maxResultChars = Math.min(RESULT_LINE_HARD_MAX, Math.max(MIN_RESULT_LINE_CHARS, Number(columns || 80) - 7));
|
|
256
|
+
const model = deriveToolCardModel({
|
|
257
|
+
name,
|
|
258
|
+
args,
|
|
259
|
+
result,
|
|
260
|
+
rawResult,
|
|
261
|
+
isError,
|
|
262
|
+
errorCount,
|
|
263
|
+
callErrorCount,
|
|
264
|
+
exitErrorCount,
|
|
265
|
+
count: displayGroupCount,
|
|
266
|
+
completedCount: doneCount,
|
|
267
|
+
startedAt,
|
|
268
|
+
completedAt,
|
|
269
|
+
headerFinalized,
|
|
270
|
+
nowMs,
|
|
271
|
+
}, { truncate: truncateToWidth, maxResultChars });
|
|
272
|
+
const {
|
|
273
|
+
labelText,
|
|
274
|
+
summaryText,
|
|
275
|
+
headerFailureText: headerFailureStatus,
|
|
276
|
+
detailLine: collapsedDetailLine,
|
|
277
|
+
detailIsPlaceholder,
|
|
278
|
+
terminalStatus,
|
|
279
|
+
normalizedName,
|
|
280
|
+
isShellSurface,
|
|
281
|
+
isAgentSurfaceCard,
|
|
282
|
+
isAgentResponse,
|
|
283
|
+
isBackgroundMetadataResult,
|
|
284
|
+
hasDisplayResult,
|
|
285
|
+
hasDisplayBody,
|
|
286
|
+
displayedResultBodyText,
|
|
287
|
+
firstResultLine,
|
|
288
|
+
totalLines,
|
|
289
|
+
resultSummary,
|
|
290
|
+
shellCollapsedSummary,
|
|
291
|
+
toolArgPath,
|
|
292
|
+
} = model;
|
|
293
|
+
const lines = displayedResultBodyText ? displayedResultBodyText.split('\n') : [];
|
|
311
294
|
const resultColor = theme.text;
|
|
312
|
-
const firstResultLine = hasDisplayResult ? String(lines[0] ?? '') : '';
|
|
313
295
|
const firstResultLineClipped = hasDisplayBody && stringWidth(firstResultLine) > maxResultChars;
|
|
314
296
|
const hasHiddenDetail = !pending && hasDisplayBody && (totalLines > 1 || firstResultLineClipped || Boolean(resultSummary));
|
|
315
|
-
const shellStatus = isShellSurface ? shellDisplayStatus({ pending, failedCount, exitFailedCount, isError, result: displayedResultText }) : '';
|
|
316
|
-
const shellElapsed = isShellSurface ? (shellResultElapsed(displayedResultText) || elapsed) : '';
|
|
317
|
-
const backgroundElapsed = backgroundMeta
|
|
318
|
-
? backgroundTaskElapsed(backgroundMeta, elapsed)
|
|
319
|
-
: (isBackgroundTaskTool(normalizedName) ? backgroundTaskElapsed(parsedArgs, elapsed) : '');
|
|
320
|
-
|
|
321
|
-
const toolArgPath = parsedArgs?.path ?? parsedArgs?.file_path ?? parsedArgs?.file ?? '';
|
|
322
|
-
// Audit HIGH: on a FAILED view_image the path detail used to win over the
|
|
323
|
-
// error cause (nonShellDetail order puts imageDetail before genericDetail),
|
|
324
|
-
// so the card showed the filename instead of why it failed. Suppress the
|
|
325
|
-
// path detail on error; the error-cause summary/first line takes the row.
|
|
326
|
-
const imageDetail = normalizedName === 'view_image' && toolArgPath && !isError ? String(toolArgPath) : '';
|
|
327
|
-
const isBackgroundResult = !pending && isBackgroundTaskTool(normalizedName) && Boolean(backgroundMeta);
|
|
328
|
-
const isBackgroundResponse = isBackgroundResult && (backgroundMeta?.hasResponse || isBackgroundTaskResponseArgs(normalizedName, parsedArgs));
|
|
329
|
-
const isBackgroundMetadataResult = isBackgroundResult && !isBackgroundResponse && Boolean(backgroundMeta);
|
|
330
|
-
const backgroundMetadataFailureLabel = isBackgroundMetadataResult
|
|
331
|
-
? backgroundTaskFailureDetail(backgroundMeta, parsedArgs)
|
|
332
|
-
: '';
|
|
333
|
-
const backgroundMetadataHeaderFailure = Boolean(backgroundMetadataFailureLabel) && !hasDisplayResult
|
|
334
|
-
? backgroundMetadataFailureLabel
|
|
335
|
-
: '';
|
|
336
|
-
const agentHeaderFailure = !pending && isAgentTool(normalizedName) && isError && parsedArgs?.error && !hasDisplayResult
|
|
337
|
-
? backgroundTaskFailureStatusLabel(parsedArgs?.status, parsedArgs?.error, { surface: 'agent' })
|
|
338
|
-
: '';
|
|
339
|
-
const headerFailureStatus = backgroundMetadataHeaderFailure || agentHeaderFailure || '';
|
|
340
|
-
const agentCompletionDetail = !pending && isAgentTool(normalizedName) && !agentHeaderFailure
|
|
341
|
-
? agentTerminalDetail(parsedArgs?.status, isError, elapsed, parsedArgs?.error)
|
|
342
|
-
: '';
|
|
343
|
-
const agentDetail = !pending && isAgentTool(normalizedName) && !hasDisplayResult
|
|
344
|
-
? agentCompletionDetail
|
|
345
|
-
: '';
|
|
346
|
-
const genericDetail = !pending && !isShellSurface && !agentDetail && !imageDetail && !resultSummary
|
|
347
|
-
? genericCompletedDetail({ normalizedName, label, hasResult, firstResultLine, isError })
|
|
348
|
-
: '';
|
|
349
|
-
const terminalStatus = pending
|
|
350
|
-
? 'running'
|
|
351
|
-
: (shellStatus || normalizeTerminalStatus(backgroundMeta?.status) || normalizeTerminalStatus(parsedArgs?.status) || resultTerminalStatus(displayedResultText) || (isError || failedCount > 0 ? 'failed' : 'completed'));
|
|
352
|
-
const backgroundMetadataDetail = isBackgroundMetadataResult && !backgroundMetadataHeaderFailure
|
|
353
|
-
? backgroundTaskDetail(backgroundMeta, backgroundElapsed, parsedArgs)
|
|
354
|
-
: '';
|
|
355
|
-
const backgroundResponseDetail = isBackgroundResponse && resultSummary
|
|
356
|
-
? prefixElapsed(resultSummary, backgroundElapsed)
|
|
357
|
-
: resultSummary;
|
|
358
|
-
const syncElapsedDetail = !isBackgroundResponse && shouldPrefixSyncElapsed(normalizedName, label)
|
|
359
|
-
? prefixElapsed(backgroundResponseDetail, elapsed)
|
|
360
|
-
: backgroundResponseDetail;
|
|
361
|
-
const nonShellDetail = backgroundMetadataDetail || (/^(Cancelled|Failed|Finished)$/i.test(resultSummary || '') && agentCompletionDetail
|
|
362
|
-
? agentCompletionDetail
|
|
363
|
-
: syncElapsedDetail) || agentDetail || imageDetail || genericDetail;
|
|
364
|
-
// A pending non-aggregate tool used to drop its detail row entirely
|
|
365
|
-
// (collapsedDetail = ''), so the card rendered as a single header row. But
|
|
366
|
-
// estimateTranscriptItemRows() in App.jsx reserves 2 rows for a collapsed
|
|
367
|
-
// non-aggregate tool (1 only for skill surfaces). That left a 1-row gap that
|
|
368
|
-
// closed the instant the result landed — the surviving "line-jump". Reserve the same
|
|
369
|
-
// dim placeholder detail row the aggregate card uses (`Running`) for the whole
|
|
370
|
-
// pending lifecycle so the height stays fixed at header + one detail row and
|
|
371
|
-
// the final summary just fills in place. Skill surfaces collapse to a single
|
|
372
|
-
// row in BOTH the estimate and the render (visibleDetailLines drops the row
|
|
373
|
-
// for isSkillSurface below), so they get no placeholder.
|
|
374
|
-
const pendingDetailPlaceholder = pending && !isSkillSurface
|
|
375
|
-
? (elapsed ? `Running · ${elapsed}` : 'Running')
|
|
376
|
-
: '';
|
|
377
|
-
const shellCollapsedSummary = isShellSurface && !pending && hasDisplayResult
|
|
378
|
-
? (resultSummary || truncateToWidth(firstResultLine, Math.min(120, maxResultChars)))
|
|
379
|
-
: resultSummary;
|
|
380
|
-
const collapsedDetail = pending
|
|
381
|
-
? pendingDetailPlaceholder
|
|
382
|
-
: isShellSurface
|
|
383
|
-
? prefixElapsed(mergeTerminalDetail(shellStatus, shellCollapsedSummary), shellElapsed)
|
|
384
|
-
: mergeTerminalDetail(terminalStatus, nonShellDetail);
|
|
385
297
|
const backgroundMetadataExpandable = isBackgroundMetadataResult && hasRawResult && !pending;
|
|
386
298
|
const showRawResult = expanded && (hasDisplayBody || hasRawResult)
|
|
387
299
|
&& (!isBackgroundMetadataResult || hasRawResult);
|
|
@@ -389,47 +301,11 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
389
301
|
? (agentResponseAggregate && hasRawResult
|
|
390
302
|
? stripLeadingStatusMarkerLines(rawRt.split('\n'))
|
|
391
303
|
: (hasDisplayBody ? lines : (rawRt ? stripLeadingStatusMarkerLines(rawRt.split('\n')) : [])))
|
|
392
|
-
: (
|
|
393
|
-
const isPendingPlaceholderDetail = !showRawResult &&
|
|
304
|
+
: (collapsedDetailLine ? [collapsedDetailLine] : []);
|
|
305
|
+
const isPendingPlaceholderDetail = !showRawResult && detailIsPlaceholder;
|
|
394
306
|
const detailColor = isPendingPlaceholderDetail ? theme.subtle : theme.text;
|
|
395
|
-
|
|
396
|
-
const
|
|
397
|
-
const isAgentResponse = isAgentResult && hasAgentResponseResult(rt);
|
|
398
|
-
const isAgentSurfaceCard = isAgentTool(normalizedName);
|
|
399
|
-
const agentSurfaceBriefRaw = isAgentSurfaceCard && !showRawResult
|
|
400
|
-
? summarizeAgentSurfaceBrief(name, parsedArgs, displayedResultText || '', { isError, isResponse: isAgentResponse })
|
|
401
|
-
: '';
|
|
402
|
-
const agentSurfaceBrief = agentSurfaceBriefRaw
|
|
403
|
-
? truncateToWidth(agentSurfaceBriefRaw, Math.min(AGENT_SURFACE_BRIEF_MAX, maxResultChars))
|
|
404
|
-
: '';
|
|
405
|
-
// Skill loads carry the skill name in the header already
|
|
406
|
-
// ("Loaded 1 skill (name)"); the collapsed detail row just repeats it, so
|
|
407
|
-
// drop it and keep the card a single line. Expanding (ctrl+o) still shows the
|
|
408
|
-
// full skill body via the raw-result path.
|
|
409
|
-
// Agent spawn/send/response cards show a tight brief under the ⎿ gutter when
|
|
410
|
-
// collapsed; ctrl+o expand still surfaces the full body.
|
|
411
|
-
let visibleDetailLines = detailLines;
|
|
412
|
-
if (isSkillSurface && !showRawResult) {
|
|
413
|
-
// Audit HIGH: a FAILED skill load used to drop its detail row with the
|
|
414
|
-
// success path, hiding the one-line cause entirely. Keep the detail row
|
|
415
|
-
// when the call errored; only the redundant success repeat is dropped.
|
|
416
|
-
visibleDetailLines = isError && collapsedDetail ? [collapsedDetail] : [];
|
|
417
|
-
} else if (isBackgroundMetadataResult && backgroundMetadataHeaderFailure && !showRawResult) {
|
|
418
|
-
visibleDetailLines = [];
|
|
419
|
-
} else if (isAgentSurfaceCard && !showRawResult) {
|
|
420
|
-
// Agent cards collapse to a SINGLE header row like skill surfaces. Keep the
|
|
421
|
-
// ⎿ detail row ONLY when it carries failure info: the call errored, or the
|
|
422
|
-
// brief/summary reads as a failure/cancel. A header-failure-only card (no
|
|
423
|
-
// brief) shows the cause in the header, so it stays a single row too.
|
|
424
|
-
// Expanded (ctrl+o raw) still flows through the showRawResult path above.
|
|
425
|
-
const agentDetailFallback = collapsedDetail
|
|
426
|
-
|| (pending ? (pendingDetailPlaceholder || 'Running') : 'Finished');
|
|
427
|
-
const agentDetailLine = agentSurfaceBrief
|
|
428
|
-
|| truncateToWidth(String(agentDetailFallback), Math.min(AGENT_SURFACE_BRIEF_MAX, maxResultChars));
|
|
429
|
-
const agentFailureText = /\b(Cancelled|Canceled|Failed)\b/i.test(agentSurfaceBrief || collapsedDetail || '');
|
|
430
|
-
const keepAgentDetail = (isError || agentFailureText) && !(agentHeaderFailure && !agentSurfaceBrief);
|
|
431
|
-
visibleDetailLines = keepAgentDetail ? [agentDetailLine] : [];
|
|
432
|
-
}
|
|
307
|
+
// Skill/agent collapsed gating lives in the shared model (detailLine).
|
|
308
|
+
const visibleDetailLines = detailLines;
|
|
433
309
|
const finalStatusColor = toolStatusColor({ pending, groupCount, callFailedCount, exitFailedCount, terminalStatus });
|
|
434
310
|
const dotColor = finalStatusColor;
|
|
435
311
|
// Agent surface cards use directional markers: `←` for requests going OUT
|
|
@@ -449,31 +325,6 @@ export function ToolExecution({ name, args, result, rawResult, isError, errorCou
|
|
|
449
325
|
const isDirectionalMarker = isAgentResponse || isAgentSurfaceCard;
|
|
450
326
|
const markerText = isDirectionalMarker ? `${markerGlyph} ` : markerGlyph;
|
|
451
327
|
const dotText = pending && !blinkOn ? ' ' : markerText;
|
|
452
|
-
let labelText;
|
|
453
|
-
if (isAgentResponse) labelText = agentResponseTitle(parsedArgs, displayGroupCount);
|
|
454
|
-
else if (isBackgroundResponse) labelText = backgroundTaskResultTitle(normalizedName, backgroundMeta || parsedArgs);
|
|
455
|
-
else if (isBackgroundMetadataResult) labelText = backgroundTaskActionTitle(normalizedName, backgroundMeta);
|
|
456
|
-
else if (isShellSurface) labelText = shellHeader(shellStatus, displayGroupCount);
|
|
457
|
-
else labelText = (isAgentTool(normalizedName) ? agentActionTitle(parsedArgs) : '') || statusCopy(name, label, displayGroupCount, doneCount, headerPending, isError, parsedArgs);
|
|
458
|
-
labelText = safeInlineText(labelText);
|
|
459
|
-
// Show the parenthesized arg summary for grouped cards too, matching single
|
|
460
|
-
// calls so the header carries the same context.
|
|
461
|
-
const toolSearchSummary = !pending && normalizedName === 'load_tool' && hasResult
|
|
462
|
-
? toolSearchLoadedSummary(displayedResultText)
|
|
463
|
-
: '';
|
|
464
|
-
const rawSummaryText = safeInlineText(isAgentResponse || isBackgroundResponse
|
|
465
|
-
? ''
|
|
466
|
-
: toolSearchSummary || (isAgentTool(normalizedName) ? agentActionSummary(parsedArgs, summary) : summary));
|
|
467
|
-
// Drop the parenthesized arg summary when it is a bare "<n> <unit>" count
|
|
468
|
-
// that the header verb already spells out (e.g. header "Searching 6 patterns"
|
|
469
|
-
// + summary "6 patterns"). Multi-arg array calls hit this; single calls keep
|
|
470
|
-
// their descriptive summary ("pattern: \"foo\"") since it never matches the
|
|
471
|
-
// header tail. Channel surfaces are unaffected — they build the summary from
|
|
472
|
-
// summarizeToolArgs directly and never render this header verb.
|
|
473
|
-
const summaryIsHeaderCount = rawSummaryText
|
|
474
|
-
&& /^\d+\s+\S+$/.test(rawSummaryText)
|
|
475
|
-
&& labelText.endsWith(rawSummaryText);
|
|
476
|
-
const summaryText = summaryIsHeaderCount ? '' : rawSummaryText;
|
|
477
328
|
// Agent cards hide their collapsed body but still expose ctrl+o expand only
|
|
478
329
|
// when expanding would actually reveal something: an agent response body, or a
|
|
479
330
|
// multiline / clipped raw result (e.g. the "agents: N …" worker list). A
|