opencode-context-indicator 1.3.1 → 1.4.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/CHANGELOG.md +45 -2
- package/README.md +53 -24
- package/index.js +14 -1
- package/lib/cache.js +1 -1
- package/lib/commands.js +59 -36
- package/lib/contributors.js +10 -41
- package/lib/dedup.js +4 -4
- package/lib/estimate.js +97 -6
- package/lib/limits.js +0 -1
- package/lib/state.js +63 -28
- package/lib/tui-data.d.ts +11 -0
- package/lib/tui-data.js +13 -25
- package/lib/tui-panel.tsx +31 -8
- package/lib/tui-sidebar.tsx +4 -0
- package/lib/v2.js +121 -30
- package/package.json +2 -2
package/lib/estimate.js
CHANGED
|
@@ -81,8 +81,10 @@ function safeCountOrNull(v) {
|
|
|
81
81
|
return typeof v === "number" && Number.isFinite(v) && v >= 0 ? v : null;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
// Normalise a categories object from untrusted input (state.json) into the
|
|
85
|
-
// known numeric fields — never trust its shape.
|
|
84
|
+
// Normalise a categories object from untrusted input (state.json) into the 8
|
|
85
|
+
// known numeric fields — never trust its shape. `toolOut` is 0 for legacy
|
|
86
|
+
// entries written before the category existed (their tool results stay folded
|
|
87
|
+
// into `other`, matching how they were measured).
|
|
86
88
|
function safeCategories(raw) {
|
|
87
89
|
if (!raw || typeof raw !== "object") return null;
|
|
88
90
|
return {
|
|
@@ -90,12 +92,60 @@ function safeCategories(raw) {
|
|
|
90
92
|
assistant: safeCount(raw.assistant),
|
|
91
93
|
reasoning: safeCount(raw.reasoning),
|
|
92
94
|
toolArgs: safeCount(raw.toolArgs),
|
|
95
|
+
toolOut: safeCount(raw.toolOut),
|
|
93
96
|
system: safeCountOrNull(raw.system),
|
|
94
97
|
toolSchemas: safeCountOrNull(raw.toolSchemas),
|
|
95
98
|
other: safeCount(raw.other),
|
|
96
99
|
};
|
|
97
100
|
}
|
|
98
101
|
|
|
102
|
+
// A session key safe to use as an object property / map key. Rejects the
|
|
103
|
+
// prototype-pollution vectors ("__proto__", "constructor", "prototype").
|
|
104
|
+
// Sum of the 8 canonical category fields (null `system`/`toolSchemas` count as
|
|
105
|
+
// 0). The single place the "total measured tokens" is derived — shared by the
|
|
106
|
+
// residual-`other` computation, the continuity test and the log/summary writers.
|
|
107
|
+
function sumCategories(c) {
|
|
108
|
+
if (!c || typeof c !== "object") return 0;
|
|
109
|
+
return (
|
|
110
|
+
safeCount(c.user) +
|
|
111
|
+
safeCount(c.assistant) +
|
|
112
|
+
safeCount(c.reasoning) +
|
|
113
|
+
safeCount(c.toolArgs) +
|
|
114
|
+
safeCount(c.toolOut) +
|
|
115
|
+
safeCount(c.system) +
|
|
116
|
+
safeCount(c.toolSchemas) +
|
|
117
|
+
safeCount(c.other)
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Normalise a breakdown record (cache record with *Chars keys, or a persisted
|
|
122
|
+
// state entry with canonical keys) into the 8 categories, deriving the residual
|
|
123
|
+
// `other` from `inputTokens`. Pure; the one converter shared by the V2 live log
|
|
124
|
+
// (lib/v2.js) and the slash-command report (lib/commands.js).
|
|
125
|
+
function breakdownCategories(rec, inputTokens) {
|
|
126
|
+
const user = rec?.userChars ?? rec?.user ?? 0;
|
|
127
|
+
const assistant = rec?.assistantChars ?? rec?.assistant ?? 0;
|
|
128
|
+
const reasoning = rec?.reasoningChars ?? rec?.reasoning ?? 0;
|
|
129
|
+
const toolArgs = rec?.toolArgsChars ?? rec?.toolArgs ?? 0;
|
|
130
|
+
const toolOut = rec?.toolOutChars ?? rec?.toolOut ?? 0;
|
|
131
|
+
const system =
|
|
132
|
+
rec?.systemChars !== undefined ? rec.systemChars : (rec?.system ?? null);
|
|
133
|
+
const toolSchemas =
|
|
134
|
+
rec?.toolSchemasChars !== undefined
|
|
135
|
+
? rec.toolSchemasChars
|
|
136
|
+
: (rec?.toolSchemas ?? null);
|
|
137
|
+
const sum =
|
|
138
|
+
user +
|
|
139
|
+
assistant +
|
|
140
|
+
reasoning +
|
|
141
|
+
toolArgs +
|
|
142
|
+
toolOut +
|
|
143
|
+
(system || 0) +
|
|
144
|
+
(toolSchemas || 0);
|
|
145
|
+
const other = Math.max(0, (inputTokens || 0) - sum);
|
|
146
|
+
return { user, assistant, reasoning, toolArgs, toolOut, system, toolSchemas, other };
|
|
147
|
+
}
|
|
148
|
+
|
|
99
149
|
// A session key safe to use as an object property / map key. Rejects the
|
|
100
150
|
// prototype-pollution vectors ("__proto__", "constructor", "prototype").
|
|
101
151
|
function isSafeSessionKey(k) {
|
|
@@ -108,6 +158,46 @@ function isSafeSessionKey(k) {
|
|
|
108
158
|
);
|
|
109
159
|
}
|
|
110
160
|
|
|
161
|
+
// Bound on the number of distinct tool names kept in a per-tool tool-result
|
|
162
|
+
// map (state.json / context payload are untrusted and a plugin could define an
|
|
163
|
+
// unbounded tool set). Everything past the cap — plus any unsafe name — is
|
|
164
|
+
// folded into a single "other tools" bucket so the total still reconciles with
|
|
165
|
+
// the toolOut category.
|
|
166
|
+
const TOOL_OUT_MAX_KEYS = 24;
|
|
167
|
+
// Normalise a per-tool tool-result map (tool name -> estimated tokens) from
|
|
168
|
+
// untrusted input. Names become object keys AND display cells, so prototype
|
|
169
|
+
// vectors are dropped and the map is bounded (see TOOL_OUT_MAX_KEYS). The
|
|
170
|
+
// overflow bucket is named "other tools" — a real tool with that exact name
|
|
171
|
+
// would merge into it (accepted: it is a display-only label).
|
|
172
|
+
function safeToolOutByTool(raw, maxKeys = TOOL_OUT_MAX_KEYS) {
|
|
173
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
|
|
174
|
+
const out = {};
|
|
175
|
+
let rest = 0;
|
|
176
|
+
let used = 0;
|
|
177
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
178
|
+
const val = safeCount(v);
|
|
179
|
+
if (!isSafeSessionKey(k) || used >= maxKeys) {
|
|
180
|
+
rest += val;
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
out[k] = (out[k] || 0) + val;
|
|
184
|
+
used++;
|
|
185
|
+
}
|
|
186
|
+
if (rest > 0) out["other tools"] = (out["other tools"] || 0) + rest;
|
|
187
|
+
return Object.keys(out).length > 0 ? out : null;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Descending [name, tokens] pairs for display, capped at `limit`; empty when
|
|
191
|
+
// the map carries no positive rows.
|
|
192
|
+
function toolOutEntries(map, limit = 8) {
|
|
193
|
+
if (!map || typeof map !== "object" || Array.isArray(map)) return [];
|
|
194
|
+
return Object.entries(map)
|
|
195
|
+
.map(([k, v]) => [k, safeCount(v)])
|
|
196
|
+
.filter(([, v]) => v > 0)
|
|
197
|
+
.sort((a, b) => b[1] - a[1])
|
|
198
|
+
.slice(0, Math.max(0, limit));
|
|
199
|
+
}
|
|
200
|
+
|
|
111
201
|
function fmt(n) {
|
|
112
202
|
return n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(n);
|
|
113
203
|
}
|
|
@@ -212,17 +302,18 @@ function fmtCell(n) {
|
|
|
212
302
|
}
|
|
213
303
|
|
|
214
304
|
export {
|
|
215
|
-
CHARS_PER_TOKEN_OTHER,
|
|
216
|
-
CHARS_PER_TOKEN_CYRILLIC,
|
|
217
|
-
CHARS_PER_TOKEN_CJK,
|
|
218
|
-
MAX_SANE_LIMIT,
|
|
219
305
|
oneLine,
|
|
220
306
|
safeCell,
|
|
221
307
|
finitePos,
|
|
222
308
|
safeCount,
|
|
223
309
|
safeCountOrNull,
|
|
224
310
|
safeCategories,
|
|
311
|
+
sumCategories,
|
|
312
|
+
breakdownCategories,
|
|
225
313
|
isSafeSessionKey,
|
|
314
|
+
safeToolOutByTool,
|
|
315
|
+
toolOutEntries,
|
|
316
|
+
TOOL_OUT_MAX_KEYS,
|
|
226
317
|
fmt,
|
|
227
318
|
estimateTokens,
|
|
228
319
|
estJson,
|
package/lib/limits.js
CHANGED
package/lib/state.js
CHANGED
|
@@ -27,6 +27,8 @@ import {
|
|
|
27
27
|
oneLine,
|
|
28
28
|
safeCount,
|
|
29
29
|
safeCountOrNull,
|
|
30
|
+
safeToolOutByTool,
|
|
31
|
+
sumCategories,
|
|
30
32
|
} from "./estimate.js";
|
|
31
33
|
import {
|
|
32
34
|
breakdownCache,
|
|
@@ -168,16 +170,6 @@ function writeLog(liveLines) {
|
|
|
168
170
|
}
|
|
169
171
|
}
|
|
170
172
|
|
|
171
|
-
// Append a single line to the file (used for final summaries / notes).
|
|
172
|
-
function appendLogLine(line) {
|
|
173
|
-
try {
|
|
174
|
-
ensureRegularFile(LOG_FILE); // refuse to follow a symlink planted at the path
|
|
175
|
-
appendFileSync(LOG_FILE, line + "\n", "utf8");
|
|
176
|
-
} catch (err) {
|
|
177
|
-
logErr("appendLogLine failed", err);
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
173
|
// Synchronous sleep for the lock retry loop. Atomics.wait is the documented
|
|
182
174
|
// non-busy way to block this thread; fall back to a bounded spin if unavailable.
|
|
183
175
|
function sleepSync(ms) {
|
|
@@ -316,6 +308,7 @@ function writeStateFile(
|
|
|
316
308
|
inputTokens,
|
|
317
309
|
ctxTokens,
|
|
318
310
|
reasoningTokens,
|
|
311
|
+
opts,
|
|
319
312
|
) {
|
|
320
313
|
try {
|
|
321
314
|
if (!sessionID) return;
|
|
@@ -426,15 +419,63 @@ function writeStateFile(
|
|
|
426
419
|
bd && bd.toolSchemasChars != null
|
|
427
420
|
? safeCount(bd.toolSchemasChars)
|
|
428
421
|
: safeCountOrNull(pc?.toolSchemas);
|
|
422
|
+
// Tool results are counted by both the hook and the fallback path, so
|
|
423
|
+
// this is a plain counted category (never null): a missing value is 0.
|
|
424
|
+
const catToolOut = bd
|
|
425
|
+
? safeCount(bd.toolOutChars)
|
|
426
|
+
: safeCount(pc?.toolOut);
|
|
427
|
+
|
|
428
|
+
// Per-tool split of the toolOut category (webfetch / read / bash / …).
|
|
429
|
+
// Untrusted names -> sanitised + bounded. A present breakdown OWNS the
|
|
430
|
+
// split (an empty one means "no tool results now" — do NOT revive a stale
|
|
431
|
+
// peer value); only fall back to the persisted map when this instance has
|
|
432
|
+
// no live breakdown at all.
|
|
433
|
+
const toolOutByTool = bd
|
|
434
|
+
? safeToolOutByTool(bd.toolOutByTool)
|
|
435
|
+
: safeToolOutByTool(prevEntry?.toolOutByTool);
|
|
436
|
+
|
|
437
|
+
// Compaction staleness: a `session.compacted` write re-persists the
|
|
438
|
+
// pre-compaction numbers (the compaction flow fires no context hook), so
|
|
439
|
+
// the entry is stale BY CONSTRUCTION — even when this instance has no live
|
|
440
|
+
// breakdown to compare against (a mirror instance must not drop the
|
|
441
|
+
// signal). Later writes inherit `compactedAt` and clear the mark only once
|
|
442
|
+
// a FRESH breakdown (bd.capturedAt newer than the compaction) lands.
|
|
443
|
+
const compactedAt = opts?.compacted
|
|
444
|
+
? new Date().toISOString()
|
|
445
|
+
: typeof prevEntry?.compactedAt === "string"
|
|
446
|
+
? prevEntry.compactedAt
|
|
447
|
+
: null;
|
|
448
|
+
let compactStale = false;
|
|
449
|
+
if (opts?.compacted === true) {
|
|
450
|
+
compactStale = true;
|
|
451
|
+
} else if (compactedAt) {
|
|
452
|
+
const cMs = Date.parse(compactedAt);
|
|
453
|
+
const measuredMs =
|
|
454
|
+
bd && typeof bd.capturedAt === "number" ? bd.capturedAt : NaN;
|
|
455
|
+
if (Number.isFinite(cMs) && Number.isFinite(measuredMs)) {
|
|
456
|
+
compactStale = measuredMs <= cMs;
|
|
457
|
+
} else {
|
|
458
|
+
// No live breakdown here: keep the previous verdict (a peer instance
|
|
459
|
+
// may have measured it).
|
|
460
|
+
compactStale = prevEntry?.compactStale === true;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
429
463
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
464
|
+
// Residual = input minus every measured category (shared sum; null
|
|
465
|
+
// system/toolSchemas count as 0).
|
|
466
|
+
const other = Math.max(
|
|
467
|
+
0,
|
|
468
|
+
safeCount(inputTokens) -
|
|
469
|
+
sumCategories({
|
|
470
|
+
user: catUser,
|
|
471
|
+
assistant: catAssistant,
|
|
472
|
+
reasoning: catReasoning,
|
|
473
|
+
toolArgs: catToolArgs,
|
|
474
|
+
toolOut: catToolOut,
|
|
475
|
+
system: catSystem,
|
|
476
|
+
toolSchemas: catToolSchemas,
|
|
477
|
+
}),
|
|
478
|
+
);
|
|
438
479
|
// Parent link for the /context-breakdown command. Prefer the live cache;
|
|
439
480
|
// fall back to the previously persisted value (backward compatible with
|
|
440
481
|
// snapshots written before parentID existed -> null). role mirrors it.
|
|
@@ -479,10 +520,14 @@ function writeStateFile(
|
|
|
479
520
|
assistant: catAssistant,
|
|
480
521
|
reasoning: catReasoning,
|
|
481
522
|
toolArgs: catToolArgs,
|
|
523
|
+
toolOut: catToolOut,
|
|
482
524
|
system: catSystem,
|
|
483
525
|
toolSchemas: catToolSchemas,
|
|
484
526
|
other,
|
|
485
527
|
},
|
|
528
|
+
toolOutByTool,
|
|
529
|
+
compactedAt,
|
|
530
|
+
compactStale,
|
|
486
531
|
updatedAt: new Date().toISOString(),
|
|
487
532
|
};
|
|
488
533
|
all[sessionID] = entry;
|
|
@@ -611,19 +656,9 @@ function pushFinalSummary(claimKey, summaryLines) {
|
|
|
611
656
|
|
|
612
657
|
export {
|
|
613
658
|
logErr,
|
|
614
|
-
renderFile,
|
|
615
659
|
writeLog,
|
|
616
|
-
appendLogLine,
|
|
617
|
-
sleepSync,
|
|
618
|
-
ensureRegularFile,
|
|
619
|
-
isStaleLock,
|
|
620
|
-
acquireStateLock,
|
|
621
|
-
releaseStateLock,
|
|
622
|
-
renameWithRetry,
|
|
623
660
|
writeStateFile,
|
|
624
|
-
appendEventLog,
|
|
625
661
|
debugEvent,
|
|
626
662
|
readStateSnapshot,
|
|
627
|
-
finalSummaries,
|
|
628
663
|
pushFinalSummary,
|
|
629
664
|
};
|
package/lib/tui-data.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ export interface Categories {
|
|
|
9
9
|
assistant?: number
|
|
10
10
|
reasoning?: number
|
|
11
11
|
toolArgs?: number
|
|
12
|
+
toolOut?: number
|
|
12
13
|
system?: number | null
|
|
13
14
|
toolSchemas?: number | null
|
|
14
15
|
other?: number
|
|
@@ -28,6 +29,12 @@ export interface StateEntry {
|
|
|
28
29
|
limit?: number | null
|
|
29
30
|
reasoning?: number
|
|
30
31
|
categories?: Categories
|
|
32
|
+
/** Per-tool tool-result split: tool name -> estimated tokens. */
|
|
33
|
+
toolOutByTool?: Record<string, number>
|
|
34
|
+
/** ISO time of the last session.compacted (marker only). */
|
|
35
|
+
compactedAt?: string | null
|
|
36
|
+
/** True while the entry still shows pre-compaction numbers. */
|
|
37
|
+
compactStale?: boolean
|
|
31
38
|
updatedAt?: string
|
|
32
39
|
}
|
|
33
40
|
|
|
@@ -43,3 +50,7 @@ export declare function fmt(n: number): string
|
|
|
43
50
|
export declare function num(v: unknown): number
|
|
44
51
|
export declare function fit(s: string, w: number): string
|
|
45
52
|
export declare function updatedAt(iso: string | undefined): string
|
|
53
|
+
export declare function toolOutEntries(
|
|
54
|
+
map: Record<string, number> | undefined,
|
|
55
|
+
limit?: number,
|
|
56
|
+
): Array<[string, number]>
|
package/lib/tui-data.js
CHANGED
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
//
|
|
13
13
|
// state.json shape (written by writeStateFile() in index.js, untrusted input):
|
|
14
14
|
// { [sessionID]: { sessionID, parentID, role, agent, model, providerID,
|
|
15
|
-
// ctx, input, usable, reserve, limit, reasoning, categories{...},
|
|
15
|
+
// ctx, input, usable, reserve, limit, reasoning, categories{...},
|
|
16
|
+
// toolOutByTool{name->tokens}, compactedAt, compactStale, updatedAt } }
|
|
16
17
|
// The authoritative TS mirror of the entry shape is the `StateEntry` interface
|
|
17
18
|
// in lib/tui-data.d.ts — keep the two structurally compatible.
|
|
18
19
|
|
|
@@ -20,6 +21,11 @@ import { readFileSync } from "node:fs";
|
|
|
20
21
|
import { tmpdir } from "node:os";
|
|
21
22
|
import { join } from "node:path";
|
|
22
23
|
import { fileURLToPath } from "node:url";
|
|
24
|
+
import { isSafeSessionKey, sumCategories } from "./estimate.js";
|
|
25
|
+
|
|
26
|
+
// Re-exported so the panel keeps importing it from this module; the
|
|
27
|
+
// implementation is the shared one (lib/estimate.js) — no second copy to drift.
|
|
28
|
+
export { toolOutEntries } from "./estimate.js";
|
|
23
29
|
|
|
24
30
|
export const STATE_FILE =
|
|
25
31
|
process.env.OPENCODE_CONTEXT_INDICATOR_STATE_FILE ||
|
|
@@ -42,19 +48,6 @@ export function isNodeModulesInstall() {
|
|
|
42
48
|
}
|
|
43
49
|
}
|
|
44
50
|
|
|
45
|
-
// Reserved object keys must never be treated as session ids: state.json is
|
|
46
|
-
// untrusted input (structural mirror of isSafeSessionKey() in lib/estimate.js,
|
|
47
|
-
// including the string/length requirement).
|
|
48
|
-
function isSafeKey(k) {
|
|
49
|
-
return (
|
|
50
|
-
typeof k === "string" &&
|
|
51
|
-
k.length > 0 &&
|
|
52
|
-
k !== "__proto__" &&
|
|
53
|
-
k !== "constructor" &&
|
|
54
|
-
k !== "prototype"
|
|
55
|
-
)
|
|
56
|
-
}
|
|
57
|
-
|
|
58
51
|
// Whole state.json snapshot ({ sessionID: entry }) or {} on any error. The
|
|
59
52
|
// sidebar resolves one entry from it and the breakdown panel walks the
|
|
60
53
|
// parentID family over it. Never throws — a missing / empty / corrupt
|
|
@@ -68,7 +61,7 @@ export function readSnapshot() {
|
|
|
68
61
|
// (__proto__ / constructor / prototype) and non-object entries in one pass.
|
|
69
62
|
const clean = {}
|
|
70
63
|
for (const [k, v] of Object.entries(all)) {
|
|
71
|
-
if (
|
|
64
|
+
if (isSafeSessionKey(k) && v && typeof v === "object") clean[k] = v
|
|
72
65
|
}
|
|
73
66
|
return clean
|
|
74
67
|
} catch {
|
|
@@ -82,16 +75,11 @@ export function readSnapshot() {
|
|
|
82
75
|
// a missing categories object is not the only trigger. `system` / `toolSchemas`
|
|
83
76
|
// may legitimately be null, hence the num() coercion.
|
|
84
77
|
function isEmptyEntry(e) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
num(
|
|
88
|
-
num(
|
|
89
|
-
|
|
90
|
-
num(c?.toolArgs) +
|
|
91
|
-
num(c?.system) +
|
|
92
|
-
num(c?.toolSchemas) +
|
|
93
|
-
num(c?.other)
|
|
94
|
-
return sum === 0 && num(e?.ctx) === 0 && num(e?.input) === 0
|
|
78
|
+
return (
|
|
79
|
+
sumCategories(e?.categories) === 0 &&
|
|
80
|
+
num(e?.ctx) === 0 &&
|
|
81
|
+
num(e?.input) === 0
|
|
82
|
+
)
|
|
95
83
|
}
|
|
96
84
|
|
|
97
85
|
// The panel row set: the root entry (panel.sessionID) plus every entry whose
|
package/lib/tui-panel.tsx
CHANGED
|
@@ -11,7 +11,7 @@ import { mkdirSync, writeFileSync } from "node:fs"
|
|
|
11
11
|
import { tmpdir } from "node:os"
|
|
12
12
|
import { basename, join } from "node:path"
|
|
13
13
|
import type { StateEntry } from "./tui-data.js"
|
|
14
|
-
import { collectFamily, fit, fmt, num, updatedAt } from "./tui-data.js"
|
|
14
|
+
import { collectFamily, fit, fmt, num, toolOutEntries, updatedAt } from "./tui-data.js"
|
|
15
15
|
import { token } from "./tui-theme.js"
|
|
16
16
|
import {
|
|
17
17
|
buildRedactedPayload,
|
|
@@ -475,11 +475,13 @@ export function BreakdownPanel(props: {
|
|
|
475
475
|
const model = safeText(r.model && r.model !== "unknown" ? r.model : "model?")
|
|
476
476
|
const ctx = fmt(num(r.ctx))
|
|
477
477
|
const d = panelDenom(r)
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
478
|
+
const base =
|
|
479
|
+
d > 0
|
|
480
|
+
? `Context breakdown \u2014 ${model} ${ctx}/${fmt(d)} (${Math.round(
|
|
481
|
+
(num(r.ctx) / d) * 100,
|
|
482
|
+
)}%)`
|
|
483
|
+
: `Context breakdown \u2014 ${model} ${ctx}`
|
|
484
|
+
return r.compactStale === true ? `${base} (stale — compacted)` : base
|
|
483
485
|
}
|
|
484
486
|
|
|
485
487
|
const layout = (): {
|
|
@@ -523,10 +525,27 @@ export function BreakdownPanel(props: {
|
|
|
523
525
|
L.gutter +
|
|
524
526
|
fit(money(costOf(e.sessionID)), L.costW) +
|
|
525
527
|
L.gutter +
|
|
526
|
-
updatedAt(e.updatedAt)
|
|
528
|
+
updatedAt(e.updatedAt) +
|
|
529
|
+
(e.compactStale === true ? " (stale)" : "")
|
|
527
530
|
)
|
|
528
531
|
}
|
|
529
532
|
|
|
533
|
+
// One compact line: the main session's tool results split by tool name
|
|
534
|
+
// (server-side aggregate; the per-call "largest contributors" block above is
|
|
535
|
+
// the measured, client-side view).
|
|
536
|
+
const byToolLine = (): string => {
|
|
537
|
+
const all = data().root?.toolOutByTool
|
|
538
|
+
const entries = toolOutEntries(all, 5)
|
|
539
|
+
if (entries.length === 0) return ""
|
|
540
|
+
const total =
|
|
541
|
+
all && typeof all === "object"
|
|
542
|
+
? Object.values(all).filter((v) => num(v) > 0).length
|
|
543
|
+
: 0
|
|
544
|
+
const parts = entries.map(([name, val]) => `${safeText(name)} ${fmt(val)}`)
|
|
545
|
+
if (total > entries.length) parts.push(`+${total - entries.length} more`)
|
|
546
|
+
return parts.join(" \u00b7 ")
|
|
547
|
+
}
|
|
548
|
+
|
|
530
549
|
return (
|
|
531
550
|
<box flexDirection="column">
|
|
532
551
|
<text fg={base()}>{header()}</text>
|
|
@@ -565,10 +584,14 @@ export function BreakdownPanel(props: {
|
|
|
565
584
|
</Show>
|
|
566
585
|
<text fg={muted()}>
|
|
567
586
|
{
|
|
568
|
-
"measured != server categories
|
|
587
|
+
"measured != server categories: contributors attribute outputs per tool call; the server's toolOut category only totals them"
|
|
569
588
|
}
|
|
570
589
|
</text>
|
|
571
590
|
</Show>
|
|
591
|
+
<Show when={byToolLine().length > 0}>
|
|
592
|
+
<text fg={base()}>{"tool results by tool"}</text>
|
|
593
|
+
<text fg={muted()}>{byToolLine()}</text>
|
|
594
|
+
</Show>
|
|
572
595
|
</Show>
|
|
573
596
|
</box>
|
|
574
597
|
)
|
package/lib/tui-sidebar.tsx
CHANGED
|
@@ -54,6 +54,7 @@ export function Sidebar(props: { entry: () => StateEntry | null }) {
|
|
|
54
54
|
<text fg={muted()}>{line("assistant", e()?.categories?.assistant)}</text>
|
|
55
55
|
<text fg={muted()}>{line("reasoning", e()?.categories?.reasoning)}</text>
|
|
56
56
|
<text fg={muted()}>{line("tool args", e()?.categories?.toolArgs)}</text>
|
|
57
|
+
<text fg={muted()}>{line("tool out", e()?.categories?.toolOut)}</text>
|
|
57
58
|
<text fg={muted()}>
|
|
58
59
|
{e()?.categories?.system == null
|
|
59
60
|
? `${"system".padEnd(LABEL_WIDTH)} n/a`
|
|
@@ -65,6 +66,9 @@ export function Sidebar(props: { entry: () => StateEntry | null }) {
|
|
|
65
66
|
: line("tool schemas", e()?.categories?.toolSchemas)}
|
|
66
67
|
</text>
|
|
67
68
|
<text fg={muted()}>{line("other", e()?.categories?.other)}</text>
|
|
69
|
+
<Show when={e()?.compactStale === true}>
|
|
70
|
+
<text fg={muted()}>{" stale — compacted"}</text>
|
|
71
|
+
</Show>
|
|
68
72
|
<text fg={muted()}>{`updated ${updatedAt(e()?.updatedAt)}`}</text>
|
|
69
73
|
</Show>
|
|
70
74
|
</box>
|