@axplusb/kepler 2.0.2 → 2.0.3

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@axplusb/kepler",
3
- "version": "2.0.2",
3
+ "version": "2.0.3",
4
4
  "description": "Kepler — AI coding agent with operating brief, preflight planning, and sub-agents. SWE-bench Lite evaluated.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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
- // Set by renderToolCall, consumed by renderToolResult so we can collapse the
276
- // "head\n ⎿ outcome\n" two-line shape into a single line whenever nothing
277
- // else printed in between. Cleared by any handler that writes interleaving
278
- // content (content/thinking/sub_agent_*/delegation/etc).
279
- let _pendingHead = null; // { callId, head }
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() { _pendingHead = null; }
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
- process.stderr.write(`\n${head}\n`);
298
- _pendingHead = { callId, head };
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
- if (eventType === 'tool_done' && callId && _renderedToolResults.has(callId)) return;
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
- const arrow = paint.text.dim('→');
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
- const duration = formatToolDuration(data);
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
- // Move up one line, clear it, rewrite as one line. No leading newline
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
- // Default two-line shape.
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: 'Run command',
3
- read_file: 'Read file',
4
- read_files: 'Read files',
5
- write_file: 'Create file',
6
- write_project: 'Create project files',
7
- edit_file: 'Edit file',
8
- delete_file: 'Delete file',
9
- list_files: 'List files',
10
- search_code: 'Search code',
11
- search_files: 'Search files',
12
- grep: 'Search text',
13
- get_file_info: 'Inspect file',
14
- validate_file: 'Validate file',
15
- validate_build: 'Validate build',
16
- validate_structure: 'Check project structure',
17
- lint_check: 'Check code quality',
18
- run_tests: 'Run tests',
19
- git_diff: 'Review changes',
20
- git_status: 'Check repository status',
21
- analyze_code: 'Analyze code',
22
- explore: 'Explore codebase',
23
- plan: 'Create implementation plan',
24
- verify: 'Verify implementation',
25
- debug: 'Debug issue',
26
- refactor: 'Refactor code',
27
- ask_user: 'Ask for input',
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) {
@@ -22,7 +22,7 @@
22
22
  */
23
23
 
24
24
  import { paint, width as visibleWidth } from './palette.mjs';
25
- import { icon, toolFamily } from './icons.mjs';
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 — icon + colored label + args.
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}${iconText} ${label}`);
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}${iconText} ${paintLabel(tool, label)}`;
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
- const tail = duration ? paint.text.dim(` · ${duration}`) : '';
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;