@axplusb/kepler 2.2.0 → 2.3.1
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/KEPLER-README.md +157 -142
- package/README.md +37 -65
- package/package.json +2 -2
- package/src/config/cli-args.mjs +14 -0
- package/src/core/agent-loop.mjs +8 -2
- package/src/core/cache-control.mjs +92 -0
- package/src/core/compact-history.mjs +127 -0
- package/src/core/headless.mjs +85 -9
- package/src/core/jsonl-writer.mjs +9 -0
- package/src/core/local-agent.mjs +121 -14
- package/src/core/local-store.mjs +20 -4
- package/src/core/resume-mode.mjs +11 -1
- package/src/core/tasks.mjs +67 -1
- package/src/core/work-scope.mjs +217 -0
- package/src/terminal/main.mjs +2 -0
- package/src/terminal/repl.mjs +249 -17
- package/src/ui/commands.mjs +5 -4
package/src/terminal/repl.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import { calculateCost, formatCostValue, formatTokens, costToCredits, formatCred
|
|
|
23
23
|
import { TarangStreamClient, EVENT_TYPES } from '../core/stream-client.mjs';
|
|
24
24
|
import { JsonlWriter } from '../core/jsonl-writer.mjs';
|
|
25
25
|
import { createToolExecutor } from '../core/tool-executor.mjs';
|
|
26
|
+
import { buildWorkScope } from '../core/work-scope.mjs';
|
|
26
27
|
import { CheckpointManager } from '../core/checkpoints.mjs';
|
|
27
28
|
import { HookRunner } from '../config/hook-runner.mjs';
|
|
28
29
|
import { runPreflight } from '../onboarding/preflight.mjs';
|
|
@@ -47,7 +48,8 @@ import { loadProjectContext } from '../core/project-context-loader.mjs';
|
|
|
47
48
|
import { buildContextEnvelope } from '../core/context-envelope.mjs';
|
|
48
49
|
import { buildResumeHistory, combineResumeSummaries, getRecentSessions, getSessionDetail, getTranscriptProjectRoots } from '../core/local-store.mjs';
|
|
49
50
|
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 { appendTask, ensureTaskFiles, loadTaskBoard, moveTask, removeTask, taskCounts, TASK_FILES, updateTask } from '../core/tasks.mjs';
|
|
52
|
+
import { applyCompactSummary, localCompactSummary, parseCompactTailCount, prepareCompactHistory } from '../core/compact-history.mjs';
|
|
51
53
|
import { toolDisplayLabel, toolDisplaySummary } from './tool-display.mjs';
|
|
52
54
|
import { createOrbit } from '../state/orbit.mjs';
|
|
53
55
|
import { attachOrbit, unmount as unmountStatusBar } from '../ui/status-bar.mjs';
|
|
@@ -195,6 +197,18 @@ function formatResumeCheckpointStatus(session) {
|
|
|
195
197
|
return c.dim(` · summarized${pct}`);
|
|
196
198
|
}
|
|
197
199
|
|
|
200
|
+
function formatResumeContextStatus(session) {
|
|
201
|
+
const full = formatCtxTokens(session?.contextTokens || 0);
|
|
202
|
+
const marker = session?.resumeSummary;
|
|
203
|
+
if (!marker || !Number(marker.sourceMessageCount)) {
|
|
204
|
+
return c.dim(`${full.padStart(5, ' ')} ctx`);
|
|
205
|
+
}
|
|
206
|
+
const resumable = formatCtxTokens(projectedTokensForChoice('checkpoint-full', session.contextTokens || 0, {
|
|
207
|
+
resumeSummary: marker,
|
|
208
|
+
}));
|
|
209
|
+
return c.dim(`${resumable.padStart(5, ' ')} resumable · ${full} full`) + formatResumeCheckpointStatus(session);
|
|
210
|
+
}
|
|
211
|
+
|
|
198
212
|
function formatRelativeTime(iso) {
|
|
199
213
|
if (!iso) return '';
|
|
200
214
|
const t = Date.parse(iso);
|
|
@@ -249,7 +263,7 @@ async function pickResumableSession(resumable, ctx) {
|
|
|
249
263
|
const ago = formatRelativeTime(s.updatedAt || s.startedAt).padEnd(9, ' ').slice(0, 9);
|
|
250
264
|
const status = endStatusMarker(s.endStatus);
|
|
251
265
|
const msgs = String(s.messageCount).padStart(3, ' ') + ' msgs';
|
|
252
|
-
const ctx =
|
|
266
|
+
const ctx = formatResumeContextStatus(s);
|
|
253
267
|
const cost = formatSessionCost(s.costUsd);
|
|
254
268
|
const partial = s.partial ? c.yellow(' ⚠partial') : '';
|
|
255
269
|
const instr = oneLineInstruction(s.instruction, 48);
|
|
@@ -307,8 +321,14 @@ async function chooseThresholdMode(ctx, decision) {
|
|
|
307
321
|
if (rl) rl.pause();
|
|
308
322
|
|
|
309
323
|
const canFull = decision.mode !== 'no-full-allowed';
|
|
324
|
+
const hasCheckpoint = Boolean(decision.resumeSummary?.sourceMessageCount);
|
|
325
|
+
const firstOption = canFull
|
|
326
|
+
? { key: 'f', value: 'full', label: 'full transcript', enabled: true }
|
|
327
|
+
: hasCheckpoint
|
|
328
|
+
? { key: 'f', value: 'checkpoint-full', label: 'checkpointed transcript', enabled: true }
|
|
329
|
+
: { key: 'f', value: 'full', label: 'full transcript', enabled: false };
|
|
310
330
|
const options = [
|
|
311
|
-
|
|
331
|
+
firstOption,
|
|
312
332
|
{ key: 's', value: 'summary', label: 'summary only', enabled: true },
|
|
313
333
|
{ key: '1', value: 'tail-10', label: 'summary + last 10 turns', enabled: true },
|
|
314
334
|
{ key: '2', value: 'tail-20', label: 'summary + last 20 turns', enabled: true },
|
|
@@ -327,7 +347,8 @@ async function chooseThresholdMode(ctx, decision) {
|
|
|
327
347
|
const projected = formatCtxTokens(decision.projected);
|
|
328
348
|
const win = formatCtxTokens(decision.windowSize);
|
|
329
349
|
const lines = [];
|
|
330
|
-
|
|
350
|
+
const rawLabel = hasCheckpoint ? 'Raw full transcript would use' : 'This session would use';
|
|
351
|
+
lines.push(` ${rawLabel} ${c.brand(`${projected} / ${win}`)} tokens (${pct}%)`);
|
|
331
352
|
if (decision.resumeSummary?.sourceMessageCount) {
|
|
332
353
|
const covered = Number(decision.resumeSummary.sourceMessageCount) || 0;
|
|
333
354
|
const full = Number(decision.resumeSummary.fullMessageCount) || 0;
|
|
@@ -336,14 +357,18 @@ async function chooseThresholdMode(ctx, decision) {
|
|
|
336
357
|
}
|
|
337
358
|
lines.push(canFull
|
|
338
359
|
? ` ${c.yellow('⚠')} ${c.dim('close to the highWatermark — consider a leaner mode:')}`
|
|
339
|
-
:
|
|
360
|
+
: hasCheckpoint
|
|
361
|
+
? ` ${c.yellow('⚠')} ${c.dim('raw full is over hardCap — checkpoint/tail modes are available:')}`
|
|
362
|
+
: ` ${c.red('⛔')} ${c.dim('over hardCap — full mode disabled:')}`);
|
|
340
363
|
lines.push('');
|
|
341
364
|
for (let i = 0; i < options.length; i++) {
|
|
342
365
|
const o = options[i];
|
|
343
366
|
const disabled = !o.enabled;
|
|
344
367
|
const marker = i === selected && !disabled ? c.brand('▸') : ' ';
|
|
345
368
|
const keyTag = c.dim('[') + (disabled ? c.dim(o.key) : c.brand(o.key)) + c.dim(']');
|
|
346
|
-
const proj = formatCtxTokens(projectedTokensForChoice(o.value, decision.projected
|
|
369
|
+
const proj = formatCtxTokens(projectedTokensForChoice(o.value, decision.projected, {
|
|
370
|
+
resumeSummary: decision.resumeSummary,
|
|
371
|
+
}));
|
|
347
372
|
const label = disabled ? c.dim(o.label) : (i === selected ? c.brand(o.label) : o.label);
|
|
348
373
|
const projCol = c.dim(`${proj.padStart(5, ' ')} ctx`);
|
|
349
374
|
const suffix = disabled ? c.dim(' (over hardCap)') : '';
|
|
@@ -388,6 +413,7 @@ async function chooseThresholdMode(ctx, decision) {
|
|
|
388
413
|
}
|
|
389
414
|
|
|
390
415
|
function resumeModeLabel(mode = 'full') {
|
|
416
|
+
if (mode === 'checkpoint-full') return 'checkpointed transcript';
|
|
391
417
|
if (mode === 'summary') return 'summary only';
|
|
392
418
|
const tailTurns = resumeTailTurnCount(mode);
|
|
393
419
|
if (tailTurns) return `summary + last ${tailTurns} turns`;
|
|
@@ -433,7 +459,7 @@ async function previewResumeSession(session, ctx) {
|
|
|
433
459
|
if (scrollOffset > maxOffset) scrollOffset = maxOffset;
|
|
434
460
|
|
|
435
461
|
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')}`);
|
|
462
|
+
lines.push(` ${c.bold('Preview:')} ${c.brand(session.project || '(unknown)')} ${c.dim('Mode:')} ${c.brand(resumeModeLabel(mode))} ${c.dim(formatCtxTokens(projectedTokensForChoice(mode, session.contextTokens, { resumeSummary: session.resumeSummary })) + ' ctx')}`);
|
|
437
463
|
lines.push(` ${c.dim('─'.repeat(60))}`);
|
|
438
464
|
for (let i = scrollOffset; i < Math.min(scrollOffset + rows, totalLines); i++) {
|
|
439
465
|
lines.push(fitAnsiLine(` ${c.dim(contentLines[i] || '')}`, cols - 1));
|
|
@@ -459,7 +485,7 @@ async function previewResumeSession(session, ctx) {
|
|
|
459
485
|
if (key === '[B') { scrollOffset += 1; render(); return; }
|
|
460
486
|
if (key === '[5~') { scrollOffset = Math.max(0, scrollOffset - 10); render(); return; }
|
|
461
487
|
if (key === '[6~') { scrollOffset += 10; render(); return; }
|
|
462
|
-
if (low === 'f') { mode = 'full'; history = rich(); scrollOffset = 0; render(); return; }
|
|
488
|
+
if (low === 'f') { mode = session.resumeSummary?.sourceMessageCount ? 'checkpoint-full' : 'full'; history = rich(); scrollOffset = 0; render(); return; }
|
|
463
489
|
if (low === 's') { mode = 'summary'; history = rich(); scrollOffset = 0; render(); return; }
|
|
464
490
|
if (low === '1') { mode = 'tail-10'; history = rich(); scrollOffset = 0; render(); return; }
|
|
465
491
|
if (low === '2') { mode = 'tail-20'; history = rich(); scrollOffset = 0; render(); return; }
|
|
@@ -658,6 +684,120 @@ async function summarizeResumeTranscript({
|
|
|
658
684
|
}
|
|
659
685
|
}
|
|
660
686
|
|
|
687
|
+
async function compactCurrentSession(ctx, rest = '') {
|
|
688
|
+
const tailCount = parseCompactTailCount(rest, 8);
|
|
689
|
+
const preparedLive = prepareCompactHistory({
|
|
690
|
+
agentHistory: session.agentHistory,
|
|
691
|
+
tailCount,
|
|
692
|
+
});
|
|
693
|
+
if (!preparedLive.ok) {
|
|
694
|
+
process.stderr.write(` ${c.gray(`Nothing to compact — ${preparedLive.reason}.`)}\n`);
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const progress = startResumeProgress('compact');
|
|
699
|
+
progress.update('preparing compact summary', 18);
|
|
700
|
+
let sourceMessages = preparedLive.sourceMessages;
|
|
701
|
+
let priorSummary = preparedLive.previousSummary || '';
|
|
702
|
+
let previousSourceMessageCount = 0;
|
|
703
|
+
let fullMessageCount = preparedLive.beforeCount;
|
|
704
|
+
let projectPath = safeCwd();
|
|
705
|
+
let sourceFrom = 'live';
|
|
706
|
+
let summaryWarning = '';
|
|
707
|
+
|
|
708
|
+
try {
|
|
709
|
+
if (session.id && ctx.jsonlWriter?.flush) {
|
|
710
|
+
progress.update('reading transcript checkpoint', 28);
|
|
711
|
+
await ctx.jsonlWriter.flush();
|
|
712
|
+
const detail = await getSessionDetail(session.id, { filePath: ctx.jsonlWriter.transcriptPath });
|
|
713
|
+
if (detail) {
|
|
714
|
+
const richHistory = buildResumeHistory({ ...detail, recapTailTurns: 8 }, `tail-${tailCount}`);
|
|
715
|
+
if (richHistory.sourceMessages?.length) {
|
|
716
|
+
sourceMessages = richHistory.sourceMessages;
|
|
717
|
+
priorSummary = richHistory.priorSummary || '';
|
|
718
|
+
previousSourceMessageCount = richHistory.summaryCheckpointMessageCount || 0;
|
|
719
|
+
fullMessageCount = richHistory.fullMessageCount || sourceMessages.length;
|
|
720
|
+
projectPath = detail.meta?.project || safeCwd();
|
|
721
|
+
sourceFrom = 'transcript';
|
|
722
|
+
} else if (richHistory.priorSummary) {
|
|
723
|
+
priorSummary = richHistory.priorSummary;
|
|
724
|
+
previousSourceMessageCount = richHistory.summaryCheckpointMessageCount || 0;
|
|
725
|
+
fullMessageCount = richHistory.fullMessageCount || preparedLive.beforeCount;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (!sourceMessages.length) {
|
|
731
|
+
progress.stop();
|
|
732
|
+
process.stderr.write(` ${c.gray('Nothing new to compact.')}\n`);
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
progress.update('summarizing compacted history', 46);
|
|
737
|
+
const backendSummary = await summarizeResumeTranscript({
|
|
738
|
+
auth: ctx.auth,
|
|
739
|
+
toolExecutor: ctx.toolExecutor,
|
|
740
|
+
sessionId: session.id,
|
|
741
|
+
projectPath,
|
|
742
|
+
messages: sourceMessages,
|
|
743
|
+
});
|
|
744
|
+
let summarySource = backendSummary?.summary ? (backendSummary.source || 'backend') : 'local fallback';
|
|
745
|
+
let deltaSummary = backendSummary?.summary || '';
|
|
746
|
+
if (!deltaSummary) {
|
|
747
|
+
summaryWarning = backendSummary?.reason || 'backend summary unavailable';
|
|
748
|
+
deltaSummary = localCompactSummary(sourceMessages);
|
|
749
|
+
}
|
|
750
|
+
const summary = combineResumeSummaries(priorSummary, deltaSummary);
|
|
751
|
+
|
|
752
|
+
progress.update('rewriting live context', 74);
|
|
753
|
+
const applied = applyCompactSummary({
|
|
754
|
+
prepared: { ...preparedLive, sourceMessages },
|
|
755
|
+
summary,
|
|
756
|
+
sessionId: session.id,
|
|
757
|
+
cwd: projectPath,
|
|
758
|
+
originalRequest: session.history.find(m => m.role === 'user')?.content || session.lastTask || '',
|
|
759
|
+
previousSourceMessageCount,
|
|
760
|
+
});
|
|
761
|
+
session.agentHistory = applied.agentHistory;
|
|
762
|
+
session.compactSummary = summary;
|
|
763
|
+
session.compactSourceMessageCount = applied.sourceMessageCount;
|
|
764
|
+
|
|
765
|
+
if (ctx.jsonlWriter) {
|
|
766
|
+
progress.update('writing summary checkpoint', 88);
|
|
767
|
+
ctx.jsonlWriter.writeKeplerEvent({
|
|
768
|
+
type: 'resume_summary',
|
|
769
|
+
data: {
|
|
770
|
+
session_id: session.id || null,
|
|
771
|
+
mode: 'compact',
|
|
772
|
+
mode_label: '/compact',
|
|
773
|
+
summary,
|
|
774
|
+
summary_source: summarySource,
|
|
775
|
+
summary_warning: summaryWarning || null,
|
|
776
|
+
source: sourceFrom,
|
|
777
|
+
source_message_count: applied.sourceMessageCount,
|
|
778
|
+
previous_source_message_count: previousSourceMessageCount,
|
|
779
|
+
full_message_count: fullMessageCount,
|
|
780
|
+
retained_tail_messages: applied.retainedCount,
|
|
781
|
+
live_before_messages: applied.beforeCount,
|
|
782
|
+
live_after_messages: applied.afterCount,
|
|
783
|
+
},
|
|
784
|
+
});
|
|
785
|
+
await ctx.jsonlWriter.flush?.();
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
progress.stop();
|
|
789
|
+
process.stderr.write(
|
|
790
|
+
` ${c.green('✓')} ${c.dim(`Compacted context: ${applied.beforeCount} → ${applied.afterCount} live messages · retained ${applied.retainedCount} · summary ${summarySource}`)}\n`
|
|
791
|
+
);
|
|
792
|
+
if (summaryWarning) {
|
|
793
|
+
process.stderr.write(` ${c.yellow('⚠')} ${c.dim(`backend summary unavailable — used local summary (${summaryWarning})`)}\n`);
|
|
794
|
+
}
|
|
795
|
+
} catch (err) {
|
|
796
|
+
progress.stop();
|
|
797
|
+
process.stderr.write(` ${c.red(`Compact failed: ${err?.message || String(err)}`)}\n`);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
661
801
|
function resumeTailTurnCount(mode = '') {
|
|
662
802
|
const match = String(mode || '').match(/^tail-(\d+)$/);
|
|
663
803
|
if (!match) return null;
|
|
@@ -1121,12 +1261,26 @@ function printBanner(auth) {
|
|
|
1121
1261
|
* Left side: last-turn summary (tools, time, cost)
|
|
1122
1262
|
* Right side: session totals (ctx%, tokens)
|
|
1123
1263
|
*/
|
|
1264
|
+
function computeCacheTotals() {
|
|
1265
|
+
let read = 0;
|
|
1266
|
+
let write = 0;
|
|
1267
|
+
for (const b of session.costBreakdown) {
|
|
1268
|
+
read += b.cache_read_tokens || 0;
|
|
1269
|
+
write += b.cache_creation_tokens || 0;
|
|
1270
|
+
}
|
|
1271
|
+
const denom = session.inputTokens + read;
|
|
1272
|
+
const hitRate = denom > 0 ? Math.round((read / denom) * 100) : 0;
|
|
1273
|
+
return { read, write, hitRate };
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1124
1276
|
function buildContextStrip() {
|
|
1125
1277
|
const totalTokens = session.inputTokens + session.outputTokens;
|
|
1126
1278
|
const elapsed = formatElapsed(session.startTime);
|
|
1279
|
+
const cache = computeCacheTotals();
|
|
1127
1280
|
|
|
1128
1281
|
const right = [
|
|
1129
1282
|
c.dim(`${formatTokens(totalTokens)} tok`),
|
|
1283
|
+
...(cache.read > 0 ? [c.dim(`cache ${cache.hitRate}%`)] : []),
|
|
1130
1284
|
c.dim(elapsed),
|
|
1131
1285
|
].join(c.dim(' · '));
|
|
1132
1286
|
|
|
@@ -1209,10 +1363,12 @@ function updateStatusBar() {
|
|
|
1209
1363
|
// buffered head as a regular two-line shape first so the interleaving
|
|
1210
1364
|
// content lands below it.
|
|
1211
1365
|
let _pendingHead = null; // { callId, head, indent }
|
|
1366
|
+
let _lastRenderedBlock = null; // 'tool' | 'content' | null
|
|
1212
1367
|
|
|
1213
1368
|
function flushPendingHead() {
|
|
1214
1369
|
if (!_pendingHead) return;
|
|
1215
|
-
process.stderr.write(
|
|
1370
|
+
process.stderr.write(`${_pendingHead.head}\n`);
|
|
1371
|
+
_lastRenderedBlock = 'tool';
|
|
1216
1372
|
_pendingHead = null;
|
|
1217
1373
|
}
|
|
1218
1374
|
|
|
@@ -1297,7 +1453,8 @@ function renderToolResult(data, eventType = 'tool_result') {
|
|
|
1297
1453
|
const cols = process.stderr.columns || 120;
|
|
1298
1454
|
const combined = `${_pendingHead.head} ${outcome}`;
|
|
1299
1455
|
if (stripAnsi(combined).length <= cols) {
|
|
1300
|
-
process.stderr.write(
|
|
1456
|
+
process.stderr.write(`${combined}\n`);
|
|
1457
|
+
_lastRenderedBlock = 'tool';
|
|
1301
1458
|
_pendingHead = null;
|
|
1302
1459
|
return;
|
|
1303
1460
|
}
|
|
@@ -1311,6 +1468,7 @@ function renderToolResult(data, eventType = 'tool_result') {
|
|
|
1311
1468
|
|
|
1312
1469
|
// Two-line shape: gutter under the (already-printed or just-flushed) head.
|
|
1313
1470
|
process.stderr.write(`${gutter}${outcome}\n`);
|
|
1471
|
+
_lastRenderedBlock = 'tool';
|
|
1314
1472
|
|
|
1315
1473
|
// Lint warnings stay visible alongside writes.
|
|
1316
1474
|
if (hasLint) {
|
|
@@ -1433,6 +1591,7 @@ function startContentStream() {
|
|
|
1433
1591
|
_streamedPartialText = '';
|
|
1434
1592
|
_renderedToolResults.clear();
|
|
1435
1593
|
_renderedContentThisTurn = false;
|
|
1594
|
+
_lastRenderedBlock = null;
|
|
1436
1595
|
stopSpinner();
|
|
1437
1596
|
}
|
|
1438
1597
|
|
|
@@ -1457,12 +1616,14 @@ function flushContent() {
|
|
|
1457
1616
|
// Any buffered tool head needs to land BEFORE this content so the order
|
|
1458
1617
|
// is preserved on screen.
|
|
1459
1618
|
flushPendingHead();
|
|
1619
|
+
if (_lastRenderedBlock === 'tool') process.stderr.write('\n');
|
|
1460
1620
|
const rendered = renderMarkdown(_streamBuffer);
|
|
1461
1621
|
for (const line of rendered.split('\n')) {
|
|
1462
1622
|
process.stdout.write(` ${line}\n`);
|
|
1463
1623
|
}
|
|
1464
1624
|
_streamBuffer = '';
|
|
1465
1625
|
_renderedContentThisTurn = true;
|
|
1626
|
+
_lastRenderedBlock = 'content';
|
|
1466
1627
|
}
|
|
1467
1628
|
|
|
1468
1629
|
// ── Event Renderer ──
|
|
@@ -1514,11 +1675,13 @@ function renderEvent(event) {
|
|
|
1514
1675
|
}
|
|
1515
1676
|
}
|
|
1516
1677
|
if (text) {
|
|
1678
|
+
if (_lastRenderedBlock === 'tool') process.stderr.write('\n');
|
|
1517
1679
|
const rendered = renderMarkdown(text);
|
|
1518
1680
|
for (const line of rendered.split('\n')) {
|
|
1519
1681
|
process.stdout.write(` ${line}\n`);
|
|
1520
1682
|
}
|
|
1521
1683
|
_renderedContentThisTurn = true;
|
|
1684
|
+
_lastRenderedBlock = 'content';
|
|
1522
1685
|
}
|
|
1523
1686
|
break;
|
|
1524
1687
|
}
|
|
@@ -1968,7 +2131,7 @@ function renderPlanOverview({ ctx, mode = 'overview' } = {}) {
|
|
|
1968
2131
|
}
|
|
1969
2132
|
|
|
1970
2133
|
renderTaskBoard(board, { showDone: mode === 'status' });
|
|
1971
|
-
process.stderr.write(`\n ${c.dim('Update: /tasks add <text> · /tasks active
|
|
2134
|
+
process.stderr.write(`\n ${c.dim('Update: /tasks add <text> · /tasks move active 1 done · /tasks edit active 1 <text>')}\n\n`);
|
|
1972
2135
|
}
|
|
1973
2136
|
|
|
1974
2137
|
function refreshTaskContext(ctx) {
|
|
@@ -1987,7 +2150,7 @@ function handleTasksCommand(rest, ctx) {
|
|
|
1987
2150
|
process.stderr.write(`\n ${c.bold('Tasks')}\n`);
|
|
1988
2151
|
process.stderr.write(` ${c.dim('─'.repeat(60))}\n`);
|
|
1989
2152
|
renderTaskBoard(board, { showDone: true });
|
|
1990
|
-
process.stderr.write(`\n ${c.dim('Update: /tasks add <text> · /tasks active
|
|
2153
|
+
process.stderr.write(`\n ${c.dim('Update: /tasks add <text> · /tasks move active 1 done · /tasks edit active 1 <text>')}\n\n`);
|
|
1991
2154
|
return;
|
|
1992
2155
|
}
|
|
1993
2156
|
|
|
@@ -1998,6 +2161,60 @@ function handleTasksCommand(rest, ctx) {
|
|
|
1998
2161
|
|
|
1999
2162
|
const parts = raw.split(/\s+/);
|
|
2000
2163
|
let verb = (parts.shift() || '').toLowerCase();
|
|
2164
|
+
|
|
2165
|
+
if (verb === 'move') {
|
|
2166
|
+
try {
|
|
2167
|
+
const [from, index, to, ...textParts] = parts;
|
|
2168
|
+
const result = moveTask({ cwd: safeCwd(), from, index, to, text: textParts.join(' ') || undefined });
|
|
2169
|
+
refreshTaskContext(ctx);
|
|
2170
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`moved ${result.from} #${result.index} → ${result.to}`)} ${result.text}\n`);
|
|
2171
|
+
} catch (err) {
|
|
2172
|
+
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
2173
|
+
process.stderr.write(` ${c.gray('Usage: /tasks move <active|backlog|blocked|done> <number> <active|backlog|blocked|done> [new text]')}\n`);
|
|
2174
|
+
}
|
|
2175
|
+
return;
|
|
2176
|
+
}
|
|
2177
|
+
|
|
2178
|
+
if (verb === 'edit' || verb === 'rename') {
|
|
2179
|
+
try {
|
|
2180
|
+
const [list, index, ...textParts] = parts;
|
|
2181
|
+
const result = updateTask({ cwd: safeCwd(), list, index, text: textParts.join(' ') });
|
|
2182
|
+
refreshTaskContext(ctx);
|
|
2183
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`updated ${result.list} #${result.index}`)} ${result.text}\n`);
|
|
2184
|
+
} catch (err) {
|
|
2185
|
+
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
2186
|
+
process.stderr.write(` ${c.gray('Usage: /tasks edit <active|backlog|blocked|done> <number> <new text>')}\n`);
|
|
2187
|
+
}
|
|
2188
|
+
return;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
if (verb === 'remove' || verb === 'rm' || verb === 'delete') {
|
|
2192
|
+
try {
|
|
2193
|
+
const [list, index] = parts;
|
|
2194
|
+
const result = removeTask({ cwd: safeCwd(), list, index });
|
|
2195
|
+
refreshTaskContext(ctx);
|
|
2196
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`removed ${result.list} #${result.index}`)} ${result.task.text}\n`);
|
|
2197
|
+
} catch (err) {
|
|
2198
|
+
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
2199
|
+
process.stderr.write(` ${c.gray('Usage: /tasks remove <active|backlog|blocked|done> <number>')}\n`);
|
|
2200
|
+
}
|
|
2201
|
+
return;
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
if (verb === 'finish' || verb === 'complete' || verb === 'block' || verb === 'unblock') {
|
|
2205
|
+
try {
|
|
2206
|
+
const [from, index, ...textParts] = parts;
|
|
2207
|
+
const to = verb === 'block' ? 'blocked' : verb === 'unblock' ? 'active' : 'done';
|
|
2208
|
+
const result = moveTask({ cwd: safeCwd(), from, index, to, text: textParts.join(' ') || undefined });
|
|
2209
|
+
refreshTaskContext(ctx);
|
|
2210
|
+
process.stderr.write(` ${c.green('✓')} ${c.dim(`moved ${result.from} #${result.index} → ${result.to}`)} ${result.text}\n`);
|
|
2211
|
+
} catch (err) {
|
|
2212
|
+
process.stderr.write(` ${c.red(err.message || String(err))}\n`);
|
|
2213
|
+
process.stderr.write(` ${c.gray(`Usage: /tasks ${verb} <active|backlog|blocked|done> <number> [new text]`)}\n`);
|
|
2214
|
+
}
|
|
2215
|
+
return;
|
|
2216
|
+
}
|
|
2217
|
+
|
|
2001
2218
|
let list = 'backlog';
|
|
2002
2219
|
if (verb === 'add' || verb === 'new') {
|
|
2003
2220
|
const maybeList = (parts[0] || '').toLowerCase();
|
|
@@ -2192,6 +2409,15 @@ async function handleCommand(input, ctx) {
|
|
|
2192
2409
|
}
|
|
2193
2410
|
process.stderr.write(` ${c.dim('CWD')} ${safeCwd()}\n`);
|
|
2194
2411
|
|
|
2412
|
+
// Cache — PRD-071 §1.2. Only surface when we have data; a fresh session
|
|
2413
|
+
// shows nothing rather than a misleading "0%".
|
|
2414
|
+
const cache = computeCacheTotals();
|
|
2415
|
+
if (cache.read > 0 || cache.write > 0) {
|
|
2416
|
+
const readLabel = formatTokens(cache.read);
|
|
2417
|
+
const writeLabel = formatTokens(cache.write);
|
|
2418
|
+
process.stderr.write(` ${c.dim('Cache')} ${cache.hitRate}% hit ${c.dim('·')} ${readLabel} read ${c.dim('·')} ${writeLabel} write\n`);
|
|
2419
|
+
}
|
|
2420
|
+
|
|
2195
2421
|
// Permissions
|
|
2196
2422
|
process.stderr.write(`\n ${c.bold('Permissions')}\n`);
|
|
2197
2423
|
process.stderr.write(` ${c.dim('─'.repeat(44))}\n`);
|
|
@@ -2498,10 +2724,7 @@ async function handleCommand(input, ctx) {
|
|
|
2498
2724
|
}
|
|
2499
2725
|
|
|
2500
2726
|
case '/compact': {
|
|
2501
|
-
|
|
2502
|
-
if (before <= 4) { process.stderr.write(` ${c.gray('Nothing to compact.')}\n`); return; }
|
|
2503
|
-
session.agentHistory.splice(2, session.agentHistory.length - 6);
|
|
2504
|
-
process.stderr.write(` ${c.gray(`Compacted agent context: ${before} → ${session.agentHistory.length} messages`)}\n`);
|
|
2727
|
+
await compactCurrentSession(ctx, rest);
|
|
2505
2728
|
return;
|
|
2506
2729
|
}
|
|
2507
2730
|
|
|
@@ -3358,6 +3581,7 @@ export async function startTerminalRepl() {
|
|
|
3358
3581
|
if (approval.trustStore) approval.trustStore.policy = effectivePolicy.policy;
|
|
3359
3582
|
hookRunner.reload();
|
|
3360
3583
|
latestProjectContext = loadProjectContext({ cwd: safeCwd(), previous: latestProjectContext });
|
|
3584
|
+
const projectResources = toolExecutor.getProjectResources();
|
|
3361
3585
|
const promptHook = await hookRunner.run('UserPromptSubmit', {
|
|
3362
3586
|
input: { prompt: input },
|
|
3363
3587
|
turnId: String(session.turns),
|
|
@@ -3379,7 +3603,7 @@ export async function startTerminalRepl() {
|
|
|
3379
3603
|
effectivePolicy,
|
|
3380
3604
|
projectContext: latestProjectContext,
|
|
3381
3605
|
activeHints: [...hookHints, ...rejectionHints],
|
|
3382
|
-
projectResources
|
|
3606
|
+
projectResources,
|
|
3383
3607
|
agentContext: toolExecutor.getAgentContext(),
|
|
3384
3608
|
});
|
|
3385
3609
|
ctx.effectivePolicy = effectivePolicy;
|
|
@@ -3387,6 +3611,14 @@ export async function startTerminalRepl() {
|
|
|
3387
3611
|
ctx.latestEnvelope = latestEnvelope;
|
|
3388
3612
|
Object.assign(execContext, latestEnvelope);
|
|
3389
3613
|
if (skipPerms) execContext.freeswim = true;
|
|
3614
|
+
// PRD-071: seed work_scope from CLI so the backend has a byte-stable
|
|
3615
|
+
// scope block from turn 1. Uses projectResources already gathered by
|
|
3616
|
+
// the envelope above.
|
|
3617
|
+
execContext.work_scope = buildWorkScope({
|
|
3618
|
+
instruction: input,
|
|
3619
|
+
cwd: safeCwd(),
|
|
3620
|
+
projectResources,
|
|
3621
|
+
});
|
|
3390
3622
|
for (const file of latestProjectContext.changed || []) {
|
|
3391
3623
|
if (effectivePolicy.policy.context?.showReloadNotice) {
|
|
3392
3624
|
process.stderr.write(` ${c.dim(`[Context] ${file.label} updated — re-read`)}\n`);
|
package/src/ui/commands.mjs
CHANGED
|
@@ -7,12 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
import { SessionManager } from '../core/session.mjs';
|
|
9
9
|
import { CheckpointManager } from '../core/checkpoints.mjs';
|
|
10
|
-
import { PromptCache } from '../core/cache.mjs';
|
|
11
10
|
import { readEnv, listEnvVars } from '../config/env.mjs';
|
|
12
11
|
import * as telemetry from '../telemetry/index.mjs';
|
|
13
12
|
|
|
14
13
|
const checkpoints = new CheckpointManager();
|
|
15
|
-
const promptCache = new PromptCache();
|
|
16
14
|
let sessionManager = null;
|
|
17
15
|
|
|
18
16
|
function getSession() {
|
|
@@ -399,11 +397,14 @@ export const COMMANDS = {
|
|
|
399
397
|
'/extra-usage': {
|
|
400
398
|
description: 'Show detailed usage stats',
|
|
401
399
|
handler(args, state) {
|
|
402
|
-
const cacheStats = promptCache.getStats();
|
|
403
400
|
const telemetryStats = telemetry.getStats();
|
|
401
|
+
const cacheRead = state.tokenUsage.cache_read || 0;
|
|
402
|
+
const cacheWrite = state.tokenUsage.cache_creation || 0;
|
|
403
|
+
const denom = state.tokenUsage.input + cacheRead;
|
|
404
|
+
const rate = denom > 0 ? Math.round((cacheRead / denom) * 100) : 0;
|
|
404
405
|
return [
|
|
405
406
|
`Tokens: in=${state.tokenUsage.input}, out=${state.tokenUsage.output}`,
|
|
406
|
-
`Cache:
|
|
407
|
+
`Cache: read=${cacheRead}, write=${cacheWrite}, hit-rate=${rate}%`,
|
|
407
408
|
`Telemetry: ${telemetryStats.totalEvents} events`,
|
|
408
409
|
].join('\n');
|
|
409
410
|
},
|