@xenosystem/agent-conversation 0.1.36 → 0.1.38
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/dist/adapters/xenoAgentHostCatalog.d.ts.map +1 -1
- package/dist/adapters/xenoAgentHostCatalog.js +20 -5
- package/dist/adapters/xenoAgentHostCatalog.js.map +1 -1
- package/dist/components/agent/AgentView.d.ts +9 -0
- package/dist/components/agent/AgentView.d.ts.map +1 -1
- package/dist/components/agent/AgentView.js +883 -1045
- package/dist/components/agent/AgentView.js.map +1 -1
- package/dist/components/agent/GhostTextArea.d.ts +6 -0
- package/dist/components/agent/GhostTextArea.d.ts.map +1 -1
- package/dist/components/agent/GhostTextArea.js +15 -5
- package/dist/components/agent/GhostTextArea.js.map +1 -1
- package/dist/components/agent/LiveConversationCollaboration.d.ts +3 -1
- package/dist/components/agent/LiveConversationCollaboration.d.ts.map +1 -1
- package/dist/components/agent/LiveConversationCollaboration.js +3 -1
- package/dist/components/agent/LiveConversationCollaboration.js.map +1 -1
- package/dist/components/agent/MessageText.d.ts +36 -0
- package/dist/components/agent/MessageText.d.ts.map +1 -0
- package/dist/components/agent/MessageText.js +76 -0
- package/dist/components/agent/MessageText.js.map +1 -0
- package/dist/components/agent/SdkSessionPanel.d.ts +2 -0
- package/dist/components/agent/SdkSessionPanel.d.ts.map +1 -1
- package/dist/components/agent/SdkSessionPanel.js +1 -1
- package/dist/components/agent/SdkSessionPanel.js.map +1 -1
- package/dist/components/agent/composer/Composer.d.ts +282 -0
- package/dist/components/agent/composer/Composer.d.ts.map +1 -0
- package/dist/components/agent/composer/Composer.js +393 -0
- package/dist/components/agent/composer/Composer.js.map +1 -0
- package/dist/components/agent/composer/EffortCells.d.ts +12 -0
- package/dist/components/agent/composer/EffortCells.d.ts.map +1 -0
- package/dist/components/agent/composer/EffortCells.js +146 -0
- package/dist/components/agent/composer/EffortCells.js.map +1 -0
- package/dist/components/agent/composer/Tip.d.ts +19 -0
- package/dist/components/agent/composer/Tip.d.ts.map +1 -0
- package/dist/components/agent/composer/Tip.js +86 -0
- package/dist/components/agent/composer/Tip.js.map +1 -0
- package/dist/components/agent/composer/TypedMenu.d.ts +56 -0
- package/dist/components/agent/composer/TypedMenu.d.ts.map +1 -0
- package/dist/components/agent/composer/TypedMenu.js +156 -0
- package/dist/components/agent/composer/TypedMenu.js.map +1 -0
- package/dist/components/agent/mainAssistant.d.ts +9 -2
- package/dist/components/agent/mainAssistant.d.ts.map +1 -1
- package/dist/components/agent/mainAssistant.js +9 -2
- package/dist/components/agent/mainAssistant.js.map +1 -1
- package/dist/components/agent/toolCallDerive.d.ts +82 -0
- package/dist/components/agent/toolCallDerive.d.ts.map +1 -0
- package/dist/components/agent/toolCallDerive.js +741 -0
- package/dist/components/agent/toolCallDerive.js.map +1 -0
- package/dist/components/agent/transcript/Transcript.d.ts +127 -0
- package/dist/components/agent/transcript/Transcript.d.ts.map +1 -0
- package/dist/components/agent/transcript/Transcript.js +544 -0
- package/dist/components/agent/transcript/Transcript.js.map +1 -0
- package/dist/components/agent/transcript/glyphs.d.ts +83 -0
- package/dist/components/agent/transcript/glyphs.d.ts.map +1 -0
- package/dist/components/agent/transcript/glyphs.js +124 -0
- package/dist/components/agent/transcript/glyphs.js.map +1 -0
- package/dist/components/agent/transcript/stepModel.d.ts +163 -0
- package/dist/components/agent/transcript/stepModel.d.ts.map +1 -0
- package/dist/components/agent/transcript/stepModel.js +701 -0
- package/dist/components/agent/transcript/stepModel.js.map +1 -0
- package/dist/components/agent/transcript/turnModel.d.ts +124 -0
- package/dist/components/agent/transcript/turnModel.d.ts.map +1 -0
- package/dist/components/agent/transcript/turnModel.js +166 -0
- package/dist/components/agent/transcript/turnModel.js.map +1 -0
- package/dist/components/agent/transcript/useAskActions.d.ts +23 -0
- package/dist/components/agent/transcript/useAskActions.d.ts.map +1 -0
- package/dist/components/agent/transcript/useAskActions.js +92 -0
- package/dist/components/agent/transcript/useAskActions.js.map +1 -0
- package/dist/parityFixture.d.ts +54 -0
- package/dist/parityFixture.d.ts.map +1 -0
- package/dist/parityFixture.js +195 -0
- package/dist/parityFixture.js.map +1 -0
- package/dist/platformApiTypes.d.ts +10 -1
- package/dist/platformApiTypes.d.ts.map +1 -1
- package/dist/stores/agentChatStore.d.ts +61 -1
- package/dist/stores/agentChatStore.d.ts.map +1 -1
- package/dist/stores/agentChatStore.js +89 -25
- package/dist/stores/agentChatStore.js.map +1 -1
- package/dist/types/agentPermissions.d.ts +3 -1
- package/dist/types/agentPermissions.d.ts.map +1 -1
- package/dist/utils/acpExecutionAuthority.d.ts.map +1 -1
- package/dist/utils/acpExecutionAuthority.js +2 -0
- package/dist/utils/acpExecutionAuthority.js.map +1 -1
- package/package.json +6 -5
- package/src/composer.css +337 -0
- package/src/styles.css +7 -0
- package/src/transcript.css +585 -0
|
@@ -0,0 +1,701 @@
|
|
|
1
|
+
import { compactCommandPreview, isCommandToolCall } from '../toolCardDisplay.js';
|
|
2
|
+
import { buildInlineDiffPreviewLines, countTextLines, deriveApprovalCardInfo, deriveEditorDiffStats, deriveFileToolPrimaryText, deriveTerminalToolOutput, deriveToolCallFileName, deriveToolCallFilePath, deriveToolDiffStats, formatToolDuration, humanizeToolName, isToolDisplayNoise, parseStructuredCommandContent, readToolStringParam, stripToolDisplayPrefix, toolCallLabel, } from '../toolCallDerive.js';
|
|
3
|
+
const HIDDEN_TOOLS = new Set([
|
|
4
|
+
'toolsearch', 'tool_search', 'tool_describe', 'condensationrequesttool', 'condensation', 'secrets',
|
|
5
|
+
'waitformcpservers', 'listmcpresourcestool', 'list_mcp_resources', 'list_mcp_resource_templates',
|
|
6
|
+
'taskcreate', 'taskupdate', 'taskclear', 'taskget', 'tasklist', 'todowrite', 'write_todos', 'todo',
|
|
7
|
+
]);
|
|
8
|
+
/**
|
|
9
|
+
* The calls that mean "here is the plan — may I start?". They are the one permission whose subject
|
|
10
|
+
* is the conversation's plan rather than a path or a command, so they draw the plan row's card.
|
|
11
|
+
*/
|
|
12
|
+
const PLAN_APPROVAL_TOOLS = new Set([
|
|
13
|
+
'exitplanmode', 'exit_plan_mode', 'approveplan', 'approve_plan', 'planapproval', 'plan_approval',
|
|
14
|
+
]);
|
|
15
|
+
const SUBWORK_TOOLS = new Set([
|
|
16
|
+
'agent', 'task', 'delegate_task', 'subagent', 'subagents', 'spawn', 'create_thread', 'fork_thread',
|
|
17
|
+
'send_message_to_thread', 'handoff_thread', 'new_task', 'sendmessage', 'send_message',
|
|
18
|
+
]);
|
|
19
|
+
const MACHINE_ACTIONS = {
|
|
20
|
+
screenshot: { verb: 'Captured screen', glyph: 'scan-eye' },
|
|
21
|
+
capture_screen_context: { verb: 'Captured screen', glyph: 'scan-eye' },
|
|
22
|
+
zoom: { verb: 'Zoomed', glyph: 'scan-eye' },
|
|
23
|
+
left_click: { verb: 'Clicked', glyph: 'navigation' },
|
|
24
|
+
click: { verb: 'Clicked', glyph: 'navigation' },
|
|
25
|
+
right_click: { verb: 'Right-clicked', glyph: 'navigation' },
|
|
26
|
+
middle_click: { verb: 'Middle-clicked', glyph: 'navigation' },
|
|
27
|
+
double_click: { verb: 'Double-clicked', glyph: 'navigation' },
|
|
28
|
+
triple_click: { verb: 'Triple-clicked', glyph: 'navigation' },
|
|
29
|
+
left_click_drag: { verb: 'Dragged', glyph: 'navigation' },
|
|
30
|
+
drag: { verb: 'Dragged', glyph: 'navigation' },
|
|
31
|
+
mouse_move: { verb: 'Moved', glyph: 'navigation' },
|
|
32
|
+
move: { verb: 'Moved', glyph: 'navigation' },
|
|
33
|
+
type: { verb: 'Typed', glyph: 'feather' },
|
|
34
|
+
key: { verb: 'Pressed', glyph: 'hand' },
|
|
35
|
+
keypress: { verb: 'Pressed', glyph: 'hand' },
|
|
36
|
+
hold_key: { verb: 'Held', glyph: 'hand' },
|
|
37
|
+
scroll: { verb: 'Scrolled', glyph: 'arrow-left-right' },
|
|
38
|
+
wait: { verb: 'Waited', glyph: 'clock' },
|
|
39
|
+
// what the agent DID, not what the API is called: it looked at the windows that are open
|
|
40
|
+
// ...and the bench draws that look as an eye, not as the window it looked at
|
|
41
|
+
list_apps: { verb: 'Looking up windows', glyph: 'eye' },
|
|
42
|
+
list_windows: { verb: 'Looking up windows', glyph: 'eye' },
|
|
43
|
+
window_state: { verb: 'Read the window', glyph: 'app-window' },
|
|
44
|
+
launch: { verb: 'Opened', glyph: 'app-window' },
|
|
45
|
+
};
|
|
46
|
+
const BROWSER_ACTIONS = {
|
|
47
|
+
navigate: { verb: 'Opened', glyph: 'app-window' },
|
|
48
|
+
goto: { verb: 'Opened', glyph: 'app-window' },
|
|
49
|
+
back: { verb: 'Went back', glyph: 'app-window' },
|
|
50
|
+
click: { verb: 'Clicked', glyph: 'navigation' },
|
|
51
|
+
type: { verb: 'Typed', glyph: 'feather' },
|
|
52
|
+
press: { verb: 'Pressed', glyph: 'hand' },
|
|
53
|
+
key: { verb: 'Pressed', glyph: 'hand' },
|
|
54
|
+
scroll: { verb: 'Scrolled', glyph: 'arrow-left-right' },
|
|
55
|
+
snapshot: { verb: 'Captured page', glyph: 'scan-eye' },
|
|
56
|
+
vision: { verb: 'Captured page', glyph: 'scan-eye' },
|
|
57
|
+
screenshot: { verb: 'Captured page', glyph: 'scan-eye' },
|
|
58
|
+
console: { verb: 'Read console', glyph: 'terminal' },
|
|
59
|
+
dialog: { verb: 'Answered dialog', glyph: 'help' },
|
|
60
|
+
};
|
|
61
|
+
function normalizeName(toolName) {
|
|
62
|
+
return (toolName || '').trim().toLowerCase();
|
|
63
|
+
}
|
|
64
|
+
function param(toolCall, keys) {
|
|
65
|
+
return readToolStringParam(toolCall.params, keys);
|
|
66
|
+
}
|
|
67
|
+
function numberParam(toolCall, keys) {
|
|
68
|
+
for (const key of keys) {
|
|
69
|
+
const value = toolCall.params?.[key];
|
|
70
|
+
if (typeof value === 'number' && Number.isFinite(value))
|
|
71
|
+
return value;
|
|
72
|
+
if (typeof value === 'string' && /^\d+$/.test(value.trim()))
|
|
73
|
+
return Number.parseInt(value, 10);
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
function fileName(path) {
|
|
78
|
+
const parts = path.replace(/[\\/]+$/, '').split(/[/\\]/).filter(Boolean);
|
|
79
|
+
return parts[parts.length - 1] || path;
|
|
80
|
+
}
|
|
81
|
+
function formatCount(count, noun) {
|
|
82
|
+
return `${count.toLocaleString('en-US')} ${noun}${count === 1 ? '' : 's'}`;
|
|
83
|
+
}
|
|
84
|
+
function domainOf(url) {
|
|
85
|
+
try {
|
|
86
|
+
return new URL(url).hostname.replace(/^www\./, '');
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return url;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function resultText(toolCall) {
|
|
93
|
+
return isToolDisplayNoise(toolCall.result) ? '' : (toolCall.result || '').trim();
|
|
94
|
+
}
|
|
95
|
+
function outcomeTone(toolCall) {
|
|
96
|
+
if (toolCall.outcome === 'cancelled')
|
|
97
|
+
return 'stop';
|
|
98
|
+
if (toolCall.outcome === 'failed' || toolCall.success === false)
|
|
99
|
+
return 'bad';
|
|
100
|
+
return 'none';
|
|
101
|
+
}
|
|
102
|
+
/** A settled row that failed says so in its result unless the result already carries the exit. */
|
|
103
|
+
function failedResult(toolCall, result) {
|
|
104
|
+
if (toolCall.status !== 'done')
|
|
105
|
+
return result;
|
|
106
|
+
if (toolCall.outcome === 'cancelled')
|
|
107
|
+
return 'cancelled';
|
|
108
|
+
if ((toolCall.outcome === 'failed' || toolCall.success === false) && !/^exit\b/.test(result))
|
|
109
|
+
return 'failed';
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
/** The hunks of a diff, split where the changed lines are not contiguous. */
|
|
113
|
+
export function diffHunks(diff, id) {
|
|
114
|
+
const lines = buildInlineDiffPreviewLines(diff, 400);
|
|
115
|
+
const hunks = [];
|
|
116
|
+
let current = [];
|
|
117
|
+
const flush = () => {
|
|
118
|
+
if (!current.length)
|
|
119
|
+
return;
|
|
120
|
+
const first = current.find((line) => line.kind !== 'ellipsis');
|
|
121
|
+
const start = first?.newLineNumber ?? first?.oldLineNumber ?? 0;
|
|
122
|
+
const changed = current.filter((line) => line.kind === 'added' || line.kind === 'removed').length;
|
|
123
|
+
hunks.push({ id: `${id}:${hunks.length}`, header: `@@ ${start} · ${changed} line${changed === 1 ? '' : 's'}`, lines: current });
|
|
124
|
+
current = [];
|
|
125
|
+
};
|
|
126
|
+
for (const line of lines) {
|
|
127
|
+
if (line.kind === 'ellipsis') {
|
|
128
|
+
flush();
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
current.push(line);
|
|
132
|
+
}
|
|
133
|
+
flush();
|
|
134
|
+
return hunks;
|
|
135
|
+
}
|
|
136
|
+
/** An edit expressed as old/new strings, with no diff object — the hunk is the two strings. */
|
|
137
|
+
function hunksFromReplacement(toolCall) {
|
|
138
|
+
const oldString = typeof toolCall.params?.old_string === 'string' ? toolCall.params.old_string : '';
|
|
139
|
+
const newString = typeof toolCall.params?.new_string === 'string' ? toolCall.params.new_string : '';
|
|
140
|
+
if (!oldString && !newString)
|
|
141
|
+
return [];
|
|
142
|
+
const lines = [
|
|
143
|
+
...oldString.split(/\r?\n/).map((text) => ({ kind: 'removed', text })),
|
|
144
|
+
...newString.split(/\r?\n/).map((text) => ({ kind: 'added', text })),
|
|
145
|
+
];
|
|
146
|
+
return [{ id: `${toolCall.id}:0`, header: `@@ ${lines.length} line${lines.length === 1 ? '' : 's'}`, lines }];
|
|
147
|
+
}
|
|
148
|
+
function parseGrepMatches(text) {
|
|
149
|
+
const groups = new Map();
|
|
150
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
151
|
+
const line = raw.trimEnd();
|
|
152
|
+
if (!line)
|
|
153
|
+
continue;
|
|
154
|
+
// path:line:text — the path may carry a drive letter, so the first colon after a single letter is not a separator
|
|
155
|
+
const match = line.match(/^((?:[A-Za-z]:)?[^:\n]+?):(\d+):(.*)$/);
|
|
156
|
+
if (!match)
|
|
157
|
+
continue;
|
|
158
|
+
const [, file, n, content] = match;
|
|
159
|
+
const list = groups.get(file) || [];
|
|
160
|
+
list.push({ n: Number.parseInt(n, 10).toLocaleString('en-US'), text: content.trim() });
|
|
161
|
+
groups.set(file, list);
|
|
162
|
+
}
|
|
163
|
+
return [...groups.entries()].map(([file, lines]) => ({ file, lines }));
|
|
164
|
+
}
|
|
165
|
+
function parseSources(text) {
|
|
166
|
+
const items = [];
|
|
167
|
+
const seen = new Set();
|
|
168
|
+
const blocks = text.split(/\n\s*\n/);
|
|
169
|
+
for (const block of blocks) {
|
|
170
|
+
const url = block.match(/https?:\/\/[^\s)>\]"']+/)?.[0];
|
|
171
|
+
if (!url || seen.has(url))
|
|
172
|
+
continue;
|
|
173
|
+
seen.add(url);
|
|
174
|
+
const titleLine = block.split(/\r?\n/).map((line) => line.trim()).find((line) => line && !line.includes(url));
|
|
175
|
+
const title = (titleLine || '').replace(/^(?:title:|\d+[.)]|[-*•])\s*/i, '').trim() || domainOf(url);
|
|
176
|
+
items.push({ title, url, domain: domainOf(url) });
|
|
177
|
+
}
|
|
178
|
+
return items;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The SDK reports a backgrounded command by naming the task in the tool's own output; the store
|
|
182
|
+
* already parses the same sentence to build `sessionUi.backgroundTasks`. Reading it here keeps the
|
|
183
|
+
* row and the tray describing ONE task rather than two views that can disagree.
|
|
184
|
+
*/
|
|
185
|
+
function backgroundTaskIdOf(output) {
|
|
186
|
+
const match = output.match(/(?:Background task started|Command moved to background):\s*([^\s\r\n]+)/i);
|
|
187
|
+
return match?.[1]?.trim() || null;
|
|
188
|
+
}
|
|
189
|
+
function commandStep(toolCall, live) {
|
|
190
|
+
const command = param(toolCall, ['command']);
|
|
191
|
+
const parsed = parseStructuredCommandContent(toolCall.result || '');
|
|
192
|
+
const duration = formatToolDuration(toolCall.durationMs);
|
|
193
|
+
const taskId = backgroundTaskIdOf(toolCall.result || '');
|
|
194
|
+
if (taskId) {
|
|
195
|
+
// Not "Ran": nothing has finished. The row stays a live mark until the task reports an ending,
|
|
196
|
+
// and the result names the handle, because the id is what Output and Stop act on.
|
|
197
|
+
return {
|
|
198
|
+
family: 'shell',
|
|
199
|
+
glyph: 'terminal',
|
|
200
|
+
verb: 'Started',
|
|
201
|
+
object: command ? compactCommandPreview(command) : 'a command',
|
|
202
|
+
objectMono: true,
|
|
203
|
+
result: `in the background · ${taskId}`,
|
|
204
|
+
resultLate: false,
|
|
205
|
+
tone: outcomeTone(toolCall),
|
|
206
|
+
live: true,
|
|
207
|
+
background: { taskId, action: 'started' },
|
|
208
|
+
detail: parsed.stdout || parsed.stderr
|
|
209
|
+
? { kind: 'output', text: [parsed.stdout, parsed.stderr].filter(Boolean).join('\n'), streaming: true, foot: { cwd: parsed.cwd || toolCall.cwd || undefined } }
|
|
210
|
+
: null,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
const exit = parsed.exit || (toolCall.status === 'done' && toolCall.success === true ? 'exit 0' : '');
|
|
214
|
+
const result = [exit, duration].filter(Boolean).join(' · ');
|
|
215
|
+
const output = [parsed.stdout, parsed.stderr].filter(Boolean).join('\n');
|
|
216
|
+
const rawOutput = !parsed.command && !parsed.stdout && !parsed.stderr ? resultText(toolCall) : output;
|
|
217
|
+
const failed = toolCall.success === false || /^exit [1-9]/.test(exit);
|
|
218
|
+
return {
|
|
219
|
+
family: 'shell',
|
|
220
|
+
glyph: 'terminal',
|
|
221
|
+
verb: live ? 'Running' : 'Ran',
|
|
222
|
+
object: command ? compactCommandPreview(command) : 'a command',
|
|
223
|
+
objectMono: true,
|
|
224
|
+
result: failedResult(toolCall, result),
|
|
225
|
+
resultLate: true,
|
|
226
|
+
tone: failed ? 'bad' : outcomeTone(toolCall),
|
|
227
|
+
detail: rawOutput || live
|
|
228
|
+
? { kind: 'output', text: rawOutput, error: parsed.error || undefined, streaming: live, foot: { duration: duration || undefined, exit: exit || undefined, cwd: parsed.cwd || toolCall.cwd || undefined } }
|
|
229
|
+
: null,
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
function fileStep(toolCall, name, live) {
|
|
233
|
+
const path = deriveToolCallFilePath(toolCall);
|
|
234
|
+
const displayName = path ? fileName(path) : deriveToolCallFileName(toolCall);
|
|
235
|
+
const text = resultText(toolCall);
|
|
236
|
+
if (name === 'read' || name === 'read_file') {
|
|
237
|
+
const offset = numberParam(toolCall, ['offset', 'start_line', 'startLine']);
|
|
238
|
+
const limit = numberParam(toolCall, ['limit', 'end_line', 'endLine']);
|
|
239
|
+
const ranged = offset !== null && limit !== null;
|
|
240
|
+
const from = ranged ? offset : 1;
|
|
241
|
+
const to = ranged ? (name === 'read' ? offset + limit - 1 : limit) : null;
|
|
242
|
+
const lines = text ? text.split(/\r?\n/) : [];
|
|
243
|
+
const shown = lines.slice(0, 60);
|
|
244
|
+
return {
|
|
245
|
+
family: 'files',
|
|
246
|
+
glyph: 'file',
|
|
247
|
+
verb: live ? 'Reading' : 'Read',
|
|
248
|
+
object: displayName,
|
|
249
|
+
objectMono: true,
|
|
250
|
+
result: failedResult(toolCall, ranged && to !== null ? `lines ${from}–${to}` : lines.length ? formatCount(lines.length, 'line') : ''),
|
|
251
|
+
resultLate: false,
|
|
252
|
+
detail: shown.length ? { kind: 'slice', from, lines: shown, more: lines.length - shown.length } : null,
|
|
253
|
+
filePath: path,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const primary = deriveFileToolPrimaryText(toolCall);
|
|
257
|
+
const created = primary === 'Created file' || (!toolCall.diff && (name === 'write' || name === 'write_file' || name === 'writefiles'));
|
|
258
|
+
const stats = deriveToolDiffStats(toolCall);
|
|
259
|
+
const hunks = toolCall.diff ? diffHunks(toolCall.diff, toolCall.id) : hunksFromReplacement(toolCall);
|
|
260
|
+
const contentLines = typeof toolCall.params?.content === 'string' ? countTextLines(toolCall.params.content) : 0;
|
|
261
|
+
const result = stats && !created
|
|
262
|
+
? `+${stats.added} −${stats.removed}`
|
|
263
|
+
: created && (stats?.added || contentLines)
|
|
264
|
+
? formatCount(stats?.added || contentLines, 'line')
|
|
265
|
+
: '';
|
|
266
|
+
return {
|
|
267
|
+
family: 'files',
|
|
268
|
+
glyph: created ? 'plus' : 'edit',
|
|
269
|
+
verb: live ? (created ? 'Creating' : 'Editing') : (created ? 'Created' : 'Edited'),
|
|
270
|
+
object: displayName,
|
|
271
|
+
objectMono: true,
|
|
272
|
+
result: failedResult(toolCall, result),
|
|
273
|
+
resultLate: false,
|
|
274
|
+
detail: hunks.length ? { kind: 'diff', diff: toolCall.diff ?? null, hunks, path } : null,
|
|
275
|
+
filePath: path,
|
|
276
|
+
diffStats: stats,
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function searchStep(toolCall, name, live) {
|
|
280
|
+
const text = resultText(toolCall);
|
|
281
|
+
if (name === 'glob' || name === 'file_search' || name === 'find_files' || name === 'list_files' || name === 'list_directory') {
|
|
282
|
+
const pattern = param(toolCall, ['pattern', 'glob', 'query', 'path']);
|
|
283
|
+
const paths = text.split(/\r?\n/).map((line) => line.trim()).filter((line) => line && !/^no files/i.test(line));
|
|
284
|
+
const listing = name === 'list_files' || name === 'list_directory';
|
|
285
|
+
return {
|
|
286
|
+
family: listing ? 'files' : 'search',
|
|
287
|
+
glyph: listing ? 'folder' : 'search',
|
|
288
|
+
verb: live ? (listing ? 'Listing' : 'Finding') : (listing ? 'Listed' : 'Found'),
|
|
289
|
+
object: pattern || (listing ? 'a directory' : 'files'),
|
|
290
|
+
objectMono: true,
|
|
291
|
+
result: failedResult(toolCall, paths.length ? formatCount(paths.length, listing ? 'entr' : 'file').replace('entrs', 'entries').replace(/entr$/, 'entry') : ''),
|
|
292
|
+
resultLate: false,
|
|
293
|
+
detail: paths.length ? { kind: 'list', items: paths.slice(0, 80).map((p) => ({ label: fileName(p), where: p !== fileName(p) ? p.slice(0, p.length - fileName(p).length).replace(/[\\/]$/, '') : undefined, path: p })) } : null,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const pattern = param(toolCall, ['pattern', 'query', 'regex']);
|
|
297
|
+
const where = param(toolCall, ['path', 'directory', 'cwd']);
|
|
298
|
+
const groups = parseGrepMatches(text);
|
|
299
|
+
const matchCount = groups.reduce((sum, group) => sum + group.lines.length, 0);
|
|
300
|
+
const result = matchCount
|
|
301
|
+
? `${formatCount(matchCount, 'match').replace('matchs', 'matches')} · ${formatCount(groups.length, 'file')}`
|
|
302
|
+
: text && /no matches|0 matches/i.test(text) ? 'no matches' : '';
|
|
303
|
+
return {
|
|
304
|
+
family: 'search',
|
|
305
|
+
glyph: 'search',
|
|
306
|
+
verb: live ? 'Searching' : 'Searched',
|
|
307
|
+
object: pattern ? `“${pattern}”${where ? ` in ${where}` : ''}` : 'the project',
|
|
308
|
+
objectMono: true,
|
|
309
|
+
result: failedResult(toolCall, result),
|
|
310
|
+
resultLate: false,
|
|
311
|
+
detail: groups.length ? { kind: 'matches', groups } : text ? { kind: 'prose', text } : null,
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
function webStep(toolCall, name, live) {
|
|
315
|
+
const text = resultText(toolCall);
|
|
316
|
+
const isSearch = name.includes('search');
|
|
317
|
+
if (isSearch) {
|
|
318
|
+
const query = param(toolCall, ['query', 'q', 'search']);
|
|
319
|
+
const citations = toolCall.webContext?.evidence.citations;
|
|
320
|
+
const items = citations?.length
|
|
321
|
+
? citations.map((citation) => ({ title: citation.title || domainOf(citation.url), url: citation.url, domain: domainOf(citation.url) }))
|
|
322
|
+
: parseSources(text);
|
|
323
|
+
return {
|
|
324
|
+
family: 'web',
|
|
325
|
+
glyph: 'globe',
|
|
326
|
+
verb: live ? 'Searching the web' : 'Searched the web',
|
|
327
|
+
object: query || 'the web',
|
|
328
|
+
objectMono: false,
|
|
329
|
+
result: failedResult(toolCall, items.length ? formatCount(items.length, 'result') : ''),
|
|
330
|
+
resultLate: false,
|
|
331
|
+
detail: items.length ? { kind: 'sources', items } : text ? { kind: 'prose', text } : null,
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
const url = param(toolCall, ['url', 'href']) || toolCall.webContext?.evidence.finalUrl || toolCall.webContext?.evidence.sourceUrl || '';
|
|
335
|
+
return {
|
|
336
|
+
family: 'web',
|
|
337
|
+
glyph: 'globe',
|
|
338
|
+
verb: live ? 'Reading' : 'Read',
|
|
339
|
+
object: url ? domainOf(url) + (url.replace(/^https?:\/\/[^/]+/, '').replace(/\/$/, '') || '') : 'a page',
|
|
340
|
+
objectMono: true,
|
|
341
|
+
result: failedResult(toolCall, ''),
|
|
342
|
+
resultLate: false,
|
|
343
|
+
detail: text ? { kind: 'prose', text } : null,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* A step through a connector — an MCP server, a XENO connection. The row says what the agent DID
|
|
348
|
+
* in the service's terms, not what the API call was called: `Read · Gmail · from Lil Bro · 3 threads`,
|
|
349
|
+
* never `Asked · gmail · list threads`.
|
|
350
|
+
*
|
|
351
|
+
* verb from the tool's shape: list/get/read/search/find → Read; send/post/reply → Sent;
|
|
352
|
+
* create/add → Created; update/set → Updated; delete/remove → Deleted.
|
|
353
|
+
* object the service, title-cased, then the params that narrow the read (`from Lil Bro`).
|
|
354
|
+
* result a READ answers with rows, and the row counts them in the tool's own noun
|
|
355
|
+
* (`list_threads` → threads); anything else answers with the fact that it happened.
|
|
356
|
+
* detail the rows, one per line, split into what and when on the last ` — `.
|
|
357
|
+
*/
|
|
358
|
+
function connectorStep(toolCall, input) {
|
|
359
|
+
const { service, tool, live, text, merge } = input;
|
|
360
|
+
const shape = tool.match(/^(list|get|read|search|find|fetch|query|send|post|reply|create|add|update|set|delete|remove)(?:_(.+))?$/);
|
|
361
|
+
const action = shape?.[1] ?? '';
|
|
362
|
+
const noun = (shape?.[2] ?? tool).replace(/_/g, ' ');
|
|
363
|
+
const reading = /^(list|get|read|search|find|fetch|query)$/.test(action) || !action;
|
|
364
|
+
const verb = reading ? (live ? 'Reading' : 'Read')
|
|
365
|
+
: /^(send|post|reply)$/.test(action) ? (live ? 'Sending' : 'Sent')
|
|
366
|
+
: /^(create|add)$/.test(action) ? (live ? 'Creating' : 'Created')
|
|
367
|
+
: /^(update|set)$/.test(action) ? (live ? 'Updating' : 'Updated')
|
|
368
|
+
: (live ? 'Deleting' : 'Deleted');
|
|
369
|
+
const title = service.charAt(0).toUpperCase() + service.slice(1);
|
|
370
|
+
// the params that narrow it, as phrases: `from Lil Bro`, `in Inbox`, `“demo”`
|
|
371
|
+
const params = toolCall.params && typeof toolCall.params === 'object' ? toolCall.params : {};
|
|
372
|
+
const phrases = Object.entries(params)
|
|
373
|
+
.filter(([, value]) => typeof value === 'string' && value.trim())
|
|
374
|
+
.slice(0, 2)
|
|
375
|
+
.map(([key, value]) => (/^(query|q|search|text)$/.test(key) ? `“${String(value)}”` : `${key.replace(/_/g, ' ')} ${String(value)}`));
|
|
376
|
+
const object = [title, ...phrases].join(' · ');
|
|
377
|
+
const rows = reading ? text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : [];
|
|
378
|
+
const items = rows.slice(0, 80).map((line) => {
|
|
379
|
+
const split = line.lastIndexOf(' — ');
|
|
380
|
+
return split > 0 ? { label: line.slice(0, split).trim(), where: line.slice(split + 3).trim() } : { label: line };
|
|
381
|
+
});
|
|
382
|
+
const singular = noun.replace(/s$/, '');
|
|
383
|
+
return merge({
|
|
384
|
+
family: 'connectors',
|
|
385
|
+
glyph: 'link',
|
|
386
|
+
verb,
|
|
387
|
+
object,
|
|
388
|
+
objectMono: true,
|
|
389
|
+
result: failedResult(toolCall, rows.length ? formatCount(rows.length, singular) : ''),
|
|
390
|
+
detail: items.length ? { kind: 'list', items } : text ? { kind: 'prose', text } : null,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
function askFor(toolCall, name) {
|
|
394
|
+
const permission = toolCall.permissionRequest;
|
|
395
|
+
if (permission) {
|
|
396
|
+
const info = deriveApprovalCardInfo(toolCall, permission);
|
|
397
|
+
const status = permission.status === 'pending' ? 'pending' : permission.status === 'granted' ? 'yes' : permission.status === 'denied' ? 'no' : 'cancelled';
|
|
398
|
+
const isCommand = isCommandToolCall(toolCall) || name === 'terminalwriteinput';
|
|
399
|
+
const isDirectory = !!permission.directory && !/^__.*__$/.test(permission.directory) && !isCommand;
|
|
400
|
+
const isApp = name === 'openapp' || name === 'open_app';
|
|
401
|
+
if (isDirectory || isApp) {
|
|
402
|
+
const subject = isApp ? (param(toolCall, ['app', 'appPath']) || 'an app') : permission.directory;
|
|
403
|
+
return {
|
|
404
|
+
kind: 'grant',
|
|
405
|
+
status,
|
|
406
|
+
subject: isApp ? 'app' : 'folder',
|
|
407
|
+
title: `Wants to use ${isApp ? subject : 'a folder'}`,
|
|
408
|
+
what: subject,
|
|
409
|
+
// how far the yes reaches — said beside the subject, where the person is deciding. A machine
|
|
410
|
+
// grant is scoped the way the host scopes it (`AgentMachineScope`: it dies with the SESSION);
|
|
411
|
+
// a folder is the conversation's for as long as the conversation lasts.
|
|
412
|
+
whatScope: isApp ? 'this session' : 'this conversation',
|
|
413
|
+
// what a window grant actually permits, named: seeing it, clicking, typing, keys — and its end
|
|
414
|
+
body: isApp
|
|
415
|
+
? `It can see the ${subject} window, click, type and press keys in it. Nothing outside it, and nothing after this session.`
|
|
416
|
+
: 'It can read and write inside this folder for the rest of this conversation. Nothing outside it.',
|
|
417
|
+
bodyMono: false,
|
|
418
|
+
mainLabel: isApp ? 'Allow for this session' : 'Allow for this conversation',
|
|
419
|
+
verdict: status === 'yes' ? `Allowed — it can use ${subject} until this ${isApp ? 'session' : 'conversation'} ends.` : status === 'no' ? 'Refused — it cannot use it.' : status === 'cancelled' ? 'Cancelled.' : undefined,
|
|
420
|
+
permission,
|
|
421
|
+
canAllowAlways: !!permission.allowAlwaysOptionId || !!permission.directory,
|
|
422
|
+
canDenyAlways: !!permission.rejectAlwaysOptionId,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
/*
|
|
426
|
+
* The PLAN is the ask: the agent has written the steps and wants a go before it starts any of
|
|
427
|
+
* them (bench `.xrow.plan.ask`). The steps themselves are deliberately NOT read from this call's
|
|
428
|
+
* params — the card shows the conversation's plan, the same list the plan row and the tray read,
|
|
429
|
+
* so the three can never disagree about what was approved.
|
|
430
|
+
*/
|
|
431
|
+
if (PLAN_APPROVAL_TOOLS.has(name)) {
|
|
432
|
+
return {
|
|
433
|
+
kind: 'plan',
|
|
434
|
+
status,
|
|
435
|
+
title: 'Plan — needs your go',
|
|
436
|
+
what: permission.directory || 'In the project, on this machine',
|
|
437
|
+
whatScope: 'this turn',
|
|
438
|
+
bodyMono: false,
|
|
439
|
+
mainLabel: 'Go',
|
|
440
|
+
verdict: status === 'yes' ? 'Go.' : status === 'no' ? 'Refused — nothing was started.' : status === 'cancelled' ? 'Cancelled — nothing was started.' : undefined,
|
|
441
|
+
permission,
|
|
442
|
+
canAllowAlways: false,
|
|
443
|
+
canDenyAlways: false,
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
const isWrite = name.startsWith('write') || name.startsWith('edit');
|
|
447
|
+
const isSend = /send|message|mail|post/.test(name);
|
|
448
|
+
return {
|
|
449
|
+
kind: 'approve',
|
|
450
|
+
whatScope: 'once',
|
|
451
|
+
status,
|
|
452
|
+
title: isWrite ? 'Wants to write a file' : isSend ? 'Wants to send' : 'Wants to run a command',
|
|
453
|
+
scope: 'Ask first',
|
|
454
|
+
what: permission.path && !isCommand ? permission.path : 'In the project, on this machine',
|
|
455
|
+
body: info.body,
|
|
456
|
+
bodyMono: true,
|
|
457
|
+
mainLabel: isWrite ? 'Write' : isSend ? 'Send' : info.runLabel,
|
|
458
|
+
verdict: status === 'yes' ? 'Approved — it ran once, in the project.' : status === 'no' ? 'Refused — it did not run.' : status === 'cancelled' ? 'Cancelled — it did not run.' : undefined,
|
|
459
|
+
permission,
|
|
460
|
+
canAllowAlways: !!permission.allowAlwaysOptionId || !!permission.directory,
|
|
461
|
+
canDenyAlways: !!permission.rejectAlwaysOptionId,
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
const askUser = toolCall.askUserRequest;
|
|
465
|
+
if (askUser) {
|
|
466
|
+
const status = askUser.status === 'pending' ? 'pending' : askUser.status === 'answered' ? 'yes' : 'cancelled';
|
|
467
|
+
const answer = askUser.selectedOption || askUser.answer;
|
|
468
|
+
return {
|
|
469
|
+
kind: 'question',
|
|
470
|
+
status,
|
|
471
|
+
title: 'Wants to know',
|
|
472
|
+
what: askUser.question,
|
|
473
|
+
body: askUser.context,
|
|
474
|
+
bodyMono: false,
|
|
475
|
+
options: askUser.options?.length ? askUser.options : undefined,
|
|
476
|
+
mainLabel: 'Answer',
|
|
477
|
+
verdict: status === 'yes' && answer ? `Answered — ${answer}` : status === 'cancelled' ? 'Cancelled — no answer.' : undefined,
|
|
478
|
+
answer,
|
|
479
|
+
askUser,
|
|
480
|
+
canAllowAlways: false,
|
|
481
|
+
canDenyAlways: false,
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
return null;
|
|
485
|
+
}
|
|
486
|
+
/**
|
|
487
|
+
* ONE row from ONE tool call. The family decides the glyph and the verb; the tool call's params,
|
|
488
|
+
* result and diff decide the object, the result and what opens under it.
|
|
489
|
+
*/
|
|
490
|
+
export function deriveStepRow(toolCall) {
|
|
491
|
+
const name = normalizeName(toolCall.toolName);
|
|
492
|
+
const live = toolCall.status === 'running' && !toolCall.permissionRequest && !toolCall.askUserRequest;
|
|
493
|
+
const ask = askFor(toolCall, name);
|
|
494
|
+
const base = {
|
|
495
|
+
id: toolCall.id,
|
|
496
|
+
family: 'housekeeping',
|
|
497
|
+
glyph: 'zap',
|
|
498
|
+
verb: humanizeToolName(toolCall.toolName),
|
|
499
|
+
object: '',
|
|
500
|
+
objectMono: false,
|
|
501
|
+
result: '',
|
|
502
|
+
resultLate: false,
|
|
503
|
+
tone: outcomeTone(toolCall),
|
|
504
|
+
live: live || ask?.status === 'pending',
|
|
505
|
+
hidden: HIDDEN_TOOLS.has(name),
|
|
506
|
+
detail: null,
|
|
507
|
+
ask,
|
|
508
|
+
filePath: null,
|
|
509
|
+
diffStats: null,
|
|
510
|
+
background: null,
|
|
511
|
+
toolCall,
|
|
512
|
+
};
|
|
513
|
+
const text = resultText(toolCall);
|
|
514
|
+
const merge = (part) => ({ ...base, ...part });
|
|
515
|
+
if (isCommandToolCall(toolCall))
|
|
516
|
+
return merge(commandStep(toolCall, live));
|
|
517
|
+
switch (name) {
|
|
518
|
+
case 'read':
|
|
519
|
+
case 'read_file':
|
|
520
|
+
case 'write':
|
|
521
|
+
case 'write_file':
|
|
522
|
+
case 'writefiles':
|
|
523
|
+
case 'edit':
|
|
524
|
+
case 'edit_file':
|
|
525
|
+
case 'multiedit':
|
|
526
|
+
case 'apply_patch':
|
|
527
|
+
case 'replace_in_file':
|
|
528
|
+
case 'write_to_file':
|
|
529
|
+
case 'create_file':
|
|
530
|
+
case 'str_replace_editor':
|
|
531
|
+
return merge(fileStep(toolCall, name, live));
|
|
532
|
+
case 'delete_file':
|
|
533
|
+
case 'delete':
|
|
534
|
+
return merge({ family: 'files', glyph: 'trash', verb: live ? 'Deleting' : 'Deleted', object: fileName(param(toolCall, ['path', 'file_path']) || 'a file'), objectMono: true, tone: 'bad' }); // a deletion's marker is red whether or not it succeeded — the reference says so
|
|
535
|
+
case 'undo_edit':
|
|
536
|
+
return merge({ family: 'files', glyph: 'refresh', verb: 'Reverted', object: fileName(param(toolCall, ['path', 'file_path']) || 'a file'), objectMono: true });
|
|
537
|
+
case 'glob':
|
|
538
|
+
case 'grep':
|
|
539
|
+
case 'file_search':
|
|
540
|
+
case 'find_files':
|
|
541
|
+
case 'list_files':
|
|
542
|
+
case 'list_directory':
|
|
543
|
+
case 'search_files':
|
|
544
|
+
case 'grep_search':
|
|
545
|
+
case 'codebase_search':
|
|
546
|
+
case 'search_file_content':
|
|
547
|
+
return merge(searchStep(toolCall, name, live));
|
|
548
|
+
case 'websearch':
|
|
549
|
+
case 'web_search':
|
|
550
|
+
case 'google_web_search':
|
|
551
|
+
case 'x_search':
|
|
552
|
+
case 'web_fetch':
|
|
553
|
+
case 'webfetch':
|
|
554
|
+
case 'fetch':
|
|
555
|
+
case 'web_extract':
|
|
556
|
+
case 'read_web_page':
|
|
557
|
+
return merge(webStep(toolCall, name, live));
|
|
558
|
+
case 'taskoutput':
|
|
559
|
+
case 'task_output':
|
|
560
|
+
case 'read_task_output':
|
|
561
|
+
case 'bashoutput': {
|
|
562
|
+
const taskId = param(toolCall, ['task_id', 'taskId', 'id', 'bash_id']);
|
|
563
|
+
return merge({
|
|
564
|
+
family: 'shell', glyph: 'terminal', verb: live ? 'Reading' : 'Read', object: taskId ? `background ${taskId}` : 'a background run', objectMono: true,
|
|
565
|
+
result: failedResult(toolCall, text ? formatCount(countTextLines(text), 'line') : ''),
|
|
566
|
+
...(taskId ? { background: { taskId, action: 'read' } } : {}),
|
|
567
|
+
detail: text ? { kind: 'output', text, streaming: live, foot: {} } : null,
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
case 'taskstop':
|
|
571
|
+
case 'task_stop':
|
|
572
|
+
case 'killbash':
|
|
573
|
+
case 'kill_bash': {
|
|
574
|
+
const taskId = param(toolCall, ['task_id', 'taskId', 'id', 'bash_id', 'shell_id']);
|
|
575
|
+
return merge({
|
|
576
|
+
family: 'shell', glyph: 'stop', verb: live ? 'Stopping' : 'Stopped', object: taskId ? `background ${taskId}` : 'a background run', objectMono: true,
|
|
577
|
+
result: failedResult(toolCall, toolCall.status === 'done' ? 'stopped' : ''),
|
|
578
|
+
...(taskId ? { background: { taskId, action: 'stopped' } } : {}),
|
|
579
|
+
detail: text ? { kind: 'output', text, streaming: false, foot: {} } : null,
|
|
580
|
+
});
|
|
581
|
+
}
|
|
582
|
+
case 'terminalwriteinput': {
|
|
583
|
+
const output = deriveTerminalToolOutput(toolCall);
|
|
584
|
+
return merge({ family: 'shell', glyph: 'terminal', verb: live ? 'Typing into' : 'Typed into', object: `terminal${output?.terminalId ? ` ${output.terminalId}` : ''}`, objectMono: true, result: failedResult(toolCall, ''), detail: output ? { kind: 'output', text: output.output, streaming: live, foot: { cwd: output.cwd } } : null });
|
|
585
|
+
}
|
|
586
|
+
case 'terminalreadoutput':
|
|
587
|
+
case 'terminallist': {
|
|
588
|
+
const output = deriveTerminalToolOutput(toolCall);
|
|
589
|
+
return merge({ family: 'shell', glyph: 'terminal', verb: live ? 'Reading' : 'Read', object: 'terminal output', objectMono: false, result: failedResult(toolCall, output ? formatCount(countTextLines(output.output), 'line') : ''), detail: output ? { kind: 'output', text: output.output, streaming: live, foot: { cwd: output.cwd } } : null });
|
|
590
|
+
}
|
|
591
|
+
case 'openapp':
|
|
592
|
+
case 'open_app': {
|
|
593
|
+
const app = param(toolCall, ['app', 'appPath', 'name']);
|
|
594
|
+
// the bench's rail marks a grant with a lock — the turn is held on the answer; the window itself is named on the card
|
|
595
|
+
return merge({ family: 'machine', glyph: toolCall.permissionRequest ? 'lock' : 'app-window', verb: live ? 'Opening' : 'Opened', object: app ? fileName(app) : 'an app', objectMono: false, result: failedResult(toolCall, '') });
|
|
596
|
+
}
|
|
597
|
+
case 'choosedirectory': {
|
|
598
|
+
const directory = param(toolCall, ['directory', 'path']) || toolCall.artifacts?.find((artifact) => artifact.type === 'directory')?.value || '';
|
|
599
|
+
const chooseDirectoryId = param(toolCall, ['chooseDirectoryId']);
|
|
600
|
+
if (live && chooseDirectoryId && !ask) {
|
|
601
|
+
// the folder picker: a grant the person gives by choosing, not by allowing
|
|
602
|
+
return merge({ family: 'person', glyph: 'folder', verb: 'Asking for', object: 'a folder', objectMono: false, live: true, ask: { kind: 'grant', status: 'pending', title: 'Wants a folder', scope: 'this conversation', what: 'A folder it can work in', body: 'Pick the folder this conversation may read and write. Nothing outside it.', bodyMono: false, mainLabel: 'Choose folder', canAllowAlways: false, canDenyAlways: false, host: { kind: 'choose-directory', chooseDirectoryId } } });
|
|
603
|
+
}
|
|
604
|
+
return merge({ family: 'person', glyph: 'folder', verb: live ? 'Choosing' : 'Chose', object: directory || 'a folder', objectMono: true, result: failedResult(toolCall, '') });
|
|
605
|
+
}
|
|
606
|
+
case 'chooseworkspaceplacement': {
|
|
607
|
+
const workspacePlacementId = param(toolCall, ['workspacePlacementId']);
|
|
608
|
+
if (live && workspacePlacementId && !ask) {
|
|
609
|
+
return merge({ family: 'person', glyph: 'folder', verb: 'Asking', object: 'where this conversation should live', objectMono: false, live: true, ask: { kind: 'question', status: 'pending', title: 'Wants to know', what: 'Where should this conversation live?', bodyMono: false, mainLabel: 'Choose', canAllowAlways: false, canDenyAlways: false, host: { kind: 'workspace-placement', workspacePlacementId } } });
|
|
610
|
+
}
|
|
611
|
+
return merge({ family: 'housekeeping', glyph: 'folder', verb: 'Workspace', object: stripToolDisplayPrefix(toolCall.summary && !isToolDisplayNoise(toolCall.summary) ? toolCall.summary : toolCallLabel(toolCall.toolName, toolCall.params)), objectMono: false, result: failedResult(toolCall, '') });
|
|
612
|
+
}
|
|
613
|
+
case 'workspaceselection':
|
|
614
|
+
case 'xenoworkspacemanage':
|
|
615
|
+
return merge({ family: 'housekeeping', glyph: 'folder', verb: 'Workspace', object: stripToolDisplayPrefix(toolCall.summary && !isToolDisplayNoise(toolCall.summary) ? toolCall.summary : toolCallLabel(toolCall.toolName, toolCall.params)), objectMono: false, result: failedResult(toolCall, '') });
|
|
616
|
+
case 'askuser':
|
|
617
|
+
case 'ask_user':
|
|
618
|
+
case 'askuserquestion':
|
|
619
|
+
case 'request_user_input':
|
|
620
|
+
case 'ask_followup_question':
|
|
621
|
+
case 'clarify':
|
|
622
|
+
return merge({ family: 'person', glyph: 'help', verb: ask?.status === 'pending' ? 'Asking' : ask?.status === 'yes' ? 'You said' : 'Asked', object: ask?.status === 'yes' && ask.answer ? ask.answer : (toolCall.askUserRequest?.question || param(toolCall, ['question', 'prompt']) || ''), objectMono: false });
|
|
623
|
+
case 'exitplanmode':
|
|
624
|
+
case 'exit_plan_mode':
|
|
625
|
+
case 'approveplan':
|
|
626
|
+
case 'approve_plan':
|
|
627
|
+
case 'planapproval':
|
|
628
|
+
case 'plan_approval':
|
|
629
|
+
// the head is written by the row, from the plan itself — the count belongs to the list, not to this call
|
|
630
|
+
return merge({ family: 'plan', glyph: 'list', verb: 'Planned', object: '', objectMono: false });
|
|
631
|
+
case 'think':
|
|
632
|
+
case 'thinking':
|
|
633
|
+
return merge({ family: 'plan', glyph: 'brain', verb: 'Thought', object: '', objectMono: false, detail: text ? { kind: 'prose', text } : null });
|
|
634
|
+
case 'save_memory':
|
|
635
|
+
case 'memory':
|
|
636
|
+
case 'remember':
|
|
637
|
+
return merge({ family: 'plan', glyph: 'bookmark', verb: 'Remembered', object: `“${param(toolCall, ['fact', 'content', 'text', 'memory']) || text}”`, objectMono: false });
|
|
638
|
+
case 'create_goal':
|
|
639
|
+
case 'update_goal':
|
|
640
|
+
return merge({ family: 'plan', glyph: 'target', verb: name === 'create_goal' ? 'Set goal' : 'Updated goal', object: param(toolCall, ['goal', 'objective', 'text', 'title']), objectMono: false });
|
|
641
|
+
case 'croncreate':
|
|
642
|
+
case 'cron':
|
|
643
|
+
case 'schedule':
|
|
644
|
+
case 'schedulewakeup':
|
|
645
|
+
case 'automation_update':
|
|
646
|
+
return merge({ family: 'time', glyph: 'calendar', verb: 'Scheduled', object: param(toolCall, ['name', 'title', 'prompt', 'schedule', 'cron']) || text, objectMono: false });
|
|
647
|
+
case 'view_image':
|
|
648
|
+
case 'vision_analyze':
|
|
649
|
+
return merge({ family: 'media', glyph: 'image', verb: live ? 'Looking at' : 'Looked at', object: fileName(param(toolCall, ['path', 'file_path', 'image']) || 'an image'), objectMono: true });
|
|
650
|
+
case 'image_generate':
|
|
651
|
+
case 'video_generate':
|
|
652
|
+
case 'text_to_speech':
|
|
653
|
+
case 'tts':
|
|
654
|
+
case 'music_generate':
|
|
655
|
+
return merge({ family: 'media', glyph: name === 'text_to_speech' || name === 'tts' ? 'waves' : 'image', verb: live ? 'Generating' : 'Generated', object: name.replace(/_generate$/, '').replace('text_to_speech', 'speech').replace('tts', 'speech'), objectMono: false });
|
|
656
|
+
default:
|
|
657
|
+
break;
|
|
658
|
+
}
|
|
659
|
+
if (SUBWORK_TOOLS.has(name)) {
|
|
660
|
+
const brief = param(toolCall, ['prompt', 'description', 'brief', 'task', 'message', 'instruction']);
|
|
661
|
+
return merge({ family: 'subwork', glyph: 'bot', verb: live ? 'Delegating' : 'Delegated', object: brief, objectMono: false, result: failedResult(toolCall, toolCall.status === 'done' ? 'done' : ''), detail: text ? { kind: 'prose', text } : null });
|
|
662
|
+
}
|
|
663
|
+
if (name.startsWith('browser_') || name.startsWith('browser')) {
|
|
664
|
+
const action = name.replace(/^browser_?/, '');
|
|
665
|
+
const entry = BROWSER_ACTIONS[action];
|
|
666
|
+
const target = param(toolCall, ['url', 'selector', 'ref', 'text', 'key', 'element']);
|
|
667
|
+
return merge({ family: 'browser', glyph: entry?.glyph || 'app-window', verb: entry?.verb || humanizeToolName(action || 'browser'), object: target, objectMono: true, result: failedResult(toolCall, '') });
|
|
668
|
+
}
|
|
669
|
+
if (MACHINE_ACTIONS[name] || name === 'computer' || name === 'computer_use') {
|
|
670
|
+
const action = MACHINE_ACTIONS[name] ? name : param(toolCall, ['action']);
|
|
671
|
+
const entry = MACHINE_ACTIONS[action];
|
|
672
|
+
const target = param(toolCall, ['text', 'key', 'coordinate', 'app', 'direction']);
|
|
673
|
+
/*
|
|
674
|
+
* A look at the machine answers with what it FOUND, and the row says so: one window is named,
|
|
675
|
+
* several are counted. Every other action's result is the fact that it happened, which the row
|
|
676
|
+
* already carries in its verb.
|
|
677
|
+
*/
|
|
678
|
+
const windows = /^list_(apps|windows)$/.test(action) ? text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : [];
|
|
679
|
+
const found = windows.length === 1 ? `1 ${windows[0]} window` : windows.length ? `${windows.length} windows` : '';
|
|
680
|
+
// Only embedded raster evidence is renderable here. Never fetch a URL found in tool output
|
|
681
|
+
// (or render an SVG payload); those require the host's asset/access policy.
|
|
682
|
+
const frame = /^(?:screenshot|capture_screen_context|zoom)$/.test(action)
|
|
683
|
+
? text.trim().match(/^data:image\/(?:png|jpeg|webp);base64,[A-Za-z0-9+/=\r\n]+$/)?.[0] : undefined;
|
|
684
|
+
return merge({ family: 'machine', glyph: entry?.glyph || 'monitor', verb: entry?.verb || humanizeToolName(action || 'Acted'), object: target, objectMono: !!param(toolCall, ['text']), result: failedResult(toolCall, found), detail: frame ? { kind: 'frames', frames: [{ src: frame, alt: 'Captured screen' }] } : null });
|
|
685
|
+
}
|
|
686
|
+
const mcp = name.match(/^mcp__([^_]+(?:_[^_]+)*?)__(.+)$/);
|
|
687
|
+
if (mcp) {
|
|
688
|
+
const [, server, tool] = mcp;
|
|
689
|
+
return connectorStep(toolCall, { service: server, tool: tool, live, text, merge });
|
|
690
|
+
}
|
|
691
|
+
const app = name.match(/^([a-z0-9-]+)__([a-z0-9]+)_([a-z0-9_]+)$/);
|
|
692
|
+
if (app) {
|
|
693
|
+
const [, product, domain, verb] = app;
|
|
694
|
+
return merge({ family: 'apps', glyph: 'zap', verb: `In ${product.charAt(0).toUpperCase()}${product.slice(1)}`, object: `${domain} · ${verb.replace(/_/g, ' ')}`, objectMono: false, result: failedResult(toolCall, ''), detail: text ? { kind: 'prose', text } : null });
|
|
695
|
+
}
|
|
696
|
+
const label = toolCall.summary && !isToolDisplayNoise(toolCall.summary)
|
|
697
|
+
? stripToolDisplayPrefix(toolCall.summary)
|
|
698
|
+
: stripToolDisplayPrefix(toolCallLabel(toolCall.toolName, toolCall.params));
|
|
699
|
+
return merge({ object: label === base.verb ? '' : label, result: failedResult(toolCall, ''), detail: text ? { kind: 'prose', text } : null });
|
|
700
|
+
}
|
|
701
|
+
//# sourceMappingURL=stepModel.js.map
|