@bahulam/code 2.6.13 → 2.6.14
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 +4 -4
- package/src/agents/scaffold.mjs +1 -0
- package/src/commands/agent.mjs +3 -2
- package/src/core/approval-log.mjs +22 -4
- package/src/core/approval.mjs +172 -24
- package/src/core/headless.mjs +14 -3
- package/src/core/local-agent.mjs +3 -2
- package/src/core/tool-executor.mjs +13 -3
- package/src/index.mjs +1 -1
- package/src/terminal/agents.mjs +194 -18
- package/src/terminal/repl-render.mjs +22 -4
- package/src/terminal/repl-state.mjs +1 -0
- package/src/terminal/repl.mjs +395 -21
- package/src/terminal/tool-display.mjs +135 -2
- package/src/ui/approval.mjs +200 -14
- package/src/ui/icons.mjs +11 -5
- package/src/ui/input-dock.mjs +90 -19
- package/src/ui/slash-commands.mjs +10 -0
- package/src/ui/tool-card.mjs +109 -21
- package/src/ui/tool-details.mjs +110 -11
- package/src/ui/transcript-block.mjs +2 -3
|
@@ -35,6 +35,10 @@ const TOOL_LABELS = Object.freeze({
|
|
|
35
35
|
workflow_create_multi: 'Creating workflow',
|
|
36
36
|
workflow_sync_multi: 'Syncing workflows',
|
|
37
37
|
workflow_run_multi: 'Running workflow',
|
|
38
|
+
Agent: 'Delegating',
|
|
39
|
+
agent: 'Delegating',
|
|
40
|
+
task: 'Delegating',
|
|
41
|
+
sub_agent_tools: 'Sub-agent tools',
|
|
38
42
|
explore: 'Exploring',
|
|
39
43
|
plan: 'Planning',
|
|
40
44
|
verify: 'Verifying',
|
|
@@ -43,9 +47,15 @@ const TOOL_LABELS = Object.freeze({
|
|
|
43
47
|
ask_user: 'Asking',
|
|
44
48
|
});
|
|
45
49
|
|
|
50
|
+
function labelKey(tool) {
|
|
51
|
+
const raw = String(tool || '');
|
|
52
|
+
return TOOL_LABELS[raw] ? raw : raw.toLowerCase();
|
|
53
|
+
}
|
|
54
|
+
|
|
46
55
|
export function toolDisplayLabel(tool) {
|
|
47
56
|
if (!tool) return 'Use tool';
|
|
48
|
-
|
|
57
|
+
const key = labelKey(tool);
|
|
58
|
+
if (TOOL_LABELS[key]) return TOOL_LABELS[key];
|
|
49
59
|
return tool
|
|
50
60
|
.replace(/^mcp[_-]?/i, '')
|
|
51
61
|
.split(/[_-]+/)
|
|
@@ -85,7 +95,8 @@ function firstParagraph(text) {
|
|
|
85
95
|
}
|
|
86
96
|
|
|
87
97
|
export function toolDisplaySummary(tool, args = {}, { cwd } = {}) {
|
|
88
|
-
|
|
98
|
+
const key = String(tool || '').toLowerCase();
|
|
99
|
+
switch (key) {
|
|
89
100
|
case 'shell':
|
|
90
101
|
return args.command || '(empty command)';
|
|
91
102
|
case 'read_file': {
|
|
@@ -159,6 +170,17 @@ export function toolDisplaySummary(tool, args = {}, { cwd } = {}) {
|
|
|
159
170
|
return args.name || args.slug || 'all local workflows';
|
|
160
171
|
case 'workflow_run_multi':
|
|
161
172
|
return [args.workflow_id || args.workflowId || args.name || '', args.pattern || 'sequential'].filter(Boolean).join(' · ');
|
|
173
|
+
case 'agent':
|
|
174
|
+
case 'task': {
|
|
175
|
+
const agentName = args.subagent_type || args.agent || args.name || args.type || '';
|
|
176
|
+
const task = firstParagraph(args.prompt || args.task || args.query || args.description || args.instruction || '');
|
|
177
|
+
return [agentName, task].filter(Boolean).join(' · ');
|
|
178
|
+
}
|
|
179
|
+
case 'sub_agent_tools': {
|
|
180
|
+
const total = Number(args.total || args.count || 0);
|
|
181
|
+
const agent = args.agent || args.type || '';
|
|
182
|
+
return [agent, total > 0 ? `${total} tool use${total === 1 ? '' : 's'}` : ''].filter(Boolean).join(' · ');
|
|
183
|
+
}
|
|
162
184
|
case 'explore':
|
|
163
185
|
case 'plan':
|
|
164
186
|
case 'verify':
|
|
@@ -185,6 +207,117 @@ export function shellCommandDisplay(command, { cwd = currentWorkingDirectory() }
|
|
|
185
207
|
};
|
|
186
208
|
}
|
|
187
209
|
|
|
210
|
+
const COMPACT_SHELL_CHARS = 320;
|
|
211
|
+
const COMPACT_SHELL_LINES = 2;
|
|
212
|
+
|
|
213
|
+
export function shellCommandProfile(command, {
|
|
214
|
+
cwd = currentWorkingDirectory(),
|
|
215
|
+
compactChars = COMPACT_SHELL_CHARS,
|
|
216
|
+
compactLines = COMPACT_SHELL_LINES,
|
|
217
|
+
} = {}) {
|
|
218
|
+
const original = String(command || '');
|
|
219
|
+
const display = shellCommandDisplay(original, { cwd });
|
|
220
|
+
const normalized = String(display.command || '').replace(/\r\n?/g, '\n');
|
|
221
|
+
const body = normalized || '(empty command)';
|
|
222
|
+
const lines = normalized ? normalized.split('\n') : [];
|
|
223
|
+
const commandLineCount = Math.max(1, lines.filter(line => line.trim()).length || lines.length);
|
|
224
|
+
const commandByteCount = byteLength(normalized || body);
|
|
225
|
+
const script = detectShellScript(normalized);
|
|
226
|
+
const lineCount = script?.body ? physicalLineCount(script.body) : commandLineCount;
|
|
227
|
+
const byteCount = script?.body ? byteLength(script.body) : commandByteCount;
|
|
228
|
+
const compact = Boolean(script)
|
|
229
|
+
|| commandLineCount >= compactLines
|
|
230
|
+
|| commandByteCount > compactChars;
|
|
231
|
+
const kind = script?.kind || (lineCount > 1 ? 'shell script' : 'shell command');
|
|
232
|
+
const summary = compact
|
|
233
|
+
? `${kind} · ${lineCount} line${lineCount === 1 ? '' : 's'} · ${formatBytes(byteCount)}`
|
|
234
|
+
: body;
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
original,
|
|
238
|
+
command: body,
|
|
239
|
+
cwdLabel: display.cwdLabel,
|
|
240
|
+
lineCount,
|
|
241
|
+
byteCount,
|
|
242
|
+
commandLineCount,
|
|
243
|
+
commandByteCount,
|
|
244
|
+
compact,
|
|
245
|
+
kind,
|
|
246
|
+
summary,
|
|
247
|
+
script,
|
|
248
|
+
detailHint: compact ? 'details: F2 or /last' : '',
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function detectShellScript(command) {
|
|
253
|
+
const text = String(command || '').replace(/\r\n?/g, '\n');
|
|
254
|
+
if (!text) return null;
|
|
255
|
+
|
|
256
|
+
const heredoc = text.match(/\b(python3?|node|ruby|perl|bash|sh)\b[^\n]*<<-?\s*(['"]?)([A-Za-z_][A-Za-z0-9_]*)\2[^\n]*\n([\s\S]*?)\n\3(?:\s*$|\s)/);
|
|
257
|
+
if (heredoc) {
|
|
258
|
+
const invocation = text.slice(0, text.indexOf('\n')).trim();
|
|
259
|
+
return {
|
|
260
|
+
kind: interpreterKind(heredoc[1]),
|
|
261
|
+
interpreter: heredoc[1],
|
|
262
|
+
marker: heredoc[3],
|
|
263
|
+
invocation,
|
|
264
|
+
body: heredoc[4],
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const inline = text.match(/\b(python3?|node|ruby|perl)\b\s+(?:-[A-Za-z]*[ce][A-Za-z]*|--command|--eval)\s+(['"])([\s\S]{120,})\2/);
|
|
269
|
+
if (inline) {
|
|
270
|
+
return {
|
|
271
|
+
kind: interpreterKind(inline[1]),
|
|
272
|
+
interpreter: inline[1],
|
|
273
|
+
invocation: text.slice(0, inline.index + inline[0].indexOf(inline[2])).trim(),
|
|
274
|
+
body: inline[3],
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const tempScript = text.match(/\b(python3?|node|ruby|perl|bash|sh)\b\s+((?:\/(?:private\/)?tmp|\/private\/var\/folders|\/var\/folders)[^\s;&|]+\.(?:py|mjs|js|rb|pl|sh))\b/);
|
|
279
|
+
if (tempScript) {
|
|
280
|
+
return {
|
|
281
|
+
kind: interpreterKind(tempScript[1]),
|
|
282
|
+
interpreter: tempScript[1],
|
|
283
|
+
invocation: `${tempScript[1]} ${tempScript[2]}`,
|
|
284
|
+
path: tempScript[2],
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function interpreterKind(value) {
|
|
292
|
+
const name = String(value || '').toLowerCase();
|
|
293
|
+
if (name.startsWith('python')) return 'python script';
|
|
294
|
+
if (name === 'node') return 'node script';
|
|
295
|
+
if (name === 'ruby') return 'ruby script';
|
|
296
|
+
if (name === 'perl') return 'perl script';
|
|
297
|
+
return 'shell script';
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function byteLength(value) {
|
|
301
|
+
try {
|
|
302
|
+
return Buffer.byteLength(String(value || ''), 'utf8');
|
|
303
|
+
} catch {
|
|
304
|
+
return String(value || '').length;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function physicalLineCount(value) {
|
|
309
|
+
const text = String(value || '');
|
|
310
|
+
if (!text) return 0;
|
|
311
|
+
return text.split('\n').length;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function formatBytes(bytes) {
|
|
315
|
+
const n = Number(bytes) || 0;
|
|
316
|
+
if (n < 1024) return `${n} B`;
|
|
317
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(n < 10 * 1024 ? 1 : 0)} KB`;
|
|
318
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
319
|
+
}
|
|
320
|
+
|
|
188
321
|
export function formatShellCommand(command, colors) {
|
|
189
322
|
const tokens = String(command || '').match(/"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|&&|\|\||[|;<>]|[^\s]+|\s+/g) || [];
|
|
190
323
|
let expectsCommand = true;
|
package/src/ui/approval.mjs
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
import { paint, width as visibleWidth } from './palette.mjs';
|
|
19
19
|
import { icon } from './icons.mjs';
|
|
20
|
-
import { shellCommandDisplay, toolDisplayLabel, toolDisplaySummary } from '../terminal/tool-display.mjs';
|
|
20
|
+
import { shellCommandDisplay, shellCommandProfile, toolDisplayLabel, toolDisplaySummary } from '../terminal/tool-display.mjs';
|
|
21
21
|
import { label as tierLabel, requiresExplicitApproval, TIERS } from '../core/risk-tier.mjs';
|
|
22
22
|
|
|
23
23
|
/**
|
|
@@ -29,12 +29,19 @@ import { label as tierLabel, requiresExplicitApproval, TIERS } from '../core/ris
|
|
|
29
29
|
* value — return value from the menu loop
|
|
30
30
|
* hint — secondary description shown to the right of the label
|
|
31
31
|
*/
|
|
32
|
-
export function defaultOptions(tier) {
|
|
32
|
+
export function defaultOptions(tier, { tool = '', args = {} } = {}) {
|
|
33
33
|
const approve = { key: 'y', label: 'approve once', value: 'approve', hint: 'run this call' };
|
|
34
34
|
const cancel = { key: 'n', label: 'cancel', value: 'reject', hint: 'do not run' };
|
|
35
35
|
if (requiresExplicitApproval(tier)) {
|
|
36
36
|
return [approve, cancel];
|
|
37
37
|
}
|
|
38
|
+
if (tool === 'shell') {
|
|
39
|
+
return [
|
|
40
|
+
approve,
|
|
41
|
+
{ key: 't', label: 'allow similar', value: 'allow-session', hint: `auto-approve ${shellTrustHint(args)} this session` },
|
|
42
|
+
cancel,
|
|
43
|
+
];
|
|
44
|
+
}
|
|
38
45
|
return [
|
|
39
46
|
approve,
|
|
40
47
|
{ key: 't', label: 'always allow', value: 'allow-type', hint: 'auto-approve future calls to this tool' },
|
|
@@ -65,27 +72,63 @@ export function defaultOptions(tier) {
|
|
|
65
72
|
*/
|
|
66
73
|
export function renderApprovalPrompt({
|
|
67
74
|
tool, args = {}, tier, why = '', width,
|
|
68
|
-
options, selected = 0,
|
|
75
|
+
options, selected = 0, showDetails = false,
|
|
69
76
|
} = {}) {
|
|
70
77
|
const cols = Math.max(60, Math.min(width || process.stderr.columns || 96, 120));
|
|
71
78
|
const explicit = requiresExplicitApproval(tier);
|
|
72
79
|
const accent = explicit ? paint.brand.accent : paint.brand.data;
|
|
73
|
-
const opts = options || defaultOptions(tier);
|
|
74
|
-
const title =
|
|
80
|
+
const opts = options || defaultOptions(tier, { tool, args });
|
|
81
|
+
const title = `⚠ ${approvalTitle(tier)} · ${tierLabel(tier)} · ${tool || 'tool'}`;
|
|
75
82
|
|
|
76
83
|
const lines = [
|
|
77
84
|
blockHeader(title, accent),
|
|
78
85
|
...subjectRows(tool, args, cols, accent),
|
|
86
|
+
...detailRows(tool, args, cols, accent, showDetails),
|
|
79
87
|
...riskRows(tool, args, tier, accent),
|
|
80
|
-
...reasonRows(why, cols, accent),
|
|
88
|
+
...reasonRows(tool, args, why, cols, accent),
|
|
81
89
|
blockLine(accent),
|
|
82
90
|
...decisionRows(opts, selected, accent),
|
|
83
|
-
blockLine(accent, paint.text.dim(
|
|
91
|
+
blockLine(accent, paint.text.dim(approvalFooter(tool, showDetails))),
|
|
84
92
|
];
|
|
85
93
|
|
|
86
94
|
return '\n' + lines.join('\n');
|
|
87
95
|
}
|
|
88
96
|
|
|
97
|
+
export function renderApprovalDockPrompt({
|
|
98
|
+
tool, args = {}, tier, why = '', width,
|
|
99
|
+
options, selected = 0, showDetails = false,
|
|
100
|
+
} = {}) {
|
|
101
|
+
const cols = Math.max(60, Math.min(width || process.stderr.columns || 96, 120));
|
|
102
|
+
const opts = options || defaultOptions(tier, { tool, args });
|
|
103
|
+
const subject = approvalDockSubject(tool, args, cols, showDetails);
|
|
104
|
+
const risks = riskTerms(tool, args, tier);
|
|
105
|
+
const reason = compactReason(tool, args, why);
|
|
106
|
+
const lines = [
|
|
107
|
+
...approvalDockSubjectRows(subject),
|
|
108
|
+
...(risks.length ? [`${paint.text.dim('risk ')}${paint.state.warn(risks.join(', '))}`] : []),
|
|
109
|
+
...(reason ? [`${paint.text.dim('reason ')}${paint.text.primary(truncate(reason, 120))}`] : []),
|
|
110
|
+
paint.text.dim('Decision'),
|
|
111
|
+
...opts.map((option, index) => optionToken(option, index === selected, explicitAccent(tier))),
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
prefix: '? approve › ',
|
|
116
|
+
value: truncateForDock(subject, showDetails ? 1200 : 220),
|
|
117
|
+
context: `${approvalTitle(tier)} · ${tierLabel(tier)} · ${tool || 'tool'}`,
|
|
118
|
+
meta: '',
|
|
119
|
+
tips: approvalFooter(tool, showDetails),
|
|
120
|
+
lines,
|
|
121
|
+
maxRows: showDetails ? 12 : 8,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function shellTrustHint(args = {}) {
|
|
126
|
+
const display = shellCommandDisplay(args.command || args.cmd || '');
|
|
127
|
+
const parts = String(display.command || '').trim().split(/\s+/).filter(Boolean);
|
|
128
|
+
const shape = parts.slice(0, 2).join(' ');
|
|
129
|
+
return shape ? `${shape}*` : 'similar shell commands';
|
|
130
|
+
}
|
|
131
|
+
|
|
89
132
|
export function renderTrustedApproval({ tool, args = {}, scope = 'session', ruleId = '', delaySeconds = 0 } = {}) {
|
|
90
133
|
const summary = approvalSubjectSummary(tool, args);
|
|
91
134
|
const subject = `${tool || 'tool'}${summary ? ` "${truncate(summary, 80)}"` : ''}`;
|
|
@@ -138,18 +181,22 @@ function approvalTitle(tier) {
|
|
|
138
181
|
}
|
|
139
182
|
|
|
140
183
|
function blockHeader(title, accent) {
|
|
141
|
-
return ` ${
|
|
184
|
+
return ` ${paint.bold(accent(title))}`;
|
|
142
185
|
}
|
|
143
186
|
|
|
144
187
|
function blockLine(accent, text = '') {
|
|
145
|
-
return
|
|
188
|
+
return text ? ` ${text}` : ' ';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function explicitAccent(tier) {
|
|
192
|
+
return requiresExplicitApproval(tier) ? paint.brand.accent : paint.brand.data;
|
|
146
193
|
}
|
|
147
194
|
|
|
148
195
|
function subjectRows(tool, args, cols, accent) {
|
|
149
196
|
const rows = [];
|
|
150
197
|
const available = Math.max(24, cols - 5);
|
|
151
198
|
const summary = toolDisplaySummary(tool, args, {});
|
|
152
|
-
const label =
|
|
199
|
+
const label = subjectLabel(tool);
|
|
153
200
|
const details = subjectDetails(tool, args, summary, Math.max(24, available - visibleWidth(label) - 1));
|
|
154
201
|
|
|
155
202
|
if (details.length === 1 && visibleWidth(`${label} ${details[0]}`) <= available) {
|
|
@@ -164,6 +211,14 @@ function subjectRows(tool, args, cols, accent) {
|
|
|
164
211
|
return rows;
|
|
165
212
|
}
|
|
166
213
|
|
|
214
|
+
function subjectLabel(tool) {
|
|
215
|
+
const label = toolDisplayLabel(tool);
|
|
216
|
+
if (tool === 'shell') {
|
|
217
|
+
return `${paint.text.dim('• shell ·')} ${paint.text.primary(label)}`;
|
|
218
|
+
}
|
|
219
|
+
return `${icon(tool)} ${paint.text.primary(label)}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
167
222
|
function decisionRows(opts, selected, accent) {
|
|
168
223
|
const rows = [blockLine(accent, paint.text.dim('Decision'))];
|
|
169
224
|
for (let i = 0; i < opts.length; i++) {
|
|
@@ -172,8 +227,8 @@ function decisionRows(opts, selected, accent) {
|
|
|
172
227
|
return rows;
|
|
173
228
|
}
|
|
174
229
|
|
|
175
|
-
function reasonRows(why, cols, accent) {
|
|
176
|
-
const reason =
|
|
230
|
+
function reasonRows(tool, args, why, cols, accent) {
|
|
231
|
+
const reason = compactReason(tool, args, why);
|
|
177
232
|
if (!reason) return [];
|
|
178
233
|
const label = paint.text.dim('reason ');
|
|
179
234
|
const firstWidth = Math.max(20, cols - 5 - visibleWidth(label));
|
|
@@ -192,6 +247,21 @@ function reasonRows(why, cols, accent) {
|
|
|
192
247
|
return rows;
|
|
193
248
|
}
|
|
194
249
|
|
|
250
|
+
function compactReason(tool, args = {}, why = '') {
|
|
251
|
+
const reason = String(why || '').replace(/\s+/g, ' ').trim();
|
|
252
|
+
if (!reason || tool !== 'shell') return reason;
|
|
253
|
+
|
|
254
|
+
const redundantShellPrefix = reason.match(/^Shell command requires approval:\s*(.+)$/i);
|
|
255
|
+
if (!redundantShellPrefix) return reason;
|
|
256
|
+
|
|
257
|
+
const command = String(args.command || args.cmd || '').replace(/\s+/g, ' ').trim();
|
|
258
|
+
const repeated = redundantShellPrefix[1].replace(/\s+/g, ' ').trim();
|
|
259
|
+
if (!command || repeated === command || command.startsWith(repeated) || repeated.startsWith(command)) {
|
|
260
|
+
return 'Shell command requires approval.';
|
|
261
|
+
}
|
|
262
|
+
return reason;
|
|
263
|
+
}
|
|
264
|
+
|
|
195
265
|
function riskRows(tool, args = {}, tier, accent) {
|
|
196
266
|
const terms = riskTerms(tool, args, tier);
|
|
197
267
|
if (!terms.length) return [];
|
|
@@ -230,8 +300,16 @@ function optionToken(option, selected, accent) {
|
|
|
230
300
|
|
|
231
301
|
function subjectDetails(tool, args = {}, summary = '', available = 72) {
|
|
232
302
|
if (tool === 'shell') {
|
|
233
|
-
const
|
|
234
|
-
const
|
|
303
|
+
const command = args.command || args.cmd || summary || '';
|
|
304
|
+
const profile = shellCommandProfile(command);
|
|
305
|
+
if (profile.compact) {
|
|
306
|
+
const lines = [`$ ${profile.summary}`];
|
|
307
|
+
if (profile.cwdLabel) lines.push(`in ${profile.cwdLabel}`);
|
|
308
|
+
return lines;
|
|
309
|
+
}
|
|
310
|
+
const display = shellCommandDisplay(command);
|
|
311
|
+
const lines = wrapText(display.command, Math.max(20, available - 2))
|
|
312
|
+
.map((line, index) => `${index === 0 ? '$' : '>'} ${line}`);
|
|
235
313
|
if (display.cwdLabel) lines.push(`in ${display.cwdLabel}`);
|
|
236
314
|
return lines.length ? lines : ['(empty command)'];
|
|
237
315
|
}
|
|
@@ -259,6 +337,72 @@ function subjectDetails(tool, args = {}, summary = '', available = 72) {
|
|
|
259
337
|
return wrapText(summary || JSON.stringify(args || {}), available).slice(0, 4);
|
|
260
338
|
}
|
|
261
339
|
|
|
340
|
+
function detailRows(tool, args = {}, cols, accent, showDetails) {
|
|
341
|
+
if (!showDetails || tool !== 'shell') return [];
|
|
342
|
+
const profile = shellCommandProfile(args.command || args.cmd || '');
|
|
343
|
+
const rows = [];
|
|
344
|
+
const labelWidth = 9;
|
|
345
|
+
const textWidth = Math.max(28, cols - 5 - labelWidth);
|
|
346
|
+
|
|
347
|
+
if (profile.cwdLabel) {
|
|
348
|
+
rows.push(blockLine(accent, `${paint.text.dim('cwd ')} ${paint.brand.data(profile.cwdLabel)}`));
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
rows.push(blockLine(accent, paint.text.dim('details')));
|
|
352
|
+
if (profile.script?.body) {
|
|
353
|
+
const invocation = profile.script.invocation || profile.command.split('\n')[0] || profile.command;
|
|
354
|
+
for (const line of wrapText(invocation, textWidth)) {
|
|
355
|
+
rows.push(blockLine(accent, `${paint.text.dim('cmd ')} ${paint.text.primary(line)}`));
|
|
356
|
+
}
|
|
357
|
+
rows.push(blockLine(accent, paint.text.dim('script ')));
|
|
358
|
+
rows.push(...numberedRows(profile.script.body, cols, accent, 120));
|
|
359
|
+
} else {
|
|
360
|
+
rows.push(...wrappedLabeledRows('cmd ', profile.command, cols, accent, 120));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return rows;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function approvalFooter(tool, showDetails) {
|
|
367
|
+
const details = tool === 'shell'
|
|
368
|
+
? ` · d ${showDetails ? 'hide details' : 'details'}`
|
|
369
|
+
: '';
|
|
370
|
+
return `↑↓ move · Enter pick · letter shortcut${details} · Esc cancel`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function approvalDockDetails(tool, args = {}, cols = 96) {
|
|
374
|
+
if (tool !== 'shell') return subjectDetails(tool, args, toolDisplaySummary(tool, args, {}), cols).join(' · ');
|
|
375
|
+
const profile = shellCommandProfile(args.command || args.cmd || '');
|
|
376
|
+
if (profile.script?.body) {
|
|
377
|
+
const invocation = profile.script.invocation || profile.command.split('\n')[0] || profile.command;
|
|
378
|
+
return [
|
|
379
|
+
`$ ${invocation}`,
|
|
380
|
+
...profile.script.body.split(/\r?\n/).slice(0, 12).map((line, index) => `${index + 1} ${line}`),
|
|
381
|
+
...(profile.script.body.split(/\r?\n/).length > 12 ? ['...'] : []),
|
|
382
|
+
].join('\n');
|
|
383
|
+
}
|
|
384
|
+
return profile.command;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function approvalDockSubject(tool, args = {}, cols = 96, showDetails = false) {
|
|
388
|
+
if (showDetails && tool === 'shell') return approvalDockDetails(tool, args, cols);
|
|
389
|
+
return subjectDetails(
|
|
390
|
+
tool,
|
|
391
|
+
args,
|
|
392
|
+
toolDisplaySummary(tool, args, {}),
|
|
393
|
+
Math.max(24, cols - 20),
|
|
394
|
+
).join(' · ');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function approvalDockSubjectRows(subject) {
|
|
398
|
+
const lines = String(subject || '').split('\n');
|
|
399
|
+
const first = lines.shift() || '';
|
|
400
|
+
return [
|
|
401
|
+
`${paint.text.dim('? approve ›')} ${paint.text.primary(truncate(first, 160))}`,
|
|
402
|
+
...lines.slice(0, 6).map(line => `${paint.text.dim(' ')}${paint.text.primary(truncate(line, 160))}`),
|
|
403
|
+
];
|
|
404
|
+
}
|
|
405
|
+
|
|
262
406
|
function approvalSubjectSummary(tool, args = {}) {
|
|
263
407
|
const summary = toolDisplaySummary(tool, args, {});
|
|
264
408
|
if (tool !== 'shell') return summary;
|
|
@@ -274,6 +418,48 @@ function hostFromUrl(url) {
|
|
|
274
418
|
}
|
|
275
419
|
}
|
|
276
420
|
|
|
421
|
+
function wrappedLabeledRows(label, text, cols, accent, maxLines = 120) {
|
|
422
|
+
const rows = [];
|
|
423
|
+
const labelText = paint.text.dim(label);
|
|
424
|
+
const firstWidth = Math.max(28, cols - 5 - visibleWidth(labelText) - 1);
|
|
425
|
+
const restWidth = Math.max(28, cols - 5 - visibleWidth(labelText) - 1);
|
|
426
|
+
const sourceLines = String(text || '').replace(/\r\n?/g, '\n').split('\n');
|
|
427
|
+
let emitted = 0;
|
|
428
|
+
for (const source of sourceLines) {
|
|
429
|
+
const wrapped = wrapText(source || ' ', emitted === 0 ? firstWidth : restWidth);
|
|
430
|
+
for (const line of wrapped) {
|
|
431
|
+
if (emitted >= maxLines) {
|
|
432
|
+
rows.push(blockLine(accent, `${paint.text.dim(' ')}${paint.text.dim('... detail truncated')}`));
|
|
433
|
+
return rows;
|
|
434
|
+
}
|
|
435
|
+
rows.push(blockLine(accent, `${emitted === 0 ? labelText : paint.text.dim(' ')} ${paint.text.primary(line)}`));
|
|
436
|
+
emitted++;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return rows;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function numberedRows(text, cols, accent, maxLines = 120) {
|
|
443
|
+
const rows = [];
|
|
444
|
+
const lines = String(text || '').replace(/\r\n?/g, '\n').split('\n');
|
|
445
|
+
const width = String(Math.min(lines.length, maxLines)).length;
|
|
446
|
+
const textWidth = Math.max(28, cols - 5 - width - 2);
|
|
447
|
+
for (let i = 0; i < Math.min(lines.length, maxLines); i++) {
|
|
448
|
+
const n = String(i + 1).padStart(width);
|
|
449
|
+
rows.push(blockLine(accent, `${paint.text.dim(`${n} `)}${paint.text.primary(truncate(lines[i], textWidth))}`));
|
|
450
|
+
}
|
|
451
|
+
if (lines.length > maxLines) {
|
|
452
|
+
rows.push(blockLine(accent, `${paint.text.dim(`... ${lines.length - maxLines} more line(s)`)}`));
|
|
453
|
+
}
|
|
454
|
+
return rows;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function truncateForDock(text, maxChars) {
|
|
458
|
+
const value = String(text || '').trim();
|
|
459
|
+
if (value.length <= maxChars) return value;
|
|
460
|
+
return `${value.slice(0, Math.max(0, maxChars - 1))}…`;
|
|
461
|
+
}
|
|
462
|
+
|
|
277
463
|
function wrapText(text, width) {
|
|
278
464
|
const words = String(text || '').split(/\s+/).filter(Boolean);
|
|
279
465
|
if (!words.length) return [''];
|
package/src/ui/icons.mjs
CHANGED
|
@@ -61,6 +61,10 @@ const TOOL_ICON = Object.freeze({
|
|
|
61
61
|
verify: 'subAgent',
|
|
62
62
|
debug: 'subAgent',
|
|
63
63
|
refactor: 'subAgent',
|
|
64
|
+
Agent: 'subAgent',
|
|
65
|
+
agent: 'subAgent',
|
|
66
|
+
task: 'subAgent',
|
|
67
|
+
sub_agent_tools: 'subAgent',
|
|
64
68
|
|
|
65
69
|
// Read / search
|
|
66
70
|
read_file: 'search',
|
|
@@ -133,14 +137,15 @@ export const icons = new Proxy({}, {
|
|
|
133
137
|
*/
|
|
134
138
|
export function icon(toolName) {
|
|
135
139
|
if (!toolName) return '';
|
|
136
|
-
const
|
|
140
|
+
const raw = String(toolName);
|
|
141
|
+
const key = TOOL_ICON[raw] || TOOL_ICON[raw.toLowerCase()];
|
|
137
142
|
if (key) return render(ICON_RECORDS[key]);
|
|
138
143
|
|
|
139
144
|
// MCP tools often arrive as "mcp__server__tool" — strip the prefix and
|
|
140
145
|
// try again before falling back to the generic glyph.
|
|
141
|
-
if (
|
|
142
|
-
const cleaned =
|
|
143
|
-
const fallback = TOOL_ICON[cleaned];
|
|
146
|
+
if (raw.startsWith('mcp')) {
|
|
147
|
+
const cleaned = raw.replace(/^mcp[_-]+/, '').split(/[_-]+/)[0];
|
|
148
|
+
const fallback = TOOL_ICON[cleaned] || TOOL_ICON[cleaned.toLowerCase()];
|
|
144
149
|
if (fallback) return render(ICON_RECORDS[fallback]);
|
|
145
150
|
}
|
|
146
151
|
|
|
@@ -152,7 +157,8 @@ export function icon(toolName) {
|
|
|
152
157
|
* Used by tier classification and color choice in the tool card renderer.
|
|
153
158
|
*/
|
|
154
159
|
export function toolFamily(toolName) {
|
|
155
|
-
|
|
160
|
+
const raw = String(toolName || '');
|
|
161
|
+
return TOOL_ICON[raw] || TOOL_ICON[raw.toLowerCase()] || 'other';
|
|
156
162
|
}
|
|
157
163
|
|
|
158
164
|
/**
|