@ekkos/cli 1.4.2 → 1.5.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/dist/commands/dashboard.js +317 -95
- package/dist/commands/init.js +59 -4
- package/dist/commands/living-docs.d.ts +1 -0
- package/dist/commands/living-docs.js +5 -2
- package/dist/commands/logout.d.ts +9 -0
- package/dist/commands/logout.js +104 -0
- package/dist/commands/run.js +56 -23
- package/dist/commands/workspaces.d.ts +4 -0
- package/dist/commands/workspaces.js +153 -0
- package/dist/index.js +82 -83
- package/dist/local/diff-engine.d.ts +19 -0
- package/dist/local/diff-engine.js +81 -0
- package/dist/local/entity-extractor.d.ts +18 -0
- package/dist/local/entity-extractor.js +67 -0
- package/dist/local/git-utils.d.ts +37 -0
- package/dist/local/git-utils.js +169 -0
- package/dist/local/living-docs-manager.d.ts +6 -0
- package/dist/local/living-docs-manager.js +180 -139
- package/dist/utils/notifier.d.ts +15 -0
- package/dist/utils/notifier.js +40 -0
- package/dist/utils/paths.d.ts +4 -0
- package/dist/utils/paths.js +7 -0
- package/dist/utils/state.d.ts +3 -0
- package/dist/utils/stdin-relay.d.ts +37 -0
- package/dist/utils/stdin-relay.js +155 -0
- package/package.json +4 -1
- package/templates/CLAUDE.md +3 -1
- package/dist/commands/setup.d.ts +0 -6
- package/dist/commands/setup.js +0 -389
|
@@ -68,6 +68,8 @@ const commander_1 = require("commander");
|
|
|
68
68
|
const usage_parser_js_1 = require("../lib/usage-parser.js");
|
|
69
69
|
const state_js_1 = require("../utils/state.js");
|
|
70
70
|
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
71
|
+
const MAX_OUTPUT_1M_MODELS = 64000;
|
|
72
|
+
const MAX_OUTPUT_200K_OPUS_SONNET = 16384;
|
|
71
73
|
// ── Pricing ──
|
|
72
74
|
// Pricing per MTok from https://docs.anthropic.com/en/docs/about-claude/pricing
|
|
73
75
|
// Includes prompt caching rates (5m cache write=1.25x input, cache read=0.1x input).
|
|
@@ -147,6 +149,55 @@ function calculateTurnCost(model, usage, options) {
|
|
|
147
149
|
(usage.cache_creation_tokens / 1000000) * p.cacheWrite +
|
|
148
150
|
(usage.cache_read_tokens / 1000000) * p.cacheRead);
|
|
149
151
|
}
|
|
152
|
+
function countTools(toolStr) {
|
|
153
|
+
if (!toolStr || toolStr === '-')
|
|
154
|
+
return 0;
|
|
155
|
+
return toolStr.split(',').map(tool => tool.trim()).filter(Boolean).length;
|
|
156
|
+
}
|
|
157
|
+
function mergeToolStrings(...toolStrings) {
|
|
158
|
+
const merged = new Set();
|
|
159
|
+
for (const toolStr of toolStrings) {
|
|
160
|
+
if (!toolStr || toolStr === '-')
|
|
161
|
+
continue;
|
|
162
|
+
for (const tool of toolStr.split(',').map(value => value.trim()).filter(Boolean)) {
|
|
163
|
+
merged.add(tool);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return merged.size > 0 ? Array.from(merged).join(',') : '-';
|
|
167
|
+
}
|
|
168
|
+
function compareTurnCompleteness(left, right) {
|
|
169
|
+
const leftContext = left.input + left.cacheRead + left.cacheCreate;
|
|
170
|
+
const rightContext = right.input + right.cacheRead + right.cacheCreate;
|
|
171
|
+
if (left.output !== right.output)
|
|
172
|
+
return left.output - right.output;
|
|
173
|
+
if (leftContext !== rightContext)
|
|
174
|
+
return leftContext - rightContext;
|
|
175
|
+
const leftTools = countTools(left.tools);
|
|
176
|
+
const rightTools = countTools(right.tools);
|
|
177
|
+
if (leftTools !== rightTools)
|
|
178
|
+
return leftTools - rightTools;
|
|
179
|
+
const leftTs = Date.parse(left.timestamp || '');
|
|
180
|
+
const rightTs = Date.parse(right.timestamp || '');
|
|
181
|
+
const safeLeftTs = Number.isFinite(leftTs) ? leftTs : 0;
|
|
182
|
+
const safeRightTs = Number.isFinite(rightTs) ? rightTs : 0;
|
|
183
|
+
return safeLeftTs - safeRightTs;
|
|
184
|
+
}
|
|
185
|
+
function pickMoreCompleteTurn(left, right) {
|
|
186
|
+
const preferred = compareTurnCompleteness(left, right) >= 0 ? left : right;
|
|
187
|
+
const secondary = preferred === left ? right : left;
|
|
188
|
+
return {
|
|
189
|
+
...preferred,
|
|
190
|
+
msgId: preferred.msgId || secondary.msgId,
|
|
191
|
+
tools: mergeToolStrings(left.tools, right.tools),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
function turnsMatch(left, right) {
|
|
195
|
+
if (!left || !right)
|
|
196
|
+
return false;
|
|
197
|
+
if (left.msgId && right.msgId)
|
|
198
|
+
return left.msgId === right.msgId;
|
|
199
|
+
return left.turn === right.turn;
|
|
200
|
+
}
|
|
150
201
|
function getModelCtxSize(model, contextTierHint) {
|
|
151
202
|
const normalized = (model || '').toLowerCase();
|
|
152
203
|
if (contextTierHint === '1m')
|
|
@@ -247,7 +298,7 @@ function formatContextLaneCompact(contextSize, metadata) {
|
|
|
247
298
|
const launchSize = typeof metadata?.claudeContextSize === 'number' ? metadata.claudeContextSize : undefined;
|
|
248
299
|
const resolvedSize = explicitSize || launchSize;
|
|
249
300
|
const maxOutput = metadata?.claudeMaxOutputTokens
|
|
250
|
-
|| (resolvedSize && resolvedSize >= 1000000 ?
|
|
301
|
+
|| (resolvedSize && resolvedSize >= 1000000 ? MAX_OUTPUT_1M_MODELS : MAX_OUTPUT_200K_OPUS_SONNET);
|
|
251
302
|
if (resolvedSize && resolvedSize >= 1000000) {
|
|
252
303
|
return `1M / out ${fmtK(maxOutput)}`;
|
|
253
304
|
}
|
|
@@ -264,6 +315,13 @@ function formatContextLaneCompact(contextSize, metadata) {
|
|
|
264
315
|
function buildRuntimeSignal(label, color, value) {
|
|
265
316
|
return `{${color}-fg}[${label}]{/${color}-fg} ${value}`;
|
|
266
317
|
}
|
|
318
|
+
function buildPreTurnRuntimeLeadSignal(metadata) {
|
|
319
|
+
const compactSelected = formatCompactLaunchModel(metadata);
|
|
320
|
+
if (compactSelected) {
|
|
321
|
+
return buildRuntimeSignal('SEL', 'cyan', compactSelected);
|
|
322
|
+
}
|
|
323
|
+
return buildRuntimeSignal('CACHE', 'green', '0%');
|
|
324
|
+
}
|
|
267
325
|
/** Model tag for dashboard display */
|
|
268
326
|
function modelTag(model) {
|
|
269
327
|
if (model.includes('opus'))
|
|
@@ -386,10 +444,10 @@ function parseJsonlFile(jsonlPath, sessionName, launchMetadata) {
|
|
|
386
444
|
const toolStr = msgTools && msgTools.size > 0
|
|
387
445
|
? Array.from(msgTools).map(t => t.replace(/^mcp__ekkos-memory__/, '').replace(/^ekkOS_/, '')).join(',')
|
|
388
446
|
: '-';
|
|
389
|
-
// Last entry wins — final JSONL entry for a message has the real output_tokens
|
|
390
447
|
const turnNum = isNew ? msgIdOrder.length + 1 : (turnsByMsgId.get(msgId).turn);
|
|
391
448
|
const turnData = {
|
|
392
449
|
turn: turnNum,
|
|
450
|
+
msgId,
|
|
393
451
|
contextPct,
|
|
394
452
|
modelContextSize: modelCtxSize,
|
|
395
453
|
input: inputTokens,
|
|
@@ -410,66 +468,29 @@ function parseJsonlFile(jsonlPath, sessionName, launchMetadata) {
|
|
|
410
468
|
if (msgId) {
|
|
411
469
|
if (isNew)
|
|
412
470
|
msgIdOrder.push(msgId);
|
|
413
|
-
turnsByMsgId.
|
|
471
|
+
const existingTurn = turnsByMsgId.get(msgId);
|
|
472
|
+
turnsByMsgId.set(msgId, existingTurn ? pickMoreCompleteTurn(existingTurn, turnData) : turnData);
|
|
414
473
|
}
|
|
415
474
|
}
|
|
416
475
|
}
|
|
417
476
|
catch { /* skip bad lines */ }
|
|
418
477
|
}
|
|
419
|
-
// Build ordered turns array from the Map
|
|
478
|
+
// Build ordered turns array from the Map using the richest snapshot seen per message id.
|
|
420
479
|
const turns = msgIdOrder.map(id => turnsByMsgId.get(id));
|
|
421
|
-
const totalCost = turns.reduce((s, t) => s + t.cost, 0);
|
|
422
|
-
const totalInput = turns.reduce((s, t) => s + t.input, 0);
|
|
423
|
-
const totalCacheRead = turns.reduce((s, t) => s + t.cacheRead, 0);
|
|
424
|
-
const totalCacheCreate = turns.reduce((s, t) => s + t.cacheCreate, 0);
|
|
425
|
-
const totalOutput = turns.reduce((s, t) => s + t.output, 0);
|
|
426
|
-
const maxContextPct = turns.length > 0 ? Math.max(...turns.map(t => t.contextPct)) : 0;
|
|
427
|
-
const currentContextPct = turns.length > 0 ? turns[turns.length - 1].contextPct : 0;
|
|
428
|
-
const avgCostPerTurn = turns.length > 0 ? totalCost / turns.length : 0;
|
|
429
|
-
const cacheHitRate = (totalCacheRead + totalCacheCreate) > 0
|
|
430
|
-
? (totalCacheRead / (totalCacheRead + totalCacheCreate)) * 100
|
|
431
|
-
: 0;
|
|
432
480
|
const replayAppliedCount = turns.reduce((sum, t) => sum + (t.replayState === 'applied' ? 1 : 0), 0);
|
|
433
481
|
const replaySkippedSizeCount = turns.reduce((sum, t) => sum + (t.replayState === 'skipped-size' ? 1 : 0), 0);
|
|
434
482
|
const replaySkipStoreCount = turns.reduce((sum, t) => sum + (t.replayStore === 'skip-size' ? 1 : 0), 0);
|
|
435
|
-
|
|
436
|
-
if (startedAt && turns.length > 0) {
|
|
437
|
-
const start = new Date(startedAt).getTime();
|
|
438
|
-
const end = new Date(turns[turns.length - 1].timestamp).getTime();
|
|
439
|
-
const mins = Math.round((end - start) / 60000);
|
|
440
|
-
duration = mins >= 60 ? `${Math.floor(mins / 60)}h${mins % 60}m` : `${mins}m`;
|
|
441
|
-
}
|
|
442
|
-
// Get current context tokens from the last turn's raw data
|
|
443
|
-
const lastTurn = turns.length > 0 ? turns[turns.length - 1] : null;
|
|
444
|
-
const currentContextTokens = lastTurn
|
|
445
|
-
? lastTurn.input + lastTurn.cacheRead + lastTurn.cacheCreate
|
|
446
|
-
: 0;
|
|
447
|
-
const modelContextSize = lastTurn
|
|
448
|
-
? lastTurn.modelContextSize
|
|
449
|
-
: resolveDashboardContextSize(model, fallbackContextWindow, launchMetadata);
|
|
450
|
-
return {
|
|
483
|
+
return buildDashboardData({
|
|
451
484
|
sessionName,
|
|
452
485
|
model,
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
totalTokens: totalInput + totalCacheRead + totalCacheCreate + totalOutput,
|
|
456
|
-
totalInput,
|
|
457
|
-
totalCacheRead,
|
|
458
|
-
totalCacheCreate,
|
|
459
|
-
totalOutput,
|
|
460
|
-
avgCostPerTurn,
|
|
461
|
-
maxContextPct,
|
|
462
|
-
currentContextPct,
|
|
463
|
-
currentContextTokens,
|
|
464
|
-
modelContextSize,
|
|
465
|
-
cacheHitRate,
|
|
486
|
+
startedAt,
|
|
487
|
+
turns,
|
|
466
488
|
replayAppliedCount,
|
|
467
489
|
replaySkippedSizeCount,
|
|
468
490
|
replaySkipStoreCount,
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
};
|
|
491
|
+
fallbackModelContextSize: resolveDashboardContextSize(model, fallbackContextWindow, launchMetadata),
|
|
492
|
+
cacheHitMode: 'read-vs-write',
|
|
493
|
+
});
|
|
473
494
|
}
|
|
474
495
|
function parseGeminiSessionFile(sessionPath, sessionName, launchMetadata) {
|
|
475
496
|
const raw = JSON.parse(fs.readFileSync(sessionPath, 'utf-8'));
|
|
@@ -505,6 +526,7 @@ function parseGeminiSessionFile(sessionPath, sessionName, launchMetadata) {
|
|
|
505
526
|
};
|
|
506
527
|
turns.push({
|
|
507
528
|
turn: turns.length + 1,
|
|
529
|
+
msgId: typeof message.id === 'string' ? message.id : undefined,
|
|
508
530
|
contextPct,
|
|
509
531
|
modelContextSize: modelCtxSize,
|
|
510
532
|
input: uncachedInput,
|
|
@@ -523,6 +545,38 @@ function parseGeminiSessionFile(sessionPath, sessionName, launchMetadata) {
|
|
|
523
545
|
timestamp: message.timestamp || raw.lastUpdated || raw.startTime || new Date().toISOString(),
|
|
524
546
|
});
|
|
525
547
|
}
|
|
548
|
+
return buildDashboardData({
|
|
549
|
+
sessionName,
|
|
550
|
+
model,
|
|
551
|
+
startedAt,
|
|
552
|
+
turns,
|
|
553
|
+
replayAppliedCount: 0,
|
|
554
|
+
replaySkippedSizeCount: 0,
|
|
555
|
+
replaySkipStoreCount: 0,
|
|
556
|
+
fallbackModelContextSize: resolveDashboardContextSize(model, fallbackWindow, launchMetadata, launchMetadata?.claudeContextSize),
|
|
557
|
+
cacheHitMode: 'read-vs-total-input',
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
function tokenSafePercent(numerator, denominator) {
|
|
561
|
+
if (!Number.isFinite(denominator) || denominator <= 0)
|
|
562
|
+
return 0;
|
|
563
|
+
return (numerator / denominator) * 100;
|
|
564
|
+
}
|
|
565
|
+
function resolveCacheHitRate(turns, mode) {
|
|
566
|
+
const totalInput = turns.reduce((sum, turn) => sum + turn.input, 0);
|
|
567
|
+
const totalCacheRead = turns.reduce((sum, turn) => sum + turn.cacheRead, 0);
|
|
568
|
+
const totalCacheCreate = turns.reduce((sum, turn) => sum + turn.cacheCreate, 0);
|
|
569
|
+
if (mode === 'read-vs-write') {
|
|
570
|
+
return (totalCacheRead + totalCacheCreate) > 0
|
|
571
|
+
? (totalCacheRead / (totalCacheRead + totalCacheCreate)) * 100
|
|
572
|
+
: 0;
|
|
573
|
+
}
|
|
574
|
+
return tokenSafePercent(totalCacheRead, totalInput + totalCacheRead);
|
|
575
|
+
}
|
|
576
|
+
function buildDashboardData(params) {
|
|
577
|
+
const turns = params.turns.map((turn, index) => (turn.turn === index + 1
|
|
578
|
+
? turn
|
|
579
|
+
: { ...turn, turn: index + 1 }));
|
|
526
580
|
const totalCost = turns.reduce((sum, turn) => sum + turn.cost, 0);
|
|
527
581
|
const totalInput = turns.reduce((sum, turn) => sum + turn.input, 0);
|
|
528
582
|
const totalCacheRead = turns.reduce((sum, turn) => sum + turn.cacheRead, 0);
|
|
@@ -531,21 +585,22 @@ function parseGeminiSessionFile(sessionPath, sessionName, launchMetadata) {
|
|
|
531
585
|
const maxContextPct = turns.length > 0 ? Math.max(...turns.map(turn => turn.contextPct)) : 0;
|
|
532
586
|
const currentContextPct = turns.length > 0 ? turns[turns.length - 1].contextPct : 0;
|
|
533
587
|
const avgCostPerTurn = turns.length > 0 ? totalCost / turns.length : 0;
|
|
534
|
-
const cacheHitRate =
|
|
588
|
+
const cacheHitRate = resolveCacheHitRate(turns, params.cacheHitMode);
|
|
535
589
|
let duration = '0m';
|
|
536
|
-
if (startedAt && turns.length > 0) {
|
|
537
|
-
const start = new Date(startedAt).getTime();
|
|
590
|
+
if (params.startedAt && turns.length > 0) {
|
|
591
|
+
const start = new Date(params.startedAt).getTime();
|
|
538
592
|
const end = new Date(turns[turns.length - 1].timestamp).getTime();
|
|
539
593
|
const mins = Math.max(0, Math.round((end - start) / 60000));
|
|
540
594
|
duration = mins >= 60 ? `${Math.floor(mins / 60)}h${mins % 60}m` : `${mins}m`;
|
|
541
595
|
}
|
|
542
596
|
const lastTurn = turns.length > 0 ? turns[turns.length - 1] : null;
|
|
543
597
|
return {
|
|
544
|
-
sessionName,
|
|
545
|
-
model,
|
|
598
|
+
sessionName: params.sessionName,
|
|
599
|
+
model: params.model,
|
|
546
600
|
turnCount: turns.length,
|
|
547
601
|
totalCost,
|
|
548
|
-
|
|
602
|
+
// Unique content tokens: excludes cache_read (same prefix re-billed per turn)
|
|
603
|
+
totalTokens: totalInput + totalCacheCreate + totalOutput,
|
|
549
604
|
totalInput,
|
|
550
605
|
totalCacheRead,
|
|
551
606
|
totalCacheCreate,
|
|
@@ -554,22 +609,62 @@ function parseGeminiSessionFile(sessionPath, sessionName, launchMetadata) {
|
|
|
554
609
|
maxContextPct,
|
|
555
610
|
currentContextPct,
|
|
556
611
|
currentContextTokens: lastTurn ? lastTurn.input + lastTurn.cacheRead + lastTurn.cacheCreate : 0,
|
|
557
|
-
modelContextSize: lastTurn
|
|
558
|
-
? lastTurn.modelContextSize
|
|
559
|
-
: resolveDashboardContextSize(model, fallbackWindow, launchMetadata, launchMetadata?.claudeContextSize),
|
|
612
|
+
modelContextSize: lastTurn ? lastTurn.modelContextSize : params.fallbackModelContextSize,
|
|
560
613
|
cacheHitRate,
|
|
561
|
-
replayAppliedCount: 0,
|
|
562
|
-
replaySkippedSizeCount: 0,
|
|
563
|
-
replaySkipStoreCount: 0,
|
|
564
|
-
startedAt,
|
|
614
|
+
replayAppliedCount: params.replayAppliedCount || 0,
|
|
615
|
+
replaySkippedSizeCount: params.replaySkippedSizeCount || 0,
|
|
616
|
+
replaySkipStoreCount: params.replaySkipStoreCount || 0,
|
|
617
|
+
startedAt: params.startedAt,
|
|
565
618
|
duration,
|
|
566
619
|
turns,
|
|
567
620
|
};
|
|
568
621
|
}
|
|
569
|
-
function
|
|
570
|
-
if (!
|
|
571
|
-
return
|
|
572
|
-
|
|
622
|
+
function reconcileDashboardData(previous, next) {
|
|
623
|
+
if (!previous || previous.sessionName !== next.sessionName || previous.turns.length === 0) {
|
|
624
|
+
return next;
|
|
625
|
+
}
|
|
626
|
+
const mergedTurns = next.turns.map(turn => ({ ...turn }));
|
|
627
|
+
const previousTurns = previous.turns;
|
|
628
|
+
if (mergedTurns.length === 0) {
|
|
629
|
+
return previous;
|
|
630
|
+
}
|
|
631
|
+
const previousLast = previousTurns[previousTurns.length - 1];
|
|
632
|
+
const nextLast = mergedTurns[mergedTurns.length - 1];
|
|
633
|
+
const keepLastMutable = turnsMatch(previousLast, nextLast);
|
|
634
|
+
const freezeCount = Math.min(previousTurns.length - (keepLastMutable ? 1 : 0), mergedTurns.length);
|
|
635
|
+
for (let index = 0; index < freezeCount; index++) {
|
|
636
|
+
if (!turnsMatch(previousTurns[index], mergedTurns[index]))
|
|
637
|
+
continue;
|
|
638
|
+
mergedTurns[index] = pickMoreCompleteTurn(previousTurns[index], mergedTurns[index]);
|
|
639
|
+
}
|
|
640
|
+
if (keepLastMutable) {
|
|
641
|
+
const tailIndex = previousTurns.length - 1;
|
|
642
|
+
if (tailIndex < mergedTurns.length && turnsMatch(previousTurns[tailIndex], mergedTurns[tailIndex])) {
|
|
643
|
+
mergedTurns[tailIndex] = pickMoreCompleteTurn(previousTurns[tailIndex], mergedTurns[tailIndex]);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (previousTurns.length > mergedTurns.length) {
|
|
647
|
+
const prefixMatches = mergedTurns.every((turn, index) => turnsMatch(previousTurns[index], turn));
|
|
648
|
+
if (prefixMatches) {
|
|
649
|
+
for (let index = mergedTurns.length; index < previousTurns.length; index++) {
|
|
650
|
+
mergedTurns.push({ ...previousTurns[index] });
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
const cacheHitMode = mergedTurns.some(turn => Boolean(turn.msgId))
|
|
655
|
+
? 'read-vs-write'
|
|
656
|
+
: 'read-vs-total-input';
|
|
657
|
+
return buildDashboardData({
|
|
658
|
+
sessionName: next.sessionName,
|
|
659
|
+
model: next.model,
|
|
660
|
+
startedAt: next.startedAt || previous.startedAt,
|
|
661
|
+
turns: mergedTurns,
|
|
662
|
+
replayAppliedCount: Math.max(previous.replayAppliedCount, next.replayAppliedCount),
|
|
663
|
+
replaySkippedSizeCount: Math.max(previous.replaySkippedSizeCount, next.replaySkippedSizeCount),
|
|
664
|
+
replaySkipStoreCount: Math.max(previous.replaySkipStoreCount, next.replaySkipStoreCount),
|
|
665
|
+
fallbackModelContextSize: next.modelContextSize || previous.modelContextSize,
|
|
666
|
+
cacheHitMode,
|
|
667
|
+
});
|
|
573
668
|
}
|
|
574
669
|
function readJsonFile(filePath) {
|
|
575
670
|
try {
|
|
@@ -661,9 +756,9 @@ function formatLaunchWindowLabel(metadata) {
|
|
|
661
756
|
? metadata.claudeContextSize
|
|
662
757
|
: undefined;
|
|
663
758
|
if (window === '200k')
|
|
664
|
-
return `Window ${fmtK(exactSize || 200000)} · out ${fmtK(metadata?.claudeMaxOutputTokens ||
|
|
759
|
+
return `Window ${fmtK(exactSize || 200000)} · out ${fmtK(metadata?.claudeMaxOutputTokens || MAX_OUTPUT_200K_OPUS_SONNET)}`;
|
|
665
760
|
if (window === '1m')
|
|
666
|
-
return `Window ${fmtK(exactSize || 1000000)} · out ${fmtK(metadata?.claudeMaxOutputTokens ||
|
|
761
|
+
return `Window ${fmtK(exactSize || 1000000)} · out ${fmtK(metadata?.claudeMaxOutputTokens || MAX_OUTPUT_1M_MODELS)}`;
|
|
667
762
|
if (typeof metadata?.claudeContextSize === 'number' && metadata.claudeContextSize > 0) {
|
|
668
763
|
return `Window ${fmtK(metadata.claudeContextSize)}`;
|
|
669
764
|
}
|
|
@@ -1034,6 +1129,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
1034
1129
|
const blessed = require('blessed');
|
|
1035
1130
|
const contrib = require('blessed-contrib');
|
|
1036
1131
|
const inTmux = process.env.TMUX !== undefined;
|
|
1132
|
+
const needsTmuxResizeRecovery = inTmux && process.platform === 'darwin';
|
|
1037
1133
|
// ══════════════════════════════════════════════════════════════════════════
|
|
1038
1134
|
// TMUX SPLIT PANE ISOLATION
|
|
1039
1135
|
// When dashboard runs in a separate tmux pane from `ekkos run`, blessed must
|
|
@@ -1072,13 +1168,52 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
1072
1168
|
mouse: false, // Disable ALL mouse capture (allows terminal text selection)
|
|
1073
1169
|
grabKeys: false, // Don't grab keyboard input from other panes
|
|
1074
1170
|
sendFocus: false, // Don't send focus events (breaks paste)
|
|
1075
|
-
ignoreLocked: ['C-c', 'C-q', 'f7'], // Capture
|
|
1171
|
+
ignoreLocked: ['C-c', 'C-q', 'f7', 'C-f7'], // Capture quit + redraw keys even when widgets are passive
|
|
1076
1172
|
input: ttyInput, // Use /dev/tty for input (isolated from stdout pipe)
|
|
1077
1173
|
output: ttyOutput, // Use /dev/tty for output (isolated from stdout pipe)
|
|
1078
1174
|
forceUnicode: true, // Better text rendering
|
|
1079
1175
|
terminal: 'xterm-256color',
|
|
1080
1176
|
resizeTimeout: 300, // Debounce resize events
|
|
1081
1177
|
});
|
|
1178
|
+
function readTerminalSize() {
|
|
1179
|
+
const candidates = [ttyOutput, screen.program?.output, process.stdout];
|
|
1180
|
+
for (const candidate of candidates) {
|
|
1181
|
+
if (!candidate)
|
|
1182
|
+
continue;
|
|
1183
|
+
try {
|
|
1184
|
+
if (typeof candidate.getWindowSize === 'function') {
|
|
1185
|
+
const size = candidate.getWindowSize();
|
|
1186
|
+
if (Array.isArray(size) && size.length >= 2) {
|
|
1187
|
+
const [cols, rows] = size;
|
|
1188
|
+
if (cols > 0 && rows > 0)
|
|
1189
|
+
return { cols, rows };
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
catch { }
|
|
1194
|
+
const cols = Number(candidate.columns);
|
|
1195
|
+
const rows = Number(candidate.rows);
|
|
1196
|
+
if (cols > 0 && rows > 0)
|
|
1197
|
+
return { cols, rows };
|
|
1198
|
+
}
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
function syncProgramSizeFromTTY(source) {
|
|
1202
|
+
const program = screen.program;
|
|
1203
|
+
if (!program)
|
|
1204
|
+
return false;
|
|
1205
|
+
const size = readTerminalSize();
|
|
1206
|
+
if (!size)
|
|
1207
|
+
return false;
|
|
1208
|
+
const cols = Math.max(20, size.cols);
|
|
1209
|
+
const rows = Math.max(10, size.rows);
|
|
1210
|
+
if (program.cols === cols && program.rows === rows)
|
|
1211
|
+
return false;
|
|
1212
|
+
dlog(`TTY geometry sync (${source}): ${program.cols}x${program.rows} -> ${cols}x${rows}`);
|
|
1213
|
+
program.cols = cols;
|
|
1214
|
+
program.rows = rows;
|
|
1215
|
+
return true;
|
|
1216
|
+
}
|
|
1082
1217
|
// ══════════════════════════════════════════════════════════════════════════
|
|
1083
1218
|
// DISABLE TERMINAL CONTROL SEQUENCE HIJACKING (TMUX ONLY)
|
|
1084
1219
|
// When running in a tmux split pane alongside Claude Code, prevent blessed
|
|
@@ -1115,7 +1250,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
1115
1250
|
// No percentages = no gaps.
|
|
1116
1251
|
//
|
|
1117
1252
|
// header: 3 rows (session stats + animated logo)
|
|
1118
|
-
// context: 5 rows (progress bar +
|
|
1253
|
+
// context: 5-6 rows (progress bar + runtime stats + cache/cost summary)
|
|
1119
1254
|
// chart: ~25-34% of remaining (token usage graph)
|
|
1120
1255
|
// table: ~66-75% of remaining (turn-by-turn breakdown)
|
|
1121
1256
|
// usage: 3 rows (Anthropic rate limit window)
|
|
@@ -1158,11 +1293,19 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
1158
1293
|
}
|
|
1159
1294
|
const W = '100%';
|
|
1160
1295
|
const HEADER_H = 3;
|
|
1161
|
-
const CONTEXT_H = 5;
|
|
1162
1296
|
const USAGE_H = 4;
|
|
1163
1297
|
const FOOTER_H = 3;
|
|
1164
1298
|
const MASCOT_W = 16; // Width reserved for runtime mascot in context box
|
|
1165
|
-
|
|
1299
|
+
function resolveContextHeight(height) {
|
|
1300
|
+
return height >= 36 ? 6 : 5;
|
|
1301
|
+
}
|
|
1302
|
+
function resolveChartMinHeight(height) {
|
|
1303
|
+
if (height >= 28)
|
|
1304
|
+
return 6;
|
|
1305
|
+
if (height >= 26)
|
|
1306
|
+
return 5;
|
|
1307
|
+
return 4;
|
|
1308
|
+
}
|
|
1166
1309
|
function resolveChartRatio(height) {
|
|
1167
1310
|
if (height >= 62)
|
|
1168
1311
|
return 0.25;
|
|
@@ -1174,9 +1317,14 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
1174
1317
|
}
|
|
1175
1318
|
function calcLayout() {
|
|
1176
1319
|
const H = Math.max(24, screen.height || 24);
|
|
1320
|
+
const CONTEXT_H = resolveContextHeight(H);
|
|
1321
|
+
const FIXED_H = HEADER_H + CONTEXT_H + USAGE_H + FOOTER_H;
|
|
1177
1322
|
const remaining = Math.max(8, H - FIXED_H);
|
|
1178
|
-
const
|
|
1179
|
-
const
|
|
1323
|
+
const tableMin = 5;
|
|
1324
|
+
const chartMin = resolveChartMinHeight(H);
|
|
1325
|
+
const chartTarget = Math.floor(remaining * resolveChartRatio(H));
|
|
1326
|
+
const chartH = Math.max(chartMin, Math.min(chartTarget, remaining - tableMin));
|
|
1327
|
+
const tableH = Math.max(tableMin, remaining - chartH);
|
|
1180
1328
|
return {
|
|
1181
1329
|
header: { top: 0, height: HEADER_H },
|
|
1182
1330
|
context: { top: HEADER_H, height: CONTEXT_H },
|
|
@@ -1378,13 +1526,17 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
1378
1526
|
// Track geometry so we can re-anchor widgets even if tmux resize events are flaky
|
|
1379
1527
|
let lastLayoutW = screen.width || 0;
|
|
1380
1528
|
let lastLayoutH = screen.height || 0;
|
|
1381
|
-
function ensureLayoutSynced() {
|
|
1529
|
+
function ensureLayoutSynced(source = 'layout') {
|
|
1530
|
+
const geometryChanged = syncProgramSizeFromTTY(source);
|
|
1382
1531
|
const w = screen.width || 0;
|
|
1383
1532
|
const h = screen.height || 0;
|
|
1384
1533
|
if (w === lastLayoutW && h === lastLayoutH)
|
|
1385
1534
|
return;
|
|
1386
1535
|
lastLayoutW = w;
|
|
1387
1536
|
lastLayoutH = h;
|
|
1537
|
+
if (geometryChanged && needsTmuxResizeRecovery) {
|
|
1538
|
+
scheduleResizeRecovery(source);
|
|
1539
|
+
}
|
|
1388
1540
|
try {
|
|
1389
1541
|
screen.realloc?.();
|
|
1390
1542
|
}
|
|
@@ -1478,11 +1630,14 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
1478
1630
|
try {
|
|
1479
1631
|
const launchModelLabel = formatLaunchModelLabel(launchMetadata);
|
|
1480
1632
|
const launchWindowLabel = formatLaunchWindowLabel(launchMetadata);
|
|
1481
|
-
const compactSelected = formatCompactLaunchModel(launchMetadata) || 'Awaiting profile';
|
|
1482
1633
|
const compactLane = formatContextLaneCompact(launchMetadata?.claudeContextSize, launchMetadata) || 'Awaiting lane';
|
|
1634
|
+
const extendedContext = (layout.context.height || 0) >= 6;
|
|
1483
1635
|
contextBox.setContent(` {green-fg}Session active{/green-fg} {gray-fg}${sessionName}{/gray-fg}\n` +
|
|
1484
|
-
` ${
|
|
1485
|
-
` ${[launchModelLabel, launchWindowLabel].filter(Boolean).join(' ') || 'Token and cost metrics appear after the first assistant response.'}`
|
|
1636
|
+
` ${buildPreTurnRuntimeLeadSignal(launchMetadata)} {gray-fg}│{/gray-fg} ${buildRuntimeSignal('ACT', 'magenta', 'standby')} {gray-fg}│{/gray-fg} ${buildRuntimeSignal('LANE', 'yellow', compactLane)}\n` +
|
|
1637
|
+
` ${[launchModelLabel, launchWindowLabel].filter(Boolean).join(' ') || 'Token and cost metrics appear after the first assistant response.'}` +
|
|
1638
|
+
(extendedContext
|
|
1639
|
+
? `\n {gray-fg}Cache, peak context, and routed cost appear after the first assistant response.{/gray-fg}`
|
|
1640
|
+
: ''));
|
|
1486
1641
|
turnBox.setContent(`{bold}Turns{/bold}\n` +
|
|
1487
1642
|
`{gray-fg}—{/gray-fg}`);
|
|
1488
1643
|
const timerStr = sessionStartMs
|
|
@@ -1491,8 +1646,8 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
1491
1646
|
footerBox.setLabel(` ${sessionName} `);
|
|
1492
1647
|
footerBox.setContent(` ${timerStr}{green-fg}Ready{/green-fg}` +
|
|
1493
1648
|
(inTmux
|
|
1494
|
-
? ` {gray-fg}Ctrl+Q quit F7 redraw{/gray-fg}`
|
|
1495
|
-
: ` {gray-fg}? help q quit r refresh F7 redraw{/gray-fg}`));
|
|
1649
|
+
? ` {gray-fg}Ctrl+Q quit Ctrl+F7/F7 redraw{/gray-fg}`
|
|
1650
|
+
: ` {gray-fg}? help q quit r refresh Ctrl+F7/F7 redraw{/gray-fg}`));
|
|
1496
1651
|
}
|
|
1497
1652
|
catch (err) {
|
|
1498
1653
|
dlog(`Pre-turn render: ${err.message}`);
|
|
@@ -1562,7 +1717,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
1562
1717
|
launchMetadata = readSessionLaunchMetadata(sessionName) || launchMetadata;
|
|
1563
1718
|
provider = inferDashboardProvider(initialProvider, launchMetadata, jsonlPath);
|
|
1564
1719
|
renderRuntimeMascot();
|
|
1565
|
-
data = parseTranscriptFile(jsonlPath, provider, sessionName, launchMetadata);
|
|
1720
|
+
data = reconcileDashboardData(lastData && lastData.sessionName === sessionName ? lastData : null, parseTranscriptFile(jsonlPath, provider, sessionName, launchMetadata));
|
|
1566
1721
|
lastData = data;
|
|
1567
1722
|
if (!sessionStartMs && data.startedAt) {
|
|
1568
1723
|
sessionStartMs = new Date(data.startedAt).getTime();
|
|
@@ -1610,19 +1765,24 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
1610
1765
|
// Cache stats
|
|
1611
1766
|
const hitColor = data.cacheHitRate >= 80 ? 'green' : data.cacheHitRate >= 50 ? 'yellow' : 'red';
|
|
1612
1767
|
const cappedMax = Math.min(data.maxContextPct, 100);
|
|
1768
|
+
const extendedContext = (layout.context.height || 0) >= 6;
|
|
1613
1769
|
contextBox.setContent(` ${bar}\n` +
|
|
1614
1770
|
` ${buildRuntimeSignal('SEL', 'cyan', selectedProfile)} {gray-fg}│{/gray-fg} ${buildRuntimeSignal('ACT', 'magenta', activeModel)} {gray-fg}│{/gray-fg} ${buildRuntimeSignal('LANE', 'yellow', laneLabel)}\n` +
|
|
1615
1771
|
` {${ctxColor}-fg}${ctxPct.toFixed(0)}%{/${ctxColor}-fg} ${tokensLabel}/${maxLabel}` +
|
|
1616
|
-
` {white-fg}In{/white-fg} $${breakdown.input.toFixed(2)}` +
|
|
1617
|
-
` {green-fg}Read{/green-fg} $${breakdown.read.toFixed(2)}` +
|
|
1618
|
-
` {yellow-fg}Write{/yellow-fg} $${breakdown.write.toFixed(2)}` +
|
|
1619
|
-
` {cyan-fg}Out{/cyan-fg} $${breakdown.output.toFixed(2)}` +
|
|
1620
1772
|
` {${hitColor}-fg}${data.cacheHitRate.toFixed(0)}% cache{/${hitColor}-fg}` +
|
|
1621
1773
|
` peak:${cappedMax.toFixed(0)}%` +
|
|
1622
1774
|
` avg:$${data.avgCostPerTurn.toFixed(2)}/t` +
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1775
|
+
(extendedContext
|
|
1776
|
+
? `\n {white-fg}In{/white-fg} $${breakdown.input.toFixed(2)}` +
|
|
1777
|
+
` {green-fg}Read{/green-fg} $${breakdown.read.toFixed(2)}` +
|
|
1778
|
+
` {yellow-fg}Write{/yellow-fg} $${breakdown.write.toFixed(2)}` +
|
|
1779
|
+
` {cyan-fg}Out{/cyan-fg} $${breakdown.output.toFixed(2)}` +
|
|
1780
|
+
` replay A:${data.replayAppliedCount}` +
|
|
1781
|
+
` SZ:${data.replaySkippedSizeCount}` +
|
|
1782
|
+
` ST:${data.replaySkipStoreCount}`
|
|
1783
|
+
: ` {white-fg}In{/white-fg} $${breakdown.input.toFixed(2)}` +
|
|
1784
|
+
` {cyan-fg}Out{/cyan-fg} $${breakdown.output.toFixed(2)}` +
|
|
1785
|
+
` R:${data.replayAppliedCount}/${data.replaySkippedSizeCount}/${data.replaySkipStoreCount}`));
|
|
1626
1786
|
}
|
|
1627
1787
|
catch (err) {
|
|
1628
1788
|
dlog(`Context: ${err.message}`);
|
|
@@ -1756,7 +1916,12 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
1756
1916
|
if (!d)
|
|
1757
1917
|
return;
|
|
1758
1918
|
try {
|
|
1759
|
-
|
|
1919
|
+
// Show unique content tokens: output (model-generated) + cache_create (new content cached)
|
|
1920
|
+
// + uncached input. Excludes cache_read (same prefix re-billed each turn — NOT unique content).
|
|
1921
|
+
// Previously showed cumulative API total (input+read+create+output) which inflated by ~10-20x
|
|
1922
|
+
// because the same prefix was counted on every tool round-trip.
|
|
1923
|
+
const uniqueTokens = d.totalOutput + d.totalCacheCreate + d.totalInput;
|
|
1924
|
+
const totalTokensM = (uniqueTokens / 1000000).toFixed(2);
|
|
1760
1925
|
const totalSavings = d.turns.reduce((s, t) => s + t.savings, 0);
|
|
1761
1926
|
// Model routing breakdown (uses routedModel for actual model counts)
|
|
1762
1927
|
const opusCount = d.turns.filter(t => t.routedModel.includes('opus')).length;
|
|
@@ -1781,8 +1946,8 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
1781
1946
|
` R[A:${d.replayAppliedCount} SZ:${d.replaySkippedSizeCount} ST:${d.replaySkipStoreCount}]` +
|
|
1782
1947
|
savingsStr +
|
|
1783
1948
|
(inTmux
|
|
1784
|
-
? ` {gray-fg}Ctrl+Q quit F7 redraw{/gray-fg}`
|
|
1785
|
-
: ` {gray-fg}? help q quit r refresh F7 redraw{/gray-fg}`));
|
|
1949
|
+
? ` {gray-fg}Ctrl+Q quit Ctrl+F7/F7 redraw{/gray-fg}`
|
|
1950
|
+
: ` {gray-fg}? help q quit r refresh Ctrl+F7/F7 redraw{/gray-fg}`));
|
|
1786
1951
|
}
|
|
1787
1952
|
catch (err) {
|
|
1788
1953
|
dlog(`Footer: ${err.message}`);
|
|
@@ -2361,7 +2526,18 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
2361
2526
|
// Full dashboard redraw used by F7 and manual refresh recovery paths.
|
|
2362
2527
|
let hardRedrawInFlight = false;
|
|
2363
2528
|
let lastHardRedrawAt = 0;
|
|
2529
|
+
let resizeRecoveryTimer = null;
|
|
2364
2530
|
const HARD_REDRAW_DEBOUNCE_MS = 200;
|
|
2531
|
+
function scheduleResizeRecovery(source) {
|
|
2532
|
+
if (!needsTmuxResizeRecovery)
|
|
2533
|
+
return;
|
|
2534
|
+
if (resizeRecoveryTimer)
|
|
2535
|
+
clearTimeout(resizeRecoveryTimer);
|
|
2536
|
+
resizeRecoveryTimer = setTimeout(() => {
|
|
2537
|
+
resizeRecoveryTimer = null;
|
|
2538
|
+
void hardRedraw(`${source}-resize-recovery`);
|
|
2539
|
+
}, 120);
|
|
2540
|
+
}
|
|
2365
2541
|
async function hardRedraw(source) {
|
|
2366
2542
|
const now = Date.now();
|
|
2367
2543
|
if (hardRedrawInFlight)
|
|
@@ -2408,13 +2584,18 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
2408
2584
|
// F7 escape-sequence fallback for terminals/tmux states where blessed key names
|
|
2409
2585
|
// are not emitted reliably. Supports plain and modifier variants.
|
|
2410
2586
|
const F7_ESCAPE_RE = /\x1b\[18(?:;\d+)?~/;
|
|
2587
|
+
const CTRL_F7_ESCAPE_RE = /\x1b\[18;5~/;
|
|
2411
2588
|
let detachRawF7Listener = null;
|
|
2589
|
+
let rawKeyBuffer = '';
|
|
2412
2590
|
if (ttyInput && typeof ttyInput.on === 'function') {
|
|
2413
2591
|
const onRawInput = (chunk) => {
|
|
2414
2592
|
const text = typeof chunk === 'string' ? chunk : chunk.toString('utf8');
|
|
2415
|
-
|
|
2593
|
+
rawKeyBuffer = `${rawKeyBuffer}${text}`.slice(-64);
|
|
2594
|
+
const match = rawKeyBuffer.match(F7_ESCAPE_RE);
|
|
2595
|
+
if (!match)
|
|
2416
2596
|
return;
|
|
2417
|
-
|
|
2597
|
+
rawKeyBuffer = '';
|
|
2598
|
+
void hardRedraw(CTRL_F7_ESCAPE_RE.test(match[0]) ? 'ctrl-f7-escape' : 'f7-escape');
|
|
2418
2599
|
};
|
|
2419
2600
|
ttyInput.on('data', onRawInput);
|
|
2420
2601
|
detachRawF7Listener = () => {
|
|
@@ -2427,6 +2608,43 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
2427
2608
|
catch { }
|
|
2428
2609
|
};
|
|
2429
2610
|
}
|
|
2611
|
+
let detachSigwinchListener = null;
|
|
2612
|
+
if (process.platform !== 'win32') {
|
|
2613
|
+
const onSigwinch = () => {
|
|
2614
|
+
try {
|
|
2615
|
+
if (!syncProgramSizeFromTTY('sigwinch'))
|
|
2616
|
+
return;
|
|
2617
|
+
lastLayoutW = 0;
|
|
2618
|
+
lastLayoutH = 0;
|
|
2619
|
+
ensureLayoutSynced('sigwinch');
|
|
2620
|
+
if (lastData) {
|
|
2621
|
+
updateDashboard();
|
|
2622
|
+
}
|
|
2623
|
+
else {
|
|
2624
|
+
if (lastChartSeries) {
|
|
2625
|
+
try {
|
|
2626
|
+
tokenChart.setData(lastChartSeries);
|
|
2627
|
+
}
|
|
2628
|
+
catch { }
|
|
2629
|
+
}
|
|
2630
|
+
try {
|
|
2631
|
+
screen.render();
|
|
2632
|
+
}
|
|
2633
|
+
catch { }
|
|
2634
|
+
}
|
|
2635
|
+
}
|
|
2636
|
+
catch (err) {
|
|
2637
|
+
dlog(`SIGWINCH: ${err.message}`);
|
|
2638
|
+
}
|
|
2639
|
+
};
|
|
2640
|
+
process.on('SIGWINCH', onSigwinch);
|
|
2641
|
+
detachSigwinchListener = () => {
|
|
2642
|
+
try {
|
|
2643
|
+
process.off('SIGWINCH', onSigwinch);
|
|
2644
|
+
}
|
|
2645
|
+
catch { }
|
|
2646
|
+
};
|
|
2647
|
+
}
|
|
2430
2648
|
// ── Handle terminal resize ──
|
|
2431
2649
|
// Recalculate all widget positions from new screen.height
|
|
2432
2650
|
screen.on('resize', () => {
|
|
@@ -2438,7 +2656,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
2438
2656
|
catch { }
|
|
2439
2657
|
lastLayoutW = 0;
|
|
2440
2658
|
lastLayoutH = 0;
|
|
2441
|
-
ensureLayoutSynced();
|
|
2659
|
+
ensureLayoutSynced('screen-resize');
|
|
2442
2660
|
if (lastData) {
|
|
2443
2661
|
updateDashboard();
|
|
2444
2662
|
}
|
|
@@ -2463,6 +2681,9 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
2463
2681
|
// ══════════════════════════════════════════════════════════════════════════
|
|
2464
2682
|
function shutdownDashboard() {
|
|
2465
2683
|
detachRawF7Listener?.();
|
|
2684
|
+
detachSigwinchListener?.();
|
|
2685
|
+
if (resizeRecoveryTimer)
|
|
2686
|
+
clearTimeout(resizeRecoveryTimer);
|
|
2466
2687
|
clearInterval(pollInterval);
|
|
2467
2688
|
clearInterval(windowPollInterval);
|
|
2468
2689
|
clearInterval(tickInterval);
|
|
@@ -2477,8 +2698,8 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
2477
2698
|
// F7 — Hard refresh / redraw (works in both tmux and standalone mode)
|
|
2478
2699
|
// Forces a full screen realloc + layout recalculation to fix corruption
|
|
2479
2700
|
// from rapid terminal resizing. Session data is preserved.
|
|
2480
|
-
screen.key(['f7', 'F7'], () => {
|
|
2481
|
-
void hardRedraw('f7-key');
|
|
2701
|
+
screen.key(['f7', 'F7', 'C-f7'], (_ch, key) => {
|
|
2702
|
+
void hardRedraw(key?.ctrl ? 'ctrl-f7-key' : 'f7-key');
|
|
2482
2703
|
});
|
|
2483
2704
|
if (!inTmux) {
|
|
2484
2705
|
screen.key(['q'], () => {
|
|
@@ -2539,6 +2760,7 @@ async function launchDashboard(initialSessionName, initialJsonlPath, refreshMs,
|
|
|
2539
2760
|
'\n' +
|
|
2540
2761
|
'{bold}Controls{/bold}\n' +
|
|
2541
2762
|
' r Refresh now\n' +
|
|
2763
|
+
' Ctrl+F7 Hard redraw (mac fallback)\n' +
|
|
2542
2764
|
' F7 Hard redraw (fixes corruption)\n' +
|
|
2543
2765
|
' q/Ctrl+Q Quit\n' +
|
|
2544
2766
|
'\n' +
|