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