agent-sh 0.1.0 → 0.3.0
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/README.md +66 -576
- package/dist/acp-client.d.ts +24 -0
- package/dist/acp-client.js +168 -35
- package/dist/context-manager.d.ts +6 -4
- package/dist/context-manager.js +75 -44
- package/dist/event-bus.d.ts +29 -0
- package/dist/extension-loader.js +3 -14
- package/dist/extensions/shell-exec.d.ts +24 -0
- package/dist/extensions/shell-exec.js +188 -0
- package/dist/extensions/tui-renderer.d.ts +1 -1
- package/dist/extensions/tui-renderer.js +133 -28
- package/dist/index.js +195 -6
- package/dist/input-handler.d.ts +13 -3
- package/dist/input-handler.js +259 -127
- package/dist/mcp-server.d.ts +13 -0
- package/dist/mcp-server.js +234 -0
- package/dist/output-parser.d.ts +5 -26
- package/dist/output-parser.js +16 -78
- package/dist/settings.d.ts +33 -0
- package/dist/settings.js +43 -0
- package/dist/shell.d.ts +9 -4
- package/dist/shell.js +88 -10
- package/dist/types.d.ts +4 -0
- package/dist/utils/ansi.d.ts +4 -1
- package/dist/utils/ansi.js +60 -2
- package/dist/utils/line-editor.d.ts +59 -0
- package/dist/utils/line-editor.js +381 -0
- package/dist/utils/markdown.js +4 -4
- package/dist/utils/tool-display.d.ts +11 -0
- package/dist/utils/tool-display.js +92 -9
- package/examples/pi-agent-sh.ts +166 -0
- package/package.json +1 -1
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal line editor with readline-style keybindings.
|
|
3
|
+
*
|
|
4
|
+
* Pure logic — no I/O, no rendering, no event bus. Consumers feed raw
|
|
5
|
+
* terminal input bytes and receive high-level actions back. Buffer and
|
|
6
|
+
* cursor state are public for rendering.
|
|
7
|
+
*/
|
|
8
|
+
// ── Kitty protocol keycode → readable name ──────────────────────
|
|
9
|
+
const KITTY_KEY_NAMES = {
|
|
10
|
+
9: "tab", 13: "enter", 27: "escape", 127: "backspace",
|
|
11
|
+
};
|
|
12
|
+
// ── Line editor ─────────────────────────────────────────────────
|
|
13
|
+
export class LineEditor {
|
|
14
|
+
buffer = "";
|
|
15
|
+
cursor = 0;
|
|
16
|
+
pendingSeq = ""; // buffered incomplete escape sequence
|
|
17
|
+
/** Process raw terminal input, return actions for the consumer. */
|
|
18
|
+
feed(data) {
|
|
19
|
+
// If we had a pending incomplete escape sequence, prepend it
|
|
20
|
+
if (this.pendingSeq) {
|
|
21
|
+
data = this.pendingSeq + data;
|
|
22
|
+
this.pendingSeq = "";
|
|
23
|
+
}
|
|
24
|
+
const actions = [];
|
|
25
|
+
let i = 0;
|
|
26
|
+
while (i < data.length) {
|
|
27
|
+
const ch = data[i];
|
|
28
|
+
// ── Escape sequences ────────────────────────────────
|
|
29
|
+
if (ch === "\x1b") {
|
|
30
|
+
const next = data[i + 1];
|
|
31
|
+
// Incomplete escape — buffer and wait for next feed()
|
|
32
|
+
if (next == null) {
|
|
33
|
+
this.pendingSeq = "\x1b";
|
|
34
|
+
i++;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
// CSI sequence: \x1b[...
|
|
38
|
+
if (next === "[") {
|
|
39
|
+
const { consumed, incomplete } = this.handleCSI(data, i, actions);
|
|
40
|
+
if (incomplete) {
|
|
41
|
+
this.pendingSeq = data.slice(i, i + consumed);
|
|
42
|
+
i += consumed;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
i += consumed;
|
|
46
|
+
}
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
// SS3 sequence: \x1bO... (application cursor mode — arrow keys, Home, End)
|
|
50
|
+
if (next === "O") {
|
|
51
|
+
const ss3Final = data[i + 2];
|
|
52
|
+
if (ss3Final == null) {
|
|
53
|
+
// Incomplete — buffer for next feed()
|
|
54
|
+
this.pendingSeq = data.slice(i, i + 2);
|
|
55
|
+
i += 2;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
i += 3; // consume \x1b O <final>
|
|
59
|
+
switch (ss3Final) {
|
|
60
|
+
case "A":
|
|
61
|
+
actions.push({ action: "arrow-up" });
|
|
62
|
+
break;
|
|
63
|
+
case "B":
|
|
64
|
+
actions.push({ action: "arrow-down" });
|
|
65
|
+
break;
|
|
66
|
+
case "C":
|
|
67
|
+
if (this.cursor < this.buffer.length) {
|
|
68
|
+
this.cursor++;
|
|
69
|
+
actions.push({ action: "changed" });
|
|
70
|
+
}
|
|
71
|
+
break;
|
|
72
|
+
case "D":
|
|
73
|
+
if (this.cursor > 0) {
|
|
74
|
+
this.cursor--;
|
|
75
|
+
actions.push({ action: "changed" });
|
|
76
|
+
}
|
|
77
|
+
break;
|
|
78
|
+
case "H": // Home
|
|
79
|
+
if (this.cursor > 0) {
|
|
80
|
+
this.cursor = 0;
|
|
81
|
+
actions.push({ action: "changed" });
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
case "F": // End
|
|
85
|
+
if (this.cursor < this.buffer.length) {
|
|
86
|
+
this.cursor = this.buffer.length;
|
|
87
|
+
actions.push({ action: "changed" });
|
|
88
|
+
}
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
// Alt/Option + key: \x1b followed by char
|
|
94
|
+
i += 2; // consume \x1b + next byte
|
|
95
|
+
if (next === "\x7f") {
|
|
96
|
+
// Option+Backspace: delete word backward
|
|
97
|
+
if (this.deleteWordBackward())
|
|
98
|
+
actions.push({ action: "changed" });
|
|
99
|
+
}
|
|
100
|
+
else if (next === "b") {
|
|
101
|
+
// Alt+B: word backward
|
|
102
|
+
if (this.wordBackward())
|
|
103
|
+
actions.push({ action: "changed" });
|
|
104
|
+
}
|
|
105
|
+
else if (next === "f") {
|
|
106
|
+
// Alt+F: word forward
|
|
107
|
+
if (this.wordForward())
|
|
108
|
+
actions.push({ action: "changed" });
|
|
109
|
+
}
|
|
110
|
+
else if (next === "d") {
|
|
111
|
+
// Alt+D: delete word forward
|
|
112
|
+
if (this.deleteWordForward())
|
|
113
|
+
actions.push({ action: "changed" });
|
|
114
|
+
}
|
|
115
|
+
// Other Alt+key — ignore
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// ── Control characters ──────────────────────────────
|
|
119
|
+
if (ch.charCodeAt(0) < 0x20 || ch === "\x7f") {
|
|
120
|
+
const action = this.handleControl(ch);
|
|
121
|
+
if (action)
|
|
122
|
+
actions.push(action);
|
|
123
|
+
i++;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
// ── Printable character ─────────────────────────────
|
|
127
|
+
this.buffer = this.buffer.slice(0, this.cursor) + ch + this.buffer.slice(this.cursor);
|
|
128
|
+
this.cursor++;
|
|
129
|
+
actions.push({ action: "changed" });
|
|
130
|
+
i++;
|
|
131
|
+
}
|
|
132
|
+
return actions;
|
|
133
|
+
}
|
|
134
|
+
/** Check if there's a pending incomplete escape sequence. */
|
|
135
|
+
hasPendingEscape() {
|
|
136
|
+
return this.pendingSeq.length > 0;
|
|
137
|
+
}
|
|
138
|
+
/** Flush a pending sequence — treat bare \x1b as cancel, discard incomplete CSI. */
|
|
139
|
+
flushPendingEscape() {
|
|
140
|
+
if (!this.pendingSeq)
|
|
141
|
+
return [];
|
|
142
|
+
const wasBarEscape = this.pendingSeq === "\x1b";
|
|
143
|
+
this.pendingSeq = "";
|
|
144
|
+
return wasBarEscape ? [{ action: "cancel" }] : [];
|
|
145
|
+
}
|
|
146
|
+
clear() {
|
|
147
|
+
this.buffer = "";
|
|
148
|
+
this.cursor = 0;
|
|
149
|
+
this.pendingSeq = "";
|
|
150
|
+
}
|
|
151
|
+
// ── Key bindings ────────────────────────────────────────────
|
|
152
|
+
//
|
|
153
|
+
// Single source of truth for all keybindings. Both legacy control
|
|
154
|
+
// characters and kitty protocol sequences resolve to a key name
|
|
155
|
+
// and look it up here. To add a binding, add one entry.
|
|
156
|
+
bindings = {
|
|
157
|
+
"enter": () => ({ action: "submit", buffer: this.buffer }),
|
|
158
|
+
"ctrl+c": () => ({ action: "cancel" }),
|
|
159
|
+
"tab": () => ({ action: "tab" }),
|
|
160
|
+
"backspace": () => this.deleteBackward(),
|
|
161
|
+
"ctrl+d": () => this.buffer.length === 0 ? { action: "delete-empty" } : this.deleteForward(),
|
|
162
|
+
"ctrl+a": () => this.moveTo(0),
|
|
163
|
+
"ctrl+e": () => this.moveTo(this.buffer.length),
|
|
164
|
+
"ctrl+b": () => this.moveTo(this.cursor - 1),
|
|
165
|
+
"ctrl+f": () => this.moveTo(this.cursor + 1),
|
|
166
|
+
"ctrl+u": () => this.deleteRange(0, this.cursor),
|
|
167
|
+
"ctrl+k": () => this.deleteRange(this.cursor, this.buffer.length),
|
|
168
|
+
"ctrl+w": () => this.deleteWordBackward() ? { action: "changed" } : null,
|
|
169
|
+
"shift+enter": () => this.insertAt("\n"),
|
|
170
|
+
"shift+tab": () => ({ action: "shift+tab" }),
|
|
171
|
+
};
|
|
172
|
+
/** Resolve a key name from the bindings table and execute it. */
|
|
173
|
+
dispatch(key) {
|
|
174
|
+
return this.bindings[key]?.() ?? null;
|
|
175
|
+
}
|
|
176
|
+
// ── Legacy control character mapping ───────────────────────
|
|
177
|
+
/** Map a legacy control character to a key name. */
|
|
178
|
+
static CTRL_MAP = {
|
|
179
|
+
"\r": "enter", "\x03": "ctrl+c", "\t": "tab",
|
|
180
|
+
"\x7f": "backspace", "\b": "backspace",
|
|
181
|
+
"\x01": "ctrl+a", "\x02": "ctrl+b", "\x04": "ctrl+d",
|
|
182
|
+
"\x05": "ctrl+e", "\x06": "ctrl+f", "\x0b": "ctrl+k",
|
|
183
|
+
"\x15": "ctrl+u", "\x17": "ctrl+w",
|
|
184
|
+
};
|
|
185
|
+
handleControl(ch) {
|
|
186
|
+
const key = LineEditor.CTRL_MAP[ch];
|
|
187
|
+
return key ? this.dispatch(key) : null;
|
|
188
|
+
}
|
|
189
|
+
// ── Kitty keyboard protocol ────────────────────────────────
|
|
190
|
+
/** Handle a kitty protocol CSI u sequence. Params format: "keycode;modifier". */
|
|
191
|
+
handleKittyKey(params) {
|
|
192
|
+
const [kc, mod] = params.split(";").map(Number);
|
|
193
|
+
const keycode = kc;
|
|
194
|
+
const mods = (mod ?? 1) - 1; // kitty modifier bits
|
|
195
|
+
// Build key name from modifier + keycode
|
|
196
|
+
const modNames = [];
|
|
197
|
+
if (mods & 4)
|
|
198
|
+
modNames.push("ctrl");
|
|
199
|
+
if (mods & 1)
|
|
200
|
+
modNames.push("shift");
|
|
201
|
+
if (mods & 2)
|
|
202
|
+
modNames.push("alt");
|
|
203
|
+
const keyName = KITTY_KEY_NAMES[keycode] ?? String.fromCharCode(keycode);
|
|
204
|
+
const fullName = [...modNames, keyName].join("+");
|
|
205
|
+
// Try exact binding first, then fall back to ctrl char mapping
|
|
206
|
+
return this.dispatch(fullName)
|
|
207
|
+
?? ((mods & 4) && keycode >= 97 && keycode <= 122
|
|
208
|
+
? this.dispatch(`ctrl+${String.fromCharCode(keycode)}`)
|
|
209
|
+
: null)
|
|
210
|
+
?? (mods === 0 ? this.handleControl(String.fromCharCode(keycode)) : null);
|
|
211
|
+
}
|
|
212
|
+
// ── Editing primitives ─────────────────────────────────────
|
|
213
|
+
insertAt(ch) {
|
|
214
|
+
this.buffer = this.buffer.slice(0, this.cursor) + ch + this.buffer.slice(this.cursor);
|
|
215
|
+
this.cursor++;
|
|
216
|
+
return { action: "changed" };
|
|
217
|
+
}
|
|
218
|
+
moveTo(pos) {
|
|
219
|
+
const clamped = Math.max(0, Math.min(pos, this.buffer.length));
|
|
220
|
+
if (clamped === this.cursor)
|
|
221
|
+
return null;
|
|
222
|
+
this.cursor = clamped;
|
|
223
|
+
return { action: "changed" };
|
|
224
|
+
}
|
|
225
|
+
deleteBackward() {
|
|
226
|
+
if (this.buffer.length === 0)
|
|
227
|
+
return { action: "delete-empty" };
|
|
228
|
+
if (this.cursor <= 0)
|
|
229
|
+
return null;
|
|
230
|
+
this.buffer = this.buffer.slice(0, this.cursor - 1) + this.buffer.slice(this.cursor);
|
|
231
|
+
this.cursor--;
|
|
232
|
+
return { action: "changed" };
|
|
233
|
+
}
|
|
234
|
+
deleteForward() {
|
|
235
|
+
if (this.cursor >= this.buffer.length)
|
|
236
|
+
return null;
|
|
237
|
+
this.buffer = this.buffer.slice(0, this.cursor) + this.buffer.slice(this.cursor + 1);
|
|
238
|
+
return { action: "changed" };
|
|
239
|
+
}
|
|
240
|
+
deleteRange(start, end) {
|
|
241
|
+
if (start >= end)
|
|
242
|
+
return null;
|
|
243
|
+
this.buffer = this.buffer.slice(0, start) + this.buffer.slice(end);
|
|
244
|
+
this.cursor = start;
|
|
245
|
+
return { action: "changed" };
|
|
246
|
+
}
|
|
247
|
+
// ── CSI sequence handling ───────────────────────────────────
|
|
248
|
+
/**
|
|
249
|
+
* Parse and handle a CSI sequence (\x1b[...) starting at `start`.
|
|
250
|
+
* Returns the number of bytes consumed and whether the sequence was incomplete.
|
|
251
|
+
*/
|
|
252
|
+
handleCSI(data, start, actions) {
|
|
253
|
+
// Skip \x1b[
|
|
254
|
+
let j = start + 2;
|
|
255
|
+
// Accumulate parameter bytes (0x20-0x3F: digits, semicolons, etc.)
|
|
256
|
+
let params = "";
|
|
257
|
+
while (j < data.length && data.charCodeAt(j) >= 0x20 && data.charCodeAt(j) < 0x40) {
|
|
258
|
+
params += data[j];
|
|
259
|
+
j++;
|
|
260
|
+
}
|
|
261
|
+
// If we ran out of data before the final byte, sequence is incomplete
|
|
262
|
+
if (j >= data.length) {
|
|
263
|
+
return { consumed: j - start, incomplete: true };
|
|
264
|
+
}
|
|
265
|
+
const final = data[j];
|
|
266
|
+
const consumed = j - start + 1;
|
|
267
|
+
// Dispatch on final byte
|
|
268
|
+
switch (final) {
|
|
269
|
+
case "A": // Up arrow
|
|
270
|
+
actions.push({ action: "arrow-up" });
|
|
271
|
+
break;
|
|
272
|
+
case "B": // Down arrow
|
|
273
|
+
actions.push({ action: "arrow-down" });
|
|
274
|
+
break;
|
|
275
|
+
case "C": // Right (or modified right: 1;3C, 1;5C = word right)
|
|
276
|
+
if (params.includes(";")) {
|
|
277
|
+
if (this.wordForward())
|
|
278
|
+
actions.push({ action: "changed" });
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
if (this.cursor < this.buffer.length) {
|
|
282
|
+
this.cursor++;
|
|
283
|
+
actions.push({ action: "changed" });
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
break;
|
|
287
|
+
case "D": // Left (or modified left: 1;3D, 1;5D = word left)
|
|
288
|
+
if (params.includes(";")) {
|
|
289
|
+
if (this.wordBackward())
|
|
290
|
+
actions.push({ action: "changed" });
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
if (this.cursor > 0) {
|
|
294
|
+
this.cursor--;
|
|
295
|
+
actions.push({ action: "changed" });
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
break;
|
|
299
|
+
case "H": // Home
|
|
300
|
+
if (this.cursor > 0) {
|
|
301
|
+
this.cursor = 0;
|
|
302
|
+
actions.push({ action: "changed" });
|
|
303
|
+
}
|
|
304
|
+
break;
|
|
305
|
+
case "F": // End
|
|
306
|
+
if (this.cursor < this.buffer.length) {
|
|
307
|
+
this.cursor = this.buffer.length;
|
|
308
|
+
actions.push({ action: "changed" });
|
|
309
|
+
}
|
|
310
|
+
break;
|
|
311
|
+
case "Z": // Shift+Tab (legacy CSI sequence)
|
|
312
|
+
actions.push({ action: "shift+tab" });
|
|
313
|
+
break;
|
|
314
|
+
case "u": { // Kitty keyboard protocol: \x1b[<keycode>;<modifier>u
|
|
315
|
+
const action = this.handleKittyKey(params);
|
|
316
|
+
if (action)
|
|
317
|
+
actions.push(action);
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
case "~": // Extended keys: Delete (3~), etc.
|
|
321
|
+
if (params === "3") {
|
|
322
|
+
// Delete key: delete char under cursor
|
|
323
|
+
if (this.cursor < this.buffer.length) {
|
|
324
|
+
this.buffer = this.buffer.slice(0, this.cursor) + this.buffer.slice(this.cursor + 1);
|
|
325
|
+
actions.push({ action: "changed" });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
break;
|
|
329
|
+
// All other CSI sequences — silently ignored
|
|
330
|
+
}
|
|
331
|
+
return { consumed };
|
|
332
|
+
}
|
|
333
|
+
// ── Word movement / deletion helpers ────────────────────────
|
|
334
|
+
wordBackward() {
|
|
335
|
+
if (this.cursor === 0)
|
|
336
|
+
return false;
|
|
337
|
+
let pos = this.cursor;
|
|
338
|
+
// Skip spaces
|
|
339
|
+
while (pos > 0 && this.buffer[pos - 1] === " ")
|
|
340
|
+
pos--;
|
|
341
|
+
// Skip word chars
|
|
342
|
+
while (pos > 0 && this.buffer[pos - 1] !== " ")
|
|
343
|
+
pos--;
|
|
344
|
+
if (pos === this.cursor)
|
|
345
|
+
return false;
|
|
346
|
+
this.cursor = pos;
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
wordForward() {
|
|
350
|
+
if (this.cursor >= this.buffer.length)
|
|
351
|
+
return false;
|
|
352
|
+
let pos = this.cursor;
|
|
353
|
+
// Skip word chars
|
|
354
|
+
while (pos < this.buffer.length && this.buffer[pos] !== " ")
|
|
355
|
+
pos++;
|
|
356
|
+
// Skip spaces
|
|
357
|
+
while (pos < this.buffer.length && this.buffer[pos] === " ")
|
|
358
|
+
pos++;
|
|
359
|
+
if (pos === this.cursor)
|
|
360
|
+
return false;
|
|
361
|
+
this.cursor = pos;
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
deleteWordBackward() {
|
|
365
|
+
if (this.cursor === 0)
|
|
366
|
+
return false;
|
|
367
|
+
const start = this.cursor;
|
|
368
|
+
this.wordBackward();
|
|
369
|
+
this.buffer = this.buffer.slice(0, this.cursor) + this.buffer.slice(start);
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
deleteWordForward() {
|
|
373
|
+
if (this.cursor >= this.buffer.length)
|
|
374
|
+
return false;
|
|
375
|
+
const start = this.cursor;
|
|
376
|
+
this.wordForward();
|
|
377
|
+
this.buffer = this.buffer.slice(0, start) + this.buffer.slice(this.cursor);
|
|
378
|
+
this.cursor = start;
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
}
|
package/dist/utils/markdown.js
CHANGED
|
@@ -108,13 +108,13 @@ export class MarkdownRenderer {
|
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
printTopBorder() {
|
|
111
|
-
const
|
|
112
|
-
process.stdout.write(`${p.dim}${p.accent}${"─".repeat(
|
|
111
|
+
const termW = process.stdout.columns || 80;
|
|
112
|
+
process.stdout.write(`${p.dim}${p.accent}${"─".repeat(termW)}${p.reset}\n`);
|
|
113
113
|
this.firstLine = true;
|
|
114
114
|
}
|
|
115
115
|
printBottomBorder() {
|
|
116
|
-
const
|
|
117
|
-
process.stdout.write(`${p.dim}${p.accent}${"─".repeat(
|
|
116
|
+
const termW = process.stdout.columns || 80;
|
|
117
|
+
process.stdout.write(`${p.dim}${p.accent}${"─".repeat(termW)}${p.reset}\n`);
|
|
118
118
|
}
|
|
119
119
|
processBuffer() {
|
|
120
120
|
const lines = this.buffer.split("\n");
|
|
@@ -4,6 +4,15 @@ export interface ToolCallRender {
|
|
|
4
4
|
title: string;
|
|
5
5
|
/** Optional command string for bash-like tools. */
|
|
6
6
|
command?: string;
|
|
7
|
+
/** Tool kind from ACP (read, edit, execute, search, etc.). */
|
|
8
|
+
kind?: string;
|
|
9
|
+
/** File locations affected by the tool call. */
|
|
10
|
+
locations?: {
|
|
11
|
+
path: string;
|
|
12
|
+
line?: number | null;
|
|
13
|
+
}[];
|
|
14
|
+
/** Raw input parameters sent to the tool. */
|
|
15
|
+
rawInput?: unknown;
|
|
7
16
|
}
|
|
8
17
|
export interface ToolResultRender {
|
|
9
18
|
exitCode: number | null;
|
|
@@ -29,5 +38,7 @@ export declare function createSpinner(): SpinnerState;
|
|
|
29
38
|
*/
|
|
30
39
|
export declare function startSpinner(label: string, opts?: {
|
|
31
40
|
color?: string;
|
|
41
|
+
hint?: string;
|
|
42
|
+
startTime?: number;
|
|
32
43
|
}): SpinnerState;
|
|
33
44
|
export declare function stopSpinner(state: SpinnerState): void;
|
|
@@ -37,24 +37,104 @@ export function selectToolDisplayMode(width) {
|
|
|
37
37
|
return "compact";
|
|
38
38
|
return "summary";
|
|
39
39
|
}
|
|
40
|
+
// ── Kind icons ──────────────────────────────────────────────────
|
|
41
|
+
const KIND_ICONS = {
|
|
42
|
+
read: "◆",
|
|
43
|
+
edit: "✎",
|
|
44
|
+
delete: "✕",
|
|
45
|
+
move: "↗",
|
|
46
|
+
search: "⌕",
|
|
47
|
+
execute: "▶",
|
|
48
|
+
think: "◇",
|
|
49
|
+
fetch: "↓",
|
|
50
|
+
switch_mode: "⇄",
|
|
51
|
+
};
|
|
52
|
+
function kindIcon(kind) {
|
|
53
|
+
return kind ? (KIND_ICONS[kind] ?? "▶") : "▶";
|
|
54
|
+
}
|
|
40
55
|
// ── Tool call rendering ──────────────────────────────────────────
|
|
41
56
|
export function renderToolCall(tool, width) {
|
|
42
57
|
const mode = selectToolDisplayMode(width);
|
|
58
|
+
const icon = kindIcon(tool.kind);
|
|
43
59
|
if (mode === "summary") {
|
|
44
|
-
const text = truncateVisible(
|
|
60
|
+
const text = truncateVisible(`${icon} ${tool.title}`, width);
|
|
45
61
|
return [`${p.warning}${text}${p.reset}`];
|
|
46
62
|
}
|
|
47
63
|
const lines = [];
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
64
|
+
// Build a compact detail string to append after the title
|
|
65
|
+
let detail = "";
|
|
66
|
+
if (mode === "full") {
|
|
67
|
+
if (tool.command) {
|
|
68
|
+
detail = `$ ${tool.command}`;
|
|
69
|
+
}
|
|
70
|
+
else if (tool.locations && tool.locations.length > 0) {
|
|
71
|
+
const loc = tool.locations[0];
|
|
72
|
+
const lineInfo = loc.line ? `:${loc.line}` : "";
|
|
73
|
+
detail = `${loc.path}${lineInfo}`;
|
|
74
|
+
}
|
|
75
|
+
else if (tool.rawInput) {
|
|
76
|
+
const raw = tool.rawInput;
|
|
77
|
+
if (raw && typeof raw === "object") {
|
|
78
|
+
if (typeof raw.command === "string") {
|
|
79
|
+
detail = `$ ${raw.command}`;
|
|
80
|
+
}
|
|
81
|
+
else if (typeof raw.operation === "string") {
|
|
82
|
+
detail = raw.operation;
|
|
83
|
+
if (raw.ids && Array.isArray(raw.ids)) {
|
|
84
|
+
detail += ` #${raw.ids.join(",")}`;
|
|
85
|
+
}
|
|
86
|
+
if (typeof raw.query === "string") {
|
|
87
|
+
detail += ` "${raw.query}"`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
detail = formatRawInput(tool.rawInput, width - tool.title.length - 6);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Render as single line: ► title: detail
|
|
97
|
+
const maxDetailW = Math.max(1, width - tool.title.length - 6);
|
|
98
|
+
if (detail) {
|
|
99
|
+
if (detail.length > maxDetailW)
|
|
100
|
+
detail = detail.slice(0, maxDetailW - 1) + "…";
|
|
101
|
+
lines.push(`${p.warning}${p.bold}${icon} ${tool.title}${p.reset}${p.dim}: ${detail}${p.reset}`);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
lines.push(`${p.warning}${p.bold}${icon} ${tool.title}${p.reset}`);
|
|
105
|
+
}
|
|
106
|
+
// Show additional file locations on separate lines (if more than one)
|
|
107
|
+
if (mode === "full" && tool.locations && tool.locations.length > 1) {
|
|
108
|
+
for (const loc of tool.locations.slice(1)) {
|
|
109
|
+
const lineInfo = loc.line ? `:${loc.line}` : "";
|
|
110
|
+
lines.push(` ${p.dim}${loc.path}${lineInfo}${p.reset}`);
|
|
111
|
+
}
|
|
55
112
|
}
|
|
56
113
|
return lines;
|
|
57
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Format raw input parameters into a compact single-line summary.
|
|
117
|
+
*/
|
|
118
|
+
function formatRawInput(raw, maxWidth) {
|
|
119
|
+
if (raw == null)
|
|
120
|
+
return "";
|
|
121
|
+
if (typeof raw === "string") {
|
|
122
|
+
return raw.length > maxWidth ? raw.slice(0, maxWidth - 1) + "…" : raw;
|
|
123
|
+
}
|
|
124
|
+
if (typeof raw !== "object")
|
|
125
|
+
return String(raw);
|
|
126
|
+
// Show key=value pairs for objects
|
|
127
|
+
const obj = raw;
|
|
128
|
+
const parts = [];
|
|
129
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
130
|
+
if (val == null)
|
|
131
|
+
continue;
|
|
132
|
+
const valStr = typeof val === "string" ? val : JSON.stringify(val);
|
|
133
|
+
parts.push(`${key}=${valStr}`);
|
|
134
|
+
}
|
|
135
|
+
const joined = parts.join(" ");
|
|
136
|
+
return joined.length > maxWidth ? joined.slice(0, maxWidth - 1) + "…" : joined;
|
|
137
|
+
}
|
|
58
138
|
// ── Tool result rendering ────────────────────────────────────────
|
|
59
139
|
export function renderToolResult(result, width) {
|
|
60
140
|
const mode = selectToolDisplayMode(width);
|
|
@@ -113,12 +193,15 @@ export function createSpinner() {
|
|
|
113
193
|
*/
|
|
114
194
|
export function startSpinner(label, opts) {
|
|
115
195
|
const state = createSpinner();
|
|
196
|
+
if (opts?.startTime)
|
|
197
|
+
state.startTime = opts.startTime;
|
|
116
198
|
const color = opts?.color ?? p.accent;
|
|
199
|
+
const hint = opts?.hint ? ` ${p.dim}${opts.hint}${p.reset}` : "";
|
|
117
200
|
state.interval = setInterval(() => {
|
|
118
201
|
const frame = SPINNER_FRAMES[state.frame % SPINNER_FRAMES.length];
|
|
119
202
|
const elapsed = formatElapsed(Date.now() - state.startTime);
|
|
120
203
|
const timer = elapsed ? ` ${p.dim}${elapsed}${p.reset}` : "";
|
|
121
|
-
process.stdout.write(`\r ${color}${frame} ${label}...${p.reset}${timer}\x1b[K`);
|
|
204
|
+
process.stdout.write(`\r ${color}${frame} ${label}...${p.reset}${timer}${hint}\x1b[K`);
|
|
122
205
|
state.frame++;
|
|
123
206
|
}, 80);
|
|
124
207
|
return state;
|