@axplusb/kepler 1.0.9 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -2
- 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/pricing.mjs +23 -1
- package/src/core/risk-tier.mjs +239 -0
- package/src/core/tool-executor.mjs +78 -5
- package/src/onboarding/preflight.mjs +274 -0
- package/src/state/orbit.mjs +263 -0
- package/src/state/verbosity.mjs +99 -0
- package/src/terminal/ansi.mjs +47 -27
- package/src/terminal/repl.mjs +407 -121
- 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 +314 -0
- package/src/ui/tool-details.mjs +277 -0
package/src/terminal/repl.mjs
CHANGED
|
@@ -16,13 +16,21 @@
|
|
|
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
|
-
import { calculateCost, formatCostValue, formatTokens } from '../core/pricing.mjs';
|
|
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 { renderMissionReport, saveReport, toMarkdown as missionMarkdown } from '../ui/mission-report.mjs';
|
|
28
|
+
import {
|
|
29
|
+
getVerbosity,
|
|
30
|
+
setVerbosity,
|
|
31
|
+
label as verbosityLabel,
|
|
32
|
+
MODES as V_MODES,
|
|
33
|
+
} from '../state/verbosity.mjs';
|
|
26
34
|
import { persistProjectArtifacts } from '../core/project-artifacts.mjs';
|
|
27
35
|
import { TarangAuth } from '../auth/tarang-auth.mjs';
|
|
28
36
|
import { ApprovalManager } from '../core/approval.mjs';
|
|
@@ -30,7 +38,27 @@ import { resolveBackendUrl } from '../core/backend-url.mjs';
|
|
|
30
38
|
import { BUILTIN_AGENTS, runAgent } from './agents.mjs';
|
|
31
39
|
import { SessionManager } from '../core/session-manager.mjs';
|
|
32
40
|
import { parseArgs } from '../config/cli-args.mjs';
|
|
33
|
-
import {
|
|
41
|
+
import { toolDisplayLabel } from './tool-display.mjs';
|
|
42
|
+
import { createOrbit } from '../state/orbit.mjs';
|
|
43
|
+
import { attachOrbit, unmount as unmountStatusBar } from '../ui/status-bar.mjs';
|
|
44
|
+
import { term } from '../ui/term.mjs';
|
|
45
|
+
import {
|
|
46
|
+
formatCardHead,
|
|
47
|
+
summarizeResult,
|
|
48
|
+
recordCard,
|
|
49
|
+
lastCard,
|
|
50
|
+
getCard,
|
|
51
|
+
allCards,
|
|
52
|
+
} from '../ui/tool-card.mjs';
|
|
53
|
+
import { detailFor } from '../ui/tool-details.mjs';
|
|
54
|
+
import { paint } from '../ui/palette.mjs';
|
|
55
|
+
import {
|
|
56
|
+
renderSubAgentOpen,
|
|
57
|
+
renderSubAgentClose,
|
|
58
|
+
subAgentIndent,
|
|
59
|
+
inSubAgent as inSubAgentBlock,
|
|
60
|
+
resetSubAgents,
|
|
61
|
+
} from '../ui/sub-agent.mjs';
|
|
34
62
|
|
|
35
63
|
import { createRequire } from 'node:module';
|
|
36
64
|
const __require = createRequire(import.meta.url);
|
|
@@ -63,6 +91,7 @@ function safeCwd() {
|
|
|
63
91
|
// ── Session State ──
|
|
64
92
|
|
|
65
93
|
let _sessionMgr = null; // Set in startTerminalRepl, used by renderEvent
|
|
94
|
+
let _orbit = null; // Mission Control orbit state machine; set in startTerminalRepl
|
|
66
95
|
|
|
67
96
|
const session = {
|
|
68
97
|
id: null, // set by backend on first turn via session_info event
|
|
@@ -82,6 +111,13 @@ const session = {
|
|
|
82
111
|
inSubAgent: false, // true while a sub-agent is running (for indented tool display)
|
|
83
112
|
filesChanged: [], // files modified this session
|
|
84
113
|
lastTurnDuration: 0,
|
|
114
|
+
toolCounts: {}, // per-tool histogram (mission report)
|
|
115
|
+
subAgentCounts: {}, // per-sub-agent histogram (mission report)
|
|
116
|
+
savedUsd: 0, // total sub-agent cost (for "saved by routing")
|
|
117
|
+
lastTask: '', // most recent user prompt (mission report title)
|
|
118
|
+
lastReasoning: '', // captured from agent for /why
|
|
119
|
+
budgetUsd: null, // /budget cap, null = unlimited
|
|
120
|
+
budgetExceeded: false,
|
|
85
121
|
costBreakdown: [], // per-model usage: [{ model, role, input_tokens, output_tokens, cost }]
|
|
86
122
|
totalCost: 0, // accumulated session cost (USD)
|
|
87
123
|
costAccurate: false, // true if backend provides per-model breakdown
|
|
@@ -100,6 +136,19 @@ const COMMANDS = {
|
|
|
100
136
|
'/diff': 'Git diff',
|
|
101
137
|
'/cost': 'Show session cost',
|
|
102
138
|
'/history': 'Show conversation',
|
|
139
|
+
'/last': 'Expand last tool output',
|
|
140
|
+
'/expand': 'Expand tool output by index (or "all")',
|
|
141
|
+
'/fold': 'Hide previously expanded tool output',
|
|
142
|
+
'/checkpoint':'List recent file checkpoints',
|
|
143
|
+
'/undo': 'Restore the last file checkpoint',
|
|
144
|
+
'/preflight':'Re-run the onboarding diagnostic',
|
|
145
|
+
'/report': 'Save the mission report as markdown',
|
|
146
|
+
'/why': 'Print the agent reasoning for the last decision',
|
|
147
|
+
'/map': 'Show the registered project tree',
|
|
148
|
+
'/budget': 'Set / clear a hard session cost cap',
|
|
149
|
+
'/quiet': 'Verbosity: hide sub-agent inner tools',
|
|
150
|
+
'/verbose': 'Verbosity: show sub-agent inner tools',
|
|
151
|
+
'/surgical': 'Verbosity: show everything (reasoning, expanded tools)',
|
|
103
152
|
'/compact': 'Compact conversation context',
|
|
104
153
|
'/agents': 'List available agents',
|
|
105
154
|
'/explore': 'Code explorer agent',
|
|
@@ -163,13 +212,12 @@ function printBanner(auth) {
|
|
|
163
212
|
*/
|
|
164
213
|
function buildContextStrip() {
|
|
165
214
|
const totalTokens = session.inputTokens + session.outputTokens;
|
|
166
|
-
const
|
|
215
|
+
const credits = formatCredits(costToCredits(session.totalCost));
|
|
167
216
|
const elapsed = formatElapsed(session.startTime);
|
|
168
217
|
|
|
169
|
-
// Right side — always shown
|
|
170
218
|
const right = [
|
|
171
219
|
c.dim(`${formatTokens(totalTokens)} tok`),
|
|
172
|
-
c.dim(
|
|
220
|
+
c.dim(credits),
|
|
173
221
|
c.dim(elapsed),
|
|
174
222
|
].join(c.dim(' · '));
|
|
175
223
|
|
|
@@ -196,11 +244,27 @@ function printPromptBlock() {
|
|
|
196
244
|
* Print a turn summary after a response completes.
|
|
197
245
|
* Shows only when there's something meaningful to report.
|
|
198
246
|
*/
|
|
247
|
+
/**
|
|
248
|
+
* Pull blocker bullet points from the completion payload — used by the
|
|
249
|
+
* failure variant of the mission report.
|
|
250
|
+
*/
|
|
251
|
+
function extractBlockers(data) {
|
|
252
|
+
const out = [];
|
|
253
|
+
if (data?.error) out.push(String(data.error).slice(0, 160));
|
|
254
|
+
if (Array.isArray(data?.failed_tests)) {
|
|
255
|
+
for (const t of data.failed_tests.slice(0, 6)) {
|
|
256
|
+
if (typeof t === 'string') out.push(t);
|
|
257
|
+
else if (t?.name) out.push(`${t.name}${t.message ? ': ' + t.message : ''}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return out;
|
|
261
|
+
}
|
|
262
|
+
|
|
199
263
|
function printTurnSummary(toolCount, durationS, turnCost) {
|
|
200
264
|
const parts = [];
|
|
201
265
|
if (toolCount > 0) parts.push(`${toolCount} tools`);
|
|
202
266
|
if (durationS) parts.push(`${Number(durationS).toFixed(1)}s`);
|
|
203
|
-
if (turnCost > 0) parts.push(
|
|
267
|
+
if (turnCost > 0) parts.push(formatCredits(costToCredits(turnCost)));
|
|
204
268
|
if (parts.length > 0) {
|
|
205
269
|
process.stderr.write(`\n ${c.green('✓')} ${c.dim(parts.join(' · '))}\n`);
|
|
206
270
|
}
|
|
@@ -213,31 +277,25 @@ function updateStatusBar() {
|
|
|
213
277
|
// ── Tool Display Renderer ──
|
|
214
278
|
|
|
215
279
|
/**
|
|
216
|
-
* Render a tool call
|
|
217
|
-
*
|
|
280
|
+
* Render a tool call as the head of a Mission Control card — icon + label +
|
|
281
|
+
* args. The result arrives later via `renderToolResult` and is appended as a
|
|
282
|
+
* gutter line. Sub-agent calls are indented per session.inSubAgent.
|
|
218
283
|
*/
|
|
219
284
|
function renderToolCall(data) {
|
|
220
285
|
const tool = data?.tool || 'unknown';
|
|
221
|
-
const label = toolDisplayLabel(tool);
|
|
222
286
|
const args = data?.args || {};
|
|
223
|
-
const indent =
|
|
287
|
+
const indent = subAgentIndent();
|
|
288
|
+
const callId = data?.call_id || data?._callId || `${tool}:${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
224
289
|
|
|
225
|
-
const
|
|
290
|
+
const head = formatCardHead(tool, args, {
|
|
291
|
+
cwd: safeCwd(),
|
|
292
|
+
columns: process.stderr.columns || 120,
|
|
293
|
+
indent,
|
|
294
|
+
});
|
|
226
295
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
const maxSummary = Math.max(60, cols - label.length - 10);
|
|
231
|
-
let displaySummary = summary || '';
|
|
232
|
-
if (displaySummary.length > maxSummary) {
|
|
233
|
-
displaySummary = '...' + displaySummary.slice(-(maxSummary - 3));
|
|
234
|
-
}
|
|
235
|
-
const summaryStr = displaySummary
|
|
236
|
-
? c.gray('(') + (tool === 'shell'
|
|
237
|
-
? formatShellCommand(displaySummary, c)
|
|
238
|
-
: c.white(displaySummary)) + c.gray(')')
|
|
239
|
-
: '';
|
|
240
|
-
process.stderr.write(`\n${indent}${c.brand('⏺')} ${c.bold(label)}${summaryStr}\n`);
|
|
296
|
+
recordCard({ id: callId, tool, args, head, startedAt: Date.now() });
|
|
297
|
+
session.toolCounts[tool] = (session.toolCounts[tool] || 0) + 1;
|
|
298
|
+
process.stderr.write(`\n${head}\n`);
|
|
241
299
|
}
|
|
242
300
|
|
|
243
301
|
/**
|
|
@@ -251,77 +309,71 @@ function formatToolDuration(data) {
|
|
|
251
309
|
return ms < 1000 ? `${Math.round(ms)}ms` : `${(ms / 1000).toFixed(1)}s`;
|
|
252
310
|
}
|
|
253
311
|
|
|
254
|
-
function firstOutputLine(data) {
|
|
255
|
-
const output = data?.output_preview || data?.output || data?.message || '';
|
|
256
|
-
return String(output).split('\n').map(line => line.trim()).find(Boolean) || '';
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function fileTypeLabel(filePath) {
|
|
260
|
-
const ext = path.extname(filePath || '').toLowerCase();
|
|
261
|
-
if (ext === '.md' || ext === '.mdx') return 'Markdown';
|
|
262
|
-
if (ext === '.json' || ext === '.jsonl') return 'JSON';
|
|
263
|
-
if (ext === '.yaml' || ext === '.yml') return 'YAML';
|
|
264
|
-
if (ext === '.toml') return 'TOML';
|
|
265
|
-
if (ext === '.csv' || ext === '.tsv') return 'tabular data';
|
|
266
|
-
if (['.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.py', '.go', '.rs', '.java', '.rb', '.php', '.swift'].includes(ext)) {
|
|
267
|
-
return 'source';
|
|
268
|
-
}
|
|
269
|
-
return ext ? `${ext.slice(1).toUpperCase()} file` : 'file';
|
|
270
|
-
}
|
|
271
|
-
|
|
272
312
|
function renderToolResult(data, eventType = 'tool_result') {
|
|
273
313
|
if (!data) return;
|
|
274
|
-
const indent =
|
|
275
|
-
const gutter = `${indent}${
|
|
314
|
+
const indent = subAgentIndent();
|
|
315
|
+
const gutter = `${indent}${paint.text.dim('⎿')} `;
|
|
276
316
|
const callId = data.call_id || data._callId;
|
|
277
317
|
if (eventType === 'tool_done' && callId && _renderedToolResults.has(callId)) return;
|
|
278
318
|
if (callId) _renderedToolResults.add(callId);
|
|
319
|
+
|
|
320
|
+
const tool = data.tool || data._tool || '';
|
|
321
|
+
const durationMs = data?.duration_ms ?? (data?.duration_s != null ? data.duration_s * 1000 : null);
|
|
322
|
+
|
|
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;
|
|
279
334
|
const duration = formatToolDuration(data);
|
|
280
|
-
const
|
|
335
|
+
const tail = duration ? paint.text.dim(` · ${duration}`) : '';
|
|
336
|
+
process.stderr.write(`${gutter}${arrow} ${painter(text || 'done')}${tail}\n`);
|
|
281
337
|
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
process.stderr.write(`${gutter}${
|
|
285
|
-
return;
|
|
338
|
+
// Lint warnings stay visible alongside writes.
|
|
339
|
+
if ((tool === 'write_file' || tool === 'edit_file') && data.lint) {
|
|
340
|
+
process.stderr.write(`${gutter}${paint.state.warn('⚠ ' + String(data.lint).split('\n')[0].slice(0, 80))}\n`);
|
|
286
341
|
}
|
|
342
|
+
}
|
|
287
343
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
344
|
+
// ── Expand handler — `d`, `/last`, `/expand` ───────────────────────────
|
|
345
|
+
//
|
|
346
|
+
// All three call into the same renderer so output is consistent across
|
|
347
|
+
// keypress and slash-command paths. `expandLast` and `expandIndex` write
|
|
348
|
+
// directly to stderr.
|
|
349
|
+
|
|
350
|
+
function expandLast() {
|
|
351
|
+
const card = lastCard();
|
|
352
|
+
if (!card) {
|
|
353
|
+
process.stderr.write(` ${paint.text.dim('(no tool to expand yet)')}\n`);
|
|
291
354
|
return;
|
|
292
355
|
}
|
|
356
|
+
process.stderr.write('\n' + detailFor(card) + '\n\n');
|
|
357
|
+
}
|
|
293
358
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
} else if (tool === 'read_files') {
|
|
301
|
-
summary = 'Files read';
|
|
302
|
-
} else if (tool === 'search_code' || tool === 'list_files') {
|
|
303
|
-
const lines = String(data.output || '').split('\n').filter(line => line.trim()).length;
|
|
304
|
-
summary = lines > 0 ? `${lines} result${lines === 1 ? '' : 's'}` : 'No results';
|
|
305
|
-
} else if (tool === 'write_file' || tool === 'edit_file' || tool === 'write_project') {
|
|
306
|
-
summary = 'Updated';
|
|
307
|
-
} else if (tool === 'delete_file') {
|
|
308
|
-
summary = 'Deleted';
|
|
309
|
-
} else if (data.server_side) {
|
|
310
|
-
summary = firstOutputLine(data).slice(0, 100) || 'Completed server-side';
|
|
311
|
-
} else if (tool === 'shell') {
|
|
312
|
-
summary = firstOutputLine(data).slice(0, 100) || 'Command completed';
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const renderedSummary = tool === 'shell' ? c.green(summary) : c.white(summary);
|
|
316
|
-
process.stderr.write(`${gutter}${renderedSummary}${suffix}\n`);
|
|
317
|
-
|
|
318
|
-
// For writes, show lint warnings
|
|
319
|
-
if (tool === 'write_file' || tool === 'edit_file') {
|
|
320
|
-
const lint = data.lint;
|
|
321
|
-
if (lint) {
|
|
322
|
-
process.stderr.write(`${gutter}${c.yellow('⚠ ' + lint.split('\n')[0].slice(0, 80))}\n`);
|
|
359
|
+
function expandIndex(idxOrAll) {
|
|
360
|
+
if (idxOrAll === 'all') {
|
|
361
|
+
const cards = allCards();
|
|
362
|
+
if (!cards.length) {
|
|
363
|
+
process.stderr.write(` ${paint.text.dim('(no tools to expand yet)')}\n`);
|
|
364
|
+
return;
|
|
323
365
|
}
|
|
366
|
+
process.stderr.write('\n');
|
|
367
|
+
for (const c of cards) process.stderr.write(detailFor(c) + '\n');
|
|
368
|
+
process.stderr.write('\n');
|
|
369
|
+
return;
|
|
324
370
|
}
|
|
371
|
+
const card = getCard(idxOrAll);
|
|
372
|
+
if (!card) {
|
|
373
|
+
process.stderr.write(` ${paint.text.dim('(no card at index ' + idxOrAll + ')')}\n`);
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
process.stderr.write('\n' + detailFor(card) + '\n\n');
|
|
325
377
|
}
|
|
326
378
|
|
|
327
379
|
/**
|
|
@@ -410,6 +462,11 @@ function flushContent() {
|
|
|
410
462
|
function renderEvent(event) {
|
|
411
463
|
const { type, data } = event;
|
|
412
464
|
|
|
465
|
+
// Push every event into the orbit state machine before rendering so the
|
|
466
|
+
// bottom status bar reflects what is happening this very moment. The orbit
|
|
467
|
+
// module is a no-op when status-bar is not mounted (non-TTY, --headless).
|
|
468
|
+
if (_orbit) _orbit.onEvent(event);
|
|
469
|
+
|
|
413
470
|
switch (type) {
|
|
414
471
|
case 'status': {
|
|
415
472
|
const msg = data?.message || '';
|
|
@@ -422,6 +479,8 @@ function renderEvent(event) {
|
|
|
422
479
|
const text = data?.message || data?.text || '';
|
|
423
480
|
if (text && !text.startsWith('Processing')) {
|
|
424
481
|
startSpinner(text.slice(0, 80));
|
|
482
|
+
// Capture reasoning so /why can replay it.
|
|
483
|
+
session.lastReasoning = text;
|
|
425
484
|
}
|
|
426
485
|
break;
|
|
427
486
|
}
|
|
@@ -483,7 +542,7 @@ function renderEvent(event) {
|
|
|
483
542
|
case 'approval_denied': {
|
|
484
543
|
const reason = data?.reason || 'User denied';
|
|
485
544
|
const toolName = data?.tool || '';
|
|
486
|
-
const indent =
|
|
545
|
+
const indent = subAgentIndent();
|
|
487
546
|
process.stderr.write(`${indent}${c.red('✗')} ${c.dim(`Denied ${toolName}: ${reason}`)}\n`);
|
|
488
547
|
break;
|
|
489
548
|
}
|
|
@@ -577,21 +636,19 @@ function renderEvent(event) {
|
|
|
577
636
|
|
|
578
637
|
case 'sub_agent_start': {
|
|
579
638
|
stopSpinner();
|
|
580
|
-
session.inSubAgent = true;
|
|
581
639
|
const agentType = data?.type || 'sub-agent';
|
|
582
640
|
const model = data?.model || '';
|
|
583
641
|
const query = data?.query || '';
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
if (query) process.stderr.write(` ${c.gray('query:')} ${c.dim(query)}\n`);
|
|
642
|
+
process.stderr.write(renderSubAgentOpen({ type: agentType, model, query }) + '\n');
|
|
643
|
+
session.inSubAgent = inSubAgentBlock(); // kept for legacy readers
|
|
644
|
+
session.subAgentCounts[agentType] = (session.subAgentCounts[agentType] || 0) + 1;
|
|
588
645
|
startSpinner(`${agentType}: working...`);
|
|
589
646
|
break;
|
|
590
647
|
}
|
|
591
648
|
|
|
592
649
|
case 'sub_agent_tool': {
|
|
593
|
-
//
|
|
594
|
-
//
|
|
650
|
+
// The regular tool_call event renders the card, indented by the
|
|
651
|
+
// sub-agent stack depth. Just update the spinner text here.
|
|
595
652
|
const agentType = data?.type || 'sub-agent';
|
|
596
653
|
const tool = data?.tool || '';
|
|
597
654
|
if (tool) updateSpinner(`${agentType} → ${tool}`);
|
|
@@ -600,24 +657,25 @@ function renderEvent(event) {
|
|
|
600
657
|
|
|
601
658
|
case 'sub_agent_complete': {
|
|
602
659
|
stopSpinner();
|
|
603
|
-
session.inSubAgent = false;
|
|
604
660
|
const agentType = data?.type || 'sub-agent';
|
|
605
|
-
const model = data?.model || '';
|
|
606
|
-
const resultLen = data?.result_length || 0;
|
|
607
661
|
const usage = data?.usage || {};
|
|
608
662
|
const tokens = (usage.input_tokens || 0) + (usage.output_tokens || 0);
|
|
609
|
-
const
|
|
610
|
-
if (
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
663
|
+
const costUsd = usage.cost_usd ?? usage.total_cost_usd ?? data?.cost_usd ?? null;
|
|
664
|
+
if (typeof costUsd === 'number') session.savedUsd += costUsd;
|
|
665
|
+
const summary = data?.result_summary
|
|
666
|
+
|| (data?.result_length > 0 ? `${agentType} returned ${data.result_length} chars` : '');
|
|
667
|
+
process.stderr.write(renderSubAgentClose({
|
|
668
|
+
type: agentType,
|
|
669
|
+
success: data?.success !== false,
|
|
670
|
+
summary,
|
|
671
|
+
costUsd,
|
|
672
|
+
tokens,
|
|
673
|
+
durationS: data?.duration_s,
|
|
674
|
+
toolCalls: data?.tool_calls,
|
|
675
|
+
iterations: data?.iterations,
|
|
676
|
+
error: data?.error,
|
|
677
|
+
}) + '\n\n');
|
|
678
|
+
session.inSubAgent = inSubAgentBlock();
|
|
621
679
|
break;
|
|
622
680
|
}
|
|
623
681
|
|
|
@@ -654,6 +712,8 @@ function renderEvent(event) {
|
|
|
654
712
|
case 'complete': {
|
|
655
713
|
stopSpinner();
|
|
656
714
|
flushContent();
|
|
715
|
+
resetSubAgents();
|
|
716
|
+
session.inSubAgent = false;
|
|
657
717
|
|
|
658
718
|
const summary = data?.summary || '';
|
|
659
719
|
if (summary && !_renderedContentThisTurn) {
|
|
@@ -696,9 +756,38 @@ function renderEvent(event) {
|
|
|
696
756
|
|
|
697
757
|
session.lastTurnDuration = data?.duration_s || 0;
|
|
698
758
|
|
|
759
|
+
// Sync cumulative session cost into the orbit (status bar shows it).
|
|
760
|
+
if (_orbit) _orbit.onCost(session.totalCost);
|
|
761
|
+
|
|
699
762
|
// Compact turn summary
|
|
700
763
|
const tools = data?.tool_calls || session.toolCalls || 0;
|
|
701
|
-
|
|
764
|
+
|
|
765
|
+
// Mission report — replaces the trailing "Done" when the turn did real
|
|
766
|
+
// work (touched files or invoked tools). Plain chat turns keep the
|
|
767
|
+
// tight printTurnSummary so the report does not feel ceremonial.
|
|
768
|
+
const didRealWork = tools > 0 || session.filesChanged.length > 0;
|
|
769
|
+
if (didRealWork) {
|
|
770
|
+
const successOverall = data?.success !== false;
|
|
771
|
+
const report = renderMissionReport({
|
|
772
|
+
task: session.lastTask,
|
|
773
|
+
success: successOverall,
|
|
774
|
+
filesChanged: session.filesChanged,
|
|
775
|
+
toolCounts: session.toolCounts,
|
|
776
|
+
subAgents: { ...session.subAgentCounts, savedUsd: session.savedUsd },
|
|
777
|
+
costUsd: turnCost || session.totalCost,
|
|
778
|
+
durationS: data?.duration_s,
|
|
779
|
+
testsPass: data?.tests_passed != null
|
|
780
|
+
? { passed: data.tests_passed, total: data.tests_total || data.tests_passed }
|
|
781
|
+
: null,
|
|
782
|
+
blockers: !successOverall ? (data?.blockers || extractBlockers(data)) : null,
|
|
783
|
+
nextActions: successOverall
|
|
784
|
+
? ['/commit', '/pr', '/undo', '/report']
|
|
785
|
+
: ['/why', '/undo', '/re-plan'],
|
|
786
|
+
});
|
|
787
|
+
process.stderr.write(report + '\n');
|
|
788
|
+
} else {
|
|
789
|
+
printTurnSummary(tools, data?.duration_s, turnCost);
|
|
790
|
+
}
|
|
702
791
|
break;
|
|
703
792
|
}
|
|
704
793
|
|
|
@@ -737,7 +826,8 @@ async function handleCommand(input, ctx) {
|
|
|
737
826
|
process.stderr.write(` ${c.brand(name.padEnd(14))} ${desc}\n`);
|
|
738
827
|
}
|
|
739
828
|
process.stderr.write(`\n ${c.bold('Keyboard')}\n`);
|
|
740
|
-
process.stderr.write(` ${c.gray('Ctrl+C')} exit ${c.gray('↑↓')} history ${c.gray('Tab')} autocomplete\n
|
|
829
|
+
process.stderr.write(` ${c.gray('Ctrl+C')} exit ${c.gray('↑↓')} history ${c.gray('Tab')} autocomplete\n`);
|
|
830
|
+
process.stderr.write(` ${c.gray('d')} expand last tool ${c.gray('Space')} pause/resume ${c.gray('Esc')} interrupt\n\n`);
|
|
741
831
|
return;
|
|
742
832
|
|
|
743
833
|
case '/login':
|
|
@@ -783,7 +873,7 @@ async function handleCommand(input, ctx) {
|
|
|
783
873
|
process.stderr.write(` ${c.dim('Turns')} ${session.turns}\n`);
|
|
784
874
|
process.stderr.write(` ${c.dim('Tools')} ${session.totalToolCalls} total, ${session.toolCalls} last turn\n`);
|
|
785
875
|
process.stderr.write(` ${c.dim('Duration')} ${formatElapsed(session.startTime)}\n`);
|
|
786
|
-
process.stderr.write(` ${c.dim('
|
|
876
|
+
process.stderr.write(` ${c.dim('Credits')} ${formatCredits(costToCredits(session.totalCost))}${session.costAccurate ? '' : c.dim(' (est)')}\n`);
|
|
787
877
|
process.stderr.write(` ${c.dim('CWD')} ${safeCwd()}\n`);
|
|
788
878
|
|
|
789
879
|
// Permissions
|
|
@@ -851,29 +941,29 @@ async function handleCommand(input, ctx) {
|
|
|
851
941
|
process.stderr.write(` ${c.gray('Turns:')} ${session.turns}\n`);
|
|
852
942
|
process.stderr.write(` ${c.gray('Tools:')} ${session.toolCalls}\n`);
|
|
853
943
|
process.stderr.write(` ${c.gray('Blocked:')} ${session.blockedOps}\n`);
|
|
854
|
-
process.stderr.write(` ${c.gray('
|
|
944
|
+
process.stderr.write(` ${c.gray('Credits:')} ${formatCredits(costToCredits(session.totalCost))}${session.costAccurate ? '' : c.dim(' (est)')}\n`);
|
|
855
945
|
process.stderr.write(` ${c.gray('Elapsed:')} ${formatElapsed(session.startTime)}\n\n`);
|
|
856
946
|
return;
|
|
857
947
|
}
|
|
858
948
|
|
|
859
949
|
case '/cost': {
|
|
860
|
-
process.stderr.write(`\n ${c.bold('Session
|
|
950
|
+
process.stderr.write(`\n ${c.bold('Session Credits')} ${c.brand(formatCredits(costToCredits(session.totalCost)))}`);
|
|
861
951
|
if (!session.costAccurate) {
|
|
862
|
-
process.stderr.write(` ${c.yellow('(estimated
|
|
952
|
+
process.stderr.write(` ${c.yellow('(estimated)')}`);
|
|
863
953
|
}
|
|
864
954
|
process.stderr.write('\n');
|
|
865
955
|
process.stderr.write(` ${c.dim('─'.repeat(70))}\n`);
|
|
866
956
|
|
|
867
957
|
if (session.costBreakdown.length > 0) {
|
|
868
958
|
// Header
|
|
869
|
-
process.stderr.write(` ${c.dim('Model'.padEnd(36))}${c.dim('Input'.padStart(10))}${c.dim('Output'.padStart(10))}${c.dim('Cache'.padStart(10))}${c.dim('
|
|
959
|
+
process.stderr.write(` ${c.dim('Model'.padEnd(36))}${c.dim('Input'.padStart(10))}${c.dim('Output'.padStart(10))}${c.dim('Cache'.padStart(10))}${c.dim('Credits'.padStart(10))}\n`);
|
|
870
960
|
process.stderr.write(` ${c.dim('─'.repeat(70))}\n`);
|
|
871
961
|
|
|
872
962
|
for (const b of session.costBreakdown) {
|
|
873
963
|
const modelLabel = b.model === 'unknown' ? c.yellow('unknown model') : b.model;
|
|
874
964
|
const roleTag = b.role && b.role !== 'unknown' ? ` ${c.dim(`(${b.role})`)}` : '';
|
|
875
965
|
const cacheTokens = (b.cache_read_tokens || 0) + (b.cache_creation_tokens || 0);
|
|
876
|
-
const costStr = b.free ? c.green('free') :
|
|
966
|
+
const costStr = b.free ? c.green('free') : formatCredits(costToCredits(b.cost));
|
|
877
967
|
|
|
878
968
|
process.stderr.write(
|
|
879
969
|
` ${(modelLabel + roleTag).padEnd(36)}` +
|
|
@@ -892,9 +982,9 @@ async function handleCommand(input, ctx) {
|
|
|
892
982
|
`${formatTokens(session.inputTokens).padStart(10)}` +
|
|
893
983
|
`${formatTokens(session.outputTokens).padStart(10)}` +
|
|
894
984
|
`${''.padStart(10)}` +
|
|
895
|
-
`${
|
|
985
|
+
`${formatCredits(costToCredits(session.totalCost)).padStart(10)}\n`
|
|
896
986
|
);
|
|
897
|
-
process.stderr.write(` ${c.dim(`Turns: ${session.turns} Duration: ${formatElapsed(session.startTime)}`)}\n\n`);
|
|
987
|
+
process.stderr.write(` ${c.dim(`Turns: ${session.turns} Duration: ${formatElapsed(session.startTime)} Provider: ${formatCostValue(session.totalCost)}`)}\n\n`);
|
|
898
988
|
return;
|
|
899
989
|
}
|
|
900
990
|
|
|
@@ -909,6 +999,143 @@ async function handleCommand(input, ctx) {
|
|
|
909
999
|
process.stderr.write('\n');
|
|
910
1000
|
return;
|
|
911
1001
|
|
|
1002
|
+
case '/last':
|
|
1003
|
+
expandLast();
|
|
1004
|
+
return;
|
|
1005
|
+
|
|
1006
|
+
case '/expand': {
|
|
1007
|
+
const arg = rest.trim();
|
|
1008
|
+
if (!arg) { expandLast(); return; }
|
|
1009
|
+
if (arg === 'all') { expandIndex('all'); return; }
|
|
1010
|
+
const n = Number(arg);
|
|
1011
|
+
if (!Number.isFinite(n)) {
|
|
1012
|
+
process.stderr.write(` ${c.gray('Usage: /expand [n|all] — n is the 1-based index from the start of the session')}\n`);
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
// Users pass 1-based; getCard accepts negative (-1 = last) or positive index.
|
|
1016
|
+
expandIndex(n > 0 ? n - 1 : n);
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
case '/fold':
|
|
1021
|
+
process.stderr.write(` ${c.gray('Output is folded by default — there is nothing to hide. Use /last or d to expand.')}\n`);
|
|
1022
|
+
return;
|
|
1023
|
+
|
|
1024
|
+
case '/undo': {
|
|
1025
|
+
const result = ctx.checkpoints?.undo();
|
|
1026
|
+
if (!result) {
|
|
1027
|
+
process.stderr.write(` ${c.gray('No checkpoints to undo.')}\n`);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
if (result.restored) {
|
|
1031
|
+
process.stderr.write(` ${c.green('↩')} ${c.dim('Restored')} ${result.filePath}\n`);
|
|
1032
|
+
} else {
|
|
1033
|
+
process.stderr.write(` ${c.red('✗')} ${c.dim('Undo failed: ' + (result.error || 'unknown error'))}\n`);
|
|
1034
|
+
}
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
case '/checkpoint': {
|
|
1039
|
+
const list = ctx.checkpoints?.list(10) || [];
|
|
1040
|
+
if (!list.length) {
|
|
1041
|
+
process.stderr.write(` ${c.gray('No checkpoints recorded yet — they are taken automatically before each edit.')}\n`);
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
process.stderr.write(`\n ${c.bold('Recent checkpoints')}\n ${c.gray('─'.repeat(40))}\n`);
|
|
1045
|
+
for (const ckpt of list) {
|
|
1046
|
+
const when = String(ckpt.timestamp).slice(11, 19);
|
|
1047
|
+
process.stderr.write(` ${c.gray(when)} ${c.white(ckpt.file)} ${c.gray(formatTokens(ckpt.size) + ' bytes')}\n`);
|
|
1048
|
+
}
|
|
1049
|
+
process.stderr.write(`\n ${c.gray('/undo restores the most recent one')}\n\n`);
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
case '/preflight': {
|
|
1054
|
+
await runPreflight({ auth: ctx.auth, cwd: safeCwd(), version: VERSION });
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
case '/report': {
|
|
1059
|
+
if (Object.keys(session.toolCounts).length === 0 && session.filesChanged.length === 0) {
|
|
1060
|
+
process.stderr.write(` ${c.gray('Nothing to report yet — run a task first.')}\n`);
|
|
1061
|
+
return;
|
|
1062
|
+
}
|
|
1063
|
+
const state = {
|
|
1064
|
+
task: session.lastTask,
|
|
1065
|
+
success: true,
|
|
1066
|
+
filesChanged: session.filesChanged,
|
|
1067
|
+
toolCounts: session.toolCounts,
|
|
1068
|
+
subAgents: { ...session.subAgentCounts, savedUsd: session.savedUsd },
|
|
1069
|
+
costUsd: session.totalCost,
|
|
1070
|
+
durationS: (Date.now() - session.startTime) / 1000,
|
|
1071
|
+
nextActions: ['/commit', '/pr', '/undo'],
|
|
1072
|
+
};
|
|
1073
|
+
const out = saveReport(state, { cwd: safeCwd() });
|
|
1074
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Saved')} ${out}\n`);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
case '/why': {
|
|
1079
|
+
if (!session.lastReasoning) {
|
|
1080
|
+
process.stderr.write(` ${c.gray('No reasoning captured yet for this session.')}\n`);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
process.stderr.write(`\n ${c.bold('Last reasoning')}\n ${c.gray('─'.repeat(40))}\n`);
|
|
1084
|
+
for (const line of String(session.lastReasoning).split('\n')) {
|
|
1085
|
+
process.stderr.write(` ${c.dim(line)}\n`);
|
|
1086
|
+
}
|
|
1087
|
+
process.stderr.write('\n');
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
case '/map': {
|
|
1092
|
+
try {
|
|
1093
|
+
const resources = ctx.toolExecutor?.getProjectResources?.() || [];
|
|
1094
|
+
if (!resources.length) {
|
|
1095
|
+
process.stderr.write(` ${c.gray('No project resources registered yet. Use get_project_overview to register one.')}\n`);
|
|
1096
|
+
return;
|
|
1097
|
+
}
|
|
1098
|
+
process.stderr.write(`\n ${c.bold('Registered projects')}\n ${c.gray('─'.repeat(40))}\n`);
|
|
1099
|
+
for (const r of resources) {
|
|
1100
|
+
process.stderr.write(` ${c.brand('•')} ${c.white(r.id || r.name || '?')} ${c.dim(r.root || r.path || '')}\n`);
|
|
1101
|
+
}
|
|
1102
|
+
process.stderr.write('\n');
|
|
1103
|
+
} catch (err) {
|
|
1104
|
+
process.stderr.write(` ${c.red('/map failed: ' + err.message)}\n`);
|
|
1105
|
+
}
|
|
1106
|
+
return;
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
case '/budget': {
|
|
1110
|
+
const arg = rest.trim();
|
|
1111
|
+
if (!arg || arg === 'clear' || arg === 'off') {
|
|
1112
|
+
session.budgetUsd = null;
|
|
1113
|
+
session.budgetExceeded = false;
|
|
1114
|
+
process.stderr.write(` ${c.gray('Budget cap cleared.')}\n`);
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
const n = Number(arg.replace(/^\$/, ''));
|
|
1118
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
1119
|
+
process.stderr.write(` ${c.gray('Usage: /budget <amount in USD> or /budget clear')}\n`);
|
|
1120
|
+
return;
|
|
1121
|
+
}
|
|
1122
|
+
session.budgetUsd = n;
|
|
1123
|
+
session.budgetExceeded = false;
|
|
1124
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Budget set: ')} $${n.toFixed(2)}\n`);
|
|
1125
|
+
return;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
case '/quiet':
|
|
1129
|
+
case '/verbose':
|
|
1130
|
+
case '/surgical': {
|
|
1131
|
+
const mode = cmd === '/quiet' ? V_MODES.QUIET
|
|
1132
|
+
: cmd === '/verbose' ? V_MODES.VERBOSE
|
|
1133
|
+
: V_MODES.SURGICAL;
|
|
1134
|
+
setVerbosity(mode);
|
|
1135
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim('Verbosity: ')} ${c.brand(verbosityLabel(mode))}\n`);
|
|
1136
|
+
return;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
912
1139
|
case '/compact': {
|
|
913
1140
|
const before = session.history.length;
|
|
914
1141
|
if (before <= 4) { process.stderr.write(` ${c.gray('Nothing to compact.')}\n`); return; }
|
|
@@ -1133,7 +1360,9 @@ export async function startTerminalRepl() {
|
|
|
1133
1360
|
const auth = new TarangAuth();
|
|
1134
1361
|
|
|
1135
1362
|
// Projects are registered and indexed on demand through get_project_overview.
|
|
1136
|
-
|
|
1363
|
+
// CheckpointManager records per-file snapshots before edits so /undo works.
|
|
1364
|
+
const checkpoints = new CheckpointManager(safeCwd());
|
|
1365
|
+
const toolExecutor = createToolExecutor({ checkpoints });
|
|
1137
1366
|
const skipPerms = cliArgs.freeswim;
|
|
1138
1367
|
const approval = new ApprovalManager({ autoApprove: skipPerms });
|
|
1139
1368
|
|
|
@@ -1147,10 +1376,30 @@ export async function startTerminalRepl() {
|
|
|
1147
1376
|
// Persistent stream client — session_id captured from backend on first turn
|
|
1148
1377
|
let streamClient = null;
|
|
1149
1378
|
|
|
1150
|
-
const ctx = { auth, toolExecutor, approval, jsonlWriter, sessionMgr };
|
|
1379
|
+
const ctx = { auth, toolExecutor, approval, jsonlWriter, sessionMgr, checkpoints };
|
|
1380
|
+
|
|
1381
|
+
// ── Mission Control orbit + status bar ──
|
|
1382
|
+
// Opt-out via KEPLER_STATUS_BAR=0 (debugging) or KEPLER_PLAIN=1 (PRD-055).
|
|
1383
|
+
// status-bar.mjs already no-ops when stdout is not a TTY, but the explicit
|
|
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
|
+
}
|
|
1151
1393
|
|
|
1152
1394
|
printBanner(auth);
|
|
1153
1395
|
|
|
1396
|
+
// Preflight diagnostic (PRD-055 §9). Non-blocking; opt-out via
|
|
1397
|
+
// KEPLER_NO_PREFLIGHT=1 (used by tests / scripted runs).
|
|
1398
|
+
if (process.env.KEPLER_NO_PREFLIGHT !== '1' && !cliArgs.freeswim) {
|
|
1399
|
+
try { await runPreflight({ auth, cwd: safeCwd(), version: VERSION }); }
|
|
1400
|
+
catch { /* preflight is best-effort */ }
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1154
1403
|
// ── Initialization ──
|
|
1155
1404
|
process.stderr.write(` ${c.brand('⠋')} ${c.dim('Initializing...')}\r`);
|
|
1156
1405
|
await fetchUser(ctx);
|
|
@@ -1231,10 +1480,27 @@ export async function startTerminalRepl() {
|
|
|
1231
1480
|
return;
|
|
1232
1481
|
}
|
|
1233
1482
|
|
|
1483
|
+
// Budget cap (PRD-055 §10). Stop before the next paid call when exceeded.
|
|
1484
|
+
if (session.budgetUsd && session.totalCost >= session.budgetUsd) {
|
|
1485
|
+
session.budgetExceeded = true;
|
|
1486
|
+
process.stderr.write(` ${c.yellow('⏹')} ${c.dim(`Budget reached ($${session.totalCost.toFixed(2)} of $${session.budgetUsd.toFixed(2)}). Use /budget clear to continue.`)}\n`);
|
|
1487
|
+
showPrompt();
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1234
1491
|
// Regular prompt
|
|
1235
1492
|
session.history.push({ role: 'user', content: input });
|
|
1236
1493
|
session.turns++;
|
|
1237
1494
|
session.toolCalls = 0;
|
|
1495
|
+
session.lastTask = input;
|
|
1496
|
+
// Reset per-turn counts so the mission report reflects this turn only.
|
|
1497
|
+
session.toolCounts = {};
|
|
1498
|
+
session.subAgentCounts = {};
|
|
1499
|
+
session.savedUsd = 0;
|
|
1500
|
+
|
|
1501
|
+
// Tell the orbit a new turn started — switches to DISCOVERY and updates
|
|
1502
|
+
// task / turn counters in the status bar.
|
|
1503
|
+
if (_orbit) _orbit.onUserInput(input);
|
|
1238
1504
|
|
|
1239
1505
|
// Start session tracking on first turn
|
|
1240
1506
|
if (session.turns === 1) {
|
|
@@ -1273,6 +1539,7 @@ export async function startTerminalRepl() {
|
|
|
1273
1539
|
let executionPaused = false;
|
|
1274
1540
|
let keypressCleanup = null;
|
|
1275
1541
|
let execListenerActive = false;
|
|
1542
|
+
let lastCtrlCAt = 0; // PRD-055 §8.4: first Ctrl+C cancels, second exits
|
|
1276
1543
|
|
|
1277
1544
|
if (process.stdin.isTTY) {
|
|
1278
1545
|
rl.pause();
|
|
@@ -1299,20 +1566,39 @@ export async function startTerminalRepl() {
|
|
|
1299
1566
|
executionPaused = false;
|
|
1300
1567
|
process.stderr.write(` ${c.green('▶')} ${c.dim('Resumed')}\n`);
|
|
1301
1568
|
client.resume();
|
|
1569
|
+
if (_orbit) _orbit.onResume();
|
|
1302
1570
|
} else {
|
|
1303
1571
|
executionPaused = true;
|
|
1304
1572
|
stopSpinner();
|
|
1305
1573
|
process.stderr.write(` ${c.yellow('⏸')} ${c.dim('Paused — press Space to resume, Esc to cancel')}\n`);
|
|
1306
1574
|
client.pause();
|
|
1575
|
+
if (_orbit) _orbit.onPause();
|
|
1307
1576
|
}
|
|
1308
1577
|
return;
|
|
1309
1578
|
}
|
|
1310
1579
|
|
|
1311
|
-
// Ctrl+C during execution
|
|
1580
|
+
// Ctrl+C during execution — PRD-055 §8.4 two-step semantics:
|
|
1581
|
+
// first press → cancel current backend run, stay in REPL
|
|
1582
|
+
// second press within 2s → exit the CLI
|
|
1312
1583
|
if (bytes[0] === 0x03) {
|
|
1313
1584
|
stopSpinner();
|
|
1314
|
-
|
|
1315
|
-
|
|
1585
|
+
const now = Date.now();
|
|
1586
|
+
if (lastCtrlCAt && (now - lastCtrlCAt) < 2000) {
|
|
1587
|
+
process.stderr.write(`\n ${c.dim('exiting…')}\n`);
|
|
1588
|
+
try { client.cancel(); } catch {}
|
|
1589
|
+
process.exit(0);
|
|
1590
|
+
}
|
|
1591
|
+
lastCtrlCAt = now;
|
|
1592
|
+
process.stderr.write(`\n ${c.yellow('⏹')} ${c.dim('Cancelled. Press Ctrl+C again within 2s to exit.')}\n`);
|
|
1593
|
+
try { client.cancel(); } catch {}
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
// `d` — expand last tool card (Mission Control §6.2)
|
|
1598
|
+
if (bytes.length === 1 && (bytes[0] === 0x64 || bytes[0] === 0x44)) {
|
|
1599
|
+
stopSpinner();
|
|
1600
|
+
expandLast();
|
|
1601
|
+
return;
|
|
1316
1602
|
}
|
|
1317
1603
|
};
|
|
1318
1604
|
|