@axplusb/kepler 2.0.7 → 2.2.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 +2 -2
- package/src/config/hook-runner.mjs +100 -0
- package/src/config/memory-loader.mjs +32 -0
- package/src/config/settings-loader.mjs +45 -0
- package/src/core/approval-log.mjs +104 -0
- package/src/core/approval.mjs +164 -24
- package/src/core/context-envelope.mjs +54 -0
- package/src/core/headless.mjs +14 -1
- package/src/core/jsonl-writer.mjs +50 -0
- package/src/core/local-store.mjs +486 -5
- package/src/core/policy-resolver.mjs +156 -0
- package/src/core/project-context-loader.mjs +139 -0
- package/src/core/rate-limit-display.mjs +97 -0
- package/src/core/resume-mode.mjs +154 -0
- package/src/core/risk-tier.mjs +88 -2
- package/src/core/safety.mjs +3 -0
- package/src/core/session-manager.mjs +53 -10
- package/src/core/stream-client.mjs +69 -10
- package/src/core/system-prompt.mjs +6 -1
- package/src/core/tasks.mjs +130 -0
- package/src/core/tool-executor.mjs +72 -6
- package/src/core/trust.mjs +158 -0
- package/src/onboarding/preflight.mjs +27 -11
- package/src/permissions/command-classifier.mjs +78 -0
- package/src/terminal/init.mjs +145 -0
- package/src/terminal/main.mjs +7 -0
- package/src/terminal/repl.mjs +1735 -140
- package/src/tools/bash.mjs +57 -9
- package/src/tools/project-overview.mjs +83 -10
- package/src/ui/approval.mjs +154 -35
- package/src/ui/formatter.mjs +39 -5
- package/src/ui/mission-report.mjs +55 -22
- package/src/ui/slash-commands.mjs +57 -5
- package/src/ui/tool-card.mjs +51 -1
package/src/terminal/repl.mjs
CHANGED
|
@@ -17,12 +17,14 @@
|
|
|
17
17
|
|
|
18
18
|
import * as readline from 'node:readline';
|
|
19
19
|
import * as fs from 'node:fs';
|
|
20
|
+
import * as path from 'node:path';
|
|
20
21
|
import { c, progressBar, spinner, inPlace, renderMarkdown, renderDiff, formatElapsed, formatCost, stripAnsi } from './ansi.mjs';
|
|
21
22
|
import { calculateCost, formatCostValue, formatTokens, costToCredits, formatCredits } from '../core/pricing.mjs';
|
|
22
23
|
import { TarangStreamClient, EVENT_TYPES } from '../core/stream-client.mjs';
|
|
23
24
|
import { JsonlWriter } from '../core/jsonl-writer.mjs';
|
|
24
25
|
import { createToolExecutor } from '../core/tool-executor.mjs';
|
|
25
26
|
import { CheckpointManager } from '../core/checkpoints.mjs';
|
|
27
|
+
import { HookRunner } from '../config/hook-runner.mjs';
|
|
26
28
|
import { runPreflight } from '../onboarding/preflight.mjs';
|
|
27
29
|
import { printBanner as printBrandedBanner } from '../ui/banner.mjs';
|
|
28
30
|
import { renderMissionReport, saveReport, toMarkdown as missionMarkdown } from '../ui/mission-report.mjs';
|
|
@@ -36,10 +38,17 @@ import { persistProjectArtifacts } from '../core/project-artifacts.mjs';
|
|
|
36
38
|
import { TarangAuth } from '../auth/tarang-auth.mjs';
|
|
37
39
|
import { ApprovalManager } from '../core/approval.mjs';
|
|
38
40
|
import { resolveBackendUrl } from '../core/backend-url.mjs';
|
|
41
|
+
import { formatMessageWindow, lowWindowStatus, messagesRemaining } from '../core/rate-limit-display.mjs';
|
|
39
42
|
import { BUILTIN_AGENTS, runAgent } from './agents.mjs';
|
|
40
43
|
import { SessionManager } from '../core/session-manager.mjs';
|
|
41
44
|
import { parseArgs } from '../config/cli-args.mjs';
|
|
42
|
-
import {
|
|
45
|
+
import { loadEffectivePolicy, formatPolicySourceRows } from '../core/policy-resolver.mjs';
|
|
46
|
+
import { loadProjectContext } from '../core/project-context-loader.mjs';
|
|
47
|
+
import { buildContextEnvelope } from '../core/context-envelope.mjs';
|
|
48
|
+
import { buildResumeHistory, combineResumeSummaries, getRecentSessions, getSessionDetail, getTranscriptProjectRoots } from '../core/local-store.mjs';
|
|
49
|
+
import { decideResumeMode, projectedTokensForChoice, formatTokens as formatCtxTokens } from '../core/resume-mode.mjs';
|
|
50
|
+
import { appendTask, ensureTaskFiles, loadTaskBoard, taskCounts, TASK_FILES } from '../core/tasks.mjs';
|
|
51
|
+
import { toolDisplayLabel, toolDisplaySummary } from './tool-display.mjs';
|
|
43
52
|
import { createOrbit } from '../state/orbit.mjs';
|
|
44
53
|
import { attachOrbit, unmount as unmountStatusBar } from '../ui/status-bar.mjs';
|
|
45
54
|
import { term } from '../ui/term.mjs';
|
|
@@ -89,6 +98,667 @@ function safeCwd() {
|
|
|
89
98
|
}
|
|
90
99
|
}
|
|
91
100
|
|
|
101
|
+
function messageCountLabel(count) {
|
|
102
|
+
return `${count} ${count === 1 ? 'message' : 'messages'}`;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function sessionListTimestamp(s) {
|
|
106
|
+
const value = s.updatedAt || s.startedAt;
|
|
107
|
+
return value ? new Date(value).toLocaleString() : '?';
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function oneLineInstruction(text, max = 72) {
|
|
111
|
+
const compact = String(text || '(no instruction)').replace(/\s+/g, ' ').trim();
|
|
112
|
+
return compact.length > max ? compact.slice(0, max - 3) + '...' : compact;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function fitAnsiLine(text, maxColumns) {
|
|
116
|
+
const max = Math.max(1, Number(maxColumns) || 80);
|
|
117
|
+
const value = String(text || '');
|
|
118
|
+
if (stripAnsi(value).length <= max) return value;
|
|
119
|
+
|
|
120
|
+
let visible = 0;
|
|
121
|
+
let out = '';
|
|
122
|
+
for (let i = 0; i < value.length; i++) {
|
|
123
|
+
if (value[i] === '\x1b') {
|
|
124
|
+
const match = value.slice(i).match(/^\x1b\[[0-9;]*m/);
|
|
125
|
+
if (match) {
|
|
126
|
+
out += match[0];
|
|
127
|
+
i += match[0].length - 1;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
if (visible >= max - 1) break;
|
|
132
|
+
out += value[i];
|
|
133
|
+
visible++;
|
|
134
|
+
}
|
|
135
|
+
return `${out}${c.dim('…')}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function normalizeResumableSession(s) {
|
|
139
|
+
return {
|
|
140
|
+
sessionId: s.sessionId,
|
|
141
|
+
instruction: s.firstPrompt || s.instruction || '(no instruction)',
|
|
142
|
+
startedAt: s.startTime || s.startedAt || '',
|
|
143
|
+
updatedAt: s.endTime || s.updatedAt || (s.mtime ? new Date(s.mtime).toISOString() : ''),
|
|
144
|
+
project: s.project ? path.basename(s.project) : s.projectName || s.project || '',
|
|
145
|
+
projectPath: s.project || s.projectPath || '',
|
|
146
|
+
transcriptPath: s.filePath || s.transcriptPath || '',
|
|
147
|
+
messageCount: (s.userMessages || 0) + (s.assistantMessages || 0),
|
|
148
|
+
// PRD-068 §5.14.11 derived fields for the picker
|
|
149
|
+
endStatus: s.endStatus || 'unknown', // 'completed' | 'interrupted' | 'errored' | 'unknown'
|
|
150
|
+
contextTokens: s.contextTokens || 0, // projected transcript token count
|
|
151
|
+
resumeSummary: s.resumeSummary || null, // latest resume_summary checkpoint metadata
|
|
152
|
+
costUsd: typeof s.costUsd === 'number' ? s.costUsd : 0,
|
|
153
|
+
partial: !!s.partial, // true if the transcript file was partially malformed
|
|
154
|
+
source: 'transcript',
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function listResumableSessions() {
|
|
159
|
+
// PRD-068 §5.14.6: JSONL is the single source of truth. The legacy
|
|
160
|
+
// per-project state-only entries never had a transcript, so they can't be
|
|
161
|
+
// replayed — silently dropping them removes a source of "picked a session
|
|
162
|
+
// and got a flat history" surprises.
|
|
163
|
+
const rich = (await getRecentSessions(Infinity)).map(normalizeResumableSession);
|
|
164
|
+
return rich.sort((a, b) => {
|
|
165
|
+
const at = Date.parse(a.updatedAt || a.startedAt || 0) || 0;
|
|
166
|
+
const bt = Date.parse(b.updatedAt || b.startedAt || 0) || 0;
|
|
167
|
+
return bt - at;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── PRD-068 §5.14 helpers ────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
function endStatusMarker(status) {
|
|
174
|
+
switch (status) {
|
|
175
|
+
case 'completed': return c.green('✓');
|
|
176
|
+
case 'interrupted': return c.yellow('⚠');
|
|
177
|
+
case 'errored': return c.red('✗');
|
|
178
|
+
default: return c.dim('·');
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function formatSessionCost(usd) {
|
|
183
|
+
const n = Number(usd);
|
|
184
|
+
if (!Number.isFinite(n) || n <= 0) return c.dim(' ');
|
|
185
|
+
if (n < 0.01) return c.dim('<$0.01');
|
|
186
|
+
return c.dim(`$${n.toFixed(2)}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function formatResumeCheckpointStatus(session) {
|
|
190
|
+
const marker = session?.resumeSummary;
|
|
191
|
+
if (!marker || !Number(marker.sourceMessageCount)) return '';
|
|
192
|
+
const full = Number(marker.fullMessageCount) || 0;
|
|
193
|
+
const covered = Number(marker.sourceMessageCount) || 0;
|
|
194
|
+
const pct = full > 0 ? ` ${Math.min(100, Math.round((covered / full) * 100))}%` : '';
|
|
195
|
+
return c.dim(` · summarized${pct}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function formatRelativeTime(iso) {
|
|
199
|
+
if (!iso) return '';
|
|
200
|
+
const t = Date.parse(iso);
|
|
201
|
+
if (!Number.isFinite(t)) return '';
|
|
202
|
+
const ago = Date.now() - t;
|
|
203
|
+
const m = Math.floor(ago / 60_000);
|
|
204
|
+
if (m < 1) return 'just now';
|
|
205
|
+
if (m < 60) return `${m}m ago`;
|
|
206
|
+
const h = Math.floor(m / 60);
|
|
207
|
+
if (h < 24) return `${h}h ago`;
|
|
208
|
+
const d = Math.floor(h / 24);
|
|
209
|
+
if (d < 30) return `${d}d ago`;
|
|
210
|
+
const mo = Math.floor(d / 30);
|
|
211
|
+
return `${mo}mo ago`;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* PRD-068 §5.14.2 — enriched one-prompt picker.
|
|
216
|
+
* Returns the picked session, `null` on cancel, or `{ action: 'preview', session }`
|
|
217
|
+
* when the user hits P.
|
|
218
|
+
*/
|
|
219
|
+
async function pickResumableSession(resumable, ctx) {
|
|
220
|
+
const rl = ctx._rl || null;
|
|
221
|
+
if (rl) rl.pause();
|
|
222
|
+
|
|
223
|
+
return await new Promise((resolve) => {
|
|
224
|
+
if (!process.stdin.isTTY) { resolve(null); return; }
|
|
225
|
+
const wasRaw = process.stdin.isRaw;
|
|
226
|
+
const pageSize = Math.min(10, resumable.length);
|
|
227
|
+
const numWidth = String(resumable.length).length;
|
|
228
|
+
let selected = 0;
|
|
229
|
+
let offset = 0;
|
|
230
|
+
let renderedLines = 0;
|
|
231
|
+
|
|
232
|
+
const renderMenu = () => {
|
|
233
|
+
if (renderedLines > 0) {
|
|
234
|
+
process.stderr.write(`\x1b[${renderedLines}F\r\x1b[J`);
|
|
235
|
+
}
|
|
236
|
+
if (selected < offset) offset = selected;
|
|
237
|
+
if (selected >= offset + pageSize) offset = selected - pageSize + 1;
|
|
238
|
+
|
|
239
|
+
const cols = Math.max(60, process.stderr.columns || 120);
|
|
240
|
+
const lines = [];
|
|
241
|
+
lines.push(` ${c.bold('Resume a session')}`);
|
|
242
|
+
lines.push('');
|
|
243
|
+
const end = Math.min(offset + pageSize, resumable.length);
|
|
244
|
+
for (let i = offset; i < end; i++) {
|
|
245
|
+
const s = resumable[i];
|
|
246
|
+
const marker = i === selected ? c.brand('▸') : ' ';
|
|
247
|
+
const num = c.dim(`[${String(i + 1).padStart(numWidth, ' ')}]`);
|
|
248
|
+
const project = (s.project || '(unknown)').padEnd(18, ' ').slice(0, 18);
|
|
249
|
+
const ago = formatRelativeTime(s.updatedAt || s.startedAt).padEnd(9, ' ').slice(0, 9);
|
|
250
|
+
const status = endStatusMarker(s.endStatus);
|
|
251
|
+
const msgs = String(s.messageCount).padStart(3, ' ') + ' msgs';
|
|
252
|
+
const ctx = c.dim(`${formatCtxTokens(s.contextTokens).padStart(5, ' ')} ctx`) + formatResumeCheckpointStatus(s);
|
|
253
|
+
const cost = formatSessionCost(s.costUsd);
|
|
254
|
+
const partial = s.partial ? c.yellow(' ⚠partial') : '';
|
|
255
|
+
const instr = oneLineInstruction(s.instruction, 48);
|
|
256
|
+
lines.push(fitAnsiLine(
|
|
257
|
+
` ${marker} ${num} ${c.brand(project)} ${c.dim(ago)} ${status} ${c.dim(msgs)} ${ctx} ${cost}${partial} ${c.dim(instr)}`,
|
|
258
|
+
cols - 1
|
|
259
|
+
));
|
|
260
|
+
}
|
|
261
|
+
lines.push('');
|
|
262
|
+
lines.push(fitAnsiLine(
|
|
263
|
+
` ${c.dim(`↑↓ move · Enter resume · P preview · Esc cancel · ${selected + 1}/${resumable.length}`)}`,
|
|
264
|
+
cols - 1
|
|
265
|
+
));
|
|
266
|
+
process.stderr.write(lines.join('\n') + '\n');
|
|
267
|
+
renderedLines = lines.length;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
const cleanup = (value) => {
|
|
271
|
+
process.stdin.removeListener('data', onData);
|
|
272
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
273
|
+
if (rl) rl.resume();
|
|
274
|
+
resolve(value);
|
|
275
|
+
};
|
|
276
|
+
const onData = (data) => {
|
|
277
|
+
const key = data.toString('utf8');
|
|
278
|
+
if (key === '' || key === '') { cleanup(null); return; }
|
|
279
|
+
if (key === '\r' || key === '\n') { cleanup({ action: 'resume', session: resumable[selected] }); return; }
|
|
280
|
+
if (key === 'p' || key === 'P') { cleanup({ action: 'preview', session: resumable[selected] }); return; }
|
|
281
|
+
if (key === '[A') { selected = Math.max(0, selected - 1); renderMenu(); return; }
|
|
282
|
+
if (key === '[B') { selected = Math.min(resumable.length - 1, selected + 1); renderMenu(); return; }
|
|
283
|
+
if (key === '[5~') { selected = Math.max(0, selected - pageSize); renderMenu(); return; }
|
|
284
|
+
if (key === '[6~') { selected = Math.min(resumable.length - 1, selected + pageSize); renderMenu(); return; }
|
|
285
|
+
if (key === '[H' || key === '[1~') { selected = 0; renderMenu(); return; }
|
|
286
|
+
if (key === '[F' || key === '[4~') { selected = resumable.length - 1; renderMenu(); return; }
|
|
287
|
+
if (/^[1-9]$/.test(key)) {
|
|
288
|
+
const index = Number(key) - 1;
|
|
289
|
+
if (index < resumable.length) cleanup({ action: 'resume', session: resumable[index] });
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
process.stdin.setRawMode(true);
|
|
294
|
+
process.stdin.resume();
|
|
295
|
+
process.stdin.on('data', onData);
|
|
296
|
+
renderMenu();
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* PRD-068 §5.14.4 — tri-choice overlay shown only when projected ctx > highWatermark.
|
|
302
|
+
* Returns 'full' | 'summary' | 'tail-10' | 'tail-20' | null (cancel).
|
|
303
|
+
*/
|
|
304
|
+
async function chooseThresholdMode(ctx, decision) {
|
|
305
|
+
if (!process.stdin.isTTY) return decision.defaultChoice;
|
|
306
|
+
const rl = ctx._rl || null;
|
|
307
|
+
if (rl) rl.pause();
|
|
308
|
+
|
|
309
|
+
const canFull = decision.mode !== 'no-full-allowed';
|
|
310
|
+
const options = [
|
|
311
|
+
{ key: 'f', value: 'full', label: 'full transcript', enabled: canFull },
|
|
312
|
+
{ key: 's', value: 'summary', label: 'summary only', enabled: true },
|
|
313
|
+
{ key: '1', value: 'tail-10', label: 'summary + last 10 turns', enabled: true },
|
|
314
|
+
{ key: '2', value: 'tail-20', label: 'summary + last 20 turns', enabled: true },
|
|
315
|
+
];
|
|
316
|
+
let selected = options.findIndex(o => o.value === decision.defaultChoice && o.enabled);
|
|
317
|
+
if (selected < 0) selected = options.findIndex(o => o.enabled);
|
|
318
|
+
|
|
319
|
+
return await new Promise((resolve) => {
|
|
320
|
+
const wasRaw = process.stdin.isRaw;
|
|
321
|
+
let renderedLines = 0;
|
|
322
|
+
|
|
323
|
+
const render = () => {
|
|
324
|
+
if (renderedLines > 0) process.stderr.write(`\x1b[${renderedLines}F\r\x1b[J`);
|
|
325
|
+
const cols = Math.max(60, process.stderr.columns || 120);
|
|
326
|
+
const pct = Math.round(decision.usageRatio * 100);
|
|
327
|
+
const projected = formatCtxTokens(decision.projected);
|
|
328
|
+
const win = formatCtxTokens(decision.windowSize);
|
|
329
|
+
const lines = [];
|
|
330
|
+
lines.push(` This session would use ${c.brand(`${projected} / ${win}`)} tokens (${pct}%)`);
|
|
331
|
+
if (decision.resumeSummary?.sourceMessageCount) {
|
|
332
|
+
const covered = Number(decision.resumeSummary.sourceMessageCount) || 0;
|
|
333
|
+
const full = Number(decision.resumeSummary.fullMessageCount) || 0;
|
|
334
|
+
const suffix = full > 0 ? ` (${covered}/${full} resume messages)` : '';
|
|
335
|
+
lines.push(` ${c.green('✓')} ${c.dim(`summary checkpoint available${suffix}; summary/tail modes reuse it`)}`);
|
|
336
|
+
}
|
|
337
|
+
lines.push(canFull
|
|
338
|
+
? ` ${c.yellow('⚠')} ${c.dim('close to the highWatermark — consider a leaner mode:')}`
|
|
339
|
+
: ` ${c.red('⛔')} ${c.dim('over hardCap — full mode disabled:')}`);
|
|
340
|
+
lines.push('');
|
|
341
|
+
for (let i = 0; i < options.length; i++) {
|
|
342
|
+
const o = options[i];
|
|
343
|
+
const disabled = !o.enabled;
|
|
344
|
+
const marker = i === selected && !disabled ? c.brand('▸') : ' ';
|
|
345
|
+
const keyTag = c.dim('[') + (disabled ? c.dim(o.key) : c.brand(o.key)) + c.dim(']');
|
|
346
|
+
const proj = formatCtxTokens(projectedTokensForChoice(o.value, decision.projected));
|
|
347
|
+
const label = disabled ? c.dim(o.label) : (i === selected ? c.brand(o.label) : o.label);
|
|
348
|
+
const projCol = c.dim(`${proj.padStart(5, ' ')} ctx`);
|
|
349
|
+
const suffix = disabled ? c.dim(' (over hardCap)') : '';
|
|
350
|
+
lines.push(fitAnsiLine(` ${marker} ${keyTag} ${label.padEnd(30, ' ')} ${projCol}${suffix}`, cols - 1));
|
|
351
|
+
}
|
|
352
|
+
lines.push('');
|
|
353
|
+
lines.push(fitAnsiLine(` ${c.dim('↑↓ move · Enter pick · f/s/1/2 shortcut · Esc cancel')}`, cols - 1));
|
|
354
|
+
process.stderr.write(lines.join('\n') + '\n');
|
|
355
|
+
renderedLines = lines.length;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const cleanup = (value) => {
|
|
359
|
+
process.stdin.removeListener('data', onData);
|
|
360
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
361
|
+
if (rl) rl.resume();
|
|
362
|
+
resolve(value);
|
|
363
|
+
};
|
|
364
|
+
const onData = (data) => {
|
|
365
|
+
const key = data.toString('utf8');
|
|
366
|
+
const low = key.toLowerCase();
|
|
367
|
+
if (key === '' || key === '') { cleanup(null); return; }
|
|
368
|
+
if (key === '\r' || key === '\n') { cleanup(options[selected]?.value || null); return; }
|
|
369
|
+
if (key === '[A') {
|
|
370
|
+
// step to previous enabled option
|
|
371
|
+
for (let i = selected - 1; i >= 0; i--) if (options[i].enabled) { selected = i; render(); return; }
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (key === '[B') {
|
|
375
|
+
for (let i = selected + 1; i < options.length; i++) if (options[i].enabled) { selected = i; render(); return; }
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
for (let i = 0; i < options.length; i++) {
|
|
379
|
+
if (options[i].key === low && options[i].enabled) { cleanup(options[i].value); return; }
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
process.stdin.setRawMode(true);
|
|
384
|
+
process.stdin.resume();
|
|
385
|
+
process.stdin.on('data', onData);
|
|
386
|
+
render();
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function resumeModeLabel(mode = 'full') {
|
|
391
|
+
if (mode === 'summary') return 'summary only';
|
|
392
|
+
const tailTurns = resumeTailTurnCount(mode);
|
|
393
|
+
if (tailTurns) return `summary + last ${tailTurns} turns`;
|
|
394
|
+
return mode || 'full';
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* PRD-068 §5.14.5 — preview overlay for a session/mode. Read-only, `q` to return.
|
|
399
|
+
* If user hits Enter, resolve to the currently-previewed mode so caller can activate.
|
|
400
|
+
*/
|
|
401
|
+
async function previewResumeSession(session, ctx) {
|
|
402
|
+
if (!process.stdin.isTTY) return null;
|
|
403
|
+
const detail = await getSessionDetail(session.sessionId, { filePath: session.transcriptPath });
|
|
404
|
+
if (!detail) return null;
|
|
405
|
+
|
|
406
|
+
const rl = ctx._rl || null;
|
|
407
|
+
if (rl) rl.pause();
|
|
408
|
+
|
|
409
|
+
let mode = 'summary';
|
|
410
|
+
const rich = () => buildResumeHistory({ ...detail, recapTailTurns: 8 }, mode);
|
|
411
|
+
let history = rich();
|
|
412
|
+
|
|
413
|
+
return await new Promise((resolve) => {
|
|
414
|
+
const wasRaw = process.stdin.isRaw;
|
|
415
|
+
let renderedLines = 0;
|
|
416
|
+
let scrollOffset = 0;
|
|
417
|
+
|
|
418
|
+
const render = () => {
|
|
419
|
+
if (renderedLines > 0) process.stderr.write(`\x1b[${renderedLines}F\r\x1b[J`);
|
|
420
|
+
const cols = Math.max(60, process.stderr.columns || 120);
|
|
421
|
+
const rows = Math.max(10, Math.min((process.stderr.rows || 30) - 6, 20));
|
|
422
|
+
const contentLines = (history.summary || '').split('\n');
|
|
423
|
+
// For tail/full modes, also append serialized tail so preview reflects
|
|
424
|
+
// what the agent will actually receive.
|
|
425
|
+
if (mode !== 'summary') {
|
|
426
|
+
contentLines.push('', c.dim('── conversation tail ──'));
|
|
427
|
+
for (const msg of history.agentHistory.slice(1)) {
|
|
428
|
+
contentLines.push(`${msg.role === 'user' ? c.dim('You:') : c.brand('Kepler:')} ${String(msg.content).slice(0, 300)}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
const totalLines = contentLines.length;
|
|
432
|
+
const maxOffset = Math.max(0, totalLines - rows);
|
|
433
|
+
if (scrollOffset > maxOffset) scrollOffset = maxOffset;
|
|
434
|
+
|
|
435
|
+
const lines = [];
|
|
436
|
+
lines.push(` ${c.bold('Preview:')} ${c.brand(session.project || '(unknown)')} ${c.dim('Mode:')} ${c.brand(resumeModeLabel(mode))} ${c.dim(formatCtxTokens(projectedTokensForChoice(mode, session.contextTokens)) + ' ctx')}`);
|
|
437
|
+
lines.push(` ${c.dim('─'.repeat(60))}`);
|
|
438
|
+
for (let i = scrollOffset; i < Math.min(scrollOffset + rows, totalLines); i++) {
|
|
439
|
+
lines.push(fitAnsiLine(` ${c.dim(contentLines[i] || '')}`, cols - 1));
|
|
440
|
+
}
|
|
441
|
+
lines.push('');
|
|
442
|
+
lines.push(fitAnsiLine(` ${c.dim(`↑↓/PgUp/PgDn scroll · f/s/1/2 switch mode · Enter resume this · q back · ${scrollOffset + 1}-${Math.min(scrollOffset + rows, totalLines)}/${totalLines}`)}`, cols - 1));
|
|
443
|
+
process.stderr.write(lines.join('\n') + '\n');
|
|
444
|
+
renderedLines = lines.length;
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const cleanup = (value) => {
|
|
448
|
+
process.stdin.removeListener('data', onData);
|
|
449
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
450
|
+
if (rl) rl.resume();
|
|
451
|
+
resolve(value);
|
|
452
|
+
};
|
|
453
|
+
const onData = (data) => {
|
|
454
|
+
const key = data.toString('utf8');
|
|
455
|
+
const low = key.toLowerCase();
|
|
456
|
+
if (key === '' || key === '' || low === 'q') { cleanup({ action: 'back' }); return; }
|
|
457
|
+
if (key === '\r' || key === '\n') { cleanup({ action: 'resume', mode }); return; }
|
|
458
|
+
if (key === '[A') { scrollOffset = Math.max(0, scrollOffset - 1); render(); return; }
|
|
459
|
+
if (key === '[B') { scrollOffset += 1; render(); return; }
|
|
460
|
+
if (key === '[5~') { scrollOffset = Math.max(0, scrollOffset - 10); render(); return; }
|
|
461
|
+
if (key === '[6~') { scrollOffset += 10; render(); return; }
|
|
462
|
+
if (low === 'f') { mode = 'full'; history = rich(); scrollOffset = 0; render(); return; }
|
|
463
|
+
if (low === 's') { mode = 'summary'; history = rich(); scrollOffset = 0; render(); return; }
|
|
464
|
+
if (low === '1') { mode = 'tail-10'; history = rich(); scrollOffset = 0; render(); return; }
|
|
465
|
+
if (low === '2') { mode = 'tail-20'; history = rich(); scrollOffset = 0; render(); return; }
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
process.stdin.setRawMode(true);
|
|
469
|
+
process.stdin.resume();
|
|
470
|
+
process.stdin.on('data', onData);
|
|
471
|
+
render();
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* PRD-068 §5.14.7 — explicit cwd confirmation when the picked session lives
|
|
477
|
+
* elsewhere. Returns 'switch' | 'stay' | 'cancel'.
|
|
478
|
+
*/
|
|
479
|
+
async function confirmCwdSwitch(ctx, savedPath, currentPath) {
|
|
480
|
+
if (!process.stdin.isTTY) return 'switch';
|
|
481
|
+
process.stderr.write(`\n ${c.dim('This session lives in another repo:')}\n`);
|
|
482
|
+
process.stderr.write(` ${c.dim('→')} ${c.brand(savedPath)} ${c.dim(`(current cwd: ${currentPath})`)}\n`);
|
|
483
|
+
process.stderr.write(` ${c.dim('[Enter]')} switch cwd and resume · ${c.dim('[s]')} stay here and resume anyway · ${c.dim('[n]')} cancel `);
|
|
484
|
+
const rl = ctx._rl || null;
|
|
485
|
+
if (rl) rl.pause();
|
|
486
|
+
return await new Promise((resolve) => {
|
|
487
|
+
const wasRaw = process.stdin.isRaw;
|
|
488
|
+
const cleanup = (value) => {
|
|
489
|
+
process.stdin.removeListener('data', onData);
|
|
490
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
491
|
+
if (rl) rl.resume();
|
|
492
|
+
process.stderr.write('\n');
|
|
493
|
+
resolve(value);
|
|
494
|
+
};
|
|
495
|
+
const onData = (data) => {
|
|
496
|
+
const key = data.toString('utf8').toLowerCase();
|
|
497
|
+
if (key === '' || key === '' || key === 'n') { cleanup('cancel'); return; }
|
|
498
|
+
if (key === '\r' || key === '\n') { cleanup('switch'); return; }
|
|
499
|
+
if (key === 's') { cleanup('stay'); return; }
|
|
500
|
+
};
|
|
501
|
+
process.stdin.setRawMode(true);
|
|
502
|
+
process.stdin.resume();
|
|
503
|
+
process.stdin.on('data', onData);
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Legacy prompt (kept as a fallback for callers that force compact/full explicitly).
|
|
508
|
+
async function chooseResumeHistoryMode(ctx, { defaultMode = 'compact' } = {}) {
|
|
509
|
+
if (!process.stdin.isTTY) return defaultMode;
|
|
510
|
+
process.stderr.write(`\n ${c.dim('Load history for agent:')} ${c.brand('[c]')} ${c.dim('compact summary')} ${c.brand('[f]')} ${c.dim('full transcript')} ${c.dim('(Enter = compact, Esc = cancel):')} `);
|
|
511
|
+
const rl = ctx._rl || null;
|
|
512
|
+
if (rl) rl.pause();
|
|
513
|
+
return await new Promise((resolve) => {
|
|
514
|
+
const wasRaw = process.stdin.isRaw;
|
|
515
|
+
const cleanup = (value) => {
|
|
516
|
+
process.stdin.removeListener('data', onData);
|
|
517
|
+
process.stdin.setRawMode(wasRaw || false);
|
|
518
|
+
if (rl) rl.resume();
|
|
519
|
+
process.stderr.write('\n');
|
|
520
|
+
resolve(value);
|
|
521
|
+
};
|
|
522
|
+
const onData = (data) => {
|
|
523
|
+
const key = data.toString('utf8').toLowerCase();
|
|
524
|
+
if (key === '\u0003' || key === '\u001b') { cleanup(null); return; }
|
|
525
|
+
if (key === '\r' || key === '\n' || key === 'c') { cleanup('compact'); return; }
|
|
526
|
+
if (key === 'f') { cleanup('full'); }
|
|
527
|
+
};
|
|
528
|
+
process.stdin.setRawMode(true);
|
|
529
|
+
process.stdin.resume();
|
|
530
|
+
process.stdin.on('data', onData);
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
function historyRoleLabel(role) {
|
|
535
|
+
return role === 'user'
|
|
536
|
+
? c.white('You')
|
|
537
|
+
: role === 'tool'
|
|
538
|
+
? c.dim('Tool')
|
|
539
|
+
: c.brand('Kepler');
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function renderHistoryEntries(entries, { limit = 20, maxChars = 120, title = 'Conversation' } = {}) {
|
|
543
|
+
const shown = limit === Infinity ? entries : entries.slice(-limit);
|
|
544
|
+
process.stderr.write(`\n ${c.bold(title)} (${shown.length}${shown.length === entries.length ? '' : ` of ${entries.length}`} entries)\n`);
|
|
545
|
+
process.stderr.write(` ${c.gray('─'.repeat(80))}\n`);
|
|
546
|
+
for (const msg of shown) {
|
|
547
|
+
const content = String(msg.content || '').replace(/\s+/g, ' ').trim();
|
|
548
|
+
process.stderr.write(` ${historyRoleLabel(msg.role)}: ${content.slice(0, maxChars)}${content.length > maxChars ? '...' : ''}\n`);
|
|
549
|
+
}
|
|
550
|
+
process.stderr.write('\n');
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function renderResumePreview(resumed) {
|
|
554
|
+
const tailTurns = resumeTailTurnCount(resumed.historyMode);
|
|
555
|
+
if (resumed.historyMode === 'compact' || resumed.historyMode === 'summary') {
|
|
556
|
+
if (!resumed.summary) return;
|
|
557
|
+
process.stderr.write(`\n ${c.bold('Continuity Summary Sent To Agent')}\n`);
|
|
558
|
+
process.stderr.write(` ${c.gray('─'.repeat(80))}\n`);
|
|
559
|
+
for (const line of resumed.summary.split('\n')) {
|
|
560
|
+
process.stderr.write(` ${c.dim(line)}\n`);
|
|
561
|
+
}
|
|
562
|
+
process.stderr.write('\n');
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
if (tailTurns && resumed.summary) {
|
|
567
|
+
process.stderr.write(`\n ${c.bold(`Summary + Last ${tailTurns} Turns`)}\n`);
|
|
568
|
+
process.stderr.write(` ${c.gray('─'.repeat(80))}\n`);
|
|
569
|
+
for (const line of resumed.summary.split('\n')) {
|
|
570
|
+
process.stderr.write(` ${c.dim(line)}\n`);
|
|
571
|
+
}
|
|
572
|
+
process.stderr.write('\n');
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (resumed.replayEvents?.length) {
|
|
576
|
+
const replayStartOrder = replayStartOrderForMode(resumed.history || [], resumed.historyMode);
|
|
577
|
+
const replayEvents = filterResumeReplayEvents(resumed.replayEvents)
|
|
578
|
+
.filter(item => replayStartOrder == null || !Number.isFinite(Number(item.order)) || Number(item.order) >= replayStartOrder);
|
|
579
|
+
const userTurns = (resumed.history || [])
|
|
580
|
+
.filter(m => m.role === 'user')
|
|
581
|
+
.filter(m => replayStartOrder == null || !Number.isFinite(Number(m.order)) || Number(m.order) >= replayStartOrder);
|
|
582
|
+
const replayItems = mergeResumeReplayItems(userTurns, replayEvents);
|
|
583
|
+
const replayTitle = tailTurns ? `Last ${tailTurns} Turns Replay` : 'Replayed Live Session Events';
|
|
584
|
+
process.stderr.write(`\n ${c.bold(replayTitle)} (${userTurns.length} turns, ${replayEvents.length} events)\n`);
|
|
585
|
+
process.stderr.write(` ${c.gray('─'.repeat(80))}\n`);
|
|
586
|
+
const sessionSnapshot = JSON.parse(JSON.stringify(session));
|
|
587
|
+
const savedOrbit = _orbit;
|
|
588
|
+
const savedSessionMgr = _sessionMgr;
|
|
589
|
+
_orbit = null;
|
|
590
|
+
_sessionMgr = null;
|
|
591
|
+
try {
|
|
592
|
+
startContentStream();
|
|
593
|
+
for (const item of replayItems) {
|
|
594
|
+
if (item.kind === 'user') {
|
|
595
|
+
flushContent();
|
|
596
|
+
stopSpinner();
|
|
597
|
+
const content = String(item.message.content || '').replace(/\s+/g, ' ').trim();
|
|
598
|
+
process.stderr.write(`\n ${historyRoleLabel('user')}: ${content}\n`);
|
|
599
|
+
continue;
|
|
600
|
+
}
|
|
601
|
+
renderEvent(item.event.event);
|
|
602
|
+
}
|
|
603
|
+
flushContent();
|
|
604
|
+
stopSpinner();
|
|
605
|
+
} finally {
|
|
606
|
+
_orbit = savedOrbit;
|
|
607
|
+
_sessionMgr = savedSessionMgr;
|
|
608
|
+
for (const key of Object.keys(session)) delete session[key];
|
|
609
|
+
Object.assign(session, sessionSnapshot);
|
|
610
|
+
}
|
|
611
|
+
process.stderr.write('\n');
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
if (resumed.history?.length) {
|
|
616
|
+
renderHistoryEntries(resumed.history, {
|
|
617
|
+
limit: Infinity,
|
|
618
|
+
maxChars: 220,
|
|
619
|
+
title: tailTurns ? `Last ${tailTurns} Turns` : 'Replayed Session History',
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
async function summarizeResumeTranscript({
|
|
625
|
+
auth,
|
|
626
|
+
toolExecutor,
|
|
627
|
+
sessionId,
|
|
628
|
+
projectPath,
|
|
629
|
+
messages,
|
|
630
|
+
}) {
|
|
631
|
+
const creds = auth?.loadCredentials?.() || {};
|
|
632
|
+
if (!creds.backendUrl || !creds.token || !Array.isArray(messages) || messages.length === 0) {
|
|
633
|
+
return {
|
|
634
|
+
ok: false,
|
|
635
|
+
source: 'local',
|
|
636
|
+
reason: !creds.backendUrl || !creds.token ? 'missing backend credentials' : 'empty transcript',
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
try {
|
|
640
|
+
const client = new TarangStreamClient({
|
|
641
|
+
baseUrl: creds.backendUrl,
|
|
642
|
+
token: creds.token,
|
|
643
|
+
toolExecutor,
|
|
644
|
+
});
|
|
645
|
+
const result = await client.summarizeSession(messages, {
|
|
646
|
+
sessionId,
|
|
647
|
+
projectPath,
|
|
648
|
+
maxTokens: 800,
|
|
649
|
+
timeoutMs: 15000,
|
|
650
|
+
});
|
|
651
|
+
return { ...result, ok: true };
|
|
652
|
+
} catch (err) {
|
|
653
|
+
return {
|
|
654
|
+
ok: false,
|
|
655
|
+
source: 'local',
|
|
656
|
+
reason: err?.message || 'backend summary request failed',
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
function resumeTailTurnCount(mode = '') {
|
|
662
|
+
const match = String(mode || '').match(/^tail-(\d+)$/);
|
|
663
|
+
if (!match) return null;
|
|
664
|
+
return Math.max(1, Number(match[1]) || 1);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function replayStartOrderForMode(history = [], mode = '') {
|
|
668
|
+
const match = String(mode || '').match(/^tail-(\d+)$/);
|
|
669
|
+
if (!match) return null;
|
|
670
|
+
const wanted = Math.max(1, Number(match[1]) || 1);
|
|
671
|
+
let seen = 0;
|
|
672
|
+
const userTurns = history.filter(m => m.role === 'user' && typeof m.content === 'string');
|
|
673
|
+
for (let i = userTurns.length - 1; i >= 0; i--) {
|
|
674
|
+
seen++;
|
|
675
|
+
if (seen >= wanted) {
|
|
676
|
+
const order = Number(userTurns[i].order);
|
|
677
|
+
return Number.isFinite(order) ? order : null;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
return null;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function filterResumeReplayEvents(events = []) {
|
|
684
|
+
return events.filter(item => {
|
|
685
|
+
const type = item?.event?.type;
|
|
686
|
+
return !['status', 'session_info', 'complete', 'resumed', 'paused'].includes(type);
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function mergeResumeReplayItems(userTurns = [], replayEvents = []) {
|
|
691
|
+
const items = [];
|
|
692
|
+
let order = 0;
|
|
693
|
+
for (const message of userTurns) {
|
|
694
|
+
items.push({
|
|
695
|
+
kind: 'user',
|
|
696
|
+
message,
|
|
697
|
+
fileOrder: Number.isFinite(Number(message.order)) ? Number(message.order) : null,
|
|
698
|
+
order: order++,
|
|
699
|
+
time: Date.parse(message.timestamp || '') || 0,
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
for (const event of replayEvents) {
|
|
703
|
+
items.push({
|
|
704
|
+
kind: 'event',
|
|
705
|
+
event,
|
|
706
|
+
fileOrder: Number.isFinite(Number(event.order)) ? Number(event.order) : null,
|
|
707
|
+
order: order++,
|
|
708
|
+
time: Date.parse(event.timestamp || '') || 0,
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
return items.sort((a, b) => {
|
|
712
|
+
if (a.fileOrder !== null && b.fileOrder !== null && a.fileOrder !== b.fileOrder) {
|
|
713
|
+
return a.fileOrder - b.fileOrder;
|
|
714
|
+
}
|
|
715
|
+
if (a.fileOrder !== null && b.fileOrder === null) return -1;
|
|
716
|
+
if (a.fileOrder === null && b.fileOrder !== null) return 1;
|
|
717
|
+
const at = a.time || Number.MAX_SAFE_INTEGER;
|
|
718
|
+
const bt = b.time || Number.MAX_SAFE_INTEGER;
|
|
719
|
+
return at - bt || a.order - b.order;
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function resumeProgressBar(percent, width = 12) {
|
|
724
|
+
const p = Math.max(0, Math.min(100, Math.round(percent)));
|
|
725
|
+
const filled = Math.round((p / 100) * width);
|
|
726
|
+
return `${c.brand('█'.repeat(filled))}${c.gray('░'.repeat(width - filled))} ${String(p).padStart(3)}%`;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function startResumeProgress(mode = 'full') {
|
|
730
|
+
let percent = 8;
|
|
731
|
+
let label = `resuming as ${resumeModeLabel(mode)}`;
|
|
732
|
+
let active = true;
|
|
733
|
+
const started = Date.now();
|
|
734
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
735
|
+
let frame = 0;
|
|
736
|
+
|
|
737
|
+
const render = () => {
|
|
738
|
+
if (!active) return;
|
|
739
|
+
const glyph = frames[frame % frames.length];
|
|
740
|
+
frame++;
|
|
741
|
+
inPlace(` ${c.brand(glyph)} ${c.dim(label)} ${resumeProgressBar(percent)} ${c.dim(formatElapsed(started))}`);
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
render();
|
|
745
|
+
const timer = setInterval(render, 100);
|
|
746
|
+
return {
|
|
747
|
+
update(nextLabel, nextPercent) {
|
|
748
|
+
if (!active) return;
|
|
749
|
+
if (nextLabel) label = nextLabel;
|
|
750
|
+
if (Number.isFinite(nextPercent)) percent = Math.max(percent, Math.min(98, nextPercent));
|
|
751
|
+
render();
|
|
752
|
+
},
|
|
753
|
+
stop() {
|
|
754
|
+
if (!active) return;
|
|
755
|
+
active = false;
|
|
756
|
+
clearInterval(timer);
|
|
757
|
+
inPlace('');
|
|
758
|
+
},
|
|
759
|
+
};
|
|
760
|
+
}
|
|
761
|
+
|
|
92
762
|
// ── Session State ──
|
|
93
763
|
|
|
94
764
|
let _sessionMgr = null; // Set in startTerminalRepl, used by renderEvent
|
|
@@ -102,7 +772,8 @@ const session = {
|
|
|
102
772
|
toolCalls: 0,
|
|
103
773
|
totalToolCalls: 0, // across all turns
|
|
104
774
|
turns: 0,
|
|
105
|
-
history: [], //
|
|
775
|
+
history: [], // display transcript (can include reconstructed tool entries)
|
|
776
|
+
agentHistory: [], // backend continuity payload (compact or full)
|
|
106
777
|
inputHistory: [], // previous prompts (for Up/Down)
|
|
107
778
|
user: null, // { github_username, email, role }
|
|
108
779
|
model: null, // from backend user profile
|
|
@@ -111,6 +782,7 @@ const session = {
|
|
|
111
782
|
phases: [], // phase history: { name, time }
|
|
112
783
|
inSubAgent: false, // true while a sub-agent is running (for indented tool display)
|
|
113
784
|
filesChanged: [], // files modified this session
|
|
785
|
+
filesRead: [], // files read this turn
|
|
114
786
|
lastTurnDuration: 0,
|
|
115
787
|
toolCounts: {}, // per-tool histogram (mission report)
|
|
116
788
|
subAgentCounts: {}, // per-sub-agent histogram (mission report)
|
|
@@ -132,6 +804,8 @@ const session = {
|
|
|
132
804
|
creditsLimit: null, // per-period included credits limit
|
|
133
805
|
creditsCharged: 0, // session-cumulative server-reported charges
|
|
134
806
|
creditsLowWarned: false, // emit the low-balance hint only once per turn
|
|
807
|
+
rateLimit: null, // rolling message-window state from backend
|
|
808
|
+
msgsLowWarned: false, // emit the low-window hint only once per turn
|
|
135
809
|
};
|
|
136
810
|
|
|
137
811
|
// ── Commands ──
|
|
@@ -141,12 +815,15 @@ const COMMANDS = {
|
|
|
141
815
|
'/login': 'Sign in via browser',
|
|
142
816
|
'/whoami': 'Show logged-in user',
|
|
143
817
|
'/status': 'Session status & system info',
|
|
818
|
+
'/plan': 'Show plan/tasks',
|
|
819
|
+
'/tasks': 'Show or update project tasks',
|
|
144
820
|
'/stats': 'Progress bars & metrics',
|
|
145
821
|
'/clear': 'Clear conversation',
|
|
146
822
|
'/git': 'Git status',
|
|
147
823
|
'/diff': 'Git diff',
|
|
148
824
|
'/cost': 'Show session cost',
|
|
149
825
|
'/history': 'Show conversation',
|
|
826
|
+
'/settings': 'Show policy/settings',
|
|
150
827
|
'/last': 'Expand last tool output',
|
|
151
828
|
'/expand': 'Expand tool output by index (or "all")',
|
|
152
829
|
'/fold': 'Hide previously expanded tool output',
|
|
@@ -173,6 +850,240 @@ const COMMANDS = {
|
|
|
173
850
|
'/exit': 'Exit CLI',
|
|
174
851
|
};
|
|
175
852
|
|
|
853
|
+
const HELP_GROUPS = [
|
|
854
|
+
{
|
|
855
|
+
key: 'plan',
|
|
856
|
+
title: 'Plan',
|
|
857
|
+
summary: 'plan and project tasks',
|
|
858
|
+
commands: [
|
|
859
|
+
['/plan', 'Plan and task overview'],
|
|
860
|
+
['/plan status', 'Plan owner and task files'],
|
|
861
|
+
['/plan edit', 'Show editable task/plan paths'],
|
|
862
|
+
['/tasks', 'List project tasks'],
|
|
863
|
+
['/tasks add <text>', 'Add backlog task'],
|
|
864
|
+
['/tasks active|blocked|done <text>', 'Append to a task list'],
|
|
865
|
+
],
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
key: 'status',
|
|
869
|
+
title: 'Status',
|
|
870
|
+
summary: 'session, usage, budget',
|
|
871
|
+
commands: [
|
|
872
|
+
['/status', 'Session snapshot'],
|
|
873
|
+
['/status context', 'Loaded .kepler context'],
|
|
874
|
+
['/status metrics', 'Progress bars and runtime metrics'],
|
|
875
|
+
['/status cost', 'Credits and message window'],
|
|
876
|
+
['/status budget <amount|clear>', 'Set or clear session budget'],
|
|
877
|
+
],
|
|
878
|
+
},
|
|
879
|
+
{
|
|
880
|
+
key: 'history',
|
|
881
|
+
title: 'History',
|
|
882
|
+
summary: 'transcript, reports, undo',
|
|
883
|
+
commands: [
|
|
884
|
+
['/history', 'Recent transcript'],
|
|
885
|
+
['/history approvals', 'Approval log'],
|
|
886
|
+
['/history last', 'Expand last tool output'],
|
|
887
|
+
['/history expand [n|all]', 'Expand tool output'],
|
|
888
|
+
['/history checkpoint', 'List checkpoints'],
|
|
889
|
+
['/history undo', 'Restore latest checkpoint'],
|
|
890
|
+
['/history report', 'Save mission report'],
|
|
891
|
+
],
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
key: 'settings',
|
|
895
|
+
title: 'Settings',
|
|
896
|
+
summary: 'auth, policy, verbosity',
|
|
897
|
+
commands: [
|
|
898
|
+
['/settings policy', 'Effective project policy'],
|
|
899
|
+
['/settings login', 'Sign in'],
|
|
900
|
+
['/settings logout', 'Sign out'],
|
|
901
|
+
['/settings whoami', 'Current user'],
|
|
902
|
+
['/settings quiet|verbose|surgical', 'Verbosity'],
|
|
903
|
+
['/settings revoke', 'Revoke auto-approvals'],
|
|
904
|
+
],
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
key: 'worktree',
|
|
908
|
+
title: 'Worktree',
|
|
909
|
+
summary: 'git and files',
|
|
910
|
+
commands: [
|
|
911
|
+
['/git', 'Git status'],
|
|
912
|
+
['/diff', 'Git diff'],
|
|
913
|
+
['/map', 'Registered project tree'],
|
|
914
|
+
['/preflight', 'Onboarding diagnostic'],
|
|
915
|
+
['/safety', 'Safety guardrail status'],
|
|
916
|
+
],
|
|
917
|
+
},
|
|
918
|
+
{
|
|
919
|
+
key: 'agents',
|
|
920
|
+
title: 'Agents',
|
|
921
|
+
summary: 'specialist modes',
|
|
922
|
+
commands: [
|
|
923
|
+
['/agents', 'List built-in agents'],
|
|
924
|
+
['/explore <instruction>', 'Explore code'],
|
|
925
|
+
['/review <instruction>', 'Review code'],
|
|
926
|
+
['/architect <instruction>', 'Design an approach'],
|
|
927
|
+
],
|
|
928
|
+
},
|
|
929
|
+
{
|
|
930
|
+
key: 'session',
|
|
931
|
+
title: 'Session',
|
|
932
|
+
summary: 'resume and clear',
|
|
933
|
+
commands: [
|
|
934
|
+
['/sessions', 'List resumable sessions'],
|
|
935
|
+
['/resume [id]', 'Resume a session'],
|
|
936
|
+
['/compact', 'Compact conversation context'],
|
|
937
|
+
['/clear', 'Clear conversation'],
|
|
938
|
+
['/exit', 'Exit CLI'],
|
|
939
|
+
],
|
|
940
|
+
},
|
|
941
|
+
];
|
|
942
|
+
|
|
943
|
+
const HELP_GROUP_ALIASES = new Map(
|
|
944
|
+
HELP_GROUPS.flatMap(group => [[group.key, group], [group.title.toLowerCase(), group]])
|
|
945
|
+
);
|
|
946
|
+
|
|
947
|
+
const LEGACY_COMMAND_HINTS = {
|
|
948
|
+
'/stats': '/status metrics',
|
|
949
|
+
'/cost': '/status cost',
|
|
950
|
+
'/budget': '/status budget',
|
|
951
|
+
'/last': '/history last',
|
|
952
|
+
'/expand': '/history expand',
|
|
953
|
+
'/fold': '/history fold',
|
|
954
|
+
'/undo': '/history undo',
|
|
955
|
+
'/checkpoint': '/history checkpoint',
|
|
956
|
+
'/report': '/history report',
|
|
957
|
+
'/login': '/settings login',
|
|
958
|
+
'/logout': '/settings logout',
|
|
959
|
+
'/whoami': '/settings whoami',
|
|
960
|
+
'/quiet': '/settings quiet',
|
|
961
|
+
'/verbose': '/settings verbose',
|
|
962
|
+
'/surgical': '/settings surgical',
|
|
963
|
+
'/revoke': '/settings revoke',
|
|
964
|
+
};
|
|
965
|
+
|
|
966
|
+
const NAMESPACED_COMMANDS = {
|
|
967
|
+
'/status': {
|
|
968
|
+
metrics: '/stats',
|
|
969
|
+
stats: '/stats',
|
|
970
|
+
cost: '/cost',
|
|
971
|
+
credits: '/cost',
|
|
972
|
+
budget: '/budget',
|
|
973
|
+
},
|
|
974
|
+
'/history': {
|
|
975
|
+
last: '/last',
|
|
976
|
+
expand: '/expand',
|
|
977
|
+
fold: '/fold',
|
|
978
|
+
undo: '/undo',
|
|
979
|
+
checkpoint: '/checkpoint',
|
|
980
|
+
checkpoints: '/checkpoint',
|
|
981
|
+
report: '/report',
|
|
982
|
+
},
|
|
983
|
+
'/settings': {
|
|
984
|
+
login: '/login',
|
|
985
|
+
logout: '/logout',
|
|
986
|
+
whoami: '/whoami',
|
|
987
|
+
quiet: '/quiet',
|
|
988
|
+
verbose: '/verbose',
|
|
989
|
+
surgical: '/surgical',
|
|
990
|
+
revoke: '/revoke',
|
|
991
|
+
},
|
|
992
|
+
};
|
|
993
|
+
|
|
994
|
+
function normalizeCommandInput(input) {
|
|
995
|
+
const parts = input.trim().split(/\s+/).filter(Boolean);
|
|
996
|
+
const rawCmd = (parts[0] || '').toLowerCase();
|
|
997
|
+
const restParts = parts.slice(1);
|
|
998
|
+
const sub = (restParts[0] || '').toLowerCase();
|
|
999
|
+
const namespaced = NAMESPACED_COMMANDS[rawCmd]?.[sub];
|
|
1000
|
+
if (namespaced) {
|
|
1001
|
+
return {
|
|
1002
|
+
cmd: namespaced,
|
|
1003
|
+
rest: restParts.slice(1).join(' '),
|
|
1004
|
+
rawCmd,
|
|
1005
|
+
aliasTarget: null,
|
|
1006
|
+
};
|
|
1007
|
+
}
|
|
1008
|
+
return {
|
|
1009
|
+
cmd: rawCmd,
|
|
1010
|
+
rest: restParts.join(' '),
|
|
1011
|
+
rawCmd,
|
|
1012
|
+
aliasTarget: LEGACY_COMMAND_HINTS[rawCmd] || null,
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function renderHelp(topic = '') {
|
|
1017
|
+
const key = String(topic || '').trim().toLowerCase();
|
|
1018
|
+
if (!key) {
|
|
1019
|
+
process.stderr.write(`\n ${c.bold('Kepler Commands')}\n`);
|
|
1020
|
+
process.stderr.write(` ${c.gray('─'.repeat(52))}\n`);
|
|
1021
|
+
const top = [
|
|
1022
|
+
['/help', 'Grouped command help'],
|
|
1023
|
+
['/status', 'Session snapshot'],
|
|
1024
|
+
['/plan', 'Task list and plan'],
|
|
1025
|
+
['/tasks', 'Project task files'],
|
|
1026
|
+
['/history', 'Transcript, approvals, undo'],
|
|
1027
|
+
['/settings', 'Policy, auth, verbosity'],
|
|
1028
|
+
['/why', 'Explain last reasoning'],
|
|
1029
|
+
];
|
|
1030
|
+
for (const [name, desc] of top) {
|
|
1031
|
+
process.stderr.write(` ${c.brand(name.padEnd(14))} ${desc}\n`);
|
|
1032
|
+
}
|
|
1033
|
+
process.stderr.write(`\n ${c.bold('Categories')}\n`);
|
|
1034
|
+
for (const group of HELP_GROUPS) {
|
|
1035
|
+
process.stderr.write(` ${c.brand(('/help ' + group.key).padEnd(20))} ${c.dim(group.summary)}\n`);
|
|
1036
|
+
}
|
|
1037
|
+
process.stderr.write(`\n ${c.dim('Use /help all for legacy command aliases.')}\n`);
|
|
1038
|
+
renderKeyboardHelp();
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
if (key === 'all' || key === 'commands') {
|
|
1043
|
+
process.stderr.write(`\n ${c.bold('All Commands')}\n`);
|
|
1044
|
+
process.stderr.write(` ${c.gray('─'.repeat(52))}\n`);
|
|
1045
|
+
for (const [name, desc] of Object.entries(COMMANDS)) {
|
|
1046
|
+
const alias = LEGACY_COMMAND_HINTS[name] ? c.dim(` alias for ${LEGACY_COMMAND_HINTS[name]}`) : '';
|
|
1047
|
+
process.stderr.write(` ${c.brand(name.padEnd(14))} ${desc}${alias}\n`);
|
|
1048
|
+
}
|
|
1049
|
+
process.stderr.write('\n');
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
const group = HELP_GROUP_ALIASES.get(key);
|
|
1054
|
+
if (!group) {
|
|
1055
|
+
process.stderr.write(` ${c.gray(`Unknown help category: ${key}. Use /help.`)}\n`);
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
process.stderr.write(`\n ${c.bold(group.title)} ${c.dim(group.summary)}\n`);
|
|
1060
|
+
process.stderr.write(` ${c.gray('─'.repeat(52))}\n`);
|
|
1061
|
+
for (const [name, desc] of group.commands) {
|
|
1062
|
+
process.stderr.write(` ${c.brand(name.padEnd(30))} ${desc}\n`);
|
|
1063
|
+
}
|
|
1064
|
+
process.stderr.write('\n');
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function renderKeyboardHelp() {
|
|
1068
|
+
process.stderr.write(`\n ${c.bold('Keyboard')}\n`);
|
|
1069
|
+
process.stderr.write(` ${c.gray('Ctrl+C')} exit ${c.gray('↑↓')} history ${c.gray('Tab')} autocomplete\n`);
|
|
1070
|
+
process.stderr.write(` ${c.gray('d')} expand last tool ${c.gray('Space')} pause/resume ${c.gray('Esc')} interrupt\n\n`);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
function commandCompletions(line) {
|
|
1074
|
+
if (line.startsWith('/help ')) {
|
|
1075
|
+
const topic = line.slice('/help '.length).toLowerCase();
|
|
1076
|
+
const categories = ['all', ...HELP_GROUPS.map(g => g.key)];
|
|
1077
|
+
const hits = categories.map(c => `/help ${c}`).filter(cmd => cmd.startsWith(`/help ${topic}`));
|
|
1078
|
+
return hits.length ? hits : categories.map(c => `/help ${c}`);
|
|
1079
|
+
}
|
|
1080
|
+
const top = ['/help', '/status', '/plan', '/tasks', '/history', '/settings', '/why'];
|
|
1081
|
+
const namespaced = HELP_GROUPS.flatMap(g => g.commands.map(([name]) => name.split(/\s+/)[0]));
|
|
1082
|
+
const all = [...new Set([...top, ...namespaced, ...Object.keys(COMMANDS), '/quit'])].sort();
|
|
1083
|
+
const hits = all.filter(cmd => cmd.startsWith(line));
|
|
1084
|
+
return hits.length ? hits : all;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
176
1087
|
// ── Banner ──
|
|
177
1088
|
|
|
178
1089
|
function printBanner(auth) {
|
|
@@ -214,15 +1125,8 @@ function buildContextStrip() {
|
|
|
214
1125
|
const totalTokens = session.inputTokens + session.outputTokens;
|
|
215
1126
|
const elapsed = formatElapsed(session.startTime);
|
|
216
1127
|
|
|
217
|
-
// BYOK: user pays the provider directly, suppress credits entirely.
|
|
218
|
-
// Otherwise prefer the server-authoritative session counter, falling back
|
|
219
|
-
// to the local estimate when the backend hasn't pushed any number yet.
|
|
220
|
-
const usedCr = session.creditsCharged > 0
|
|
221
|
-
? session.creditsCharged
|
|
222
|
-
: costToCredits(session.totalCost);
|
|
223
1128
|
const right = [
|
|
224
1129
|
c.dim(`${formatTokens(totalTokens)} tok`),
|
|
225
|
-
...(session.isByok ? [] : [c.dim(formatCredits(usedCr))]),
|
|
226
1130
|
c.dim(elapsed),
|
|
227
1131
|
].join(c.dim(' · '));
|
|
228
1132
|
|
|
@@ -269,12 +1173,21 @@ function printTurnSummary(toolCount, durationS, turnCost) {
|
|
|
269
1173
|
const parts = [];
|
|
270
1174
|
if (toolCount > 0) parts.push(`${toolCount} tools`);
|
|
271
1175
|
if (durationS) parts.push(`${Number(durationS).toFixed(1)}s`);
|
|
272
|
-
if (turnCost > 0 && !session.isByok) parts.push(formatCredits(costToCredits(turnCost)));
|
|
273
1176
|
if (parts.length > 0) {
|
|
274
1177
|
process.stderr.write(`\n ${c.green('✓')} ${c.dim(parts.join(' · '))}\n`);
|
|
275
1178
|
}
|
|
276
1179
|
}
|
|
277
1180
|
|
|
1181
|
+
function formatMessageChip(rateLimit) {
|
|
1182
|
+
const remaining = messagesRemaining(rateLimit);
|
|
1183
|
+
if (remaining === Infinity) return 'unlimited messages';
|
|
1184
|
+
const limit = Number(rateLimit?.msgs_per_window);
|
|
1185
|
+
if (typeof remaining === 'number' && Number.isFinite(limit)) {
|
|
1186
|
+
return `${remaining}/${limit} messages`;
|
|
1187
|
+
}
|
|
1188
|
+
return 'messages';
|
|
1189
|
+
}
|
|
1190
|
+
|
|
278
1191
|
function updateStatusBar() {
|
|
279
1192
|
// No-op: status is printed inline via printPromptBlock before each prompt
|
|
280
1193
|
}
|
|
@@ -355,6 +1268,7 @@ function renderToolResult(data, eventType = 'tool_result') {
|
|
|
355
1268
|
|
|
356
1269
|
const tool = data.tool || data._tool || '';
|
|
357
1270
|
const durationMs = data?.duration_ms ?? (data?.duration_s != null ? data.duration_s * 1000 : null);
|
|
1271
|
+
recordReadActivity(tool, data.args || {});
|
|
358
1272
|
|
|
359
1273
|
// Update the card buffer so /last and `d` can find it.
|
|
360
1274
|
if (callId) recordCard({ id: callId, tool, args: data.args, result: data, durationMs });
|
|
@@ -379,7 +1293,7 @@ function renderToolResult(data, eventType = 'tool_result') {
|
|
|
379
1293
|
// If the head for this call is still buffered (no interleaving content
|
|
380
1294
|
// landed), and the combined line fits the terminal width, emit ONE line
|
|
381
1295
|
// and skip the gutter entirely.
|
|
382
|
-
if (_pendingHead && _pendingHead.callId === callId && !hasLint) {
|
|
1296
|
+
if (_pendingHead && _pendingHead.callId === callId && !hasLint && !_pendingHead.head.includes('\n')) {
|
|
383
1297
|
const cols = process.stderr.columns || 120;
|
|
384
1298
|
const combined = `${_pendingHead.head} ${outcome}`;
|
|
385
1299
|
if (stripAnsi(combined).length <= cols) {
|
|
@@ -451,6 +1365,31 @@ function shortPath(p) {
|
|
|
451
1365
|
return parts.length > 2 ? parts.slice(-2).join('/') : p;
|
|
452
1366
|
}
|
|
453
1367
|
|
|
1368
|
+
function rememberReadFile(filePath) {
|
|
1369
|
+
const file = shortPath(String(filePath || '').trim());
|
|
1370
|
+
if (file && !session.filesRead.includes(file)) session.filesRead.push(file);
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
function recordReadActivity(tool, args = {}) {
|
|
1374
|
+
const normalized = String(tool || '').toLowerCase();
|
|
1375
|
+
if (normalized === 'read_file' || normalized === 'read') {
|
|
1376
|
+
rememberReadFile(args.file_path || args.path);
|
|
1377
|
+
return;
|
|
1378
|
+
}
|
|
1379
|
+
if (normalized === 'read_files') {
|
|
1380
|
+
const files = args.file_paths || args.paths || args.files || [];
|
|
1381
|
+
for (const file of Array.isArray(files) ? files : []) {
|
|
1382
|
+
rememberReadFile(typeof file === 'string' ? file : file?.file_path || file?.path);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
|
|
1387
|
+
function thinkingKind(text) {
|
|
1388
|
+
return /\b(read|reading|inspect|scan|search|open|trace|look(?:ing)?\s+at)\b/i.test(text)
|
|
1389
|
+
? 'Reading'
|
|
1390
|
+
: 'Thinking';
|
|
1391
|
+
}
|
|
1392
|
+
|
|
454
1393
|
// ── Live Spinner ──
|
|
455
1394
|
// A real animated spinner that ticks on an interval, not just per-call.
|
|
456
1395
|
// Shows what's happening right now — thinking, tool executing, etc.
|
|
@@ -553,7 +1492,7 @@ function renderEvent(event) {
|
|
|
553
1492
|
if (text.length > 12 && text !== session._lastEmittedThinking) {
|
|
554
1493
|
flushPendingHead();
|
|
555
1494
|
stopSpinner();
|
|
556
|
-
process.stderr.write(` ${c.italic(c.dim(text.slice(0, 200)))}\n`);
|
|
1495
|
+
process.stderr.write(` ${c.dim(thinkingKind(text) + ' · ')}${c.italic(c.dim(text.slice(0, 200)))}\n`);
|
|
557
1496
|
session._lastEmittedThinking = text;
|
|
558
1497
|
}
|
|
559
1498
|
startSpinner(text.slice(0, 80));
|
|
@@ -612,8 +1551,17 @@ function renderEvent(event) {
|
|
|
612
1551
|
}
|
|
613
1552
|
|
|
614
1553
|
case 'approval_granted': {
|
|
615
|
-
//
|
|
616
|
-
//
|
|
1554
|
+
// Human approvals are rendered by approval.mjs. Auto-read grants are
|
|
1555
|
+
// otherwise invisible, so show one dim confirmation before the tool card.
|
|
1556
|
+
const scope = data?.grant_scope || data?.scope || '';
|
|
1557
|
+
if (scope === 'auto_read') {
|
|
1558
|
+
const toolName = data?.tool || data?.tool_name || '';
|
|
1559
|
+
const args = data?.args || data?.input || {};
|
|
1560
|
+
const summary = toolDisplaySummary(toolName, args);
|
|
1561
|
+
const label = toolDisplayLabel(toolName);
|
|
1562
|
+
const subject = summary ? `${label} ${summary}` : label;
|
|
1563
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`${subject} · auto-approved read`)}\n`);
|
|
1564
|
+
}
|
|
617
1565
|
break;
|
|
618
1566
|
}
|
|
619
1567
|
|
|
@@ -790,6 +1738,7 @@ function renderEvent(event) {
|
|
|
790
1738
|
if (typeof bal.included === 'number') session.creditsIncluded = bal.included;
|
|
791
1739
|
if (typeof bal.purchased === 'number') session.creditsPurchased = bal.purchased;
|
|
792
1740
|
}
|
|
1741
|
+
if (data?.rate_limit) session.rateLimit = data.rate_limit;
|
|
793
1742
|
break;
|
|
794
1743
|
}
|
|
795
1744
|
|
|
@@ -848,12 +1797,24 @@ function renderEvent(event) {
|
|
|
848
1797
|
}
|
|
849
1798
|
|
|
850
1799
|
session.lastTurnDuration = data?.duration_s || 0;
|
|
1800
|
+
if (data?.rate_limit) session.rateLimit = data.rate_limit;
|
|
851
1801
|
|
|
852
1802
|
// ── Server-authoritative credits ──
|
|
853
1803
|
// Backend sends usage.credits_charged (this turn) + balance (remaining)
|
|
854
1804
|
// in the complete event. CLI uses these instead of the local
|
|
855
1805
|
// costToCredits estimate so /status and /cost match the dashboard.
|
|
856
1806
|
if (!session.isByok) {
|
|
1807
|
+
const msgStatus = lowWindowStatus(session.rateLimit);
|
|
1808
|
+
if (!session.msgsLowWarned && msgStatus !== 'ok') {
|
|
1809
|
+
const windowLine = formatMessageWindow(session.rateLimit);
|
|
1810
|
+
if (msgStatus === 'exhausted') {
|
|
1811
|
+
process.stderr.write(`\n ${c.red('✗')} ${c.dim(`${windowLine}. Wait for the window to reset or upgrade at codekepler.ai/pricing.`)}\n`);
|
|
1812
|
+
} else {
|
|
1813
|
+
process.stderr.write(`\n ${c.yellow('⚠')} ${c.dim(`${windowLine}. Message window is running low.`)}\n`);
|
|
1814
|
+
}
|
|
1815
|
+
session.msgsLowWarned = true;
|
|
1816
|
+
}
|
|
1817
|
+
|
|
857
1818
|
const charged = data?.usage?.credits_charged;
|
|
858
1819
|
if (typeof charged === 'number') session.creditsCharged += charged;
|
|
859
1820
|
const bal = data?.balance;
|
|
@@ -863,11 +1824,12 @@ function renderEvent(event) {
|
|
|
863
1824
|
if (typeof bal.purchased === 'number') session.creditsPurchased = bal.purchased;
|
|
864
1825
|
}
|
|
865
1826
|
// Warn once per turn when the remaining credits drop below 20% of the
|
|
866
|
-
// tier's included limit (or below 10 absolute for tiny tiers).
|
|
1827
|
+
// tier's included limit (or below 10 absolute for tiny tiers). Credits
|
|
1828
|
+
// stay out of the always-on prompt strip; this warning is the exception.
|
|
867
1829
|
if (!session.creditsLowWarned && typeof session.creditsTotal === 'number' && session.creditsLimit) {
|
|
868
1830
|
const threshold = Math.max(10, Math.floor(session.creditsLimit * 0.2));
|
|
869
1831
|
if (session.creditsTotal <= threshold && session.creditsTotal > 0) {
|
|
870
|
-
process.stderr.write(`\n ${c.yellow('⚠')} ${c.dim(`${session.creditsTotal} of ${session.creditsLimit} credits remaining on the ${session.subscriptionTier || 'free'} plan. Upgrade at codekepler.ai/pricing.`)}\n`);
|
|
1832
|
+
process.stderr.write(`\n ${c.yellow('⚠')} ${c.dim(`${session.creditsTotal} of ${session.creditsLimit} credits remaining on the ${session.subscriptionTier || 'free'} plan. Upgrade or top up at codekepler.ai/pricing.`)}\n`);
|
|
871
1833
|
session.creditsLowWarned = true;
|
|
872
1834
|
} else if (session.creditsTotal <= 0) {
|
|
873
1835
|
process.stderr.write(`\n ${c.red('✗')} ${c.dim(`Credit balance exhausted on the ${session.subscriptionTier || 'free'} plan. Purchase credits at codekepler.ai/pricing or switch to BYOK.`)}\n`);
|
|
@@ -892,10 +1854,10 @@ function renderEvent(event) {
|
|
|
892
1854
|
task: session.lastTask,
|
|
893
1855
|
success: successOverall,
|
|
894
1856
|
filesChanged: session.filesChanged,
|
|
1857
|
+
filesRead: session.filesRead,
|
|
895
1858
|
toolCounts: session.toolCounts,
|
|
896
|
-
subAgents: { ...session.subAgentCounts, savedUsd:
|
|
897
|
-
|
|
898
|
-
costUsd: session.isByok ? null : (turnCost || session.totalCost),
|
|
1859
|
+
subAgents: { ...session.subAgentCounts, savedUsd: 0 },
|
|
1860
|
+
costUsd: null,
|
|
899
1861
|
durationS: data?.duration_s,
|
|
900
1862
|
testsPass: data?.tests_passed != null
|
|
901
1863
|
? { passed: data.tests_passed, total: data.tests_total || data.tests_passed }
|
|
@@ -904,6 +1866,7 @@ function renderEvent(event) {
|
|
|
904
1866
|
nextActions: successOverall
|
|
905
1867
|
? ['/commit', '/pr', '/undo', '/report']
|
|
906
1868
|
: ['/why', '/undo', '/re-plan'],
|
|
1869
|
+
cwd: safeCwd(),
|
|
907
1870
|
});
|
|
908
1871
|
process.stderr.write(report + '\n');
|
|
909
1872
|
} else {
|
|
@@ -935,22 +1898,194 @@ function renderEvent(event) {
|
|
|
935
1898
|
|
|
936
1899
|
// ── Slash Commands ──
|
|
937
1900
|
|
|
1901
|
+
function taskListLabel(list) {
|
|
1902
|
+
return {
|
|
1903
|
+
active: 'Active',
|
|
1904
|
+
backlog: 'Backlog',
|
|
1905
|
+
blocked: 'Blocked',
|
|
1906
|
+
done: 'Done',
|
|
1907
|
+
}[list] || list;
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
function firstMeaningfulLines(content, limit = 6) {
|
|
1911
|
+
return String(content || '')
|
|
1912
|
+
.split(/\r?\n/)
|
|
1913
|
+
.map(line => line.trim())
|
|
1914
|
+
.filter(line => line && !line.startsWith('#'))
|
|
1915
|
+
.slice(0, limit);
|
|
1916
|
+
}
|
|
1917
|
+
|
|
1918
|
+
function renderTaskBoard(board, { showDone = false } = {}) {
|
|
1919
|
+
const order = showDone
|
|
1920
|
+
? ['active', 'blocked', 'backlog', 'done']
|
|
1921
|
+
: ['active', 'blocked', 'backlog'];
|
|
1922
|
+
let any = false;
|
|
1923
|
+
for (const list of order) {
|
|
1924
|
+
const tasks = board.lists[list]?.tasks || [];
|
|
1925
|
+
if (!tasks.length) continue;
|
|
1926
|
+
any = true;
|
|
1927
|
+
process.stderr.write(`\n ${c.bold(taskListLabel(list))} ${c.dim(board.lists[list].fileName)}\n`);
|
|
1928
|
+
for (const task of tasks.slice(0, 12)) {
|
|
1929
|
+
const marker = task.checked ? c.green('[x]') : c.dim('[ ]');
|
|
1930
|
+
const section = task.section && task.section !== taskListLabel(list) ? c.dim(` · ${task.section}`) : '';
|
|
1931
|
+
process.stderr.write(` ${marker} ${task.text}${section}\n`);
|
|
1932
|
+
}
|
|
1933
|
+
if (tasks.length > 12) {
|
|
1934
|
+
process.stderr.write(` ${c.dim(`+${tasks.length - 12} more`)}\n`);
|
|
1935
|
+
}
|
|
1936
|
+
}
|
|
1937
|
+
if (!any) {
|
|
1938
|
+
process.stderr.write(` ${c.dim('No project tasks yet. Add one with /tasks add <text>.')}\n`);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
function renderPlanOverview({ ctx, mode = 'overview' } = {}) {
|
|
1943
|
+
const cwd = safeCwd();
|
|
1944
|
+
ensureTaskFiles({ cwd });
|
|
1945
|
+
const board = loadTaskBoard({ cwd });
|
|
1946
|
+
const counts = taskCounts(board);
|
|
1947
|
+
const planLines = firstMeaningfulLines(board.plan.content, 8);
|
|
1948
|
+
const goalLines = firstMeaningfulLines(board.goal.content, 3);
|
|
1949
|
+
const effective = ctx.effectivePolicy || loadEffectivePolicy({ cwd });
|
|
1950
|
+
const owner = effective.policy?.planning?.owner || 'auto';
|
|
1951
|
+
|
|
1952
|
+
process.stderr.write(`\n ${c.bold('Plan')}\n`);
|
|
1953
|
+
process.stderr.write(` ${c.dim('─'.repeat(60))}\n`);
|
|
1954
|
+
process.stderr.write(` ${c.dim('Owner')} ${c.brand(owner)}\n`);
|
|
1955
|
+
process.stderr.write(` ${c.dim('Tasks')} ${counts.active} active, ${counts.blocked} blocked, ${counts.backlog} backlog, ${counts.done} done\n`);
|
|
1956
|
+
if (mode === 'status') {
|
|
1957
|
+
process.stderr.write(` ${c.dim('Plan file')} ${board.plan.exists ? board.plan.path : c.dim('(none)')}\n`);
|
|
1958
|
+
process.stderr.write(` ${c.dim('Tasks dir')} ${board.dir}\n`);
|
|
1959
|
+
}
|
|
1960
|
+
|
|
1961
|
+
if (goalLines.length) {
|
|
1962
|
+
process.stderr.write(`\n ${c.bold('Goal')}\n`);
|
|
1963
|
+
for (const line of goalLines) process.stderr.write(` ${line}\n`);
|
|
1964
|
+
}
|
|
1965
|
+
if (planLines.length) {
|
|
1966
|
+
process.stderr.write(`\n ${c.bold('Current Plan')}\n`);
|
|
1967
|
+
for (const line of planLines) process.stderr.write(` ${line}\n`);
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
renderTaskBoard(board, { showDone: mode === 'status' });
|
|
1971
|
+
process.stderr.write(`\n ${c.dim('Update: /tasks add <text> · /tasks active|blocked|done <text>')}\n\n`);
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
function refreshTaskContext(ctx) {
|
|
1975
|
+
try {
|
|
1976
|
+
const previous = ctx.latestProjectContext || null;
|
|
1977
|
+
ctx.latestProjectContext = loadProjectContext({ cwd: safeCwd(), previous });
|
|
1978
|
+
ctx.latestEnvelope = null;
|
|
1979
|
+
} catch { /* best effort */ }
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
function handleTasksCommand(rest, ctx) {
|
|
1983
|
+
const raw = String(rest || '').trim();
|
|
1984
|
+
ensureTaskFiles({ cwd: safeCwd() });
|
|
1985
|
+
if (!raw || raw === 'list') {
|
|
1986
|
+
const board = loadTaskBoard({ cwd: safeCwd() });
|
|
1987
|
+
process.stderr.write(`\n ${c.bold('Tasks')}\n`);
|
|
1988
|
+
process.stderr.write(` ${c.dim('─'.repeat(60))}\n`);
|
|
1989
|
+
renderTaskBoard(board, { showDone: true });
|
|
1990
|
+
process.stderr.write(`\n ${c.dim('Update: /tasks add <text> · /tasks active|backlog|blocked|done <text>')}\n\n`);
|
|
1991
|
+
return;
|
|
1992
|
+
}
|
|
1993
|
+
|
|
1994
|
+
if (raw === 'help') {
|
|
1995
|
+
renderHelp('plan');
|
|
1996
|
+
return;
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
const parts = raw.split(/\s+/);
|
|
2000
|
+
let verb = (parts.shift() || '').toLowerCase();
|
|
2001
|
+
let list = 'backlog';
|
|
2002
|
+
if (verb === 'add' || verb === 'new') {
|
|
2003
|
+
const maybeList = (parts[0] || '').toLowerCase();
|
|
2004
|
+
if (maybeList in TASK_FILES || ['todo', 'pending', 'current', 'doing', 'complete', 'completed'].includes(maybeList)) {
|
|
2005
|
+
list = parts.shift();
|
|
2006
|
+
}
|
|
2007
|
+
} else if (verb in TASK_FILES || ['todo', 'pending', 'current', 'doing', 'complete', 'completed'].includes(verb)) {
|
|
2008
|
+
list = verb;
|
|
2009
|
+
} else {
|
|
2010
|
+
parts.unshift(verb);
|
|
2011
|
+
verb = 'add';
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
try {
|
|
2015
|
+
const result = appendTask({ cwd: safeCwd(), list, text: parts.join(' ') });
|
|
2016
|
+
refreshTaskContext(ctx);
|
|
2017
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`added to ${result.list}`)} ${result.text}\n`);
|
|
2018
|
+
} catch (err) {
|
|
2019
|
+
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
2020
|
+
}
|
|
2021
|
+
}
|
|
2022
|
+
|
|
938
2023
|
async function handleCommand(input, ctx) {
|
|
939
|
-
const
|
|
940
|
-
|
|
941
|
-
|
|
2024
|
+
const { cmd, rest, aliasTarget } = normalizeCommandInput(input);
|
|
2025
|
+
if (aliasTarget) {
|
|
2026
|
+
process.stderr.write(` ${c.dim(`Legacy alias: use ${aliasTarget}`)}\n`);
|
|
2027
|
+
}
|
|
942
2028
|
|
|
943
2029
|
switch (cmd) {
|
|
944
|
-
case '/help':
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
2030
|
+
case '/help': {
|
|
2031
|
+
renderHelp(rest);
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
|
|
2035
|
+
case '/plan': {
|
|
2036
|
+
const mode = rest.trim().toLowerCase();
|
|
2037
|
+
if (mode === 'help') {
|
|
2038
|
+
renderHelp('plan');
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
if (mode === 'edit') {
|
|
2042
|
+
ensureTaskFiles({ cwd: safeCwd() });
|
|
2043
|
+
const board = loadTaskBoard({ cwd: safeCwd() });
|
|
2044
|
+
process.stderr.write(`\n ${c.bold('Editable Plan Files')}\n`);
|
|
2045
|
+
process.stderr.write(` ${c.dim('Plan')} ${board.plan.path}\n`);
|
|
2046
|
+
process.stderr.write(` ${c.dim('Active')} ${board.lists.active.path}\n`);
|
|
2047
|
+
process.stderr.write(` ${c.dim('Backlog')} ${board.lists.backlog.path}\n`);
|
|
2048
|
+
process.stderr.write(` ${c.dim('Blocked')} ${board.lists.blocked.path}\n`);
|
|
2049
|
+
process.stderr.write(` ${c.dim('Done')} ${board.lists.done.path}\n\n`);
|
|
2050
|
+
return;
|
|
2051
|
+
}
|
|
2052
|
+
renderPlanOverview({ ctx, mode: mode === 'status' ? 'status' : 'overview' });
|
|
2053
|
+
return;
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
case '/tasks':
|
|
2057
|
+
handleTasksCommand(rest, ctx);
|
|
2058
|
+
return;
|
|
2059
|
+
|
|
2060
|
+
case '/history': {
|
|
2061
|
+
if (rest.trim() === 'fold') {
|
|
2062
|
+
process.stderr.write(` ${c.gray('Output is folded by default — there is nothing to hide. Use /history last or d to expand.')}\n`);
|
|
2063
|
+
return;
|
|
2064
|
+
}
|
|
2065
|
+
if (rest.trim() === 'help') {
|
|
2066
|
+
renderHelp('history');
|
|
2067
|
+
return;
|
|
949
2068
|
}
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
2069
|
+
if (rest.trim() === 'approvals') {
|
|
2070
|
+
const entries = ctx.approval?.approvalLog?.readRecent?.(20) || [];
|
|
2071
|
+
if (!entries.length) {
|
|
2072
|
+
process.stderr.write(` ${c.gray('No approval log entries yet.')}\n`);
|
|
2073
|
+
return;
|
|
2074
|
+
}
|
|
2075
|
+
process.stderr.write(`\n ${c.bold('Approval History')}\n`);
|
|
2076
|
+
process.stderr.write(` ${c.gray('─'.repeat(80))}\n`);
|
|
2077
|
+
for (const e of entries) {
|
|
2078
|
+
const when = e.ts ? String(e.ts).slice(0, 19).replace('T', ' ') : '';
|
|
2079
|
+
const decision = e.decision?.includes('reject') || e.decision?.includes('deny') ? c.red(e.decision) : c.green(e.decision);
|
|
2080
|
+
process.stderr.write(` ${c.dim(when)} ${decision} ${c.brand(e.tool || '?')} ${c.dim(e.scope || 'once')} ${c.dim(e.args || '')}\n`);
|
|
2081
|
+
}
|
|
2082
|
+
process.stderr.write('\n');
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
if (session.history.length === 0) { process.stderr.write(` ${c.gray('No conversation yet.')}\n`); return; }
|
|
2086
|
+
renderHistoryEntries(session.history, { limit: 20, maxChars: 120 });
|
|
953
2087
|
return;
|
|
2088
|
+
}
|
|
954
2089
|
|
|
955
2090
|
case '/login':
|
|
956
2091
|
process.stderr.write(`${c.brand('Starting login flow...')}\n`);
|
|
@@ -977,6 +2112,46 @@ async function handleCommand(input, ctx) {
|
|
|
977
2112
|
}
|
|
978
2113
|
|
|
979
2114
|
case '/status': {
|
|
2115
|
+
if (rest.trim() === 'help') {
|
|
2116
|
+
renderHelp('status');
|
|
2117
|
+
return;
|
|
2118
|
+
}
|
|
2119
|
+
if (rest.trim() === 'context') {
|
|
2120
|
+
const current = ctx.latestProjectContext || loadProjectContext({ cwd: safeCwd() });
|
|
2121
|
+
const envelope = ctx.latestEnvelope || buildContextEnvelope({
|
|
2122
|
+
cwd: safeCwd(),
|
|
2123
|
+
effectivePolicy: ctx.effectivePolicy,
|
|
2124
|
+
projectContext: current,
|
|
2125
|
+
projectResources: ctx.toolExecutor?.getProjectResources?.() || [],
|
|
2126
|
+
agentContext: ctx.toolExecutor?.getAgentContext?.() || {},
|
|
2127
|
+
});
|
|
2128
|
+
process.stderr.write(`\n ${c.bold('Context')}\n`);
|
|
2129
|
+
process.stderr.write(` ${c.dim('─'.repeat(60))}\n`);
|
|
2130
|
+
const loaded = current.loaded || [];
|
|
2131
|
+
if (!loaded.length) {
|
|
2132
|
+
process.stderr.write(` ${c.dim('No .kepler context files loaded yet.')}\n`);
|
|
2133
|
+
} else {
|
|
2134
|
+
for (const file of loaded) {
|
|
2135
|
+
const changed = file.changed ? ` ${c.yellow('updated')}` : '';
|
|
2136
|
+
process.stderr.write(` ${c.brand(file.label.padEnd(18))} ${c.dim(file.hash)} ${c.dim(file.path)}${changed}\n`);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
process.stderr.write(`\n ${c.bold('Command Context')}\n`);
|
|
2140
|
+
process.stderr.write(` ${c.dim('Active')} ${envelope.command_context.active_command || c.dim('(none)')}\n`);
|
|
2141
|
+
process.stderr.write(` ${c.dim('Source')} ${envelope.command_context.source}\n`);
|
|
2142
|
+
process.stderr.write(` ${c.dim('Timeout')} ${envelope.command_context.runtime_limits.command_timeout_seconds}s command, ${envelope.command_context.runtime_limits.tool_timeout_seconds}s tool\n`);
|
|
2143
|
+
process.stderr.write(` ${c.dim('Plan owner')} ${envelope.effective_options.plan_owner}\n`);
|
|
2144
|
+
process.stderr.write(` ${c.dim('HITL scope')} ${envelope.effective_options.hitl_default_scope} ${c.dim(`(reask ${envelope.effective_options.reask_after_minutes}m)`)}\n`);
|
|
2145
|
+
if (envelope.available_skills.length) {
|
|
2146
|
+
process.stderr.write(`\n ${c.bold('Skills')}\n`);
|
|
2147
|
+
for (const skill of envelope.available_skills.slice(0, 12)) {
|
|
2148
|
+
process.stderr.write(` ${c.brand(skill.name)} ${c.dim(skill.scope)} ${c.dim(skill.description || '')}\n`);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
process.stderr.write('\n');
|
|
2152
|
+
return;
|
|
2153
|
+
}
|
|
2154
|
+
|
|
980
2155
|
const creds = ctx.auth.loadCredentials();
|
|
981
2156
|
const env = process.env.TARANG_ENV || 'production';
|
|
982
2157
|
const os = await import('node:os');
|
|
@@ -1003,6 +2178,10 @@ async function handleCommand(input, ctx) {
|
|
|
1003
2178
|
if (session.subscriptionTier) {
|
|
1004
2179
|
process.stderr.write(` ${c.dim('Plan')} ${c.brand(session.subscriptionTier.toUpperCase())}\n`);
|
|
1005
2180
|
}
|
|
2181
|
+
const messageWindow = formatMessageWindow(session.rateLimit);
|
|
2182
|
+
if (messageWindow) {
|
|
2183
|
+
process.stderr.write(` ${c.dim('Messages')} ${messageWindow}\n`);
|
|
2184
|
+
}
|
|
1006
2185
|
if (typeof session.creditsTotal === 'number') {
|
|
1007
2186
|
const limit = session.creditsLimit ? ` ${c.dim('/ ' + formatCredits(session.creditsLimit))}` : '';
|
|
1008
2187
|
const used = session.creditsCharged ? ` ${c.dim(`(${formatCredits(session.creditsCharged)} used this session)`)}` : '';
|
|
@@ -1023,6 +2202,9 @@ async function handleCommand(input, ctx) {
|
|
|
1023
2202
|
if (approvalSummary.autoApprovedTypes.length > 0) {
|
|
1024
2203
|
process.stderr.write(` ${c.dim('Auto-types')} ${approvalSummary.autoApprovedTypes.join(', ')}\n`);
|
|
1025
2204
|
}
|
|
2205
|
+
if (approvalSummary.trust) {
|
|
2206
|
+
process.stderr.write(` ${c.dim('Trust')} ${approvalSummary.trust.sessionRules} session, ${approvalSummary.trust.projectRules} project rules\n`);
|
|
2207
|
+
}
|
|
1026
2208
|
process.stderr.write(` ${c.dim('Blocked')} ${session.blockedOps} by safety guardrails\n`);
|
|
1027
2209
|
|
|
1028
2210
|
// Orchestration
|
|
@@ -1062,6 +2244,34 @@ async function handleCommand(input, ctx) {
|
|
|
1062
2244
|
return;
|
|
1063
2245
|
}
|
|
1064
2246
|
|
|
2247
|
+
case '/settings': {
|
|
2248
|
+
const sub = rest.trim() || 'policy';
|
|
2249
|
+
if (sub === 'help') {
|
|
2250
|
+
renderHelp('settings');
|
|
2251
|
+
return;
|
|
2252
|
+
}
|
|
2253
|
+
if (sub !== 'policy') {
|
|
2254
|
+
process.stderr.write(` ${c.gray('Usage: /settings policy or /help settings')}\n`);
|
|
2255
|
+
return;
|
|
2256
|
+
}
|
|
2257
|
+
const effective = loadEffectivePolicy({ cwd: safeCwd() });
|
|
2258
|
+
ctx.effectivePolicy = effective;
|
|
2259
|
+
const rows = formatPolicySourceRows(effective);
|
|
2260
|
+
process.stderr.write(`\n ${c.bold('Effective Policy')}\n`);
|
|
2261
|
+
process.stderr.write(` ${c.dim('─'.repeat(86))}\n`);
|
|
2262
|
+
for (const row of rows.slice(0, 80)) {
|
|
2263
|
+
const value = typeof row.value === 'string' ? row.value : JSON.stringify(row.value);
|
|
2264
|
+
process.stderr.write(` ${c.brand(row.key.padEnd(38))} ${c.dim(row.source.padEnd(8))} ${String(value).slice(0, 34)}\n`);
|
|
2265
|
+
}
|
|
2266
|
+
if (rows.length > 80) process.stderr.write(` ${c.dim(`...and ${rows.length - 80} more`)}\n`);
|
|
2267
|
+
const projectLayer = effective.layers.find(l => l.name === 'project');
|
|
2268
|
+
if (projectLayer?.error) {
|
|
2269
|
+
process.stderr.write(`\n ${c.yellow('Project config error:')} ${projectLayer.error}\n`);
|
|
2270
|
+
}
|
|
2271
|
+
process.stderr.write('\n');
|
|
2272
|
+
return;
|
|
2273
|
+
}
|
|
2274
|
+
|
|
1065
2275
|
case '/stats': {
|
|
1066
2276
|
const os = await import('node:os');
|
|
1067
2277
|
const mem = process.memoryUsage();
|
|
@@ -1103,6 +2313,10 @@ async function handleCommand(input, ctx) {
|
|
|
1103
2313
|
const limit = session.creditsLimit ? ` / ${formatCredits(session.creditsLimit)}` : '';
|
|
1104
2314
|
process.stderr.write(` ${c.dim('Plan')} ${c.brand(session.subscriptionTier.toUpperCase())} ${c.dim('· remaining')} ${c.brand(remaining)}${c.dim(limit)}\n`);
|
|
1105
2315
|
}
|
|
2316
|
+
const messageWindow = formatMessageWindow(session.rateLimit);
|
|
2317
|
+
if (messageWindow) {
|
|
2318
|
+
process.stderr.write(` ${c.dim('Messages')} ${messageWindow}\n`);
|
|
2319
|
+
}
|
|
1106
2320
|
process.stderr.write(` ${c.dim('─'.repeat(70))}\n`);
|
|
1107
2321
|
|
|
1108
2322
|
if (session.costBreakdown.length > 0) {
|
|
@@ -1135,21 +2349,10 @@ async function handleCommand(input, ctx) {
|
|
|
1135
2349
|
`${''.padStart(10)}` +
|
|
1136
2350
|
`${formatCredits(costToCredits(session.totalCost)).padStart(10)}\n`
|
|
1137
2351
|
);
|
|
1138
|
-
process.stderr.write(` ${c.dim(`Turns: ${session.turns} Duration: ${formatElapsed(session.startTime)}
|
|
2352
|
+
process.stderr.write(` ${c.dim(`Turns: ${session.turns} Duration: ${formatElapsed(session.startTime)}`)}\n\n`);
|
|
1139
2353
|
return;
|
|
1140
2354
|
}
|
|
1141
2355
|
|
|
1142
|
-
case '/history':
|
|
1143
|
-
if (session.history.length === 0) { process.stderr.write(` ${c.gray('No conversation yet.')}\n`); return; }
|
|
1144
|
-
process.stderr.write(`\n ${c.bold('Conversation')} (${session.history.length} messages)\n`);
|
|
1145
|
-
process.stderr.write(` ${c.gray('─'.repeat(40))}\n`);
|
|
1146
|
-
for (const msg of session.history.slice(-20)) {
|
|
1147
|
-
const role = msg.role === 'user' ? c.white('You') : c.brand('Kepler');
|
|
1148
|
-
process.stderr.write(` ${role}: ${msg.content.slice(0, 80)}${msg.content.length > 80 ? '...' : ''}\n`);
|
|
1149
|
-
}
|
|
1150
|
-
process.stderr.write('\n');
|
|
1151
|
-
return;
|
|
1152
|
-
|
|
1153
2356
|
case '/last':
|
|
1154
2357
|
expandLast();
|
|
1155
2358
|
return;
|
|
@@ -1207,7 +2410,7 @@ async function handleCommand(input, ctx) {
|
|
|
1207
2410
|
}
|
|
1208
2411
|
|
|
1209
2412
|
case '/report': {
|
|
1210
|
-
if (Object.keys(session.toolCounts).length === 0 && session.filesChanged.length === 0) {
|
|
2413
|
+
if (Object.keys(session.toolCounts).length === 0 && session.filesChanged.length === 0 && session.filesRead.length === 0) {
|
|
1211
2414
|
process.stderr.write(` ${c.gray('Nothing to report yet — run a task first.')}\n`);
|
|
1212
2415
|
return;
|
|
1213
2416
|
}
|
|
@@ -1215,11 +2418,13 @@ async function handleCommand(input, ctx) {
|
|
|
1215
2418
|
task: session.lastTask,
|
|
1216
2419
|
success: true,
|
|
1217
2420
|
filesChanged: session.filesChanged,
|
|
2421
|
+
filesRead: session.filesRead,
|
|
1218
2422
|
toolCounts: session.toolCounts,
|
|
1219
2423
|
subAgents: { ...session.subAgentCounts, savedUsd: session.isByok ? 0 : session.savedUsd },
|
|
1220
2424
|
costUsd: session.isByok ? null : session.totalCost,
|
|
1221
2425
|
durationS: (Date.now() - session.startTime) / 1000,
|
|
1222
2426
|
nextActions: ['/commit', '/pr', '/undo'],
|
|
2427
|
+
cwd: safeCwd(),
|
|
1223
2428
|
};
|
|
1224
2429
|
const out = saveReport(state, { cwd: safeCwd() });
|
|
1225
2430
|
process.stderr.write(` ${c.green('✓')} ${c.dim('Saved')} ${out}\n`);
|
|
@@ -1259,7 +2464,12 @@ async function handleCommand(input, ctx) {
|
|
|
1259
2464
|
|
|
1260
2465
|
case '/budget': {
|
|
1261
2466
|
const arg = rest.trim();
|
|
1262
|
-
if (!arg
|
|
2467
|
+
if (!arg) {
|
|
2468
|
+
const current = session.budgetUsd ? `$${session.budgetUsd.toFixed(2)}` : 'not set';
|
|
2469
|
+
process.stderr.write(` ${c.dim('Budget cap:')} ${c.brand(current)} ${c.dim('· set with /status budget <amount> or clear with /status budget clear')}\n`);
|
|
2470
|
+
return;
|
|
2471
|
+
}
|
|
2472
|
+
if (arg === 'clear' || arg === 'off') {
|
|
1263
2473
|
session.budgetUsd = null;
|
|
1264
2474
|
session.budgetExceeded = false;
|
|
1265
2475
|
process.stderr.write(` ${c.gray('Budget cap cleared.')}\n`);
|
|
@@ -1288,15 +2498,16 @@ async function handleCommand(input, ctx) {
|
|
|
1288
2498
|
}
|
|
1289
2499
|
|
|
1290
2500
|
case '/compact': {
|
|
1291
|
-
const before = session.history.length;
|
|
2501
|
+
const before = session.agentHistory.length || session.history.length;
|
|
1292
2502
|
if (before <= 4) { process.stderr.write(` ${c.gray('Nothing to compact.')}\n`); return; }
|
|
1293
|
-
session.
|
|
1294
|
-
process.stderr.write(` ${c.gray(`Compacted: ${before} → ${session.
|
|
2503
|
+
session.agentHistory.splice(2, session.agentHistory.length - 6);
|
|
2504
|
+
process.stderr.write(` ${c.gray(`Compacted agent context: ${before} → ${session.agentHistory.length} messages`)}\n`);
|
|
1295
2505
|
return;
|
|
1296
2506
|
}
|
|
1297
2507
|
|
|
1298
2508
|
case '/clear':
|
|
1299
2509
|
session.history.length = 0;
|
|
2510
|
+
session.agentHistory.length = 0;
|
|
1300
2511
|
session.toolCalls = 0;
|
|
1301
2512
|
process.stderr.write(` ${c.gray('Conversation cleared.')}\n`);
|
|
1302
2513
|
return;
|
|
@@ -1348,7 +2559,7 @@ async function handleCommand(input, ctx) {
|
|
|
1348
2559
|
}
|
|
1349
2560
|
|
|
1350
2561
|
case '/sessions': {
|
|
1351
|
-
const resumable =
|
|
2562
|
+
const resumable = await listResumableSessions();
|
|
1352
2563
|
if (resumable.length === 0) {
|
|
1353
2564
|
process.stderr.write(` ${c.gray('No resumable sessions found.')}\n`);
|
|
1354
2565
|
return;
|
|
@@ -1356,89 +2567,168 @@ async function handleCommand(input, ctx) {
|
|
|
1356
2567
|
process.stderr.write(`\n ${c.bold('Resumable Sessions')}\n`);
|
|
1357
2568
|
process.stderr.write(` ${c.dim('─'.repeat(60))}\n`);
|
|
1358
2569
|
for (const s of resumable) {
|
|
1359
|
-
const date =
|
|
1360
|
-
const instr = s.instruction
|
|
1361
|
-
|
|
2570
|
+
const date = sessionListTimestamp(s);
|
|
2571
|
+
const instr = oneLineInstruction(s.instruction, 72);
|
|
2572
|
+
const project = s.project || path.basename(s.projectPath || '') || '(unknown)';
|
|
2573
|
+
process.stderr.write(` ${c.brand(s.sessionId)} ${c.brand(project)} ${c.dim(date)} ${messageCountLabel(s.messageCount)} ${c.dim(instr)}\n`);
|
|
1362
2574
|
}
|
|
1363
2575
|
process.stderr.write(`\n ${c.dim('Resume with:')} kepler --resume <sessionId>\n`);
|
|
1364
2576
|
return;
|
|
1365
2577
|
}
|
|
1366
2578
|
|
|
1367
2579
|
case '/resume': {
|
|
1368
|
-
|
|
1369
|
-
|
|
2580
|
+
// PRD-068 §5.14: one-prompt picker, context-length driven auto-decision,
|
|
2581
|
+
// direct-resume mode flags (--full, --tail10, --tail20, --summary).
|
|
2582
|
+
const parts = input.split(/\s+/).filter(Boolean);
|
|
2583
|
+
const forcedFlag = parts.find(p => /^(--full|--tail10|--tail20|--recap|--summary|-f|-1|-2|-r|-s)$/.test(p));
|
|
2584
|
+
const forcedMode = forcedFlag
|
|
2585
|
+
? ({ '--full': 'full', '-f': 'full',
|
|
2586
|
+
'--tail10': 'tail-10', '-1': 'tail-10',
|
|
2587
|
+
'--tail20': 'tail-20', '-2': 'tail-20',
|
|
2588
|
+
'--recap': 'tail-20', '-r': 'tail-20',
|
|
2589
|
+
'--summary': 'summary', '-s': 'summary' })[forcedFlag]
|
|
2590
|
+
: null;
|
|
2591
|
+
const targetId = parts.slice(1).find(p => !p.startsWith('-'));
|
|
2592
|
+
|
|
2593
|
+
const resumable = await listResumableSessions();
|
|
2594
|
+
if (resumable.length === 0) {
|
|
2595
|
+
process.stderr.write(` ${c.gray('No resumable sessions found.')}\n`);
|
|
2596
|
+
return;
|
|
2597
|
+
}
|
|
1370
2598
|
|
|
2599
|
+
// 1. Resolve which session to resume.
|
|
2600
|
+
let picked = null;
|
|
1371
2601
|
if (targetId) {
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
process.stderr.write(` ${c.yellow('!')} ${c.dim('No conversation found for session ' + targetId)}\n`);
|
|
2602
|
+
picked = resumable.find(s => s.sessionId === targetId || s.sessionId?.startsWith(targetId));
|
|
2603
|
+
if (!picked) {
|
|
2604
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim(`No session found matching id: ${targetId}`)}\n`);
|
|
1376
2605
|
return;
|
|
1377
2606
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
2607
|
+
} else {
|
|
2608
|
+
while (!picked) {
|
|
2609
|
+
const pickResult = await pickResumableSession(resumable, ctx);
|
|
2610
|
+
if (!pickResult) { process.stderr.write(`\n ${c.dim('Cancelled.')}\n`); return; }
|
|
2611
|
+
if (pickResult.action === 'resume') {
|
|
2612
|
+
if (!pickResult.session) {
|
|
2613
|
+
process.stderr.write(`\n ${c.yellow('!')} ${c.dim('Empty session — pick another.')}\n`);
|
|
2614
|
+
continue;
|
|
2615
|
+
}
|
|
2616
|
+
picked = pickResult.session;
|
|
2617
|
+
break;
|
|
2618
|
+
}
|
|
2619
|
+
if (pickResult.action === 'preview') {
|
|
2620
|
+
// Yield to the event loop so any queued keystrokes from the picker
|
|
2621
|
+
// don't spill into the preview's stdin listener (PRD-068 §5.14 bugfix).
|
|
2622
|
+
await new Promise(r => setImmediate(r));
|
|
2623
|
+
const previewResult = await previewResumeSession(pickResult.session, ctx);
|
|
2624
|
+
if (previewResult && previewResult.action === 'resume') {
|
|
2625
|
+
picked = pickResult.session;
|
|
2626
|
+
// Preview already committed to a mode — skip the threshold overlay.
|
|
2627
|
+
picked._presetMode = previewResult.mode;
|
|
2628
|
+
break;
|
|
2629
|
+
}
|
|
2630
|
+
if (previewResult === null) {
|
|
2631
|
+
// getSessionDetail failed — file missing, unreadable, or huge.
|
|
2632
|
+
// Tell the user why before looping back to the picker.
|
|
2633
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim('Could not load transcript for preview — pick another session or press Enter to resume without preview.')}\n`);
|
|
2634
|
+
}
|
|
2635
|
+
// Yield again so the loop-back render doesn't collide with the
|
|
2636
|
+
// just-closed preview's stdin cleanup.
|
|
2637
|
+
await new Promise(r => setImmediate(r));
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
1383
2640
|
}
|
|
1384
2641
|
|
|
1385
|
-
//
|
|
1386
|
-
|
|
1387
|
-
if (
|
|
1388
|
-
|
|
1389
|
-
|
|
2642
|
+
// 2. Decide the mode. Force-flag > preview-preset > auto-decide > overlay.
|
|
2643
|
+
let mode = forcedMode || picked._presetMode || null;
|
|
2644
|
+
if (!mode) {
|
|
2645
|
+
const currentModel = session?.model || session?.user?.default_reasoning_model || null;
|
|
2646
|
+
const decision = decideResumeMode({
|
|
2647
|
+
transcriptTokens: picked.contextTokens,
|
|
2648
|
+
model: currentModel,
|
|
2649
|
+
settings: ctx.effectivePolicy?.policy?.resume ? { resume: ctx.effectivePolicy.policy.resume } : {},
|
|
2650
|
+
});
|
|
2651
|
+
decision.resumeSummary = picked.resumeSummary || null;
|
|
2652
|
+
if (decision.mode === 'full') {
|
|
2653
|
+
mode = 'full';
|
|
2654
|
+
} else {
|
|
2655
|
+
// Yield before attaching the overlay listener — same race guard as
|
|
2656
|
+
// the preview branch above. Prevents a queued Enter from the picker
|
|
2657
|
+
// slipping into the overlay's stdin, which otherwise looked like a
|
|
2658
|
+
// duplicate picker render.
|
|
2659
|
+
await new Promise(r => setImmediate(r));
|
|
2660
|
+
const chosen = await chooseThresholdMode(ctx, decision);
|
|
2661
|
+
if (!chosen) { process.stderr.write(`\n ${c.dim('Cancelled.')}\n`); return; }
|
|
2662
|
+
mode = chosen;
|
|
2663
|
+
}
|
|
1390
2664
|
}
|
|
1391
2665
|
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
process.stderr.write(` ${c.dim(instr)}\n`);
|
|
1401
|
-
}
|
|
1402
|
-
process.stderr.write(`\n ${c.dim('Enter number (or Esc to cancel):')} `);
|
|
1403
|
-
|
|
1404
|
-
// Read single key for selection
|
|
1405
|
-
const rl = ctx._rl || null;
|
|
1406
|
-
if (rl) rl.pause();
|
|
1407
|
-
const choice = await new Promise((resolve) => {
|
|
1408
|
-
if (!process.stdin.isTTY) { resolve(null); return; }
|
|
1409
|
-
const wasRaw = process.stdin.isRaw;
|
|
1410
|
-
process.stdin.setRawMode(true);
|
|
1411
|
-
process.stdin.resume();
|
|
1412
|
-
process.stdin.once('data', (data) => {
|
|
1413
|
-
process.stdin.setRawMode(wasRaw || false);
|
|
1414
|
-
if (rl) rl.resume();
|
|
1415
|
-
const bytes = [...data];
|
|
1416
|
-
if (bytes[0] === 0x1b || bytes[0] === 0x03) { resolve(null); return; }
|
|
1417
|
-
const num = parseInt(data.toString(), 10);
|
|
1418
|
-
resolve(isNaN(num) ? null : num);
|
|
2666
|
+
// 3. Activate.
|
|
2667
|
+
const source = targetId ? 'direct' : 'picker';
|
|
2668
|
+
const progress = startResumeProgress(mode);
|
|
2669
|
+
let resumed;
|
|
2670
|
+
try {
|
|
2671
|
+
resumed = await ctx.activateResumedSession(picked.sessionId, source, mode, picked, {
|
|
2672
|
+
onProgress: progress.update,
|
|
2673
|
+
onProgressStop: progress.stop,
|
|
1419
2674
|
});
|
|
1420
|
-
}
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
2675
|
+
} finally {
|
|
2676
|
+
progress.stop();
|
|
2677
|
+
}
|
|
2678
|
+
if (!resumed.ok) {
|
|
2679
|
+
if (resumed.reason === 'cwd-cancelled') {
|
|
2680
|
+
process.stderr.write(`\n ${c.dim('Cancelled.')}\n`);
|
|
2681
|
+
} else {
|
|
2682
|
+
process.stderr.write(`\n ${c.yellow('!')} ${c.dim(resumed.reason || 'No messages in that session.')}\n`);
|
|
2683
|
+
}
|
|
1424
2684
|
return;
|
|
1425
2685
|
}
|
|
1426
2686
|
|
|
1427
|
-
|
|
1428
|
-
const
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
2687
|
+
// 4. Report — succinct honest single line, then optional hydration warning.
|
|
2688
|
+
const toolSummary = resumed.stats && resumed.stats.toolCalls
|
|
2689
|
+
? `${resumed.stats.toolCalls} tool calls`
|
|
2690
|
+
: `${resumed.messages} msgs`;
|
|
2691
|
+
const summaryLabel = mode !== 'full' && resumed.summarySource
|
|
2692
|
+
? ` · summary: ${resumed.summarySource}`
|
|
2693
|
+
: '';
|
|
2694
|
+
process.stderr.write(`\n ${c.green('↺')} ${c.dim('Resumed')} ${c.brand(picked.project || path.basename(safeCwd()))} ${c.dim(`· ${resumed.messages} msgs · ${toolSummary} · mode: ${resumeModeLabel(mode)}${summaryLabel}`)}\n`);
|
|
2695
|
+
if (resumed.summaryWarning) {
|
|
2696
|
+
process.stderr.write(` ${c.yellow('⚠')} ${c.dim(`backend summary unavailable — using local summary (${resumed.summaryWarning})`)}\n`);
|
|
2697
|
+
}
|
|
2698
|
+
if (resumed.hydrationFailures?.length) {
|
|
2699
|
+
for (const failure of resumed.hydrationFailures) {
|
|
2700
|
+
process.stderr.write(` ${c.yellow('⚠')} ${c.dim(`could not re-read project root: ${failure}`)}\n`);
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
if (resumed.stayedInCwd) {
|
|
2704
|
+
process.stderr.write(` ${c.yellow('⚠')} ${c.dim(`resumed transcript from ${resumed.savedProjectPath} — running against ${safeCwd()}`)}\n`);
|
|
1432
2705
|
}
|
|
1433
2706
|
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
if (
|
|
1439
|
-
|
|
2707
|
+
// 5. Show continuity context. Non-summary modes use the captured
|
|
2708
|
+
// kepler_event stream when available so the terminal replay matches
|
|
2709
|
+
// the original styled interaction; older sessions fall back to
|
|
2710
|
+
// reconstructed text.
|
|
2711
|
+
if (mode === 'summary' && resumed.summary) {
|
|
2712
|
+
// In summary mode the agent gets only the summary block. Show it so
|
|
2713
|
+
// the user knows what continuity context was included.
|
|
2714
|
+
process.stderr.write(`\n ${c.bold('Continuity Summary')}\n`);
|
|
2715
|
+
process.stderr.write(` ${c.gray('─'.repeat(80))}\n`);
|
|
2716
|
+
for (const line of resumed.summary.split('\n')) {
|
|
2717
|
+
process.stderr.write(` ${c.dim(line)}\n`);
|
|
2718
|
+
}
|
|
2719
|
+
process.stderr.write('\n');
|
|
2720
|
+
} else if (resumed.replayEvents?.length) {
|
|
2721
|
+
renderResumePreview(resumed);
|
|
2722
|
+
} else if (resumed.history?.length) {
|
|
2723
|
+
// Full/tail modes feed real conversation to the agent — show
|
|
2724
|
+
// the tail so the user has visual context. Cap at 30 entries to avoid
|
|
2725
|
+
// flooding the terminal on long sessions.
|
|
2726
|
+
renderHistoryEntries(resumed.history, {
|
|
2727
|
+
limit: 30,
|
|
2728
|
+
maxChars: 200,
|
|
2729
|
+
title: mode?.startsWith('tail-') ? `Recent turns (${mode.replace('tail-', 'last ')})` : 'Conversation history (last 30 entries)',
|
|
2730
|
+
});
|
|
1440
2731
|
}
|
|
1441
|
-
process.stderr.write('\n');
|
|
1442
2732
|
return;
|
|
1443
2733
|
}
|
|
1444
2734
|
|
|
@@ -1512,22 +2802,239 @@ export async function startTerminalRepl() {
|
|
|
1512
2802
|
|
|
1513
2803
|
// Projects are registered and indexed on demand through get_project_overview.
|
|
1514
2804
|
// CheckpointManager records per-file snapshots before edits so /undo works.
|
|
1515
|
-
|
|
1516
|
-
|
|
2805
|
+
let checkpoints = new CheckpointManager(safeCwd());
|
|
2806
|
+
let effectivePolicy = loadEffectivePolicy({ cwd: safeCwd() });
|
|
2807
|
+
let latestProjectContext = null;
|
|
2808
|
+
let latestEnvelope = null;
|
|
2809
|
+
let hookRunner = new HookRunner({ cwd: safeCwd() });
|
|
2810
|
+
let toolExecutor = createToolExecutor({ checkpoints, hookRunner });
|
|
1517
2811
|
const skipPerms = cliArgs.freeswim;
|
|
1518
|
-
|
|
2812
|
+
let approval = new ApprovalManager({ autoApprove: skipPerms, cwd: safeCwd(), policy: effectivePolicy.policy });
|
|
1519
2813
|
|
|
1520
2814
|
// Session manager — persists conversation messages to .kepler/conversations/
|
|
1521
|
-
|
|
2815
|
+
let sessionMgr = new SessionManager(safeCwd());
|
|
1522
2816
|
_sessionMgr = sessionMgr; // expose to renderEvent
|
|
1523
2817
|
|
|
1524
2818
|
// Local JSONL writer — writes cc-lens compatible session data to ~/.kepler/
|
|
1525
|
-
|
|
2819
|
+
let jsonlWriter = new JsonlWriter(safeCwd(), VERSION);
|
|
1526
2820
|
|
|
1527
2821
|
// Persistent stream client — session_id captured from backend on first turn
|
|
1528
2822
|
let streamClient = null;
|
|
1529
2823
|
|
|
1530
|
-
const ctx = { auth, toolExecutor, approval, jsonlWriter, sessionMgr, checkpoints };
|
|
2824
|
+
const ctx = { auth, toolExecutor, approval, jsonlWriter, sessionMgr, checkpoints, effectivePolicy, latestProjectContext, latestEnvelope };
|
|
2825
|
+
|
|
2826
|
+
/**
|
|
2827
|
+
* Activate a previously-recorded session for continuation.
|
|
2828
|
+
*
|
|
2829
|
+
* Contract (PRD-068 §5.14 and follow-up clarification):
|
|
2830
|
+
* 1. Keep the same sessionId. The resumed session IS the same session,
|
|
2831
|
+
* not a fork. Future turns are appended to the SAME .jsonl file that
|
|
2832
|
+
* was read here.
|
|
2833
|
+
* 2. Do not re-write the loaded transcript back to disk. The file already
|
|
2834
|
+
* contains every historical entry; the load path is read-only. Any
|
|
2835
|
+
* duplication would double-count tokens on the next resume.
|
|
2836
|
+
* 3. Fresh sessions (kepler started without /resume) get a fresh UUID
|
|
2837
|
+
* the first time jsonlWriter.writeUserTurn() runs — that path is
|
|
2838
|
+
* untouched by resume, so brand-new sessions never inherit an old id.
|
|
2839
|
+
* 4. In-memory history (session.history / session.agentHistory) mirrors
|
|
2840
|
+
* what the agent will see next turn; it is NOT written back to the
|
|
2841
|
+
* transcript at activation time.
|
|
2842
|
+
*/
|
|
2843
|
+
async function activateResumedSession(sessionId, source = 'resume', historyMode = 'full', resumeEntry = null, options = {}) {
|
|
2844
|
+
const onProgress = typeof options.onProgress === 'function' ? options.onProgress : () => {};
|
|
2845
|
+
const onProgressStop = typeof options.onProgressStop === 'function' ? options.onProgressStop : () => {};
|
|
2846
|
+
// PRD-068 §5.14.6: JSONL is the only source. No legacy conversation fallback.
|
|
2847
|
+
onProgress('reading saved transcript', 14);
|
|
2848
|
+
const detail = await getSessionDetail(sessionId, { filePath: resumeEntry?.transcriptPath });
|
|
2849
|
+
if (!detail) {
|
|
2850
|
+
return { ok: false, reason: `No transcript found for session ${sessionId}` };
|
|
2851
|
+
}
|
|
2852
|
+
onProgress('building resume context', 28);
|
|
2853
|
+
const richHistory = buildResumeHistory({ ...detail, recapTailTurns: 8 }, historyMode);
|
|
2854
|
+
const displayHistory = richHistory.displayHistory;
|
|
2855
|
+
if (!displayHistory.length) {
|
|
2856
|
+
return { ok: false, reason: `Session ${sessionId} has no readable messages` };
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2859
|
+
// PRD-068 §5.14.7: explicit cwd confirmation if saved path differs.
|
|
2860
|
+
const savedProjectPath = detail?.meta?.project || '';
|
|
2861
|
+
let summarySource = 'local';
|
|
2862
|
+
let summaryWarning = '';
|
|
2863
|
+
if (historyMode !== 'full' && richHistory.sourceMessages?.length) {
|
|
2864
|
+
onProgress('summarizing transcript', 34);
|
|
2865
|
+
const backendSummary = await summarizeResumeTranscript({
|
|
2866
|
+
auth,
|
|
2867
|
+
toolExecutor,
|
|
2868
|
+
sessionId,
|
|
2869
|
+
projectPath: savedProjectPath || safeCwd(),
|
|
2870
|
+
messages: richHistory.sourceMessages,
|
|
2871
|
+
});
|
|
2872
|
+
if (backendSummary?.summary) {
|
|
2873
|
+
richHistory.summary = combineResumeSummaries(richHistory.priorSummary, backendSummary.summary);
|
|
2874
|
+
const summaryIndex = Number.isInteger(richHistory.summaryMessageIndex)
|
|
2875
|
+
? richHistory.summaryMessageIndex
|
|
2876
|
+
: 0;
|
|
2877
|
+
if (richHistory.agentHistory?.[summaryIndex]) {
|
|
2878
|
+
const tailTurns = resumeTailTurnCount(historyMode);
|
|
2879
|
+
const prefix = tailTurns
|
|
2880
|
+
? `Summary of earlier turns before the retained last ${tailTurns} conversation messages:\n`
|
|
2881
|
+
: 'Session continuity summary:\n';
|
|
2882
|
+
richHistory.agentHistory[summaryIndex] = {
|
|
2883
|
+
...richHistory.agentHistory[summaryIndex],
|
|
2884
|
+
content: `${prefix}${richHistory.summary}`,
|
|
2885
|
+
};
|
|
2886
|
+
}
|
|
2887
|
+
summarySource = backendSummary.source || 'backend';
|
|
2888
|
+
} else {
|
|
2889
|
+
summarySource = 'local fallback';
|
|
2890
|
+
summaryWarning = backendSummary?.reason || 'backend summary unavailable';
|
|
2891
|
+
}
|
|
2892
|
+
} else if (historyMode !== 'full') {
|
|
2893
|
+
summarySource = 'not needed';
|
|
2894
|
+
summaryWarning = resumeTailTurnCount(historyMode)
|
|
2895
|
+
? 'retained tail covers the whole transcript'
|
|
2896
|
+
: 'empty transcript';
|
|
2897
|
+
}
|
|
2898
|
+
const agentHistory = richHistory.agentHistory;
|
|
2899
|
+
const originalCwd = safeCwd();
|
|
2900
|
+
let switchedProject = false;
|
|
2901
|
+
let projectMissing = false;
|
|
2902
|
+
let stayedInCwd = false;
|
|
2903
|
+
onProgress('checking project cwd', 40);
|
|
2904
|
+
if (savedProjectPath && savedProjectPath !== originalCwd) {
|
|
2905
|
+
if (fs.existsSync(savedProjectPath)) {
|
|
2906
|
+
onProgressStop();
|
|
2907
|
+
const choice = await confirmCwdSwitch(ctx, savedProjectPath, originalCwd);
|
|
2908
|
+
if (choice === 'cancel') return { ok: false, reason: 'cwd-cancelled' };
|
|
2909
|
+
if (choice === 'switch') {
|
|
2910
|
+
try {
|
|
2911
|
+
process.chdir(savedProjectPath);
|
|
2912
|
+
_cachedCwd = process.cwd();
|
|
2913
|
+
switchedProject = true;
|
|
2914
|
+
} catch {
|
|
2915
|
+
projectMissing = true;
|
|
2916
|
+
}
|
|
2917
|
+
} else if (choice === 'stay') {
|
|
2918
|
+
stayedInCwd = true;
|
|
2919
|
+
}
|
|
2920
|
+
} else {
|
|
2921
|
+
projectMissing = true;
|
|
2922
|
+
process.stderr.write(` ${c.yellow('⚠')} ${c.dim(`saved project path unavailable: ${savedProjectPath} — using current cwd`)}\n`);
|
|
2923
|
+
}
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
onProgress('rebuilding local session state', 55);
|
|
2927
|
+
checkpoints = new CheckpointManager(safeCwd());
|
|
2928
|
+
effectivePolicy = loadEffectivePolicy({ cwd: safeCwd() });
|
|
2929
|
+
hookRunner = new HookRunner({ cwd: safeCwd(), sessionId });
|
|
2930
|
+
toolExecutor = createToolExecutor({ checkpoints, hookRunner });
|
|
2931
|
+
approval = new ApprovalManager({ autoApprove: skipPerms, cwd: safeCwd(), policy: effectivePolicy.policy });
|
|
2932
|
+
if (ctx._rl) approval.setReadline(ctx._rl);
|
|
2933
|
+
sessionMgr = new SessionManager(safeCwd());
|
|
2934
|
+
sessionMgr.activateSession(sessionId, {
|
|
2935
|
+
instruction: detail?.meta?.firstPrompt || '',
|
|
2936
|
+
started_at: detail?.meta?.startTime || new Date().toISOString(),
|
|
2937
|
+
}, displayHistory.filter(m => m.role === 'user' || m.role === 'assistant'));
|
|
2938
|
+
_sessionMgr = sessionMgr;
|
|
2939
|
+
|
|
2940
|
+
try { await jsonlWriter.close(); } catch {}
|
|
2941
|
+
jsonlWriter = new JsonlWriter(safeCwd(), VERSION);
|
|
2942
|
+
jsonlWriter.setSessionId(sessionId);
|
|
2943
|
+
if (
|
|
2944
|
+
historyMode !== 'full'
|
|
2945
|
+
&& richHistory.summary
|
|
2946
|
+
&& Number(richHistory.summaryCoveredMessageCount) > Number(richHistory.summaryCheckpointMessageCount || 0)
|
|
2947
|
+
) {
|
|
2948
|
+
jsonlWriter.writeKeplerEvent({
|
|
2949
|
+
type: 'resume_summary',
|
|
2950
|
+
data: {
|
|
2951
|
+
session_id: sessionId,
|
|
2952
|
+
mode: historyMode,
|
|
2953
|
+
mode_label: resumeModeLabel(historyMode),
|
|
2954
|
+
summary: richHistory.summary,
|
|
2955
|
+
summary_source: summarySource,
|
|
2956
|
+
summary_warning: summaryWarning || null,
|
|
2957
|
+
source_message_count: richHistory.summaryCoveredMessageCount,
|
|
2958
|
+
previous_source_message_count: richHistory.summaryCheckpointMessageCount || 0,
|
|
2959
|
+
full_message_count: richHistory.fullMessageCount || 0,
|
|
2960
|
+
},
|
|
2961
|
+
});
|
|
2962
|
+
}
|
|
2963
|
+
jsonlWriter.writeKeplerEvent({
|
|
2964
|
+
type: 'resume_context',
|
|
2965
|
+
data: {
|
|
2966
|
+
session_id: sessionId,
|
|
2967
|
+
source,
|
|
2968
|
+
mode: historyMode,
|
|
2969
|
+
mode_label: resumeModeLabel(historyMode),
|
|
2970
|
+
messages: displayHistory.length,
|
|
2971
|
+
summary_source: summarySource,
|
|
2972
|
+
summary_injected: historyMode !== 'full' && Boolean(richHistory.agentHistory?.[richHistory.summaryMessageIndex ?? 0]?.content),
|
|
2973
|
+
summary_warning: summaryWarning || null,
|
|
2974
|
+
summary_source_message_count: richHistory.summaryCoveredMessageCount || 0,
|
|
2975
|
+
previous_summary_source_message_count: richHistory.summaryCheckpointMessageCount || 0,
|
|
2976
|
+
project_path: savedProjectPath || safeCwd(),
|
|
2977
|
+
},
|
|
2978
|
+
});
|
|
2979
|
+
|
|
2980
|
+
streamClient = null;
|
|
2981
|
+
latestProjectContext = loadProjectContext({ cwd: safeCwd() });
|
|
2982
|
+
latestEnvelope = null;
|
|
2983
|
+
|
|
2984
|
+
// PRD-068 §5.14.8: report hydration failures instead of swallowing them.
|
|
2985
|
+
const hydrationFailures = [];
|
|
2986
|
+
const resumeRoots = getTranscriptProjectRoots(detail);
|
|
2987
|
+
const rootsToRegister = [...new Set([safeCwd(), ...resumeRoots].filter(Boolean))];
|
|
2988
|
+
onProgress('hydrating project roots', 68);
|
|
2989
|
+
for (let i = 0; i < rootsToRegister.length; i++) {
|
|
2990
|
+
const root = rootsToRegister[i];
|
|
2991
|
+
onProgress(`hydrating project root ${i + 1}/${rootsToRegister.length}`, 68 + Math.round((i / Math.max(1, rootsToRegister.length)) * 18));
|
|
2992
|
+
try {
|
|
2993
|
+
await toolExecutor.execute('get_project_overview', { path: root });
|
|
2994
|
+
} catch {
|
|
2995
|
+
hydrationFailures.push(root);
|
|
2996
|
+
}
|
|
2997
|
+
}
|
|
2998
|
+
|
|
2999
|
+
onProgress('preparing replay', 92);
|
|
3000
|
+
session.history = displayHistory;
|
|
3001
|
+
session.agentHistory = agentHistory;
|
|
3002
|
+
session.id = sessionId;
|
|
3003
|
+
session.turns = displayHistory.filter(m => m.role === 'user').length;
|
|
3004
|
+
session.lastTask = detail?.meta?.firstPrompt || session.history.find(m => m.role === 'user')?.content || '';
|
|
3005
|
+
|
|
3006
|
+
Object.assign(ctx, {
|
|
3007
|
+
toolExecutor,
|
|
3008
|
+
approval,
|
|
3009
|
+
jsonlWriter,
|
|
3010
|
+
sessionMgr,
|
|
3011
|
+
checkpoints,
|
|
3012
|
+
effectivePolicy,
|
|
3013
|
+
latestProjectContext,
|
|
3014
|
+
latestEnvelope,
|
|
3015
|
+
});
|
|
3016
|
+
|
|
3017
|
+
return {
|
|
3018
|
+
ok: true,
|
|
3019
|
+
messages: displayHistory.length,
|
|
3020
|
+
projectPath: savedProjectPath || safeCwd(),
|
|
3021
|
+
savedProjectPath,
|
|
3022
|
+
switchedProject,
|
|
3023
|
+
projectMissing,
|
|
3024
|
+
stayedInCwd,
|
|
3025
|
+
hydrationFailures,
|
|
3026
|
+
instruction: detail?.meta?.firstPrompt || '',
|
|
3027
|
+
historyMode,
|
|
3028
|
+
summary: richHistory.summary || '',
|
|
3029
|
+
summarySource,
|
|
3030
|
+
summaryWarning,
|
|
3031
|
+
history: displayHistory,
|
|
3032
|
+
replayEvents: detail.replayEvents || [],
|
|
3033
|
+
stats: richHistory.stats,
|
|
3034
|
+
source,
|
|
3035
|
+
};
|
|
3036
|
+
}
|
|
3037
|
+
ctx.activateResumedSession = activateResumedSession;
|
|
1531
3038
|
|
|
1532
3039
|
// ── Print banner + preflight + init BEFORE mounting the status bar ──
|
|
1533
3040
|
// The status bar shrinks the scroll region; if it mounts first, the
|
|
@@ -1558,18 +3065,18 @@ export async function startTerminalRepl() {
|
|
|
1558
3065
|
: sessionMgr.getLastSession();
|
|
1559
3066
|
|
|
1560
3067
|
if (lastSession) {
|
|
1561
|
-
const
|
|
1562
|
-
if (
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
process.stderr.write(`
|
|
1567
|
-
if (
|
|
1568
|
-
|
|
1569
|
-
}
|
|
3068
|
+
const resumed = await activateResumedSession(lastSession.sessionId, 'startup');
|
|
3069
|
+
if (resumed.ok) {
|
|
3070
|
+
process.stderr.write(` ${c.green('↺')} ${c.dim(`Resumed session: ${messageCountLabel(resumed.messages)}`)}`);
|
|
3071
|
+
process.stderr.write(` ${c.dim('· project')} ${c.brand(path.basename(safeCwd()))}`);
|
|
3072
|
+
process.stderr.write(` ${c.dim(`· agent ${resumed.historyMode}`)}`);
|
|
3073
|
+
if (resumed.switchedProject) process.stderr.write(` ${c.dim('(cwd restored)')}`);
|
|
3074
|
+
if (resumed.projectMissing) process.stderr.write(` ${c.yellow('(saved project path unavailable; using current cwd)')}`);
|
|
3075
|
+
if (resumed.instruction) process.stderr.write(` ${c.dim('—')} ${c.dim(resumed.instruction.slice(0, 50))}`);
|
|
1570
3076
|
process.stderr.write('\n');
|
|
3077
|
+
renderResumePreview(resumed);
|
|
1571
3078
|
} else {
|
|
1572
|
-
process.stderr.write(` ${c.yellow('!')} ${c.dim('No conversation found for session ' + lastSession.sessionId)}\n`);
|
|
3079
|
+
process.stderr.write(` ${c.yellow('!')} ${c.dim(resumed.reason || 'No conversation found for session ' + lastSession.sessionId)}\n`);
|
|
1573
3080
|
}
|
|
1574
3081
|
} else {
|
|
1575
3082
|
process.stderr.write(` ${c.yellow('!')} ${c.dim('No previous session to resume')}\n`);
|
|
@@ -1615,8 +3122,7 @@ export async function startTerminalRepl() {
|
|
|
1615
3122
|
prompt: userPrompt(),
|
|
1616
3123
|
completer: (line) => {
|
|
1617
3124
|
if (line.startsWith('/')) {
|
|
1618
|
-
|
|
1619
|
-
return [hits.length ? hits : Object.keys(COMMANDS), line];
|
|
3125
|
+
return [commandCompletions(line), line];
|
|
1620
3126
|
}
|
|
1621
3127
|
return [[], line];
|
|
1622
3128
|
},
|
|
@@ -1637,7 +3143,44 @@ export async function startTerminalRepl() {
|
|
|
1637
3143
|
|
|
1638
3144
|
showPrompt();
|
|
1639
3145
|
|
|
3146
|
+
// Guard against concurrent line handlers.
|
|
3147
|
+
//
|
|
3148
|
+
// rl.on('line', async ...) does NOT wait for the previous async handler to
|
|
3149
|
+
// complete before firing again. So a stray '\n' (from readline processing
|
|
3150
|
+
// '\r\n' as two events after an interactive picker resumes it, or from a
|
|
3151
|
+
// paste) can spawn a second handleCommand run while the first is still
|
|
3152
|
+
// awaiting inside /resume, /login, etc. Symptom: the picker or overlay
|
|
3153
|
+
// appears to render twice.
|
|
3154
|
+
//
|
|
3155
|
+
// Rules:
|
|
3156
|
+
// - Only one command runs at a time.
|
|
3157
|
+
// - Empty lines that arrive while a command is in flight are dropped
|
|
3158
|
+
// (they are almost always stray '\n's from raw-mode key handling).
|
|
3159
|
+
// - Non-empty lines are queued and replayed after the current command
|
|
3160
|
+
// finishes, so the user can queue up work while a long-running command
|
|
3161
|
+
// is still executing.
|
|
3162
|
+
let _lineInFlight = false;
|
|
3163
|
+
const _queuedLines = [];
|
|
1640
3164
|
rl.on('line', async (line) => {
|
|
3165
|
+
if (_lineInFlight) {
|
|
3166
|
+
if (line && line.trim()) _queuedLines.push(line);
|
|
3167
|
+
return;
|
|
3168
|
+
}
|
|
3169
|
+
_lineInFlight = true;
|
|
3170
|
+
try {
|
|
3171
|
+
await _handleLine(line);
|
|
3172
|
+
} finally {
|
|
3173
|
+
_lineInFlight = false;
|
|
3174
|
+
if (_queuedLines.length) {
|
|
3175
|
+
const next = _queuedLines.shift();
|
|
3176
|
+
// Fire the next queued line asynchronously so we don't hold this
|
|
3177
|
+
// finally block open while the next command runs.
|
|
3178
|
+
setImmediate(() => rl.emit('line', next));
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
});
|
|
3182
|
+
|
|
3183
|
+
async function _handleLine(line) {
|
|
1641
3184
|
const input = line.trim();
|
|
1642
3185
|
if (!input) { rl.prompt(); return; }
|
|
1643
3186
|
|
|
@@ -1660,16 +3203,20 @@ export async function startTerminalRepl() {
|
|
|
1660
3203
|
}
|
|
1661
3204
|
|
|
1662
3205
|
// Regular prompt
|
|
1663
|
-
|
|
3206
|
+
const userMessage = { role: 'user', content: input };
|
|
3207
|
+
session.history.push(userMessage);
|
|
3208
|
+
session.agentHistory.push(userMessage);
|
|
1664
3209
|
session.turns++;
|
|
1665
3210
|
session.toolCalls = 0;
|
|
1666
3211
|
session.lastTask = input;
|
|
1667
3212
|
// Reset per-turn counts so the mission report reflects this turn only.
|
|
1668
3213
|
session.toolCounts = {};
|
|
1669
3214
|
session.subAgentCounts = {};
|
|
3215
|
+
session.filesRead = [];
|
|
1670
3216
|
session.savedUsd = 0;
|
|
1671
3217
|
session._lastEmittedThinking = '';
|
|
1672
3218
|
session.creditsLowWarned = false;
|
|
3219
|
+
session.msgsLowWarned = false;
|
|
1673
3220
|
|
|
1674
3221
|
// Tell the orbit a new turn started — switches to DISCOVERY and updates
|
|
1675
3222
|
// task / turn counters in the status bar.
|
|
@@ -1679,11 +3226,14 @@ export async function startTerminalRepl() {
|
|
|
1679
3226
|
if (session.turns === 1) {
|
|
1680
3227
|
sessionMgr.start(input);
|
|
1681
3228
|
}
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
3229
|
+
let userTurnWritten = false;
|
|
3230
|
+
const writeCurrentUserTurn = () => {
|
|
3231
|
+
if (userTurnWritten) return;
|
|
3232
|
+
jsonlWriter.writeUserTurn(input);
|
|
3233
|
+
jsonlWriter.writeHistory(input);
|
|
3234
|
+
userTurnWritten = true;
|
|
3235
|
+
};
|
|
3236
|
+
if (session.id) writeCurrentUserTurn();
|
|
1687
3237
|
|
|
1688
3238
|
const creds = auth.loadCredentials();
|
|
1689
3239
|
if (!creds.token) {
|
|
@@ -1705,6 +3255,9 @@ export async function startTerminalRepl() {
|
|
|
1705
3255
|
});
|
|
1706
3256
|
}
|
|
1707
3257
|
const client = streamClient;
|
|
3258
|
+
if (session.id && !client.sessionId) {
|
|
3259
|
+
client.sessionId = session.id;
|
|
3260
|
+
}
|
|
1708
3261
|
|
|
1709
3262
|
let assistantContent = '';
|
|
1710
3263
|
|
|
@@ -1800,10 +3353,48 @@ export async function startTerminalRepl() {
|
|
|
1800
3353
|
|
|
1801
3354
|
const execContext = { cwd: safeCwd() };
|
|
1802
3355
|
if (skipPerms) execContext.freeswim = true;
|
|
1803
|
-
|
|
1804
|
-
|
|
3356
|
+
effectivePolicy = loadEffectivePolicy({ cwd: safeCwd() });
|
|
3357
|
+
approval.policy = effectivePolicy.policy;
|
|
3358
|
+
if (approval.trustStore) approval.trustStore.policy = effectivePolicy.policy;
|
|
3359
|
+
hookRunner.reload();
|
|
3360
|
+
latestProjectContext = loadProjectContext({ cwd: safeCwd(), previous: latestProjectContext });
|
|
3361
|
+
const promptHook = await hookRunner.run('UserPromptSubmit', {
|
|
3362
|
+
input: { prompt: input },
|
|
3363
|
+
turnId: String(session.turns),
|
|
3364
|
+
});
|
|
3365
|
+
const hookHints = (promptHook.results || [])
|
|
3366
|
+
.map(r => r.parsed?.feedback)
|
|
3367
|
+
.filter(Boolean)
|
|
3368
|
+
.map(text => ({ source: 'hook', kind: 'feedback', text, ttl_turns: 1, priority: 'medium' }));
|
|
3369
|
+
const rejectionHints = (approval.consumeRejectionHints?.() || [])
|
|
3370
|
+
.map(h => ({
|
|
3371
|
+
source: 'hitl',
|
|
3372
|
+
kind: 'approval_rejection',
|
|
3373
|
+
text: `${h.decision === 'replan' ? 'User requested a re-plan' : 'User rejected approval'} for ${h.tool}. ${h.note ? `Reason: ${h.note}` : h.reason}. Adjust the approach before retrying.`,
|
|
3374
|
+
ttl_turns: 1,
|
|
3375
|
+
priority: 'high',
|
|
3376
|
+
}));
|
|
3377
|
+
latestEnvelope = buildContextEnvelope({
|
|
3378
|
+
cwd: safeCwd(),
|
|
3379
|
+
effectivePolicy,
|
|
3380
|
+
projectContext: latestProjectContext,
|
|
3381
|
+
activeHints: [...hookHints, ...rejectionHints],
|
|
3382
|
+
projectResources: toolExecutor.getProjectResources(),
|
|
3383
|
+
agentContext: toolExecutor.getAgentContext(),
|
|
3384
|
+
});
|
|
3385
|
+
ctx.effectivePolicy = effectivePolicy;
|
|
3386
|
+
ctx.latestProjectContext = latestProjectContext;
|
|
3387
|
+
ctx.latestEnvelope = latestEnvelope;
|
|
3388
|
+
Object.assign(execContext, latestEnvelope);
|
|
3389
|
+
if (skipPerms) execContext.freeswim = true;
|
|
3390
|
+
for (const file of latestProjectContext.changed || []) {
|
|
3391
|
+
if (effectivePolicy.policy.context?.showReloadNotice) {
|
|
3392
|
+
process.stderr.write(` ${c.dim(`[Context] ${file.label} updated — re-read`)}\n`);
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
1805
3395
|
|
|
1806
|
-
for await (const event of client.execute(input, execContext, session.
|
|
3396
|
+
for await (const event of client.execute(input, execContext, session.agentHistory)) {
|
|
3397
|
+
jsonlWriter.writeKeplerEvent(event);
|
|
1807
3398
|
if (event.type === 'plan_created' || event.type === 'goal_created') {
|
|
1808
3399
|
persistProjectArtifacts(
|
|
1809
3400
|
event.data,
|
|
@@ -1833,6 +3424,8 @@ export async function startTerminalRepl() {
|
|
|
1833
3424
|
// Local JSONL: capture session ID from backend
|
|
1834
3425
|
if (event.type === 'session_info' && event.data?.session_id) {
|
|
1835
3426
|
jsonlWriter.setSessionId(event.data.session_id);
|
|
3427
|
+
hookRunner.sessionId = event.data.session_id;
|
|
3428
|
+
writeCurrentUserTurn();
|
|
1836
3429
|
}
|
|
1837
3430
|
|
|
1838
3431
|
// Local JSONL: accumulate tool calls
|
|
@@ -1865,15 +3458,17 @@ export async function startTerminalRepl() {
|
|
|
1865
3458
|
}
|
|
1866
3459
|
|
|
1867
3460
|
if (assistantContent) {
|
|
1868
|
-
|
|
1869
|
-
|
|
3461
|
+
const assistantMessage = { role: 'assistant', content: assistantContent };
|
|
3462
|
+
session.history.push(assistantMessage);
|
|
3463
|
+
session.agentHistory.push(assistantMessage);
|
|
1870
3464
|
}
|
|
1871
3465
|
|
|
1872
3466
|
showPrompt();
|
|
1873
|
-
}
|
|
3467
|
+
}
|
|
1874
3468
|
|
|
1875
3469
|
rl.on('close', async () => {
|
|
1876
3470
|
stopSpinner();
|
|
3471
|
+
await hookRunner.run('Stop', { input: { session_id: session.id || '' } });
|
|
1877
3472
|
await jsonlWriter.close();
|
|
1878
3473
|
process.stderr.write(`\n ${c.dim('session ended')}\n\n`);
|
|
1879
3474
|
process.exit(0);
|