@axplusb/kepler 2.0.2 → 2.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/terminal/repl.mjs +63 -22
- package/src/terminal/tool-display.mjs +29 -26
- package/src/ui/tool-card.mjs +17 -7
package/package.json
CHANGED
package/src/terminal/repl.mjs
CHANGED
|
@@ -272,13 +272,28 @@ function updateStatusBar() {
|
|
|
272
272
|
* args. The result arrives later via `renderToolResult` and is appended as a
|
|
273
273
|
* gutter line. Sub-agent calls are indented per session.inSubAgent.
|
|
274
274
|
*/
|
|
275
|
-
//
|
|
276
|
-
//
|
|
277
|
-
//
|
|
278
|
-
//
|
|
279
|
-
|
|
275
|
+
// Deferred-head strategy: we DON'T print the tool head when tool_call fires.
|
|
276
|
+
// Instead we buffer it and let renderToolResult emit one combined line
|
|
277
|
+
// "head → outcome · duration\n". A spinner shows what's running in the
|
|
278
|
+
// meantime so the user still has feedback during slow tools.
|
|
279
|
+
//
|
|
280
|
+
// If something else needs to print before the result arrives (a streamed
|
|
281
|
+
// content event, a sub-agent open, an error, completion), we flush the
|
|
282
|
+
// buffered head as a regular two-line shape first so the interleaving
|
|
283
|
+
// content lands below it.
|
|
284
|
+
let _pendingHead = null; // { callId, head, indent }
|
|
285
|
+
|
|
286
|
+
function flushPendingHead() {
|
|
287
|
+
if (!_pendingHead) return;
|
|
288
|
+
process.stderr.write(`\n${_pendingHead.head}\n`);
|
|
289
|
+
_pendingHead = null;
|
|
290
|
+
}
|
|
280
291
|
|
|
281
|
-
function clearPendingHead() {
|
|
292
|
+
function clearPendingHead() {
|
|
293
|
+
// Called by interleaving handlers — flush as 2-line shape (because we are
|
|
294
|
+
// about to print something else) and continue.
|
|
295
|
+
flushPendingHead();
|
|
296
|
+
}
|
|
282
297
|
|
|
283
298
|
function renderToolCall(data) {
|
|
284
299
|
const tool = data?.tool || 'unknown';
|
|
@@ -286,6 +301,10 @@ function renderToolCall(data) {
|
|
|
286
301
|
const indent = subAgentIndent();
|
|
287
302
|
const callId = data?.call_id || data?._callId || `${tool}:${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
288
303
|
|
|
304
|
+
// If a previous head is still pending (no result yet), flush it as a
|
|
305
|
+
// regular two-line shape before starting the next one.
|
|
306
|
+
flushPendingHead();
|
|
307
|
+
|
|
289
308
|
const head = formatCardHead(tool, args, {
|
|
290
309
|
cwd: safeCwd(),
|
|
291
310
|
columns: process.stderr.columns || 120,
|
|
@@ -294,8 +313,9 @@ function renderToolCall(data) {
|
|
|
294
313
|
|
|
295
314
|
recordCard({ id: callId, tool, args, head, startedAt: Date.now() });
|
|
296
315
|
session.toolCounts[tool] = (session.toolCounts[tool] || 0) + 1;
|
|
297
|
-
|
|
298
|
-
|
|
316
|
+
_pendingHead = { callId, head, indent };
|
|
317
|
+
// Spinner shows what's running until the result arrives.
|
|
318
|
+
startSpinner(`${tool}…`);
|
|
299
319
|
}
|
|
300
320
|
|
|
301
321
|
/**
|
|
@@ -314,7 +334,9 @@ function renderToolResult(data, eventType = 'tool_result') {
|
|
|
314
334
|
const indent = subAgentIndent();
|
|
315
335
|
const gutter = `${indent}${paint.text.dim('⎿')} `;
|
|
316
336
|
const callId = data.call_id || data._callId;
|
|
317
|
-
|
|
337
|
+
// Either tool_result or tool_done is allowed to render — whichever wins
|
|
338
|
+
// the race. Subsequent events for the same callId are duplicates.
|
|
339
|
+
if (callId && _renderedToolResults.has(callId)) return;
|
|
318
340
|
if (callId) _renderedToolResults.add(callId);
|
|
319
341
|
|
|
320
342
|
const tool = data.tool || data._tool || '';
|
|
@@ -326,35 +348,40 @@ function renderToolResult(data, eventType = 'tool_result') {
|
|
|
326
348
|
if (data._blocked) session.blockedOps++;
|
|
327
349
|
|
|
328
350
|
const { text, tone: t } = summarizeResult(tool, data);
|
|
329
|
-
|
|
351
|
+
// Em dash reads more like prose than a system arrow.
|
|
352
|
+
const arrow = paint.text.dim('—');
|
|
330
353
|
const painter = t === 'success' ? paint.state.success
|
|
331
354
|
: t === 'warn' ? paint.state.warn
|
|
332
355
|
: t === 'danger' ? paint.state.danger
|
|
333
356
|
: paint.text.dim;
|
|
334
|
-
|
|
357
|
+
// Skip the duration tail when the tool was effectively instant (<200ms) —
|
|
358
|
+
// "1ms" / "0ms" was noise that hurt the prose feel.
|
|
359
|
+
const duration = (durationMs != null && durationMs < 200) ? '' : formatToolDuration(data);
|
|
335
360
|
const tail = duration ? paint.text.dim(` · ${duration}`) : '';
|
|
336
361
|
const outcome = `${arrow} ${painter(text || 'done')}${tail}`;
|
|
337
|
-
|
|
338
|
-
// ── Single-line collapse ──
|
|
339
|
-
// If nothing has interleaved between renderToolCall and this result, rewrite
|
|
340
|
-
// the head line in-place as "<head> → outcome · duration" — saves a full
|
|
341
|
-
// row per tool call. Falls back to the two-line gutter form when the head
|
|
342
|
-
// is gone (something scrolled it away) or the combined line would not fit.
|
|
343
362
|
const hasLint = (tool === 'write_file' || tool === 'edit_file') && data.lint;
|
|
363
|
+
|
|
364
|
+
// ── Single-line combined emit ──
|
|
365
|
+
// If the head for this call is still buffered (no interleaving content
|
|
366
|
+
// landed), and the combined line fits the terminal width, emit ONE line
|
|
367
|
+
// and skip the gutter entirely.
|
|
344
368
|
if (_pendingHead && _pendingHead.callId === callId && !hasLint) {
|
|
345
369
|
const cols = process.stderr.columns || 120;
|
|
346
370
|
const combined = `${_pendingHead.head} ${outcome}`;
|
|
347
371
|
if (stripAnsi(combined).length <= cols) {
|
|
348
|
-
|
|
349
|
-
// because the cursor is already at the start of the (now-cleared) line.
|
|
350
|
-
process.stderr.write(`\x1b[1A\x1b[2K\r${combined}\n`);
|
|
372
|
+
process.stderr.write(`\n${combined}\n`);
|
|
351
373
|
_pendingHead = null;
|
|
352
374
|
return;
|
|
353
375
|
}
|
|
376
|
+
// Combined too wide — flush the head as 2-line and fall through.
|
|
377
|
+
flushPendingHead();
|
|
378
|
+
} else if (_pendingHead) {
|
|
379
|
+
// Stale pending head (different callId) — flush it before printing this
|
|
380
|
+
// result's gutter line below.
|
|
381
|
+
flushPendingHead();
|
|
354
382
|
}
|
|
355
|
-
_pendingHead = null;
|
|
356
383
|
|
|
357
|
-
//
|
|
384
|
+
// Two-line shape: gutter under the (already-printed or just-flushed) head.
|
|
358
385
|
process.stderr.write(`${gutter}${outcome}\n`);
|
|
359
386
|
|
|
360
387
|
// Lint warnings stay visible alongside writes.
|
|
@@ -474,6 +501,9 @@ function flushContent() {
|
|
|
474
501
|
if (!_streamBuffer) return;
|
|
475
502
|
|
|
476
503
|
stopSpinner();
|
|
504
|
+
// Any buffered tool head needs to land BEFORE this content so the order
|
|
505
|
+
// is preserved on screen.
|
|
506
|
+
flushPendingHead();
|
|
477
507
|
const rendered = renderMarkdown(_streamBuffer);
|
|
478
508
|
for (const line of rendered.split('\n')) {
|
|
479
509
|
process.stdout.write(` ${line}\n`);
|
|
@@ -503,6 +533,15 @@ function renderEvent(event) {
|
|
|
503
533
|
case 'thinking': {
|
|
504
534
|
const text = data?.message || data?.text || '';
|
|
505
535
|
if (text && !text.startsWith('Processing')) {
|
|
536
|
+
// Surface substantive thinking text as visible prose so the user can
|
|
537
|
+
// follow the agent's reasoning, not just see a spinner blip. We
|
|
538
|
+
// print at most one line per distinct thought, dim italic.
|
|
539
|
+
if (text.length > 12 && text !== session._lastEmittedThinking) {
|
|
540
|
+
flushPendingHead();
|
|
541
|
+
stopSpinner();
|
|
542
|
+
process.stderr.write(` ${c.italic(c.dim(text.slice(0, 200)))}\n`);
|
|
543
|
+
session._lastEmittedThinking = text;
|
|
544
|
+
}
|
|
506
545
|
startSpinner(text.slice(0, 80));
|
|
507
546
|
// Capture reasoning so /why can replay it.
|
|
508
547
|
session.lastReasoning = text;
|
|
@@ -831,6 +870,7 @@ function renderEvent(event) {
|
|
|
831
870
|
|
|
832
871
|
case 'paused':
|
|
833
872
|
stopSpinner();
|
|
873
|
+
flushPendingHead();
|
|
834
874
|
process.stderr.write(` ${c.yellow('⏸')} Paused${data?.reason ? ' ' + c.dim(data.reason) : ''}\n`);
|
|
835
875
|
break;
|
|
836
876
|
|
|
@@ -1561,6 +1601,7 @@ export async function startTerminalRepl() {
|
|
|
1561
1601
|
session.toolCounts = {};
|
|
1562
1602
|
session.subAgentCounts = {};
|
|
1563
1603
|
session.savedUsd = 0;
|
|
1604
|
+
session._lastEmittedThinking = '';
|
|
1564
1605
|
|
|
1565
1606
|
// Tell the orbit a new turn started — switches to DISCOVERY and updates
|
|
1566
1607
|
// task / turn counters in the status bar.
|
|
@@ -1,30 +1,33 @@
|
|
|
1
|
+
// Present-progressive verbs — read more conversationally than "Read file":
|
|
2
|
+
// "Reading auth.py — 47 lines" reads like the agent narrating, not a log.
|
|
1
3
|
const TOOL_LABELS = Object.freeze({
|
|
2
|
-
shell: '
|
|
3
|
-
read_file: '
|
|
4
|
-
read_files: '
|
|
5
|
-
write_file: '
|
|
6
|
-
write_project: '
|
|
7
|
-
edit_file: '
|
|
8
|
-
delete_file: '
|
|
9
|
-
list_files: '
|
|
10
|
-
search_code: '
|
|
11
|
-
search_files: '
|
|
12
|
-
grep: '
|
|
13
|
-
get_file_info: '
|
|
14
|
-
validate_file: '
|
|
15
|
-
validate_build: '
|
|
16
|
-
validate_structure: '
|
|
17
|
-
lint_check: '
|
|
18
|
-
run_tests: '
|
|
19
|
-
git_diff: '
|
|
20
|
-
git_status: '
|
|
21
|
-
analyze_code: '
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
4
|
+
shell: 'Running',
|
|
5
|
+
read_file: 'Reading',
|
|
6
|
+
read_files: 'Reading',
|
|
7
|
+
write_file: 'Writing',
|
|
8
|
+
write_project: 'Writing files',
|
|
9
|
+
edit_file: 'Editing',
|
|
10
|
+
delete_file: 'Deleting',
|
|
11
|
+
list_files: 'Listing',
|
|
12
|
+
search_code: 'Searching',
|
|
13
|
+
search_files: 'Searching files',
|
|
14
|
+
grep: 'Searching for',
|
|
15
|
+
get_file_info: 'Inspecting',
|
|
16
|
+
validate_file: 'Validating',
|
|
17
|
+
validate_build: 'Validating build',
|
|
18
|
+
validate_structure: 'Checking structure',
|
|
19
|
+
lint_check: 'Linting',
|
|
20
|
+
run_tests: 'Running tests',
|
|
21
|
+
git_diff: 'Reviewing changes',
|
|
22
|
+
git_status: 'Checking git',
|
|
23
|
+
analyze_code: 'Analyzing',
|
|
24
|
+
get_project_overview: 'Indexing project',
|
|
25
|
+
explore: 'Exploring',
|
|
26
|
+
plan: 'Planning',
|
|
27
|
+
verify: 'Verifying',
|
|
28
|
+
debug: 'Debugging',
|
|
29
|
+
refactor: 'Refactoring',
|
|
30
|
+
ask_user: 'Asking',
|
|
28
31
|
});
|
|
29
32
|
|
|
30
33
|
export function toolDisplayLabel(tool) {
|
package/src/ui/tool-card.mjs
CHANGED
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
*/
|
|
23
23
|
|
|
24
24
|
import { paint, width as visibleWidth } from './palette.mjs';
|
|
25
|
-
import {
|
|
25
|
+
import { toolFamily } from './icons.mjs';
|
|
26
26
|
import { term } from './term.mjs';
|
|
27
27
|
import {
|
|
28
28
|
toolDisplayLabel,
|
|
@@ -203,7 +203,15 @@ function tone(text, t) {
|
|
|
203
203
|
// ── Card head (printed at invocation) ────────────────────────────────────
|
|
204
204
|
|
|
205
205
|
/**
|
|
206
|
-
* Render the leading half of a card —
|
|
206
|
+
* Render the leading half of a card — colored verb + args.
|
|
207
|
+
*
|
|
208
|
+
* v2.0.3: dropped the leading tool icon (🔭/🛠️/⚙️). The label itself is a
|
|
209
|
+
* present-progressive verb so the line reads like prose:
|
|
210
|
+
* "Reading src/ui/banner.mjs · lines 31-65 — 36 lines"
|
|
211
|
+
* The icon was decorative noise that broke the conversational feel.
|
|
212
|
+
* Mission report and sub-agent renderers still use the icons in their
|
|
213
|
+
* own contexts.
|
|
214
|
+
*
|
|
207
215
|
* Width-aware: truncates args from the left when the line would overflow.
|
|
208
216
|
*/
|
|
209
217
|
export function formatCardHead(tool, args, opts = {}) {
|
|
@@ -211,15 +219,14 @@ export function formatCardHead(tool, args, opts = {}) {
|
|
|
211
219
|
const cols = opts.columns || term().columns || 120;
|
|
212
220
|
const indent = opts.indent || ' ';
|
|
213
221
|
|
|
214
|
-
const iconText = icon(tool);
|
|
215
222
|
const label = toolDisplayLabel(tool);
|
|
216
223
|
const argsText = formatArgs(tool, args, cwd);
|
|
217
224
|
|
|
218
|
-
const leadVisible = visibleWidth(`${indent}${
|
|
225
|
+
const leadVisible = visibleWidth(`${indent}${label}`);
|
|
219
226
|
const budget = Math.max(20, cols - leadVisible - 4);
|
|
220
227
|
const argsTruncated = truncateMiddle(argsText, budget);
|
|
221
228
|
|
|
222
|
-
const head = `${indent}${
|
|
229
|
+
const head = `${indent}${paintLabel(tool, label)}`;
|
|
223
230
|
return argsTruncated ? `${head} ${argsTruncated}` : head;
|
|
224
231
|
}
|
|
225
232
|
|
|
@@ -240,9 +247,12 @@ export function formatCard({ tool, args, result, durationMs, indent, columns, cw
|
|
|
240
247
|
|
|
241
248
|
if (!summary.text && !duration) return head;
|
|
242
249
|
|
|
243
|
-
const arrow = paint.text.dim('
|
|
250
|
+
const arrow = paint.text.dim('—');
|
|
244
251
|
const body = summary.text ? tone(summary.text, summary.tone) : '';
|
|
245
|
-
|
|
252
|
+
// Hide the duration tail when the tool was effectively instant (<200ms).
|
|
253
|
+
// For fast reads, "1ms" / "0ms" was noise that broke the prose feel.
|
|
254
|
+
const showDuration = duration && (durationMs == null || durationMs >= 200);
|
|
255
|
+
const tail = showDuration ? paint.text.dim(` · ${duration}`) : '';
|
|
246
256
|
|
|
247
257
|
const candidate = `${head} ${arrow} ${body}${tail}`;
|
|
248
258
|
if (visibleWidth(candidate) <= cols) return candidate;
|