@axplusb/kepler 2.0.0 → 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 +1 -1
- package/pulse/app/api/benchmark/route.ts +113 -0
- package/pulse/app/api/benchmarks/route.ts +195 -0
- package/pulse/app/benchmarks/page.tsx +224 -0
- package/pulse/components/layout/bottom-nav.tsx +2 -1
- package/pulse/components/layout/sidebar.tsx +2 -1
- package/src/core/risk-tier.mjs +8 -2
- package/src/core/stream-client.mjs +24 -1
- package/src/core/tool-executor.mjs +9 -2
- package/src/onboarding/preflight.mjs +51 -33
- package/src/terminal/repl.mjs +156 -48
- package/src/terminal/tool-display.mjs +29 -26
- package/src/tools/project-overview.mjs +109 -16
- package/src/ui/tool-card.mjs +27 -9
|
@@ -37,31 +37,44 @@ const FAIL = (s) => `${paint.state.danger('[✗]')} ${s}`;
|
|
|
37
37
|
|
|
38
38
|
// ── Individual checks (each returns { status, label, hint? }) ──────────
|
|
39
39
|
|
|
40
|
-
function
|
|
40
|
+
async function checkAuthAndBackend(auth, { timeoutMs = 2500 } = {}) {
|
|
41
41
|
const creds = auth.loadCredentials();
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
}
|
|
42
|
+
const hasToken = !!creds.token;
|
|
43
|
+
const url = creds.backendUrl;
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
}
|
|
45
|
+
// No token: just probe whether the backend is reachable so we can hint
|
|
46
|
+
// /login when it makes sense.
|
|
47
|
+
if (!hasToken) {
|
|
48
|
+
const reachable = url ? await ping(url, timeoutMs).catch(() => false) : false;
|
|
49
|
+
return reachable
|
|
50
|
+
? { status: 'warn', label: 'Not signed in · backend ready', hint: '/login to sign in' }
|
|
51
|
+
: { status: 'warn', label: 'Not signed in · backend offline', hint: '/login once the network is back' };
|
|
52
|
+
}
|
|
54
53
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
const url = creds.backendUrl;
|
|
58
|
-
if (!url) return { status: 'warn', label: 'Backend not configured' };
|
|
54
|
+
// Token present: real authenticated round-trip against /api/user/me.
|
|
55
|
+
// Three outcomes: valid (200), expired (401/403), unreachable (network).
|
|
59
56
|
try {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
|
|
57
|
+
const ctrl = new AbortController();
|
|
58
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
59
|
+
let resp;
|
|
60
|
+
try {
|
|
61
|
+
resp = await fetch(`${url}/api/user/me`, {
|
|
62
|
+
headers: { 'Authorization': `Bearer ${creds.token}` },
|
|
63
|
+
signal: ctrl.signal,
|
|
64
|
+
});
|
|
65
|
+
} finally { clearTimeout(t); }
|
|
66
|
+
|
|
67
|
+
if (resp.ok) {
|
|
68
|
+
const user = await resp.json().catch(() => null);
|
|
69
|
+
const who = user?.github_username || user?.email || 'user';
|
|
70
|
+
return { status: 'ok', label: `Signed in as ${who} · connected` };
|
|
71
|
+
}
|
|
72
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
73
|
+
return { status: 'warn', label: 'Token expired · connected', hint: '/login again to refresh' };
|
|
74
|
+
}
|
|
75
|
+
return { status: 'warn', label: `Backend returned ${resp.status}`, hint: 'try again shortly' };
|
|
63
76
|
} catch {
|
|
64
|
-
return { status: 'warn', label:
|
|
77
|
+
return { status: 'warn', label: 'Signed in · backend offline', hint: 'check network or try again shortly' };
|
|
65
78
|
}
|
|
66
79
|
}
|
|
67
80
|
|
|
@@ -83,25 +96,32 @@ function checkGit(cwd) {
|
|
|
83
96
|
function checkLinters(cwd) {
|
|
84
97
|
const present = [];
|
|
85
98
|
const missing = [];
|
|
86
|
-
for (const
|
|
87
|
-
if (which(
|
|
88
|
-
else if (projectUses(cwd, kind)) missing.push(
|
|
99
|
+
for (const linter of LINTERS) {
|
|
100
|
+
if (which(linter.bin)) present.push(linter);
|
|
101
|
+
else if (projectUses(cwd, linter.kind)) missing.push(linter);
|
|
89
102
|
}
|
|
90
103
|
if (present.length === 0 && missing.length === 0) {
|
|
91
104
|
return { status: 'ok', label: 'Linters none required' };
|
|
92
105
|
}
|
|
93
106
|
if (missing.length === 0) {
|
|
94
|
-
return { status: 'ok', label: `Linters ${present.map(p => p.
|
|
107
|
+
return { status: 'ok', label: `Linters ${present.map(p => p.bin).join(', ')}` };
|
|
95
108
|
}
|
|
96
|
-
|
|
97
|
-
|
|
109
|
+
// Honest install command per linter. Falls back to "install via your
|
|
110
|
+
// package manager" when there is no clean one-liner (e.g. cargo).
|
|
111
|
+
const hint = missing.map(m => m.install
|
|
112
|
+
? `${m.bin}: ${m.install}`
|
|
113
|
+
: `install ${m.bin} for ${m.kind} support`
|
|
114
|
+
).join(' · ');
|
|
115
|
+
return { status: 'warn', label: `Linter (${missing.map(m => m.bin).join(', ')}) not found`, hint };
|
|
98
116
|
}
|
|
99
117
|
|
|
100
118
|
const LINTERS = [
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
119
|
+
{ bin: 'ruff', kind: 'python', install: 'pip install ruff' },
|
|
120
|
+
{ bin: 'eslint', kind: 'javascript', install: 'npm i -g eslint' },
|
|
121
|
+
{ bin: 'tsc', kind: 'typescript', install: 'npm i -g typescript' },
|
|
122
|
+
// cargo ships with rustup; no clean one-liner — surface the warning
|
|
123
|
+
// without a misleading "/install" command.
|
|
124
|
+
{ bin: 'cargo', kind: 'rust', install: null },
|
|
105
125
|
];
|
|
106
126
|
|
|
107
127
|
function projectUses(cwd, kind) {
|
|
@@ -252,9 +272,7 @@ export async function runPreflight({ auth, cwd, version, silent = false } = {})
|
|
|
252
272
|
write('\n' + header + '\n\n');
|
|
253
273
|
|
|
254
274
|
const checks = [];
|
|
255
|
-
checks.push(
|
|
256
|
-
checks.push(checkProviderKey(auth));
|
|
257
|
-
checks.push(await checkBackend(auth));
|
|
275
|
+
checks.push(await checkAuthAndBackend(auth));
|
|
258
276
|
checks.push(checkGit(cwd));
|
|
259
277
|
checks.push(checkLinters(cwd));
|
|
260
278
|
checks.push(checkProjectMap(cwd));
|
package/src/terminal/repl.mjs
CHANGED
|
@@ -24,6 +24,7 @@ import { JsonlWriter } from '../core/jsonl-writer.mjs';
|
|
|
24
24
|
import { createToolExecutor } from '../core/tool-executor.mjs';
|
|
25
25
|
import { CheckpointManager } from '../core/checkpoints.mjs';
|
|
26
26
|
import { runPreflight } from '../onboarding/preflight.mjs';
|
|
27
|
+
import { printBanner as printBrandedBanner } from '../ui/banner.mjs';
|
|
27
28
|
import { renderMissionReport, saveReport, toMarkdown as missionMarkdown } from '../ui/mission-report.mjs';
|
|
28
29
|
import {
|
|
29
30
|
getVerbosity,
|
|
@@ -121,6 +122,7 @@ const session = {
|
|
|
121
122
|
costBreakdown: [], // per-model usage: [{ model, role, input_tokens, output_tokens, cost }]
|
|
122
123
|
totalCost: 0, // accumulated session cost (USD)
|
|
123
124
|
costAccurate: false, // true if backend provides per-model breakdown
|
|
125
|
+
isByok: false, // set from session_info; hides cost + credits when true
|
|
124
126
|
};
|
|
125
127
|
|
|
126
128
|
// ── Commands ──
|
|
@@ -165,26 +167,15 @@ const COMMANDS = {
|
|
|
165
167
|
// ── Banner ──
|
|
166
168
|
|
|
167
169
|
function printBanner(auth) {
|
|
170
|
+
// Delegate the visual block to the branded banner module (PRD-055 §4.3,
|
|
171
|
+
// gradient KEPLER letters in Deep Space Purple → Stellar Magenta → Neon
|
|
172
|
+
// Cyan). The trailing status line stays here because it needs `auth`.
|
|
173
|
+
printBrandedBanner();
|
|
174
|
+
|
|
168
175
|
const creds = auth.loadCredentials();
|
|
169
176
|
const env = process.env.TARANG_ENV || 'production';
|
|
170
177
|
const authStatus = creds.token ? c.green('authenticated') : c.red('/login to start');
|
|
171
|
-
|
|
172
|
-
const CYAN = '\x1b[36m';
|
|
173
|
-
const DIM = '\x1b[2m';
|
|
174
|
-
const BOLD = '\x1b[1m';
|
|
175
|
-
const YELLOW = '\x1b[33m';
|
|
176
|
-
const RST = '\x1b[0m';
|
|
177
|
-
|
|
178
|
-
process.stderr.write('\n');
|
|
179
|
-
process.stderr.write(`${DIM} ✦${RST}\n`);
|
|
180
|
-
process.stderr.write(`${DIM} ╭──────────────────────────╮${RST}\n`);
|
|
181
|
-
process.stderr.write(`${DIM} │${RST} ${BOLD}${CYAN}K · E · P · L · E · R${RST} ${DIM}│${RST}\n`);
|
|
182
|
-
process.stderr.write(`${DIM} ╰──────── ${YELLOW}◯${RST}${DIM} ───────────────╯${RST}\n`);
|
|
183
|
-
process.stderr.write(`${DIM} ╱ ╲${RST}\n`);
|
|
184
|
-
process.stderr.write(`${DIM} the agentic os${RST}\n`);
|
|
185
|
-
process.stderr.write('\n');
|
|
186
|
-
process.stderr.write(` ${c.gray('v' + VERSION)} ${c.dim(env)} ${authStatus}\n`);
|
|
187
|
-
process.stderr.write('\n');
|
|
178
|
+
process.stderr.write(` ${c.gray('v' + VERSION)} ${c.dim(env)} ${authStatus}\n\n`);
|
|
188
179
|
}
|
|
189
180
|
|
|
190
181
|
// ── Prompt Chrome ──
|
|
@@ -212,12 +203,12 @@ function printBanner(auth) {
|
|
|
212
203
|
*/
|
|
213
204
|
function buildContextStrip() {
|
|
214
205
|
const totalTokens = session.inputTokens + session.outputTokens;
|
|
215
|
-
const credits = formatCredits(costToCredits(session.totalCost));
|
|
216
206
|
const elapsed = formatElapsed(session.startTime);
|
|
217
207
|
|
|
208
|
+
// BYOK: user pays the provider directly, suppress credits entirely.
|
|
218
209
|
const right = [
|
|
219
210
|
c.dim(`${formatTokens(totalTokens)} tok`),
|
|
220
|
-
c.dim(
|
|
211
|
+
...(session.isByok ? [] : [c.dim(formatCredits(costToCredits(session.totalCost)))]),
|
|
221
212
|
c.dim(elapsed),
|
|
222
213
|
].join(c.dim(' · '));
|
|
223
214
|
|
|
@@ -264,7 +255,7 @@ function printTurnSummary(toolCount, durationS, turnCost) {
|
|
|
264
255
|
const parts = [];
|
|
265
256
|
if (toolCount > 0) parts.push(`${toolCount} tools`);
|
|
266
257
|
if (durationS) parts.push(`${Number(durationS).toFixed(1)}s`);
|
|
267
|
-
if (turnCost > 0) parts.push(formatCredits(costToCredits(turnCost)));
|
|
258
|
+
if (turnCost > 0 && !session.isByok) parts.push(formatCredits(costToCredits(turnCost)));
|
|
268
259
|
if (parts.length > 0) {
|
|
269
260
|
process.stderr.write(`\n ${c.green('✓')} ${c.dim(parts.join(' · '))}\n`);
|
|
270
261
|
}
|
|
@@ -281,12 +272,39 @@ function updateStatusBar() {
|
|
|
281
272
|
* args. The result arrives later via `renderToolResult` and is appended as a
|
|
282
273
|
* gutter line. Sub-agent calls are indented per session.inSubAgent.
|
|
283
274
|
*/
|
|
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
|
+
}
|
|
291
|
+
|
|
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
|
+
}
|
|
297
|
+
|
|
284
298
|
function renderToolCall(data) {
|
|
285
299
|
const tool = data?.tool || 'unknown';
|
|
286
300
|
const args = data?.args || {};
|
|
287
301
|
const indent = subAgentIndent();
|
|
288
302
|
const callId = data?.call_id || data?._callId || `${tool}:${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
289
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
|
+
|
|
290
308
|
const head = formatCardHead(tool, args, {
|
|
291
309
|
cwd: safeCwd(),
|
|
292
310
|
columns: process.stderr.columns || 120,
|
|
@@ -295,7 +313,9 @@ function renderToolCall(data) {
|
|
|
295
313
|
|
|
296
314
|
recordCard({ id: callId, tool, args, head, startedAt: Date.now() });
|
|
297
315
|
session.toolCounts[tool] = (session.toolCounts[tool] || 0) + 1;
|
|
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,17 +348,44 @@ 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}`;
|
|
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.
|
|
368
|
+
if (_pendingHead && _pendingHead.callId === callId && !hasLint) {
|
|
369
|
+
const cols = process.stderr.columns || 120;
|
|
370
|
+
const combined = `${_pendingHead.head} ${outcome}`;
|
|
371
|
+
if (stripAnsi(combined).length <= cols) {
|
|
372
|
+
process.stderr.write(`\n${combined}\n`);
|
|
373
|
+
_pendingHead = null;
|
|
374
|
+
return;
|
|
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();
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Two-line shape: gutter under the (already-printed or just-flushed) head.
|
|
385
|
+
process.stderr.write(`${gutter}${outcome}\n`);
|
|
337
386
|
|
|
338
387
|
// Lint warnings stay visible alongside writes.
|
|
339
|
-
if (
|
|
388
|
+
if (hasLint) {
|
|
340
389
|
process.stderr.write(`${gutter}${paint.state.warn('⚠ ' + String(data.lint).split('\n')[0].slice(0, 80))}\n`);
|
|
341
390
|
}
|
|
342
391
|
}
|
|
@@ -436,6 +485,9 @@ function startContentStream() {
|
|
|
436
485
|
|
|
437
486
|
function appendContent(text) {
|
|
438
487
|
if (!text) return;
|
|
488
|
+
// Any streamed content between renderToolCall and renderToolResult would
|
|
489
|
+
// scroll the head off "the line above", breaking the in-place collapse.
|
|
490
|
+
clearPendingHead();
|
|
439
491
|
_streamBuffer += text;
|
|
440
492
|
_streamedPartialText += text;
|
|
441
493
|
|
|
@@ -449,6 +501,9 @@ function flushContent() {
|
|
|
449
501
|
if (!_streamBuffer) return;
|
|
450
502
|
|
|
451
503
|
stopSpinner();
|
|
504
|
+
// Any buffered tool head needs to land BEFORE this content so the order
|
|
505
|
+
// is preserved on screen.
|
|
506
|
+
flushPendingHead();
|
|
452
507
|
const rendered = renderMarkdown(_streamBuffer);
|
|
453
508
|
for (const line of rendered.split('\n')) {
|
|
454
509
|
process.stdout.write(` ${line}\n`);
|
|
@@ -478,6 +533,15 @@ function renderEvent(event) {
|
|
|
478
533
|
case 'thinking': {
|
|
479
534
|
const text = data?.message || data?.text || '';
|
|
480
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
|
+
}
|
|
481
545
|
startSpinner(text.slice(0, 80));
|
|
482
546
|
// Capture reasoning so /why can replay it.
|
|
483
547
|
session.lastReasoning = text;
|
|
@@ -621,6 +685,7 @@ function renderEvent(event) {
|
|
|
621
685
|
|
|
622
686
|
case 'delegation': {
|
|
623
687
|
stopSpinner();
|
|
688
|
+
clearPendingHead();
|
|
624
689
|
const from = data?.from || '';
|
|
625
690
|
const to = data?.to || '';
|
|
626
691
|
session.delegations.push({ from, to, time: Date.now() });
|
|
@@ -636,6 +701,7 @@ function renderEvent(event) {
|
|
|
636
701
|
|
|
637
702
|
case 'sub_agent_start': {
|
|
638
703
|
stopSpinner();
|
|
704
|
+
clearPendingHead();
|
|
639
705
|
const agentType = data?.type || 'sub-agent';
|
|
640
706
|
const model = data?.model || '';
|
|
641
707
|
const query = data?.query || '';
|
|
@@ -657,6 +723,7 @@ function renderEvent(event) {
|
|
|
657
723
|
|
|
658
724
|
case 'sub_agent_complete': {
|
|
659
725
|
stopSpinner();
|
|
726
|
+
clearPendingHead();
|
|
660
727
|
const agentType = data?.type || 'sub-agent';
|
|
661
728
|
const usage = data?.usage || {};
|
|
662
729
|
const tokens = (usage.input_tokens || 0) + (usage.output_tokens || 0);
|
|
@@ -697,6 +764,9 @@ function renderEvent(event) {
|
|
|
697
764
|
}
|
|
698
765
|
if (data?.model) session.model = data.model;
|
|
699
766
|
if (data?.user) session.user = { ...session.user, ...data.user };
|
|
767
|
+
// BYOK users pay their model provider directly; the platform does not
|
|
768
|
+
// charge them credits. Hide cost + credits when this flag is set.
|
|
769
|
+
if (typeof data?.is_byok === 'boolean') session.isByok = data.is_byok;
|
|
700
770
|
break;
|
|
701
771
|
}
|
|
702
772
|
|
|
@@ -773,8 +843,9 @@ function renderEvent(event) {
|
|
|
773
843
|
success: successOverall,
|
|
774
844
|
filesChanged: session.filesChanged,
|
|
775
845
|
toolCounts: session.toolCounts,
|
|
776
|
-
subAgents: { ...session.subAgentCounts, savedUsd: session.savedUsd },
|
|
777
|
-
|
|
846
|
+
subAgents: { ...session.subAgentCounts, savedUsd: session.isByok ? 0 : session.savedUsd },
|
|
847
|
+
// BYOK users pay their provider directly; suppress cost in the report.
|
|
848
|
+
costUsd: session.isByok ? null : (turnCost || session.totalCost),
|
|
778
849
|
durationS: data?.duration_s,
|
|
779
850
|
testsPass: data?.tests_passed != null
|
|
780
851
|
? { passed: data.tests_passed, total: data.tests_total || data.tests_passed }
|
|
@@ -799,6 +870,7 @@ function renderEvent(event) {
|
|
|
799
870
|
|
|
800
871
|
case 'paused':
|
|
801
872
|
stopSpinner();
|
|
873
|
+
flushPendingHead();
|
|
802
874
|
process.stderr.write(` ${c.yellow('⏸')} Paused${data?.reason ? ' ' + c.dim(data.reason) : ''}\n`);
|
|
803
875
|
break;
|
|
804
876
|
|
|
@@ -873,7 +945,11 @@ async function handleCommand(input, ctx) {
|
|
|
873
945
|
process.stderr.write(` ${c.dim('Turns')} ${session.turns}\n`);
|
|
874
946
|
process.stderr.write(` ${c.dim('Tools')} ${session.totalToolCalls} total, ${session.toolCalls} last turn\n`);
|
|
875
947
|
process.stderr.write(` ${c.dim('Duration')} ${formatElapsed(session.startTime)}\n`);
|
|
876
|
-
|
|
948
|
+
if (session.isByok) {
|
|
949
|
+
process.stderr.write(` ${c.dim('Billing')} ${c.green('BYOK')} ${c.dim('(provider-billed)')}\n`);
|
|
950
|
+
} else {
|
|
951
|
+
process.stderr.write(` ${c.dim('Credits')} ${formatCredits(costToCredits(session.totalCost))}${session.costAccurate ? '' : c.dim(' (est)')}\n`);
|
|
952
|
+
}
|
|
877
953
|
process.stderr.write(` ${c.dim('CWD')} ${safeCwd()}\n`);
|
|
878
954
|
|
|
879
955
|
// Permissions
|
|
@@ -941,12 +1017,20 @@ async function handleCommand(input, ctx) {
|
|
|
941
1017
|
process.stderr.write(` ${c.gray('Turns:')} ${session.turns}\n`);
|
|
942
1018
|
process.stderr.write(` ${c.gray('Tools:')} ${session.toolCalls}\n`);
|
|
943
1019
|
process.stderr.write(` ${c.gray('Blocked:')} ${session.blockedOps}\n`);
|
|
944
|
-
|
|
1020
|
+
if (session.isByok) {
|
|
1021
|
+
process.stderr.write(` ${c.gray('Billing:')} ${c.green('BYOK')} ${c.dim('(provider-billed)')}\n`);
|
|
1022
|
+
} else {
|
|
1023
|
+
process.stderr.write(` ${c.gray('Credits:')} ${formatCredits(costToCredits(session.totalCost))}${session.costAccurate ? '' : c.dim(' (est)')}\n`);
|
|
1024
|
+
}
|
|
945
1025
|
process.stderr.write(` ${c.gray('Elapsed:')} ${formatElapsed(session.startTime)}\n\n`);
|
|
946
1026
|
return;
|
|
947
1027
|
}
|
|
948
1028
|
|
|
949
1029
|
case '/cost': {
|
|
1030
|
+
if (session.isByok) {
|
|
1031
|
+
process.stderr.write(`\n ${c.bold('Billing')} ${c.green('BYOK')} ${c.dim('— you pay your model provider directly. Kepler does not charge credits for BYOK usage.')}\n\n`);
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
950
1034
|
process.stderr.write(`\n ${c.bold('Session Credits')} ${c.brand(formatCredits(costToCredits(session.totalCost)))}`);
|
|
951
1035
|
if (!session.costAccurate) {
|
|
952
1036
|
process.stderr.write(` ${c.yellow('(estimated)')}`);
|
|
@@ -1065,8 +1149,8 @@ async function handleCommand(input, ctx) {
|
|
|
1065
1149
|
success: true,
|
|
1066
1150
|
filesChanged: session.filesChanged,
|
|
1067
1151
|
toolCounts: session.toolCounts,
|
|
1068
|
-
subAgents: { ...session.subAgentCounts, savedUsd: session.savedUsd },
|
|
1069
|
-
costUsd: session.totalCost,
|
|
1152
|
+
subAgents: { ...session.subAgentCounts, savedUsd: session.isByok ? 0 : session.savedUsd },
|
|
1153
|
+
costUsd: session.isByok ? null : session.totalCost,
|
|
1070
1154
|
durationS: (Date.now() - session.startTime) / 1000,
|
|
1071
1155
|
nextActions: ['/commit', '/pr', '/undo'],
|
|
1072
1156
|
};
|
|
@@ -1378,19 +1462,9 @@ export async function startTerminalRepl() {
|
|
|
1378
1462
|
|
|
1379
1463
|
const ctx = { auth, toolExecutor, approval, jsonlWriter, sessionMgr, checkpoints };
|
|
1380
1464
|
|
|
1381
|
-
// ──
|
|
1382
|
-
//
|
|
1383
|
-
//
|
|
1384
|
-
// env opt-out is useful for debugging escape-sequence noise.
|
|
1385
|
-
const statusBarEnabled = process.env.KEPLER_STATUS_BAR !== '0' && term().isTTY && !term().plain;
|
|
1386
|
-
if (statusBarEnabled) {
|
|
1387
|
-
_orbit = createOrbit();
|
|
1388
|
-
attachOrbit(_orbit);
|
|
1389
|
-
// Always unmount before exit so the terminal scroll region is restored.
|
|
1390
|
-
process.on('beforeExit', unmountStatusBar);
|
|
1391
|
-
process.on('exit', unmountStatusBar);
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1465
|
+
// ── Print banner + preflight + init BEFORE mounting the status bar ──
|
|
1466
|
+
// The status bar shrinks the scroll region; if it mounts first, the
|
|
1467
|
+
// banner scrolls off-screen before the user ever sees it.
|
|
1394
1468
|
printBanner(auth);
|
|
1395
1469
|
|
|
1396
1470
|
// Preflight diagnostic (PRD-055 §9). Non-blocking; opt-out via
|
|
@@ -1437,12 +1511,41 @@ export async function startTerminalRepl() {
|
|
|
1437
1511
|
|
|
1438
1512
|
process.stderr.write(`\n ${c.dim('Press')} ${c.brand('Enter')} ${c.dim('to start, or type a prompt below.')}\n`);
|
|
1439
1513
|
|
|
1440
|
-
|
|
1514
|
+
// Mission Control status bar is OPT-IN as of v2.0.1.
|
|
1515
|
+
// Set KEPLER_STATUS_BAR=1 (or KEPLER_MISSION=1) to enable the persistent
|
|
1516
|
+
// bottom-anchored ORBIT bar. Default off because the DECSTBM scroll
|
|
1517
|
+
// region was eating the prompt visibility on some terminals (issue
|
|
1518
|
+
// observed during v2.0.0 testing). The orbit state machine and tool
|
|
1519
|
+
// cards still work without the bar — the bar is just the rendering.
|
|
1520
|
+
const statusBarEnabled = (
|
|
1521
|
+
process.env.KEPLER_STATUS_BAR === '1' || process.env.KEPLER_MISSION === '1'
|
|
1522
|
+
) && term().isTTY && !term().plain;
|
|
1523
|
+
if (statusBarEnabled) {
|
|
1524
|
+
_orbit = createOrbit();
|
|
1525
|
+
attachOrbit(_orbit);
|
|
1526
|
+
process.on('beforeExit', unmountStatusBar);
|
|
1527
|
+
process.on('exit', unmountStatusBar);
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
// The prompt label is the USER speaking, not the agent. Use the signed-in
|
|
1531
|
+
// GitHub handle if known, otherwise fall back to "You".
|
|
1532
|
+
//
|
|
1533
|
+
// readline counts every byte of the prompt as a visible column when it
|
|
1534
|
+
// computes cursor position for line-wrapping; ANSI color codes throw the
|
|
1535
|
+
// math off and produce duplicated text on wrap. Wrap each escape sequence
|
|
1536
|
+
// in SOH (\x01) ... STX (\x02) so readline skips it when measuring width.
|
|
1537
|
+
function rlSafe(s) {
|
|
1538
|
+
return String(s || '').replace(/\x1b\[[0-9;]*m/g, '\x01$&\x02');
|
|
1539
|
+
}
|
|
1540
|
+
function userPrompt() {
|
|
1541
|
+
const who = session.user?.github_username || session.user?.email?.split('@')[0] || 'You';
|
|
1542
|
+
return rlSafe(`${c.brand(who)} ${c.dim('›')} `);
|
|
1543
|
+
}
|
|
1441
1544
|
|
|
1442
1545
|
const rl = readline.createInterface({
|
|
1443
1546
|
input: process.stdin,
|
|
1444
1547
|
output: process.stderr,
|
|
1445
|
-
prompt:
|
|
1548
|
+
prompt: userPrompt(),
|
|
1446
1549
|
completer: (line) => {
|
|
1447
1550
|
if (line.startsWith('/')) {
|
|
1448
1551
|
const hits = Object.keys(COMMANDS).filter(cmd => cmd.startsWith(line));
|
|
@@ -1461,6 +1564,7 @@ export async function startTerminalRepl() {
|
|
|
1461
1564
|
function showPrompt() {
|
|
1462
1565
|
printPromptBlock();
|
|
1463
1566
|
process.stderr.write('\n'); // half-inch vertical gap above input line
|
|
1567
|
+
rl.setPrompt(userPrompt()); // refresh label in case session.user resolved
|
|
1464
1568
|
rl.prompt();
|
|
1465
1569
|
}
|
|
1466
1570
|
|
|
@@ -1497,6 +1601,7 @@ export async function startTerminalRepl() {
|
|
|
1497
1601
|
session.toolCounts = {};
|
|
1498
1602
|
session.subAgentCounts = {};
|
|
1499
1603
|
session.savedUsd = 0;
|
|
1604
|
+
session._lastEmittedThinking = '';
|
|
1500
1605
|
|
|
1501
1606
|
// Tell the orbit a new turn started — switches to DISCOVERY and updates
|
|
1502
1607
|
// task / turn counters in the status bar.
|
|
@@ -1555,7 +1660,10 @@ export async function startTerminalRepl() {
|
|
|
1555
1660
|
// Esc key (single byte 0x1b, not part of arrow sequence)
|
|
1556
1661
|
if (bytes.length === 1 && bytes[0] === 0x1b) {
|
|
1557
1662
|
stopSpinner();
|
|
1558
|
-
process.stderr.write(`\n ${c.yellow('⏹')} ${c.dim('
|
|
1663
|
+
process.stderr.write(`\n ${c.yellow('⏹')} ${c.dim('Cancelled.')}\n`);
|
|
1664
|
+
// cancel() now aborts the in-flight SSE reader; the for-await loop
|
|
1665
|
+
// wakes up immediately and the prompt returns. No more "stuck"
|
|
1666
|
+
// Cancelling… message.
|
|
1559
1667
|
client.cancel();
|
|
1560
1668
|
return;
|
|
1561
1669
|
}
|
|
@@ -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) {
|