@vincemakes/kiso-tui 0.32.2 → 0.33.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.d.ts CHANGED
@@ -12,7 +12,7 @@ export { Container, foldLine, foldWords, visibleWidth, SPINNER, type Component,
12
12
  export { Editor, MENU_ITEMS, PROMPT, PROMPT_WIDTH, displayWidth, charWidth, widthOf, type MenuItem, } from "./editor.js";
13
13
  export { bannerLines, COLOR_OFF, COLOR_ON, currentGround, setGround, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderResumeList, renderSessionLine, renderStatusLine, relativeTime, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, type Palette, type PathResolver, type RecapStats, type ResumeMeta, type RenderInput, type RenderResult, type RunUsage, } from "./lines.js";
14
14
  export { editFileDiff, truncateDiff, writeFileDiff, type DiffLine, type DiffResult } from "./diff.js";
15
- export { STATUS_GLYPHS, cacheHitPct, idleStatus, runningStatus, type StatusMeter } from "./status.js";
15
+ export { STATUS_GLYPHS, cacheHitPct, decodeRate, idleStatus, runningStatus, type StatusMeter } from "./status.js";
16
16
  export { contextRows, contextUnavailableRows, type ContextLedger } from "./context-ledger.js";
17
17
  export { interactivePrompt, projectTrustRows, projectTrustView, projectUntrustedNote, uncertainView, verifyOfferView, type TrustArtifact } from "./strings.js";
18
18
  export { AT_CAP, AT_SKIP, AT_VISIBLE, atEmbed, atFilter, atPanelRows, atWindow, bandHeader, longestRun, type AtItem, type AtMatch } from "./at-picker.js";
package/dist/index.js CHANGED
@@ -19,7 +19,7 @@ export { bannerLines, COLOR_OFF, COLOR_ON, currentGround, setGround, escapeTermi
19
19
  export { editFileDiff, truncateDiff, writeFileDiff } from "./diff.js";
20
20
  // KC2 §5: the status rows' formatters — the CLI keeps the state and the
21
21
  // repaint, the terminal layer owns what the row says.
22
- export { STATUS_GLYPHS, cacheHitPct, idleStatus, runningStatus } from "./status.js";
22
+ export { STATUS_GLYPHS, cacheHitPct, decodeRate, idleStatus, runningStatus } from "./status.js";
23
23
  // TUI2-R1 (E): /context's attribution rows — a pure function of the
24
24
  // counts the trace sidecar already records (the CLI reads, this renders).
25
25
  export { contextRows, contextUnavailableRows } from "./context-ledger.js";
package/dist/status.d.ts CHANGED
@@ -36,6 +36,25 @@
36
36
  * Glyphs only, no colour: intact under NO_COLOR and on any ground.
37
37
  */
38
38
  export declare const STATUS_GLYPHS: readonly ["✧", "✦", "✶", "✸", "✺", "✸", "✦"];
39
+ /**
40
+ * TPS-1 — the settled DECODE rate of one model call: its output tokens
41
+ * over the seconds from the call's first streamed event to its usage
42
+ * event. TTFT is excluded on purpose; this is the speed of the text
43
+ * arriving, which is what "tokens per second" means to the person
44
+ * watching it.
45
+ *
46
+ * The null rule, and every branch of it is the same principle: a number
47
+ * on this row is a MEASUREMENT or it is absent. No output count (the
48
+ * provider reported no usage) is not a zero. Under half a second of
49
+ * decoding is a sample too short to divide by. A non-positive elapsed is
50
+ * a clock, not a rate. And a call that decoded NOTHING has no rate to
51
+ * report: `0 tok/s` would read as a measured speed and it is not one — it
52
+ * is the absence of output, which the transcript already shows. The
53
+ * condition is on the RENDERED INTEGER rather than on the token count, so
54
+ * a slow trickle that rounds to the same `0` reaches the same absence for
55
+ * the same reason.
56
+ */
57
+ export declare function decodeRate(outputTokens: number | null, elapsedMs: number): number | null;
39
58
  /**
40
59
  * The RUNNING row: the rotating glyph, the wall seconds since `since`
41
60
  * (never below 1 — a run that just started still reads "1s", so the row
@@ -46,7 +65,7 @@ export declare const STATUS_GLYPHS: readonly ["✧", "✦", "✶", "✸", "✺",
46
65
  * — stop, and do THIS instead. The row is where the gesture is taught,
47
66
  * because it is on screen exactly when the gesture is useful.
48
67
  */
49
- export declare function runningStatus(glyph: string, since: number, outTokens: number | null, ctxRatio: number): string;
68
+ export declare function runningStatus(glyph: string, since: number, outTokens: number | null, ctxRatio: number, tokPerSec?: number | null): string;
50
69
  /**
51
70
  * TUI2-R1 (E) — the idle row's meter: what the session has SPENT, next
52
71
  * to what it has left.
@@ -71,12 +90,42 @@ export interface StatusMeter {
71
90
  * (trace ledger, /context); the field is kept so callers need not
72
91
  * change shape, and it renders NOTHING. */
73
92
  readonly costUsd: number | null;
93
+ /** TPS-1 — the decode rate of the LAST settled call, carried into the
94
+ * idle row so the figure a person watched during the turn is still
95
+ * there when the turn ends. Null renders nothing (see `decodeRate`);
96
+ * a new model binding starts with none, exactly as the cache figure
97
+ * does (DF-0311-F1: an unmeasured binding has no measurement). */
98
+ readonly tokPerSec: number | null;
74
99
  }
75
- /** The IDLE row: the approval tier as the CALLER names it, the /mode
76
- * hint, the model driving the session, the TUI2-R1 meter when there is
77
- * one, and the ctx estimate. Called without a meter — or with one that
78
- * knows nothing — the row is byte-identical to the pre-round row. */
79
- export declare function idleStatus(tier: string, model: string, ctxRatio: number, meter?: StatusMeter): string;
100
+ /**
101
+ * The IDLE row: the approval tier as the CALLER names it, the /mode hint,
102
+ * the model driving the session, the TUI2-R1 meter when there is one, and
103
+ * the ctx estimate. Called without a meter — or with one that knows
104
+ * nothing — the row is byte-identical to the pre-round row.
105
+ *
106
+ * DF-0330-F1 — THE DROP ORDER. `W` is the row's budget; given one, the row
107
+ * gives ground in a fixed order rather than letting invariant ① cut its
108
+ * end off. Found the hard way: at 100 columns the ` · N tok/s` segment
109
+ * never appeared and at 140 it did, because the row was 102 columns wide
110
+ * and the segment TPS-1 added sat last.
111
+ *
112
+ * 1. the MODEL ID is elided in its middle. It is the only segment that
113
+ * varies, it is the one that grew (the owner's is 35 columns of a
114
+ * 90-column row), and eliding it gives the row back a budget instead
115
+ * of re-allocating a deficit;
116
+ * 2. `/mode to switch` is dropped. It teaches a gesture; `/mode` and `?`
117
+ * still exist and the row is not the only place they are taught;
118
+ * 3. the FACTS are never dropped and never cut — the tier, CH, the ctx
119
+ * estimate and the rate. A row that silently drops a measurement is
120
+ * the defect this rule exists to prevent.
121
+ *
122
+ * The elision is ON THE ROW only. `/model`, the session log and the trace
123
+ * ledger all keep the id whole — the row is a view, never the record.
124
+ *
125
+ * No `W` means no dropping, which is what the callers that do not know
126
+ * their width should get: today's row, unchanged.
127
+ */
128
+ export declare function idleStatus(tier: string, model: string, ctxRatio: number, meter?: StatusMeter, W?: number): string;
80
129
  /** TUI2-R1 (E) — the cache hit rate the status row shows, from the usage
81
130
  * the CLI already tracks. The denominator is the TOTAL the model was
82
131
  * given (fresh + cacheRead), which is the E2 ruling's own: cacheRead
package/dist/status.js CHANGED
@@ -23,6 +23,7 @@
23
23
  */
24
24
  import { kUnit } from "./lines.js";
25
25
  import { TWINKLE } from "@vincemakes/kiso-tui-cells/render";
26
+ import { displayWidth } from "@vincemakes/kiso-tui-cells/width";
26
27
  /**
27
28
  * R3 (design §5.2) — the working glyph family is the TWINKLE, and the
28
29
  * CLI's 200ms spinner walks it exactly as it walked the four quadrant
@@ -45,6 +46,32 @@ export const STATUS_GLYPHS = TWINKLE;
45
46
  function ctxLeft(ratio) {
46
47
  return Number.isFinite(ratio) ? Math.round((1 - ratio) * 100) : null;
47
48
  }
49
+ /**
50
+ * TPS-1 — the settled DECODE rate of one model call: its output tokens
51
+ * over the seconds from the call's first streamed event to its usage
52
+ * event. TTFT is excluded on purpose; this is the speed of the text
53
+ * arriving, which is what "tokens per second" means to the person
54
+ * watching it.
55
+ *
56
+ * The null rule, and every branch of it is the same principle: a number
57
+ * on this row is a MEASUREMENT or it is absent. No output count (the
58
+ * provider reported no usage) is not a zero. Under half a second of
59
+ * decoding is a sample too short to divide by. A non-positive elapsed is
60
+ * a clock, not a rate. And a call that decoded NOTHING has no rate to
61
+ * report: `0 tok/s` would read as a measured speed and it is not one — it
62
+ * is the absence of output, which the transcript already shows. The
63
+ * condition is on the RENDERED INTEGER rather than on the token count, so
64
+ * a slow trickle that rounds to the same `0` reaches the same absence for
65
+ * the same reason.
66
+ */
67
+ export function decodeRate(outputTokens, elapsedMs) {
68
+ if (outputTokens === null)
69
+ return null;
70
+ if (!Number.isFinite(elapsedMs) || elapsedMs < 500)
71
+ return null;
72
+ const rate = Math.round(outputTokens / (elapsedMs / 1000));
73
+ return rate > 0 ? rate : null;
74
+ }
48
75
  /**
49
76
  * The RUNNING row: the rotating glyph, the wall seconds since `since`
50
77
  * (never below 1 — a run that just started still reads "1s", so the row
@@ -55,22 +82,101 @@ function ctxLeft(ratio) {
55
82
  * — stop, and do THIS instead. The row is where the gesture is taught,
56
83
  * because it is on screen exactly when the gesture is useful.
57
84
  */
58
- export function runningStatus(glyph, since, outTokens, ctxRatio) {
85
+ export function runningStatus(glyph, since, outTokens, ctxRatio, tokPerSec = null) {
59
86
  const out = outTokens !== null ? ` ↓ ${kUnit(outTokens)} tokens` : "";
87
+ // TPS-1: after each call SETTLES within the turn, between the tokens
88
+ // segment and the stop hint. The default is null and that is the honest
89
+ // rule spelled as a default — the recovery flow has no per-call timing
90
+ // state, so its row says nothing rather than guessing.
91
+ const rate = tokPerSec !== null ? ` · ${tokPerSec} tok/s` : "";
60
92
  const seconds = Math.max(1, Math.round((Date.now() - since) / 1000));
61
- return `${glyph} working ${seconds}s${out} · esc stop · alt+⏎ redirect · ctx left ~${ctxLeft(ctxRatio)}%`;
93
+ return `${glyph} working ${seconds}s${out}${rate} · esc stop · alt+⏎ redirect · ctx left ~${ctxLeft(ctxRatio)}%`;
94
+ }
95
+ /** DF-0330-F1 — how far the model id may be squeezed on the ROW. Twenty
96
+ * visible columns keeps a head and a tail: `deepseek-v…s-on-0910` still
97
+ * says which binding is driving, and the tail is where the parts that
98
+ * distinguish one id from its neighbours live (`-flash`, `-0910`). */
99
+ const MODEL_ON_ROW = 20;
100
+ /** Elide in the MIDDLE, keeping the head and the tail. A string already
101
+ * within budget is returned untouched, so this is a no-op for every
102
+ * ordinary model name.
103
+ *
104
+ * Budgeted in DISPLAY COLUMNS, not code points. A first version sliced
105
+ * code points and a wide-character id came out at 28 columns while the
106
+ * function claimed 20 — which would have put the row straight back over
107
+ * its budget and handed it to invariant ①'s cut, the exact defect this
108
+ * whole change exists to prevent. No such model id exists today; the
109
+ * guarantee should not depend on that staying true. */
110
+ function elideMiddle(text, max) {
111
+ if (displayWidth(text) <= max)
112
+ return text;
113
+ const chars = [...text];
114
+ const room = max - 1; // the ellipsis costs one column
115
+ const headBudget = Math.ceil(room / 2);
116
+ let head = "";
117
+ for (const c of chars) {
118
+ if (displayWidth(head + c) > headBudget)
119
+ break;
120
+ head += c;
121
+ }
122
+ let tail = "";
123
+ const tailBudget = room - displayWidth(head);
124
+ for (let i = chars.length - 1; i >= 0; i -= 1) {
125
+ if (displayWidth(chars[i] + tail) > tailBudget)
126
+ break;
127
+ tail = chars[i] + tail;
128
+ }
129
+ return `${head}…${tail}`;
62
130
  }
63
- /** The IDLE row: the approval tier as the CALLER names it, the /mode
64
- * hint, the model driving the session, the TUI2-R1 meter when there is
65
- * one, and the ctx estimate. Called without a meter — or with one that
66
- * knows nothing — the row is byte-identical to the pre-round row. */
67
- export function idleStatus(tier, model, ctxRatio, meter) {
68
- const parts = [`▸ ${tier}`, "/mode to switch", model];
69
- if (meter?.cacheHitPct != null)
70
- parts.push(`CH ${Math.round(meter.cacheHitPct)}%`);
71
- // costUsd deliberately NOT rendered — see StatusMeter.costUsd.
72
- parts.push(`ctx left ~${ctxLeft(ctxRatio)}%`);
73
- return parts.join(" · ");
131
+ /**
132
+ * The IDLE row: the approval tier as the CALLER names it, the /mode hint,
133
+ * the model driving the session, the TUI2-R1 meter when there is one, and
134
+ * the ctx estimate. Called without a meter — or with one that knows
135
+ * nothing — the row is byte-identical to the pre-round row.
136
+ *
137
+ * DF-0330-F1 — THE DROP ORDER. `W` is the row's budget; given one, the row
138
+ * gives ground in a fixed order rather than letting invariant ① cut its
139
+ * end off. Found the hard way: at 100 columns the ` · N tok/s` segment
140
+ * never appeared and at 140 it did, because the row was 102 columns wide
141
+ * and the segment TPS-1 added sat last.
142
+ *
143
+ * 1. the MODEL ID is elided in its middle. It is the only segment that
144
+ * varies, it is the one that grew (the owner's is 35 columns of a
145
+ * 90-column row), and eliding it gives the row back a budget instead
146
+ * of re-allocating a deficit;
147
+ * 2. `/mode to switch` is dropped. It teaches a gesture; `/mode` and `?`
148
+ * still exist and the row is not the only place they are taught;
149
+ * 3. the FACTS are never dropped and never cut — the tier, CH, the ctx
150
+ * estimate and the rate. A row that silently drops a measurement is
151
+ * the defect this rule exists to prevent.
152
+ *
153
+ * The elision is ON THE ROW only. `/model`, the session log and the trace
154
+ * ledger all keep the id whole — the row is a view, never the record.
155
+ *
156
+ * No `W` means no dropping, which is what the callers that do not know
157
+ * their width should get: today's row, unchanged.
158
+ */
159
+ export function idleStatus(tier, model, ctxRatio, meter, W) {
160
+ const compose = (label, hint) => {
161
+ const parts = [`▸ ${tier}`];
162
+ if (hint)
163
+ parts.push("/mode to switch");
164
+ parts.push(label);
165
+ if (meter?.cacheHitPct != null)
166
+ parts.push(`CH ${Math.round(meter.cacheHitPct)}%`);
167
+ // costUsd deliberately NOT rendered — see StatusMeter.costUsd.
168
+ parts.push(`ctx left ~${ctxLeft(ctxRatio)}%`);
169
+ if (meter?.tokPerSec != null)
170
+ parts.push(`${meter.tokPerSec} tok/s`); // TPS-1: last, after the ctx estimate
171
+ return parts.join(" · ");
172
+ };
173
+ const full = compose(model, true);
174
+ if (W === undefined || displayWidth(full) <= W)
175
+ return full;
176
+ const squeezed = compose(elideMiddle(model, MODEL_ON_ROW), true);
177
+ if (displayWidth(squeezed) <= W)
178
+ return squeezed;
179
+ return compose(elideMiddle(model, MODEL_ON_ROW), false);
74
180
  }
75
181
  /** TUI2-R1 (E) — the cache hit rate the status row shows, from the usage
76
182
  * the CLI already tracks. The denominator is the TOTAL the model was
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vincemakes/kiso-tui",
3
- "version": "0.32.2",
3
+ "version": "0.33.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.32.2"
38
+ "@vincemakes/kiso-tui-cells": "0.33.0"
39
39
  }
40
40
  }