@axplusb/kepler 1.0.10 → 2.0.2
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 +5 -2
- 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/context/retriever.mjs +42 -4
- package/src/context/symbol-indexer.mjs +375 -0
- package/src/core/approval.mjs +154 -95
- package/src/core/backend-url.mjs +2 -2
- package/src/core/headless.mjs +5 -0
- package/src/core/risk-tier.mjs +245 -0
- package/src/core/stream-client.mjs +24 -1
- package/src/core/tool-executor.mjs +58 -5
- package/src/onboarding/preflight.mjs +292 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -0
- package/src/terminal/ansi.mjs +44 -22
- package/src/terminal/repl.mjs +487 -133
- package/src/tools/project-overview.mjs +109 -16
- package/src/ui/approval.mjs +167 -0
- package/src/ui/banner.mjs +133 -122
- package/src/ui/dock.mjs +88 -0
- package/src/ui/icons.mjs +164 -0
- package/src/ui/mission-report.mjs +264 -0
- package/src/ui/palette.mjs +189 -0
- package/src/ui/spinner.mjs +116 -0
- package/src/ui/status-bar.mjs +275 -0
- package/src/ui/sub-agent.mjs +152 -0
- package/src/ui/term.mjs +159 -0
- package/src/ui/tool-card.mjs +322 -0
- package/src/ui/tool-details.mjs +277 -0
package/src/terminal/repl.mjs
CHANGED
|
@@ -16,13 +16,22 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import * as readline from 'node:readline';
|
|
19
|
-
import * as path from 'node:path';
|
|
20
19
|
import * as fs from 'node:fs';
|
|
21
20
|
import { c, progressBar, spinner, inPlace, renderMarkdown, renderDiff, formatElapsed, formatCost, stripAnsi } from './ansi.mjs';
|
|
22
21
|
import { calculateCost, formatCostValue, formatTokens, costToCredits, formatCredits } from '../core/pricing.mjs';
|
|
23
22
|
import { TarangStreamClient, EVENT_TYPES } from '../core/stream-client.mjs';
|
|
24
23
|
import { JsonlWriter } from '../core/jsonl-writer.mjs';
|
|
25
24
|
import { createToolExecutor } from '../core/tool-executor.mjs';
|
|
25
|
+
import { CheckpointManager } from '../core/checkpoints.mjs';
|
|
26
|
+
import { runPreflight } from '../onboarding/preflight.mjs';
|
|
27
|
+
import { printBanner as printBrandedBanner } from '../ui/banner.mjs';
|
|
28
|
+
import { renderMissionReport, saveReport, toMarkdown as missionMarkdown } from '../ui/mission-report.mjs';
|
|
29
|
+
import {
|
|
30
|
+
getVerbosity,
|
|
31
|
+
setVerbosity,
|
|
32
|
+
label as verbosityLabel,
|
|
33
|
+
MODES as V_MODES,
|
|
34
|
+
} from '../state/verbosity.mjs';
|
|
26
35
|
import { persistProjectArtifacts } from '../core/project-artifacts.mjs';
|
|
27
36
|
import { TarangAuth } from '../auth/tarang-auth.mjs';
|
|
28
37
|
import { ApprovalManager } from '../core/approval.mjs';
|
|
@@ -30,7 +39,27 @@ import { resolveBackendUrl } from '../core/backend-url.mjs';
|
|
|
30
39
|
import { BUILTIN_AGENTS, runAgent } from './agents.mjs';
|
|
31
40
|
import { SessionManager } from '../core/session-manager.mjs';
|
|
32
41
|
import { parseArgs } from '../config/cli-args.mjs';
|
|
33
|
-
import {
|
|
42
|
+
import { toolDisplayLabel } from './tool-display.mjs';
|
|
43
|
+
import { createOrbit } from '../state/orbit.mjs';
|
|
44
|
+
import { attachOrbit, unmount as unmountStatusBar } from '../ui/status-bar.mjs';
|
|
45
|
+
import { term } from '../ui/term.mjs';
|
|
46
|
+
import {
|
|
47
|
+
formatCardHead,
|
|
48
|
+
summarizeResult,
|
|
49
|
+
recordCard,
|
|
50
|
+
lastCard,
|
|
51
|
+
getCard,
|
|
52
|
+
allCards,
|
|
53
|
+
} from '../ui/tool-card.mjs';
|
|
54
|
+
import { detailFor } from '../ui/tool-details.mjs';
|
|
55
|
+
import { paint } from '../ui/palette.mjs';
|
|
56
|
+
import {
|
|
57
|
+
renderSubAgentOpen,
|
|
58
|
+
renderSubAgentClose,
|
|
59
|
+
subAgentIndent,
|
|
60
|
+
inSubAgent as inSubAgentBlock,
|
|
61
|
+
resetSubAgents,
|
|
62
|
+
} from '../ui/sub-agent.mjs';
|
|
34
63
|
|
|
35
64
|
import { createRequire } from 'node:module';
|
|
36
65
|
const __require = createRequire(import.meta.url);
|
|
@@ -63,6 +92,7 @@ function safeCwd() {
|
|
|
63
92
|
// ── Session State ──
|
|
64
93
|
|
|
65
94
|
let _sessionMgr = null; // Set in startTerminalRepl, used by renderEvent
|
|
95
|
+
let _orbit = null; // Mission Control orbit state machine; set in startTerminalRepl
|
|
66
96
|
|
|
67
97
|
const session = {
|
|
68
98
|
id: null, // set by backend on first turn via session_info event
|
|
@@ -82,9 +112,17 @@ const session = {
|
|
|
82
112
|
inSubAgent: false, // true while a sub-agent is running (for indented tool display)
|
|
83
113
|
filesChanged: [], // files modified this session
|
|
84
114
|
lastTurnDuration: 0,
|
|
115
|
+
toolCounts: {}, // per-tool histogram (mission report)
|
|
116
|
+
subAgentCounts: {}, // per-sub-agent histogram (mission report)
|
|
117
|
+
savedUsd: 0, // total sub-agent cost (for "saved by routing")
|
|
118
|
+
lastTask: '', // most recent user prompt (mission report title)
|
|
119
|
+
lastReasoning: '', // captured from agent for /why
|
|
120
|
+
budgetUsd: null, // /budget cap, null = unlimited
|
|
121
|
+
budgetExceeded: false,
|
|
85
122
|
costBreakdown: [], // per-model usage: [{ model, role, input_tokens, output_tokens, cost }]
|
|
86
123
|
totalCost: 0, // accumulated session cost (USD)
|
|
87
124
|
costAccurate: false, // true if backend provides per-model breakdown
|
|
125
|
+
isByok: false, // set from session_info; hides cost + credits when true
|
|
88
126
|
};
|
|
89
127
|
|
|
90
128
|
// ── Commands ──
|
|
@@ -100,6 +138,19 @@ const COMMANDS = {
|
|
|
100
138
|
'/diff': 'Git diff',
|
|
101
139
|
'/cost': 'Show session cost',
|
|
102
140
|
'/history': 'Show conversation',
|
|
141
|
+
'/last': 'Expand last tool output',
|
|
142
|
+
'/expand': 'Expand tool output by index (or "all")',
|
|
143
|
+
'/fold': 'Hide previously expanded tool output',
|
|
144
|
+
'/checkpoint':'List recent file checkpoints',
|
|
145
|
+
'/undo': 'Restore the last file checkpoint',
|
|
146
|
+
'/preflight':'Re-run the onboarding diagnostic',
|
|
147
|
+
'/report': 'Save the mission report as markdown',
|
|
148
|
+
'/why': 'Print the agent reasoning for the last decision',
|
|
149
|
+
'/map': 'Show the registered project tree',
|
|
150
|
+
'/budget': 'Set / clear a hard session cost cap',
|
|
151
|
+
'/quiet': 'Verbosity: hide sub-agent inner tools',
|
|
152
|
+
'/verbose': 'Verbosity: show sub-agent inner tools',
|
|
153
|
+
'/surgical': 'Verbosity: show everything (reasoning, expanded tools)',
|
|
103
154
|
'/compact': 'Compact conversation context',
|
|
104
155
|
'/agents': 'List available agents',
|
|
105
156
|
'/explore': 'Code explorer agent',
|
|
@@ -116,26 +167,15 @@ const COMMANDS = {
|
|
|
116
167
|
// ── Banner ──
|
|
117
168
|
|
|
118
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
|
+
|
|
119
175
|
const creds = auth.loadCredentials();
|
|
120
176
|
const env = process.env.TARANG_ENV || 'production';
|
|
121
177
|
const authStatus = creds.token ? c.green('authenticated') : c.red('/login to start');
|
|
122
|
-
|
|
123
|
-
const CYAN = '\x1b[36m';
|
|
124
|
-
const DIM = '\x1b[2m';
|
|
125
|
-
const BOLD = '\x1b[1m';
|
|
126
|
-
const YELLOW = '\x1b[33m';
|
|
127
|
-
const RST = '\x1b[0m';
|
|
128
|
-
|
|
129
|
-
process.stderr.write('\n');
|
|
130
|
-
process.stderr.write(`${DIM} ✦${RST}\n`);
|
|
131
|
-
process.stderr.write(`${DIM} ╭──────────────────────────╮${RST}\n`);
|
|
132
|
-
process.stderr.write(`${DIM} │${RST} ${BOLD}${CYAN}K · E · P · L · E · R${RST} ${DIM}│${RST}\n`);
|
|
133
|
-
process.stderr.write(`${DIM} ╰──────── ${YELLOW}◯${RST}${DIM} ───────────────╯${RST}\n`);
|
|
134
|
-
process.stderr.write(`${DIM} ╱ ╲${RST}\n`);
|
|
135
|
-
process.stderr.write(`${DIM} the agentic os${RST}\n`);
|
|
136
|
-
process.stderr.write('\n');
|
|
137
|
-
process.stderr.write(` ${c.gray('v' + VERSION)} ${c.dim(env)} ${authStatus}\n`);
|
|
138
|
-
process.stderr.write('\n');
|
|
178
|
+
process.stderr.write(` ${c.gray('v' + VERSION)} ${c.dim(env)} ${authStatus}\n\n`);
|
|
139
179
|
}
|
|
140
180
|
|
|
141
181
|
// ── Prompt Chrome ──
|
|
@@ -163,12 +203,12 @@ function printBanner(auth) {
|
|
|
163
203
|
*/
|
|
164
204
|
function buildContextStrip() {
|
|
165
205
|
const totalTokens = session.inputTokens + session.outputTokens;
|
|
166
|
-
const credits = formatCredits(costToCredits(session.totalCost));
|
|
167
206
|
const elapsed = formatElapsed(session.startTime);
|
|
168
207
|
|
|
208
|
+
// BYOK: user pays the provider directly, suppress credits entirely.
|
|
169
209
|
const right = [
|
|
170
210
|
c.dim(`${formatTokens(totalTokens)} tok`),
|
|
171
|
-
c.dim(
|
|
211
|
+
...(session.isByok ? [] : [c.dim(formatCredits(costToCredits(session.totalCost)))]),
|
|
172
212
|
c.dim(elapsed),
|
|
173
213
|
].join(c.dim(' · '));
|
|
174
214
|
|
|
@@ -195,11 +235,27 @@ function printPromptBlock() {
|
|
|
195
235
|
* Print a turn summary after a response completes.
|
|
196
236
|
* Shows only when there's something meaningful to report.
|
|
197
237
|
*/
|
|
238
|
+
/**
|
|
239
|
+
* Pull blocker bullet points from the completion payload — used by the
|
|
240
|
+
* failure variant of the mission report.
|
|
241
|
+
*/
|
|
242
|
+
function extractBlockers(data) {
|
|
243
|
+
const out = [];
|
|
244
|
+
if (data?.error) out.push(String(data.error).slice(0, 160));
|
|
245
|
+
if (Array.isArray(data?.failed_tests)) {
|
|
246
|
+
for (const t of data.failed_tests.slice(0, 6)) {
|
|
247
|
+
if (typeof t === 'string') out.push(t);
|
|
248
|
+
else if (t?.name) out.push(`${t.name}${t.message ? ': ' + t.message : ''}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
|
|
198
254
|
function printTurnSummary(toolCount, durationS, turnCost) {
|
|
199
255
|
const parts = [];
|
|
200
256
|
if (toolCount > 0) parts.push(`${toolCount} tools`);
|
|
201
257
|
if (durationS) parts.push(`${Number(durationS).toFixed(1)}s`);
|
|
202
|
-
if (turnCost > 0) parts.push(formatCredits(costToCredits(turnCost)));
|
|
258
|
+
if (turnCost > 0 && !session.isByok) parts.push(formatCredits(costToCredits(turnCost)));
|
|
203
259
|
if (parts.length > 0) {
|
|
204
260
|
process.stderr.write(`\n ${c.green('✓')} ${c.dim(parts.join(' · '))}\n`);
|
|
205
261
|
}
|
|
@@ -212,31 +268,34 @@ function updateStatusBar() {
|
|
|
212
268
|
// ── Tool Display Renderer ──
|
|
213
269
|
|
|
214
270
|
/**
|
|
215
|
-
* Render a tool call
|
|
216
|
-
*
|
|
271
|
+
* Render a tool call as the head of a Mission Control card — icon + label +
|
|
272
|
+
* args. The result arrives later via `renderToolResult` and is appended as a
|
|
273
|
+
* gutter line. Sub-agent calls are indented per session.inSubAgent.
|
|
217
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 }
|
|
280
|
+
|
|
281
|
+
function clearPendingHead() { _pendingHead = null; }
|
|
282
|
+
|
|
218
283
|
function renderToolCall(data) {
|
|
219
284
|
const tool = data?.tool || 'unknown';
|
|
220
|
-
const label = toolDisplayLabel(tool);
|
|
221
285
|
const args = data?.args || {};
|
|
222
|
-
const indent =
|
|
286
|
+
const indent = subAgentIndent();
|
|
287
|
+
const callId = data?.call_id || data?._callId || `${tool}:${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
223
288
|
|
|
224
|
-
const
|
|
289
|
+
const head = formatCardHead(tool, args, {
|
|
290
|
+
cwd: safeCwd(),
|
|
291
|
+
columns: process.stderr.columns || 120,
|
|
292
|
+
indent,
|
|
293
|
+
});
|
|
225
294
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
let displaySummary = summary || '';
|
|
231
|
-
if (displaySummary.length > maxSummary) {
|
|
232
|
-
displaySummary = '...' + displaySummary.slice(-(maxSummary - 3));
|
|
233
|
-
}
|
|
234
|
-
const summaryStr = displaySummary
|
|
235
|
-
? c.gray('(') + (tool === 'shell'
|
|
236
|
-
? formatShellCommand(displaySummary, c)
|
|
237
|
-
: c.white(displaySummary)) + c.gray(')')
|
|
238
|
-
: '';
|
|
239
|
-
process.stderr.write(`\n${indent}${c.brand('⏺')} ${c.bold(label)}${summaryStr}\n`);
|
|
295
|
+
recordCard({ id: callId, tool, args, head, startedAt: Date.now() });
|
|
296
|
+
session.toolCounts[tool] = (session.toolCounts[tool] || 0) + 1;
|
|
297
|
+
process.stderr.write(`\n${head}\n`);
|
|
298
|
+
_pendingHead = { callId, head };
|
|
240
299
|
}
|
|
241
300
|
|
|
242
301
|
/**
|
|
@@ -250,77 +309,93 @@ function formatToolDuration(data) {
|
|
|
250
309
|
return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
251
310
|
}
|
|
252
311
|
|
|
253
|
-
function firstOutputLine(data) {
|
|
254
|
-
const output = data?.output_preview || data?.output || data?.message || '';
|
|
255
|
-
return String(output).split('\n').map(line => line.trim()).find(Boolean) || '';
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function fileTypeLabel(filePath) {
|
|
259
|
-
const ext = path.extname(filePath || '').toLowerCase();
|
|
260
|
-
if (ext === '.md' || ext === '.mdx') return 'Markdown';
|
|
261
|
-
if (ext === '.json' || ext === '.jsonl') return 'JSON';
|
|
262
|
-
if (ext === '.yaml' || ext === '.yml') return 'YAML';
|
|
263
|
-
if (ext === '.toml') return 'TOML';
|
|
264
|
-
if (ext === '.csv' || ext === '.tsv') return 'tabular data';
|
|
265
|
-
if (['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.py', '.go', '.rs', '.java', '.rb', '.php', '.swift'].includes(ext)) {
|
|
266
|
-
return 'source';
|
|
267
|
-
}
|
|
268
|
-
return ext ? `${ext.slice(1).toUpperCase()} file` : 'file';
|
|
269
|
-
}
|
|
270
|
-
|
|
271
312
|
function renderToolResult(data, eventType = 'tool_result') {
|
|
272
313
|
if (!data) return;
|
|
273
|
-
const indent =
|
|
274
|
-
const gutter = `${indent}${
|
|
314
|
+
const indent = subAgentIndent();
|
|
315
|
+
const gutter = `${indent}${paint.text.dim('⎿')} `;
|
|
275
316
|
const callId = data.call_id || data._callId;
|
|
276
317
|
if (eventType === 'tool_done' && callId && _renderedToolResults.has(callId)) return;
|
|
277
318
|
if (callId) _renderedToolResults.add(callId);
|
|
278
|
-
const duration = formatToolDuration(data);
|
|
279
|
-
const suffix = duration ? c.dim(` · ${duration}`) : '';
|
|
280
319
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
process.stderr.write(`${gutter}${c.red(firstOutputLine(data) || 'Blocked by safety guardrails')}${suffix}\n`);
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
320
|
+
const tool = data.tool || data._tool || '';
|
|
321
|
+
const durationMs = data?.duration_ms ?? (data?.duration_s != null ? data.duration_s * 1000 : null);
|
|
286
322
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
323
|
+
// Update the card buffer so /last and `d` can find it.
|
|
324
|
+
if (callId) recordCard({ id: callId, tool, args: data.args, result: data, durationMs });
|
|
325
|
+
|
|
326
|
+
if (data._blocked) session.blockedOps++;
|
|
327
|
+
|
|
328
|
+
const { text, tone: t } = summarizeResult(tool, data);
|
|
329
|
+
const arrow = paint.text.dim('→');
|
|
330
|
+
const painter = t === 'success' ? paint.state.success
|
|
331
|
+
: t === 'warn' ? paint.state.warn
|
|
332
|
+
: t === 'danger' ? paint.state.danger
|
|
333
|
+
: paint.text.dim;
|
|
334
|
+
const duration = formatToolDuration(data);
|
|
335
|
+
const tail = duration ? paint.text.dim(` · ${duration}`) : '';
|
|
336
|
+
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
|
+
const hasLint = (tool === 'write_file' || tool === 'edit_file') && data.lint;
|
|
344
|
+
if (_pendingHead && _pendingHead.callId === callId && !hasLint) {
|
|
345
|
+
const cols = process.stderr.columns || 120;
|
|
346
|
+
const combined = `${_pendingHead.head} ${outcome}`;
|
|
347
|
+
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`);
|
|
351
|
+
_pendingHead = null;
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
291
354
|
}
|
|
355
|
+
_pendingHead = null;
|
|
292
356
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
} else if (tool === 'read_files') {
|
|
300
|
-
summary = 'Files read';
|
|
301
|
-
} else if (tool === 'search_code' || tool === 'list_files') {
|
|
302
|
-
const lines = String(data.output || '').split('\n').filter(line => line.trim()).length;
|
|
303
|
-
summary = lines > 0 ? `${lines} result${lines === 1 ? '' : 's'}` : 'No results';
|
|
304
|
-
} else if (tool === 'write_file' || tool === 'edit_file' || tool === 'write_project') {
|
|
305
|
-
summary = 'Updated';
|
|
306
|
-
} else if (tool === 'delete_file') {
|
|
307
|
-
summary = 'Deleted';
|
|
308
|
-
} else if (data.server_side) {
|
|
309
|
-
summary = firstOutputLine(data).slice(0, 100) || 'Completed server-side';
|
|
310
|
-
} else if (tool === 'shell') {
|
|
311
|
-
summary = firstOutputLine(data).slice(0, 100) || 'Command completed';
|
|
357
|
+
// Default two-line shape.
|
|
358
|
+
process.stderr.write(`${gutter}${outcome}\n`);
|
|
359
|
+
|
|
360
|
+
// Lint warnings stay visible alongside writes.
|
|
361
|
+
if (hasLint) {
|
|
362
|
+
process.stderr.write(`${gutter}${paint.state.warn('⚠ ' + String(data.lint).split('\n')[0].slice(0, 80))}\n`);
|
|
312
363
|
}
|
|
364
|
+
}
|
|
313
365
|
|
|
314
|
-
|
|
315
|
-
|
|
366
|
+
// ── Expand handler — `d`, `/last`, `/expand` ───────────────────────────
|
|
367
|
+
//
|
|
368
|
+
// All three call into the same renderer so output is consistent across
|
|
369
|
+
// keypress and slash-command paths. `expandLast` and `expandIndex` write
|
|
370
|
+
// directly to stderr.
|
|
371
|
+
|
|
372
|
+
function expandLast() {
|
|
373
|
+
const card = lastCard();
|
|
374
|
+
if (!card) {
|
|
375
|
+
process.stderr.write(` ${paint.text.dim('(no tool to expand yet)')}\n`);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
process.stderr.write('\n' + detailFor(card) + '\n\n');
|
|
379
|
+
}
|
|
316
380
|
|
|
317
|
-
|
|
318
|
-
if (
|
|
319
|
-
const
|
|
320
|
-
if (
|
|
321
|
-
process.stderr.write(
|
|
381
|
+
function expandIndex(idxOrAll) {
|
|
382
|
+
if (idxOrAll === 'all') {
|
|
383
|
+
const cards = allCards();
|
|
384
|
+
if (!cards.length) {
|
|
385
|
+
process.stderr.write(` ${paint.text.dim('(no tools to expand yet)')}\n`);
|
|
386
|
+
return;
|
|
322
387
|
}
|
|
388
|
+
process.stderr.write('\n');
|
|
389
|
+
for (const c of cards) process.stderr.write(detailFor(c) + '\n');
|
|
390
|
+
process.stderr.write('\n');
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const card = getCard(idxOrAll);
|
|
394
|
+
if (!card) {
|
|
395
|
+
process.stderr.write(` ${paint.text.dim('(no card at index ' + idxOrAll + ')')}\n`);
|
|
396
|
+
return;
|
|
323
397
|
}
|
|
398
|
+
process.stderr.write('\n' + detailFor(card) + '\n\n');
|
|
324
399
|
}
|
|
325
400
|
|
|
326
401
|
/**
|
|
@@ -383,6 +458,9 @@ function startContentStream() {
|
|
|
383
458
|
|
|
384
459
|
function appendContent(text) {
|
|
385
460
|
if (!text) return;
|
|
461
|
+
// Any streamed content between renderToolCall and renderToolResult would
|
|
462
|
+
// scroll the head off "the line above", breaking the in-place collapse.
|
|
463
|
+
clearPendingHead();
|
|
386
464
|
_streamBuffer += text;
|
|
387
465
|
_streamedPartialText += text;
|
|
388
466
|
|
|
@@ -409,6 +487,11 @@ function flushContent() {
|
|
|
409
487
|
function renderEvent(event) {
|
|
410
488
|
const { type, data } = event;
|
|
411
489
|
|
|
490
|
+
// Push every event into the orbit state machine before rendering so the
|
|
491
|
+
// bottom status bar reflects what is happening this very moment. The orbit
|
|
492
|
+
// module is a no-op when status-bar is not mounted (non-TTY, --headless).
|
|
493
|
+
if (_orbit) _orbit.onEvent(event);
|
|
494
|
+
|
|
412
495
|
switch (type) {
|
|
413
496
|
case 'status': {
|
|
414
497
|
const msg = data?.message || '';
|
|
@@ -421,6 +504,8 @@ function renderEvent(event) {
|
|
|
421
504
|
const text = data?.message || data?.text || '';
|
|
422
505
|
if (text && !text.startsWith('Processing')) {
|
|
423
506
|
startSpinner(text.slice(0, 80));
|
|
507
|
+
// Capture reasoning so /why can replay it.
|
|
508
|
+
session.lastReasoning = text;
|
|
424
509
|
}
|
|
425
510
|
break;
|
|
426
511
|
}
|
|
@@ -482,7 +567,7 @@ function renderEvent(event) {
|
|
|
482
567
|
case 'approval_denied': {
|
|
483
568
|
const reason = data?.reason || 'User denied';
|
|
484
569
|
const toolName = data?.tool || '';
|
|
485
|
-
const indent =
|
|
570
|
+
const indent = subAgentIndent();
|
|
486
571
|
process.stderr.write(`${indent}${c.red('✗')} ${c.dim(`Denied ${toolName}: ${reason}`)}\n`);
|
|
487
572
|
break;
|
|
488
573
|
}
|
|
@@ -561,6 +646,7 @@ function renderEvent(event) {
|
|
|
561
646
|
|
|
562
647
|
case 'delegation': {
|
|
563
648
|
stopSpinner();
|
|
649
|
+
clearPendingHead();
|
|
564
650
|
const from = data?.from || '';
|
|
565
651
|
const to = data?.to || '';
|
|
566
652
|
session.delegations.push({ from, to, time: Date.now() });
|
|
@@ -576,21 +662,20 @@ function renderEvent(event) {
|
|
|
576
662
|
|
|
577
663
|
case 'sub_agent_start': {
|
|
578
664
|
stopSpinner();
|
|
579
|
-
|
|
665
|
+
clearPendingHead();
|
|
580
666
|
const agentType = data?.type || 'sub-agent';
|
|
581
667
|
const model = data?.model || '';
|
|
582
668
|
const query = data?.query || '';
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
if (query) process.stderr.write(` ${c.gray('query:')} ${c.dim(query)}\n`);
|
|
669
|
+
process.stderr.write(renderSubAgentOpen({ type: agentType, model, query }) + '\n');
|
|
670
|
+
session.inSubAgent = inSubAgentBlock(); // kept for legacy readers
|
|
671
|
+
session.subAgentCounts[agentType] = (session.subAgentCounts[agentType] || 0) + 1;
|
|
587
672
|
startSpinner(`${agentType}: working...`);
|
|
588
673
|
break;
|
|
589
674
|
}
|
|
590
675
|
|
|
591
676
|
case 'sub_agent_tool': {
|
|
592
|
-
//
|
|
593
|
-
//
|
|
677
|
+
// The regular tool_call event renders the card, indented by the
|
|
678
|
+
// sub-agent stack depth. Just update the spinner text here.
|
|
594
679
|
const agentType = data?.type || 'sub-agent';
|
|
595
680
|
const tool = data?.tool || '';
|
|
596
681
|
if (tool) updateSpinner(`${agentType} → ${tool}`);
|
|
@@ -599,24 +684,26 @@ function renderEvent(event) {
|
|
|
599
684
|
|
|
600
685
|
case 'sub_agent_complete': {
|
|
601
686
|
stopSpinner();
|
|
602
|
-
|
|
687
|
+
clearPendingHead();
|
|
603
688
|
const agentType = data?.type || 'sub-agent';
|
|
604
|
-
const model = data?.model || '';
|
|
605
|
-
const resultLen = data?.result_length || 0;
|
|
606
689
|
const usage = data?.usage || {};
|
|
607
690
|
const tokens = (usage.input_tokens || 0) + (usage.output_tokens || 0);
|
|
608
|
-
const
|
|
609
|
-
if (
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
691
|
+
const costUsd = usage.cost_usd ?? usage.total_cost_usd ?? data?.cost_usd ?? null;
|
|
692
|
+
if (typeof costUsd === 'number') session.savedUsd += costUsd;
|
|
693
|
+
const summary = data?.result_summary
|
|
694
|
+
|| (data?.result_length > 0 ? `${agentType} returned ${data.result_length} chars` : '');
|
|
695
|
+
process.stderr.write(renderSubAgentClose({
|
|
696
|
+
type: agentType,
|
|
697
|
+
success: data?.success !== false,
|
|
698
|
+
summary,
|
|
699
|
+
costUsd,
|
|
700
|
+
tokens,
|
|
701
|
+
durationS: data?.duration_s,
|
|
702
|
+
toolCalls: data?.tool_calls,
|
|
703
|
+
iterations: data?.iterations,
|
|
704
|
+
error: data?.error,
|
|
705
|
+
}) + '\n\n');
|
|
706
|
+
session.inSubAgent = inSubAgentBlock();
|
|
620
707
|
break;
|
|
621
708
|
}
|
|
622
709
|
|
|
@@ -638,6 +725,9 @@ function renderEvent(event) {
|
|
|
638
725
|
}
|
|
639
726
|
if (data?.model) session.model = data.model;
|
|
640
727
|
if (data?.user) session.user = { ...session.user, ...data.user };
|
|
728
|
+
// BYOK users pay their model provider directly; the platform does not
|
|
729
|
+
// charge them credits. Hide cost + credits when this flag is set.
|
|
730
|
+
if (typeof data?.is_byok === 'boolean') session.isByok = data.is_byok;
|
|
641
731
|
break;
|
|
642
732
|
}
|
|
643
733
|
|
|
@@ -653,6 +743,8 @@ function renderEvent(event) {
|
|
|
653
743
|
case 'complete': {
|
|
654
744
|
stopSpinner();
|
|
655
745
|
flushContent();
|
|
746
|
+
resetSubAgents();
|
|
747
|
+
session.inSubAgent = false;
|
|
656
748
|
|
|
657
749
|
const summary = data?.summary || '';
|
|
658
750
|
if (summary && !_renderedContentThisTurn) {
|
|
@@ -695,9 +787,39 @@ function renderEvent(event) {
|
|
|
695
787
|
|
|
696
788
|
session.lastTurnDuration = data?.duration_s || 0;
|
|
697
789
|
|
|
790
|
+
// Sync cumulative session cost into the orbit (status bar shows it).
|
|
791
|
+
if (_orbit) _orbit.onCost(session.totalCost);
|
|
792
|
+
|
|
698
793
|
// Compact turn summary
|
|
699
794
|
const tools = data?.tool_calls || session.toolCalls || 0;
|
|
700
|
-
|
|
795
|
+
|
|
796
|
+
// Mission report — replaces the trailing "Done" when the turn did real
|
|
797
|
+
// work (touched files or invoked tools). Plain chat turns keep the
|
|
798
|
+
// tight printTurnSummary so the report does not feel ceremonial.
|
|
799
|
+
const didRealWork = tools > 0 || session.filesChanged.length > 0;
|
|
800
|
+
if (didRealWork) {
|
|
801
|
+
const successOverall = data?.success !== false;
|
|
802
|
+
const report = renderMissionReport({
|
|
803
|
+
task: session.lastTask,
|
|
804
|
+
success: successOverall,
|
|
805
|
+
filesChanged: session.filesChanged,
|
|
806
|
+
toolCounts: session.toolCounts,
|
|
807
|
+
subAgents: { ...session.subAgentCounts, savedUsd: session.isByok ? 0 : session.savedUsd },
|
|
808
|
+
// BYOK users pay their provider directly; suppress cost in the report.
|
|
809
|
+
costUsd: session.isByok ? null : (turnCost || session.totalCost),
|
|
810
|
+
durationS: data?.duration_s,
|
|
811
|
+
testsPass: data?.tests_passed != null
|
|
812
|
+
? { passed: data.tests_passed, total: data.tests_total || data.tests_passed }
|
|
813
|
+
: null,
|
|
814
|
+
blockers: !successOverall ? (data?.blockers || extractBlockers(data)) : null,
|
|
815
|
+
nextActions: successOverall
|
|
816
|
+
? ['/commit', '/pr', '/undo', '/report']
|
|
817
|
+
: ['/why', '/undo', '/re-plan'],
|
|
818
|
+
});
|
|
819
|
+
process.stderr.write(report + '\n');
|
|
820
|
+
} else {
|
|
821
|
+
printTurnSummary(tools, data?.duration_s, turnCost);
|
|
822
|
+
}
|
|
701
823
|
break;
|
|
702
824
|
}
|
|
703
825
|
|
|
@@ -736,7 +858,8 @@ async function handleCommand(input, ctx) {
|
|
|
736
858
|
process.stderr.write(` ${c.brand(name.padEnd(14))} ${desc}\n`);
|
|
737
859
|
}
|
|
738
860
|
process.stderr.write(`\n ${c.bold('Keyboard')}\n`);
|
|
739
|
-
process.stderr.write(` ${c.gray('Ctrl+C')} exit ${c.gray('↑↓')} history ${c.gray('Tab')} autocomplete\n
|
|
861
|
+
process.stderr.write(` ${c.gray('Ctrl+C')} exit ${c.gray('↑↓')} history ${c.gray('Tab')} autocomplete\n`);
|
|
862
|
+
process.stderr.write(` ${c.gray('d')} expand last tool ${c.gray('Space')} pause/resume ${c.gray('Esc')} interrupt\n\n`);
|
|
740
863
|
return;
|
|
741
864
|
|
|
742
865
|
case '/login':
|
|
@@ -782,7 +905,11 @@ async function handleCommand(input, ctx) {
|
|
|
782
905
|
process.stderr.write(` ${c.dim('Turns')} ${session.turns}\n`);
|
|
783
906
|
process.stderr.write(` ${c.dim('Tools')} ${session.totalToolCalls} total, ${session.toolCalls} last turn\n`);
|
|
784
907
|
process.stderr.write(` ${c.dim('Duration')} ${formatElapsed(session.startTime)}\n`);
|
|
785
|
-
|
|
908
|
+
if (session.isByok) {
|
|
909
|
+
process.stderr.write(` ${c.dim('Billing')} ${c.green('BYOK')} ${c.dim('(provider-billed)')}\n`);
|
|
910
|
+
} else {
|
|
911
|
+
process.stderr.write(` ${c.dim('Credits')} ${formatCredits(costToCredits(session.totalCost))}${session.costAccurate ? '' : c.dim(' (est)')}\n`);
|
|
912
|
+
}
|
|
786
913
|
process.stderr.write(` ${c.dim('CWD')} ${safeCwd()}\n`);
|
|
787
914
|
|
|
788
915
|
// Permissions
|
|
@@ -850,12 +977,20 @@ async function handleCommand(input, ctx) {
|
|
|
850
977
|
process.stderr.write(` ${c.gray('Turns:')} ${session.turns}\n`);
|
|
851
978
|
process.stderr.write(` ${c.gray('Tools:')} ${session.toolCalls}\n`);
|
|
852
979
|
process.stderr.write(` ${c.gray('Blocked:')} ${session.blockedOps}\n`);
|
|
853
|
-
|
|
980
|
+
if (session.isByok) {
|
|
981
|
+
process.stderr.write(` ${c.gray('Billing:')} ${c.green('BYOK')} ${c.dim('(provider-billed)')}\n`);
|
|
982
|
+
} else {
|
|
983
|
+
process.stderr.write(` ${c.gray('Credits:')} ${formatCredits(costToCredits(session.totalCost))}${session.costAccurate ? '' : c.dim(' (est)')}\n`);
|
|
984
|
+
}
|
|
854
985
|
process.stderr.write(` ${c.gray('Elapsed:')} ${formatElapsed(session.startTime)}\n\n`);
|
|
855
986
|
return;
|
|
856
987
|
}
|
|
857
988
|
|
|
858
989
|
case '/cost': {
|
|
990
|
+
if (session.isByok) {
|
|
991
|
+
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`);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
859
994
|
process.stderr.write(`\n ${c.bold('Session Credits')} ${c.brand(formatCredits(costToCredits(session.totalCost)))}`);
|
|
860
995
|
if (!session.costAccurate) {
|
|
861
996
|
process.stderr.write(` ${c.yellow('(estimated)')}`);
|
|
@@ -908,6 +1043,143 @@ async function handleCommand(input, ctx) {
|
|
|
908
1043
|
process.stderr.write('\n');
|
|
909
1044
|
return;
|
|
910
1045
|
|
|
1046
|
+
case '/last':
|
|
1047
|
+
expandLast();
|
|
1048
|
+
return;
|
|
1049
|
+
|
|
1050
|
+
case '/expand': {
|
|
1051
|
+
const arg = rest.trim();
|
|
1052
|
+
if (!arg) { expandLast(); return; }
|
|
1053
|
+
if (arg === 'all') { expandIndex('all'); return; }
|
|
1054
|
+
const n = Number(arg);
|
|
1055
|
+
if (!Number.isFinite(n)) {
|
|
1056
|
+
process.stderr.write(` ${c.gray('Usage: /expand [n|all] — n is the 1-based index from the start of the session')}\n`);
|
|
1057
|
+
return;
|
|
1058
|
+
}
|
|
1059
|
+
// Users pass 1-based; getCard accepts negative (-1 = last) or positive index.
|
|
1060
|
+
expandIndex(n > 0 ? n - 1 : n);
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
case '/fold':
|
|
1065
|
+
process.stderr.write(` ${c.gray('Output is folded by default — there is nothing to hide. Use /last or d to expand.')}\n`);
|
|
1066
|
+
return;
|
|
1067
|
+
|
|
1068
|
+
case '/undo': {
|
|
1069
|
+
const result = ctx.checkpoints?.undo();
|
|
1070
|
+
if (!result) {
|
|
1071
|
+
process.stderr.write(` ${c.gray('No checkpoints to undo.')}\n`);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
if (result.restored) {
|
|
1075
|
+
process.stderr.write(` ${c.green('↩')} ${c.dim('Restored')} ${result.filePath}\n`);
|
|
1076
|
+
} else {
|
|
1077
|
+
process.stderr.write(` ${c.red('✗')} ${c.dim('Undo failed: ' + (result.error || 'unknown error'))}\n`);
|
|
1078
|
+
}
|
|
1079
|
+
return;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
case '/checkpoint': {
|
|
1083
|
+
const list = ctx.checkpoints?.list(10) || [];
|
|
1084
|
+
if (!list.length) {
|
|
1085
|
+
process.stderr.write(` ${c.gray('No checkpoints recorded yet — they are taken automatically before each edit.')}\n`);
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
process.stderr.write(`\n ${c.bold('Recent checkpoints')}\n ${c.gray('─'.repeat(40))}\n`);
|
|
1089
|
+
for (const ckpt of list) {
|
|
1090
|
+
const when = String(ckpt.timestamp).slice(11, 19);
|
|
1091
|
+
process.stderr.write(` ${c.gray(when)} ${c.white(ckpt.file)} ${c.gray(formatTokens(ckpt.size) + ' bytes')}\n`);
|
|
1092
|
+
}
|
|
1093
|
+
process.stderr.write(`\n ${c.gray('/undo restores the most recent one')}\n\n`);
|
|
1094
|
+
return;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
case '/preflight': {
|
|
1098
|
+
await runPreflight({ auth: ctx.auth, cwd: safeCwd(), version: VERSION });
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
case '/report': {
|
|
1103
|
+
if (Object.keys(session.toolCounts).length === 0 && session.filesChanged.length === 0) {
|
|
1104
|
+
process.stderr.write(` ${c.gray('Nothing to report yet — run a task first.')}\n`);
|
|
1105
|
+
return;
|
|
1106
|
+
}
|
|
1107
|
+
const state = {
|
|
1108
|
+
task: session.lastTask,
|
|
1109
|
+
success: true,
|
|
1110
|
+
filesChanged: session.filesChanged,
|
|
1111
|
+
toolCounts: session.toolCounts,
|
|
1112
|
+
subAgents: { ...session.subAgentCounts, savedUsd: session.isByok ? 0 : session.savedUsd },
|
|
1113
|
+
costUsd: session.isByok ? null : session.totalCost,
|
|
1114
|
+
durationS: (Date.now() - session.startTime) / 1000,
|
|
1115
|
+
nextActions: ['/commit', '/pr', '/undo'],
|
|
1116
|
+
};
|
|
1117
|
+
const out = saveReport(state, { cwd: safeCwd() });
|
|
1118
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Saved')} ${out}\n`);
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
case '/why': {
|
|
1123
|
+
if (!session.lastReasoning) {
|
|
1124
|
+
process.stderr.write(` ${c.gray('No reasoning captured yet for this session.')}\n`);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
process.stderr.write(`\n ${c.bold('Last reasoning')}\n ${c.gray('─'.repeat(40))}\n`);
|
|
1128
|
+
for (const line of String(session.lastReasoning).split('\n')) {
|
|
1129
|
+
process.stderr.write(` ${c.dim(line)}\n`);
|
|
1130
|
+
}
|
|
1131
|
+
process.stderr.write('\n');
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
case '/map': {
|
|
1136
|
+
try {
|
|
1137
|
+
const resources = ctx.toolExecutor?.getProjectResources?.() || [];
|
|
1138
|
+
if (!resources.length) {
|
|
1139
|
+
process.stderr.write(` ${c.gray('No project resources registered yet. Use get_project_overview to register one.')}\n`);
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
process.stderr.write(`\n ${c.bold('Registered projects')}\n ${c.gray('─'.repeat(40))}\n`);
|
|
1143
|
+
for (const r of resources) {
|
|
1144
|
+
process.stderr.write(` ${c.brand('•')} ${c.white(r.id || r.name || '?')} ${c.dim(r.root || r.path || '')}\n`);
|
|
1145
|
+
}
|
|
1146
|
+
process.stderr.write('\n');
|
|
1147
|
+
} catch (err) {
|
|
1148
|
+
process.stderr.write(` ${c.red('/map failed: ' + err.message)}\n`);
|
|
1149
|
+
}
|
|
1150
|
+
return;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
case '/budget': {
|
|
1154
|
+
const arg = rest.trim();
|
|
1155
|
+
if (!arg || arg === 'clear' || arg === 'off') {
|
|
1156
|
+
session.budgetUsd = null;
|
|
1157
|
+
session.budgetExceeded = false;
|
|
1158
|
+
process.stderr.write(` ${c.gray('Budget cap cleared.')}\n`);
|
|
1159
|
+
return;
|
|
1160
|
+
}
|
|
1161
|
+
const n = Number(arg.replace(/^\$/, ''));
|
|
1162
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
1163
|
+
process.stderr.write(` ${c.gray('Usage: /budget <amount in USD> or /budget clear')}\n`);
|
|
1164
|
+
return;
|
|
1165
|
+
}
|
|
1166
|
+
session.budgetUsd = n;
|
|
1167
|
+
session.budgetExceeded = false;
|
|
1168
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Budget set: ')} $${n.toFixed(2)}\n`);
|
|
1169
|
+
return;
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
case '/quiet':
|
|
1173
|
+
case '/verbose':
|
|
1174
|
+
case '/surgical': {
|
|
1175
|
+
const mode = cmd === '/quiet' ? V_MODES.QUIET
|
|
1176
|
+
: cmd === '/verbose' ? V_MODES.VERBOSE
|
|
1177
|
+
: V_MODES.SURGICAL;
|
|
1178
|
+
setVerbosity(mode);
|
|
1179
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Verbosity: ')} ${c.brand(verbosityLabel(mode))}\n`);
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
911
1183
|
case '/compact': {
|
|
912
1184
|
const before = session.history.length;
|
|
913
1185
|
if (before <= 4) { process.stderr.write(` ${c.gray('Nothing to compact.')}\n`); return; }
|
|
@@ -1132,7 +1404,9 @@ export async function startTerminalRepl() {
|
|
|
1132
1404
|
const auth = new TarangAuth();
|
|
1133
1405
|
|
|
1134
1406
|
// Projects are registered and indexed on demand through get_project_overview.
|
|
1135
|
-
|
|
1407
|
+
// CheckpointManager records per-file snapshots before edits so /undo works.
|
|
1408
|
+
const checkpoints = new CheckpointManager(safeCwd());
|
|
1409
|
+
const toolExecutor = createToolExecutor({ checkpoints });
|
|
1136
1410
|
const skipPerms = cliArgs.freeswim;
|
|
1137
1411
|
const approval = new ApprovalManager({ autoApprove: skipPerms });
|
|
1138
1412
|
|
|
@@ -1146,10 +1420,20 @@ export async function startTerminalRepl() {
|
|
|
1146
1420
|
// Persistent stream client — session_id captured from backend on first turn
|
|
1147
1421
|
let streamClient = null;
|
|
1148
1422
|
|
|
1149
|
-
const ctx = { auth, toolExecutor, approval, jsonlWriter, sessionMgr };
|
|
1423
|
+
const ctx = { auth, toolExecutor, approval, jsonlWriter, sessionMgr, checkpoints };
|
|
1150
1424
|
|
|
1425
|
+
// ── Print banner + preflight + init BEFORE mounting the status bar ──
|
|
1426
|
+
// The status bar shrinks the scroll region; if it mounts first, the
|
|
1427
|
+
// banner scrolls off-screen before the user ever sees it.
|
|
1151
1428
|
printBanner(auth);
|
|
1152
1429
|
|
|
1430
|
+
// Preflight diagnostic (PRD-055 §9). Non-blocking; opt-out via
|
|
1431
|
+
// KEPLER_NO_PREFLIGHT=1 (used by tests / scripted runs).
|
|
1432
|
+
if (process.env.KEPLER_NO_PREFLIGHT !== '1' && !cliArgs.freeswim) {
|
|
1433
|
+
try { await runPreflight({ auth, cwd: safeCwd(), version: VERSION }); }
|
|
1434
|
+
catch { /* preflight is best-effort */ }
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1153
1437
|
// ── Initialization ──
|
|
1154
1438
|
process.stderr.write(` ${c.brand('⠋')} ${c.dim('Initializing...')}\r`);
|
|
1155
1439
|
await fetchUser(ctx);
|
|
@@ -1187,12 +1471,41 @@ export async function startTerminalRepl() {
|
|
|
1187
1471
|
|
|
1188
1472
|
process.stderr.write(`\n ${c.dim('Press')} ${c.brand('Enter')} ${c.dim('to start, or type a prompt below.')}\n`);
|
|
1189
1473
|
|
|
1190
|
-
|
|
1474
|
+
// Mission Control status bar is OPT-IN as of v2.0.1.
|
|
1475
|
+
// Set KEPLER_STATUS_BAR=1 (or KEPLER_MISSION=1) to enable the persistent
|
|
1476
|
+
// bottom-anchored ORBIT bar. Default off because the DECSTBM scroll
|
|
1477
|
+
// region was eating the prompt visibility on some terminals (issue
|
|
1478
|
+
// observed during v2.0.0 testing). The orbit state machine and tool
|
|
1479
|
+
// cards still work without the bar — the bar is just the rendering.
|
|
1480
|
+
const statusBarEnabled = (
|
|
1481
|
+
process.env.KEPLER_STATUS_BAR === '1' || process.env.KEPLER_MISSION === '1'
|
|
1482
|
+
) && term().isTTY && !term().plain;
|
|
1483
|
+
if (statusBarEnabled) {
|
|
1484
|
+
_orbit = createOrbit();
|
|
1485
|
+
attachOrbit(_orbit);
|
|
1486
|
+
process.on('beforeExit', unmountStatusBar);
|
|
1487
|
+
process.on('exit', unmountStatusBar);
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// The prompt label is the USER speaking, not the agent. Use the signed-in
|
|
1491
|
+
// GitHub handle if known, otherwise fall back to "You".
|
|
1492
|
+
//
|
|
1493
|
+
// readline counts every byte of the prompt as a visible column when it
|
|
1494
|
+
// computes cursor position for line-wrapping; ANSI color codes throw the
|
|
1495
|
+
// math off and produce duplicated text on wrap. Wrap each escape sequence
|
|
1496
|
+
// in SOH (\x01) ... STX (\x02) so readline skips it when measuring width.
|
|
1497
|
+
function rlSafe(s) {
|
|
1498
|
+
return String(s || '').replace(/\x1b\[[0-9;]*m/g, '\x01$&\x02');
|
|
1499
|
+
}
|
|
1500
|
+
function userPrompt() {
|
|
1501
|
+
const who = session.user?.github_username || session.user?.email?.split('@')[0] || 'You';
|
|
1502
|
+
return rlSafe(`${c.brand(who)} ${c.dim('›')} `);
|
|
1503
|
+
}
|
|
1191
1504
|
|
|
1192
1505
|
const rl = readline.createInterface({
|
|
1193
1506
|
input: process.stdin,
|
|
1194
1507
|
output: process.stderr,
|
|
1195
|
-
prompt:
|
|
1508
|
+
prompt: userPrompt(),
|
|
1196
1509
|
completer: (line) => {
|
|
1197
1510
|
if (line.startsWith('/')) {
|
|
1198
1511
|
const hits = Object.keys(COMMANDS).filter(cmd => cmd.startsWith(line));
|
|
@@ -1211,6 +1524,7 @@ export async function startTerminalRepl() {
|
|
|
1211
1524
|
function showPrompt() {
|
|
1212
1525
|
printPromptBlock();
|
|
1213
1526
|
process.stderr.write('\n'); // half-inch vertical gap above input line
|
|
1527
|
+
rl.setPrompt(userPrompt()); // refresh label in case session.user resolved
|
|
1214
1528
|
rl.prompt();
|
|
1215
1529
|
}
|
|
1216
1530
|
|
|
@@ -1230,10 +1544,27 @@ export async function startTerminalRepl() {
|
|
|
1230
1544
|
return;
|
|
1231
1545
|
}
|
|
1232
1546
|
|
|
1547
|
+
// Budget cap (PRD-055 §10). Stop before the next paid call when exceeded.
|
|
1548
|
+
if (session.budgetUsd && session.totalCost >= session.budgetUsd) {
|
|
1549
|
+
session.budgetExceeded = true;
|
|
1550
|
+
process.stderr.write(` ${c.yellow('⏹')} ${c.dim(`Budget reached ($${session.totalCost.toFixed(2)} of $${session.budgetUsd.toFixed(2)}). Use /budget clear to continue.`)}\n`);
|
|
1551
|
+
showPrompt();
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1233
1555
|
// Regular prompt
|
|
1234
1556
|
session.history.push({ role: 'user', content: input });
|
|
1235
1557
|
session.turns++;
|
|
1236
1558
|
session.toolCalls = 0;
|
|
1559
|
+
session.lastTask = input;
|
|
1560
|
+
// Reset per-turn counts so the mission report reflects this turn only.
|
|
1561
|
+
session.toolCounts = {};
|
|
1562
|
+
session.subAgentCounts = {};
|
|
1563
|
+
session.savedUsd = 0;
|
|
1564
|
+
|
|
1565
|
+
// Tell the orbit a new turn started — switches to DISCOVERY and updates
|
|
1566
|
+
// task / turn counters in the status bar.
|
|
1567
|
+
if (_orbit) _orbit.onUserInput(input);
|
|
1237
1568
|
|
|
1238
1569
|
// Start session tracking on first turn
|
|
1239
1570
|
if (session.turns === 1) {
|
|
@@ -1272,6 +1603,7 @@ export async function startTerminalRepl() {
|
|
|
1272
1603
|
let executionPaused = false;
|
|
1273
1604
|
let keypressCleanup = null;
|
|
1274
1605
|
let execListenerActive = false;
|
|
1606
|
+
let lastCtrlCAt = 0; // PRD-055 §8.4: first Ctrl+C cancels, second exits
|
|
1275
1607
|
|
|
1276
1608
|
if (process.stdin.isTTY) {
|
|
1277
1609
|
rl.pause();
|
|
@@ -1287,7 +1619,10 @@ export async function startTerminalRepl() {
|
|
|
1287
1619
|
// Esc key (single byte 0x1b, not part of arrow sequence)
|
|
1288
1620
|
if (bytes.length === 1 && bytes[0] === 0x1b) {
|
|
1289
1621
|
stopSpinner();
|
|
1290
|
-
process.stderr.write(`\n ${c.yellow('⏹')} ${c.dim('
|
|
1622
|
+
process.stderr.write(`\n ${c.yellow('⏹')} ${c.dim('Cancelled.')}\n`);
|
|
1623
|
+
// cancel() now aborts the in-flight SSE reader; the for-await loop
|
|
1624
|
+
// wakes up immediately and the prompt returns. No more "stuck"
|
|
1625
|
+
// Cancelling… message.
|
|
1291
1626
|
client.cancel();
|
|
1292
1627
|
return;
|
|
1293
1628
|
}
|
|
@@ -1298,20 +1633,39 @@ export async function startTerminalRepl() {
|
|
|
1298
1633
|
executionPaused = false;
|
|
1299
1634
|
process.stderr.write(` ${c.green('▶')} ${c.dim('Resumed')}\n`);
|
|
1300
1635
|
client.resume();
|
|
1636
|
+
if (_orbit) _orbit.onResume();
|
|
1301
1637
|
} else {
|
|
1302
1638
|
executionPaused = true;
|
|
1303
1639
|
stopSpinner();
|
|
1304
1640
|
process.stderr.write(` ${c.yellow('⏸')} ${c.dim('Paused — press Space to resume, Esc to cancel')}\n`);
|
|
1305
1641
|
client.pause();
|
|
1642
|
+
if (_orbit) _orbit.onPause();
|
|
1306
1643
|
}
|
|
1307
1644
|
return;
|
|
1308
1645
|
}
|
|
1309
1646
|
|
|
1310
|
-
// Ctrl+C during execution
|
|
1647
|
+
// Ctrl+C during execution — PRD-055 §8.4 two-step semantics:
|
|
1648
|
+
// first press → cancel current backend run, stay in REPL
|
|
1649
|
+
// second press within 2s → exit the CLI
|
|
1311
1650
|
if (bytes[0] === 0x03) {
|
|
1312
1651
|
stopSpinner();
|
|
1313
|
-
|
|
1314
|
-
|
|
1652
|
+
const now = Date.now();
|
|
1653
|
+
if (lastCtrlCAt && (now - lastCtrlCAt) < 2000) {
|
|
1654
|
+
process.stderr.write(`\n ${c.dim('exiting…')}\n`);
|
|
1655
|
+
try { client.cancel(); } catch {}
|
|
1656
|
+
process.exit(0);
|
|
1657
|
+
}
|
|
1658
|
+
lastCtrlCAt = now;
|
|
1659
|
+
process.stderr.write(`\n ${c.yellow('⏹')} ${c.dim('Cancelled. Press Ctrl+C again within 2s to exit.')}\n`);
|
|
1660
|
+
try { client.cancel(); } catch {}
|
|
1661
|
+
return;
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
// `d` — expand last tool card (Mission Control §6.2)
|
|
1665
|
+
if (bytes.length === 1 && (bytes[0] === 0x64 || bytes[0] === 0x44)) {
|
|
1666
|
+
stopSpinner();
|
|
1667
|
+
expandLast();
|
|
1668
|
+
return;
|
|
1315
1669
|
}
|
|
1316
1670
|
};
|
|
1317
1671
|
|