@vincemakes/kiso-tui 0.8.0 → 0.10.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/dist/index.js CHANGED
@@ -9,25 +9,36 @@ export { Body, Dock, CURSOR_MARKER } from "./compositor.js";
9
9
  // W21 (the v8 approval round): the approval panel — the bounded block
10
10
  // that replaces the running tool's live window while a human-chain
11
11
  // approval is pending (the shape authority is the committed preview).
12
- export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus, } from "./approval-panel.js";
13
- export { Container, foldLine, visibleWidth, SPINNER } from "./components.js";
12
+ export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus,
13
+ // TUI2-R2 ④: the pick payload the panel slot's third occupant.
14
+ PICK_MAX, modelPickView, pickAffordance, pickBlockRows, pickLeadPlain, } from "./approval-panel.js";
15
+ export { Container, foldLine, foldWords, visibleWidth, SPINNER } from "./components.js";
14
16
  export { Editor, MENU_ITEMS, PROMPT, PROMPT_WIDTH, displayWidth, charWidth, widthOf, } from "./editor.js";
15
17
  export { bannerLines, COLOR_OFF, COLOR_ON, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderResumeList, renderSessionLine, renderStatusLine, relativeTime, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, } from "./render.js";
16
18
  export { editFileDiff, truncateDiff, writeFileDiff } from "./diff.js";
17
19
  // KC2 §5: the status rows' formatters — the CLI keeps the state and the
18
20
  // repaint, the terminal layer owns what the row says.
19
- export { STATUS_GLYPHS, idleStatus, runningStatus } from "./status.js";
21
+ export { STATUS_GLYPHS, cacheHitPct, idleStatus, runningStatus } from "./status.js";
22
+ // TUI2-R1 (E): /context's attribution rows — a pure function of the
23
+ // counts the trace sidecar already records (the CLI reads, this renders).
24
+ export { contextRows, contextUnavailableRows } from "./context-ledger.js";
20
25
  // KC3 §1 (the extraction): the human-facing strings — the prompt, the
21
26
  // project-trust listing/view/note, the uncertain execution's view. The
22
27
  // FLOW (who is asked, what a verdict means) stays in the cli.
23
28
  export { interactivePrompt, projectTrustRows, projectTrustView, projectUntrustedNote, uncertainView } from "./strings.js";
24
29
  // KC3 §3/§5: the @ file picker's pure half — the subsequence filter, the
25
30
  // deterministic rank, and the ONE cap the CLI's file source shares.
26
- export { AT_CAP, AT_SKIP, AT_VISIBLE, atEmbed, atFilter, atPanelRows, atWindow, longestRun } from "./at-picker.js";
31
+ export { AT_CAP, AT_SKIP, AT_VISIBLE, atEmbed, atFilter, atPanelRows, atWindow, bandHeader, longestRun } from "./at-picker.js";
32
+ // TUI2-R2 ①–③: the session picker's pure half — the durability badge,
33
+ // the row (picked or printed), the band, and the filter. The CARDS are
34
+ // the cli's projection (session-cards.ts); this turns them into bytes.
35
+ export { BADGE_GLYPH, idColumn, sessionAge, sessionBadge, sessionCounterRow, sessionFilter, sessionListFooter, sessionListRow, sessionNote, sessionPickerRows, sessionRow, } from "./session-picker.js";
27
36
  // KC3.5 (the ask round): the ask view — the panel machinery generalized.
28
37
  // The cli composes the view and hands the answers to the tool; the keys,
29
38
  // the rows and the walk are the terminal layer's.
30
39
  export { ASK_HEADER_CAP, ASK_MAX_OPTIONS, ASK_MAX_QUESTIONS, ASK_MIN_OPTIONS, askAffordance, askAnswers, askBlockRows, askCommitCustom, askDeclineAll, askDeclineList, askKey, askLeadPlain, askStart, askStatus, askView, } from "./ask-panel.js";
31
40
  // KC3.5 §4: the interrupted-ask copy — the SAME uncertainty gate, said
32
41
  // honestly for a question nobody answered (the ① probe's surface).
33
- export { extensionsBannerText, helpRows, unansweredAskView } from "./strings.js";
42
+ // TUI2-R1 (D): the keys sheet + THE key table — one source for the ?
43
+ // overlay and /help's keys row.
44
+ export { KEY_BINDINGS, PANEL_KEYS_ROW, displayVerb, extensionsBannerText, helpRows, keysHelpRow, keysSheetRows, unansweredAskView } from "./strings.js";
@@ -0,0 +1,113 @@
1
+ /**
2
+ * TUI2-R2 slices ①–③ — the session picker's PURE half: the durability
3
+ * badge, the row, the band, and the filter.
4
+ *
5
+ * The badge is the round's whole argument. kiso's claim is that a
6
+ * session survives kill -9 and resumes from its durable prefix; until
7
+ * now that claim was a sentence in a README. A badge per row makes it a
8
+ * thing you can SEE before you pick: this one completed, this one was
9
+ * cut mid-run and will resume exactly, this one is holding a question
10
+ * for you.
11
+ *
12
+ * The vocabulary (the palette's functional set — no new colour):
13
+ *
14
+ * ✓ green the run's terminal event says completed
15
+ * ✗ red the terminal says anything else
16
+ * ▌ bold no terminal event — interrupted mid-run
17
+ * ? warn the uncertain ledger is not empty (overrides ▌)
18
+ * ◌ dim a permission request nobody has answered
19
+ *
20
+ * Purity, as everywhere in this package: the cards are DATA the CLI
21
+ * projects (session-cards.ts) and this module turns them into bytes. It
22
+ * never reads a session, never asks the runtime anything, and holds no
23
+ * state — which is what lets the picker band and the `kiso sessions`
24
+ * listing render from ONE definition instead of two that drift.
25
+ */
26
+ /** The projected card — structurally what apps/cli/src/session-cards.ts
27
+ * produces. Declared here as the tui's INPUT contract (the package
28
+ * imports nothing from the runtime, by rule). */
29
+ export interface SessionCardView {
30
+ readonly id: string;
31
+ readonly badge: "uncertain" | "ask" | "interrupted" | "completed" | "failed";
32
+ readonly turns: number;
33
+ readonly updatedAt: number;
34
+ readonly uncertain: number;
35
+ readonly asks: number;
36
+ readonly outcome: string | null;
37
+ }
38
+ /** The glyph per state — one cell each, so the badge column never
39
+ * shifts the id column (a column that moves per row reads as damage). */
40
+ export declare const BADGE_GLYPH: Readonly<Record<SessionCardView["badge"], string>>;
41
+ /** The badge, styled. The colour IS the meaning here (the mono
42
+ * discipline's three functional exceptions), so NO_COLOR degrades to
43
+ * the glyph alone — which is why the glyphs are distinct shapes and
44
+ * not three coloured dots. */
45
+ export declare function sessionBadge(badge: SessionCardView["badge"]): string;
46
+ /**
47
+ * What the row SAYS about the state. The interrupted note is the
48
+ * product's promise stated in the place the promise matters: the run
49
+ * continues from its durable prefix, so picking this row costs nothing
50
+ * that was already paid for.
51
+ *
52
+ * The ✗ note names the OUTCOME rather than flattening six endings into
53
+ * one word — "aborted" and "max turns" are different things to have
54
+ * happened, and a picker that calls both "failed" teaches the user
55
+ * nothing.
56
+ */
57
+ export declare function sessionNote(card: SessionCardView): string;
58
+ /** The compact age — the picker's column, not the banner's sentence.
59
+ * `relativeTime` says "3d ago"; a column of ages does not need the
60
+ * word repeated on every row. */
61
+ export declare function sessionAge(updatedAt: number, now: number): string;
62
+ /** The id column's width — computed over EVERY card, never over the
63
+ * filtered subset, so the columns do not jump while the user types
64
+ * (the whole reason a filter-as-you-type picker is usable). */
65
+ export declare function idColumn(cards: readonly SessionCardView[]): number;
66
+ /**
67
+ * The filter — the @ picker's muscle, aimed at the session id: a
68
+ * case-insensitive SUBSEQUENCE, ranked by the longest contiguous run,
69
+ * then by id length, then lexically. Identical determinism, identical
70
+ * feel; a row under the cursor never moves because two ids tied.
71
+ *
72
+ * An empty query matches everything and keeps the caller's order (the
73
+ * listing's newest-first), because "no query" is not a search — it is
74
+ * the list.
75
+ */
76
+ export declare function sessionFilter(cards: readonly SessionCardView[], query: string): SessionCardView[];
77
+ /**
78
+ * ONE picker row. The selection is a FULL-ROW reverse bar — the R1.5
79
+ * ⑧ ruling's shape, shared with the @ picker and the user chip: a
80
+ * two-cell marker in an eighty-column row is a selection you have to
81
+ * hunt for.
82
+ *
83
+ * The inner spans close with rvEnd inside the bar (never SGR 0, which
84
+ * would punch a hole in it) — the same composition atRow uses.
85
+ */
86
+ export declare function sessionRow(card: SessionCardView, selected: boolean, W: number, now: number, idCol: number): string;
87
+ /** The counter row — the SELECTION's 1-based place in the whole
88
+ * filtered list, which the visible window cannot tell the user. */
89
+ export declare function sessionCounterRow(selected: number, total: number, W: number): string;
90
+ /** The picker's bound state — the editor owns it, the compositor reads
91
+ * it (the @ picker's contract, one surface over). */
92
+ export interface SessionPickState {
93
+ readonly cards: readonly SessionCardView[];
94
+ readonly matches: readonly SessionCardView[];
95
+ readonly selected: number;
96
+ }
97
+ /**
98
+ * The whole band: the `sessions` header (R1.5 ⑦(b) — a band names
99
+ * itself or it reads as more scrollback), at most AT_VISIBLE windowed
100
+ * rows, then the counter. Returned as plain strings for the menu-rows
101
+ * channel, which already accounts them in chromeRows — the picker needs
102
+ * no geometry of its own, which is the entire reason it rides that
103
+ * channel.
104
+ */
105
+ export declare function sessionPickerRows(state: SessionPickState, W: number, now: number): string[];
106
+ /** Slice ③ — the `kiso sessions` TTY row: the SAME projection, printed
107
+ * rather than picked. No selection bar (nothing is selected on a
108
+ * listing) and no leading indent: this row starts at column 1 like
109
+ * every other line a shell command prints. */
110
+ export declare function sessionListRow(card: SessionCardView, W: number, now: number, idCol: number): string;
111
+ /** Slice ③ — the listing's last line: the count, and the one thing the
112
+ * user can do next. */
113
+ export declare function sessionListFooter(count: number, W: number): string;
@@ -0,0 +1,249 @@
1
+ /**
2
+ * TUI2-R2 slices ①–③ — the session picker's PURE half: the durability
3
+ * badge, the row, the band, and the filter.
4
+ *
5
+ * The badge is the round's whole argument. kiso's claim is that a
6
+ * session survives kill -9 and resumes from its durable prefix; until
7
+ * now that claim was a sentence in a README. A badge per row makes it a
8
+ * thing you can SEE before you pick: this one completed, this one was
9
+ * cut mid-run and will resume exactly, this one is holding a question
10
+ * for you.
11
+ *
12
+ * The vocabulary (the palette's functional set — no new colour):
13
+ *
14
+ * ✓ green the run's terminal event says completed
15
+ * ✗ red the terminal says anything else
16
+ * ▌ bold no terminal event — interrupted mid-run
17
+ * ? warn the uncertain ledger is not empty (overrides ▌)
18
+ * ◌ dim a permission request nobody has answered
19
+ *
20
+ * Purity, as everywhere in this package: the cards are DATA the CLI
21
+ * projects (session-cards.ts) and this module turns them into bytes. It
22
+ * never reads a session, never asks the runtime anything, and holds no
23
+ * state — which is what lets the picker band and the `kiso sessions`
24
+ * listing render from ONE definition instead of two that drift.
25
+ */
26
+ import { escapeTerminal, palette } from "./render.js";
27
+ import { visibleWidth, widthCut } from "./components.js";
28
+ import { atEmbed, bandHeader, longestRun, AT_VISIBLE, atWindow } from "./at-picker.js";
29
+ /** The glyph per state — one cell each, so the badge column never
30
+ * shifts the id column (a column that moves per row reads as damage). */
31
+ export const BADGE_GLYPH = {
32
+ completed: "✓", // ✓
33
+ failed: "✗", // ✗
34
+ interrupted: "▌", // ▌ — the input brick: this session is mid-sentence
35
+ uncertain: "?",
36
+ ask: "◌", // ◌ — the dotted circle: a question with no answer in it yet
37
+ };
38
+ /** The badge, styled. The colour IS the meaning here (the mono
39
+ * discipline's three functional exceptions), so NO_COLOR degrades to
40
+ * the glyph alone — which is why the glyphs are distinct shapes and
41
+ * not three coloured dots. */
42
+ export function sessionBadge(badge) {
43
+ const p = palette();
44
+ const g = BADGE_GLYPH[badge];
45
+ if (badge === "completed")
46
+ return `${p.green}${g}${p.reset}`;
47
+ if (badge === "failed")
48
+ return `${p.red}${g}${p.reset}`;
49
+ if (badge === "interrupted")
50
+ return `${p.bold}${g}${p.reset}`;
51
+ if (badge === "uncertain")
52
+ return `${p.warn}${g}${p.reset}`;
53
+ return `${p.dim}${g}${p.reset}`;
54
+ }
55
+ /**
56
+ * What the row SAYS about the state. The interrupted note is the
57
+ * product's promise stated in the place the promise matters: the run
58
+ * continues from its durable prefix, so picking this row costs nothing
59
+ * that was already paid for.
60
+ *
61
+ * The ✗ note names the OUTCOME rather than flattening six endings into
62
+ * one word — "aborted" and "max turns" are different things to have
63
+ * happened, and a picker that calls both "failed" teaches the user
64
+ * nothing.
65
+ */
66
+ export function sessionNote(card) {
67
+ switch (card.badge) {
68
+ case "uncertain":
69
+ return `${card.uncertain} uncertain — needs your verdict`;
70
+ case "ask":
71
+ return `${card.asks} ask${card.asks === 1 ? "" : "s"} pending`;
72
+ case "interrupted":
73
+ return "interrupted mid-run — resumes exactly";
74
+ case "completed":
75
+ return "completed clean";
76
+ default:
77
+ return card.outcome === null || card.outcome === "error" ? "failed" : card.outcome.replaceAll("_", " ");
78
+ }
79
+ }
80
+ /** The compact age — the picker's column, not the banner's sentence.
81
+ * `relativeTime` says "3d ago"; a column of ages does not need the
82
+ * word repeated on every row. */
83
+ export function sessionAge(updatedAt, now) {
84
+ const s = Math.max(0, now - updatedAt) / 1000;
85
+ if (s < 60)
86
+ return "now";
87
+ const m = Math.floor(s / 60);
88
+ if (m < 60)
89
+ return `${m}m`;
90
+ const h = Math.floor(m / 60);
91
+ if (h < 24)
92
+ return `${h}h`;
93
+ const d = Math.floor(h / 24);
94
+ if (d < 7)
95
+ return `${d}d`;
96
+ return `${Math.floor(d / 7)}w`;
97
+ }
98
+ /** The id column's width — computed over EVERY card, never over the
99
+ * filtered subset, so the columns do not jump while the user types
100
+ * (the whole reason a filter-as-you-type picker is usable). */
101
+ export function idColumn(cards) {
102
+ let w = 0;
103
+ for (const c of cards)
104
+ w = Math.max(w, visibleWidth(escapeTerminal(c.id)));
105
+ return Math.min(Math.max(w, 1), 24);
106
+ }
107
+ /**
108
+ * The filter — the @ picker's muscle, aimed at the session id: a
109
+ * case-insensitive SUBSEQUENCE, ranked by the longest contiguous run,
110
+ * then by id length, then lexically. Identical determinism, identical
111
+ * feel; a row under the cursor never moves because two ids tied.
112
+ *
113
+ * An empty query matches everything and keeps the caller's order (the
114
+ * listing's newest-first), because "no query" is not a search — it is
115
+ * the list.
116
+ */
117
+ export function sessionFilter(cards, query) {
118
+ if (query === "")
119
+ return [...cards];
120
+ const lower = query.toLowerCase();
121
+ const scored = [];
122
+ for (const card of cards) {
123
+ const hit = atEmbed(card.id.toLowerCase(), lower);
124
+ if (hit === null)
125
+ continue;
126
+ scored.push({ card, run: longestRun(hit) });
127
+ }
128
+ scored.sort((a, b) => {
129
+ if (a.run !== b.run)
130
+ return b.run - a.run;
131
+ if (a.card.id.length !== b.card.id.length)
132
+ return a.card.id.length - b.card.id.length;
133
+ return a.card.id < b.card.id ? -1 : a.card.id > b.card.id ? 1 : 0;
134
+ });
135
+ return scored.map((s) => s.card);
136
+ }
137
+ /**
138
+ * The row's spans, built against a HARD budget — the badge, the id
139
+ * column, the metadata, the note.
140
+ *
141
+ * The spans are appended in order of what the row is FOR and each one
142
+ * is dropped whole rather than half-drawn when the budget runs out:
143
+ * the badge and the id are the row's identity, the age/turns say
144
+ * whether it is the one, and the note is the sentence that explains
145
+ * the badge. A narrow terminal loses them from the right.
146
+ *
147
+ * The running width is the authority — never a formula computed up
148
+ * front. That is what the invariant-① sweep across five widths in the
149
+ * gate is for: a row that overflows does not truncate quietly here, it
150
+ * CRASHES the compositor, so the arithmetic has to be provably right at
151
+ * every width rather than right at eighty.
152
+ */
153
+ function rowSpans(card, budget, now, idCol) {
154
+ const p = palette();
155
+ let text = "";
156
+ let w = 0;
157
+ /** append a styled span iff its VISIBLE cells still fit */
158
+ const put = (plain, styled) => {
159
+ const cells = visibleWidth(plain);
160
+ if (w + cells > budget)
161
+ return;
162
+ text += styled;
163
+ w += cells;
164
+ };
165
+ // the badge: one glyph + one space, styled as a unit (the glyph's own
166
+ // SGR spans make it unmeasurable by `put`'s plain/styled pair)
167
+ if (w + 2 <= budget) {
168
+ text += `${sessionBadge(card.badge)} `;
169
+ w += 2;
170
+ }
171
+ const id = widthCut(escapeTerminal(card.id), Math.max(1, Math.min(idCol, budget - w)));
172
+ put(id, id);
173
+ // the column pad only survives while there is room for what follows
174
+ const pad = Math.max(0, Math.min(idCol - visibleWidth(id), budget - w));
175
+ put(" ".repeat(pad), " ".repeat(pad));
176
+ const meta = ` ${sessionAge(card.updatedAt, now)} · ${card.turns} turn${card.turns === 1 ? "" : "s"}`;
177
+ put(meta, `${p.dim}${meta}${p.reset}`);
178
+ const note = widthCut(sessionNote(card), Math.max(0, budget - w - 3));
179
+ if (note !== "") {
180
+ // the ? note carries the warn tint — the row's own words are what
181
+ // the user acts on, and the one that demands an action says so
182
+ put(" · ", `${p.dim} · ${p.reset}`);
183
+ put(note, card.badge === "uncertain" ? `${p.warn}${note}${p.reset}` : `${p.dim}${note}${p.reset}`);
184
+ }
185
+ return { text, width: w };
186
+ }
187
+ /**
188
+ * ONE picker row. The selection is a FULL-ROW reverse bar — the R1.5
189
+ * ⑧ ruling's shape, shared with the @ picker and the user chip: a
190
+ * two-cell marker in an eighty-column row is a selection you have to
191
+ * hunt for.
192
+ *
193
+ * The inner spans close with rvEnd inside the bar (never SGR 0, which
194
+ * would punch a hole in it) — the same composition atRow uses.
195
+ */
196
+ export function sessionRow(card, selected, W, now, idCol) {
197
+ const p = palette();
198
+ // both forms spend two cells of the width on their frame — the
199
+ // unselected row's indent, the bar's own leading/trailing cell — so
200
+ // the spans are built against the same budget either way and the
201
+ // selection cannot change the columns
202
+ const { text, width } = rowSpans(card, Math.max(0, W - 2), now, idCol);
203
+ if (!selected)
204
+ return ` ${text}`;
205
+ const inner = text.replaceAll(p.reset, `${p.reset}${p.rv}`);
206
+ return `${p.rv} ${inner}${" ".repeat(Math.max(0, W - width - 2))} ${p.rvEnd}`;
207
+ }
208
+ /** The counter row — the SELECTION's 1-based place in the whole
209
+ * filtered list, which the visible window cannot tell the user. */
210
+ export function sessionCounterRow(selected, total, W) {
211
+ const p = palette();
212
+ return `${p.dim}${widthCut(total === 0 ? " (0/0)" : ` (${selected + 1}/${total})`, W)}${p.reset}`;
213
+ }
214
+ /**
215
+ * The whole band: the `sessions` header (R1.5 ⑦(b) — a band names
216
+ * itself or it reads as more scrollback), at most AT_VISIBLE windowed
217
+ * rows, then the counter. Returned as plain strings for the menu-rows
218
+ * channel, which already accounts them in chromeRows — the picker needs
219
+ * no geometry of its own, which is the entire reason it rides that
220
+ * channel.
221
+ */
222
+ export function sessionPickerRows(state, W, now) {
223
+ const rows = [bandHeader("sessions", W)];
224
+ const col = idColumn(state.cards);
225
+ if (state.matches.length === 0) {
226
+ const p = palette();
227
+ rows.push(`${p.dim}${widthCut(" no session matches", W)}${p.reset}`);
228
+ rows.push(sessionCounterRow(0, 0, W));
229
+ return rows;
230
+ }
231
+ const { first, count } = atWindow(state.matches.length, state.selected, AT_VISIBLE);
232
+ for (let i = first; i < first + count; i += 1)
233
+ rows.push(sessionRow(state.matches[i], i === state.selected, W, now, col));
234
+ rows.push(sessionCounterRow(state.selected, state.matches.length, W));
235
+ return rows;
236
+ }
237
+ /** Slice ③ — the `kiso sessions` TTY row: the SAME projection, printed
238
+ * rather than picked. No selection bar (nothing is selected on a
239
+ * listing) and no leading indent: this row starts at column 1 like
240
+ * every other line a shell command prints. */
241
+ export function sessionListRow(card, W, now, idCol) {
242
+ return rowSpans(card, W, now, idCol).text;
243
+ }
244
+ /** Slice ③ — the listing's last line: the count, and the one thing the
245
+ * user can do next. */
246
+ export function sessionListFooter(count, W) {
247
+ const p = palette();
248
+ return `${p.dim}${widthCut(`${count} session${count === 1 ? "" : "s"} · kiso resume picks interactively`, W)}${p.reset}`;
249
+ }
package/dist/status.d.ts CHANGED
@@ -33,6 +33,36 @@ export declare const STATUS_GLYPHS: readonly ["▖", "▘", "▝", "▗"];
33
33
  * because it is on screen exactly when the gesture is useful.
34
34
  */
35
35
  export declare function runningStatus(glyph: string, since: number, outTokens: number | null, ctxRatio: number): string;
36
+ /**
37
+ * TUI2-R1 (E) — the idle row's meter: what the session has SPENT, next
38
+ * to what it has left.
39
+ *
40
+ * Both fields are optional and both are omitted when unknown, because
41
+ * the row's job is to be true rather than complete:
42
+ *
43
+ * - `cacheHitPct` is cacheRead / (fresh + cacheRead) — the E2
44
+ * denominator (the pinned sentence: it cannot exceed 100%). A
45
+ * session with no usage yet has no cache hit rate, and an
46
+ * unmeasured cache is NOT a 0% cache, so it renders nothing.
47
+ * - `costUsd` is the CANONICAL cost, which is null whenever the
48
+ * pricing table has no rate for the route. Null renders nothing.
49
+ * No rate table, no number — kiso does not invent a price.
50
+ */
51
+ export interface StatusMeter {
52
+ readonly cacheHitPct: number | null;
53
+ readonly costUsd: number | null;
54
+ }
36
55
  /** The IDLE row: the approval tier as the CALLER names it, the /mode
37
- * hint, the model driving the session, and the ctx estimate. */
38
- export declare function idleStatus(tier: string, model: string, ctxRatio: number): string;
56
+ * hint, the model driving the session, the TUI2-R1 meter when there is
57
+ * one, and the ctx estimate. Called without a meter — or with one that
58
+ * knows nothing — the row is byte-identical to the pre-round row. */
59
+ export declare function idleStatus(tier: string, model: string, ctxRatio: number, meter?: StatusMeter): string;
60
+ /** TUI2-R1 (E) — the cache hit rate the status row shows, from the usage
61
+ * the CLI already tracks. The denominator is the TOTAL the model was
62
+ * given (fresh + cacheRead), which is the E2 ruling's own: cacheRead
63
+ * over fresh alone once rendered 923%. No input at all → null, never a
64
+ * zero. */
65
+ export declare function cacheHitPct(usage: {
66
+ in: number | null;
67
+ cache: number | null;
68
+ }): number | null;
package/dist/status.js CHANGED
@@ -46,7 +46,28 @@ export function runningStatus(glyph, since, outTokens, ctxRatio) {
46
46
  return `${glyph} working ${seconds}s${out} · esc stop · alt+⏎ redirect · ctx left ~${ctxLeft(ctxRatio)}%`;
47
47
  }
48
48
  /** The IDLE row: the approval tier as the CALLER names it, the /mode
49
- * hint, the model driving the session, and the ctx estimate. */
50
- export function idleStatus(tier, model, ctxRatio) {
51
- return `▸ ${tier} · /mode to switch · ${model} · ctx left ~${ctxLeft(ctxRatio)}%`;
49
+ * hint, the model driving the session, the TUI2-R1 meter when there is
50
+ * one, and the ctx estimate. Called without a meter — or with one that
51
+ * knows nothing the row is byte-identical to the pre-round row. */
52
+ export function idleStatus(tier, model, ctxRatio, meter) {
53
+ const parts = [`▸ ${tier}`, "/mode to switch", model];
54
+ if (meter?.cacheHitPct != null)
55
+ parts.push(`CH ${Math.round(meter.cacheHitPct)}%`);
56
+ if (meter?.costUsd != null)
57
+ parts.push(`$${meter.costUsd.toFixed(4)}`);
58
+ parts.push(`ctx left ~${ctxLeft(ctxRatio)}%`);
59
+ return parts.join(" · ");
60
+ }
61
+ /** TUI2-R1 (E) — the cache hit rate the status row shows, from the usage
62
+ * the CLI already tracks. The denominator is the TOTAL the model was
63
+ * given (fresh + cacheRead), which is the E2 ruling's own: cacheRead
64
+ * over fresh alone once rendered 923%. No input at all → null, never a
65
+ * zero. */
66
+ export function cacheHitPct(usage) {
67
+ const fresh = usage.in;
68
+ const cached = usage.cache;
69
+ if (fresh === null || cached === null)
70
+ return null;
71
+ const total = fresh + cached;
72
+ return total > 0 ? (cached / total) * 100 : null;
52
73
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui",
3
- "version": "0.8.0",
3
+ "version": "0.10.0",
4
4
  "description": "kiso tui — the pure terminal layer (cell renderer, dock, raw editor, diff, palette). Zero runtime dependencies: input is data, output is bytes.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -35,6 +35,6 @@
35
35
  },
36
36
  "homepage": "https://github.com/vincemakes/kiso/tree/main/packages/tui#readme",
37
37
  "dependencies": {
38
- "@vincemakes/kiso-tui-cells": "0.8.0"
38
+ "@vincemakes/kiso-tui-cells": "0.10.0"
39
39
  }
40
40
  }