@vincemakes/kiso-tui 0.39.2 → 0.40.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/status.js CHANGED
@@ -22,6 +22,9 @@
22
22
  * widens to name the new gesture.
23
23
  */
24
24
  import { kUnit } from "./lines.js";
25
+ import { meterGlyphs } from "./context-ledger.js";
26
+ /** The compacting row's bar width — short: it shares a row. */
27
+ const BAR_ON_ROW = 6;
25
28
  import { elapsedLabel } from "@vincemakes/kiso-tui-cells";
26
29
  import { TWINKLE } from "@vincemakes/kiso-tui-cells/render";
27
30
  import { displayWidth } from "@vincemakes/kiso-tui-cells/width";
@@ -85,31 +88,88 @@ export function decodeRate(outputTokens, elapsedMs) {
85
88
  const rate = Math.round(outputTokens / (elapsedMs / 1000));
86
89
  return rate > 0 ? rate : null;
87
90
  }
88
- /**
89
- * The RUNNING row: the rotating glyph, the wall seconds since `since`
90
- * (never below 1 — a run that just started still reads "1s", so the row
91
- * never claims a turn took no time), the streamed output tokens once the
92
- * count is known, the interrupt hints, and the live ctx estimate.
93
- *
94
- * KC2 §2: the hint names BOTH gestures. Esc still stops; alt+⏎ redirects
95
- * — stop, and do THIS instead. The row is where the gesture is taught,
96
- * because it is on screen exactly when the gesture is useful.
97
- */
98
- export function runningStatus(glyph, since, outTokens, ctxRatio, tokPerSec = null) {
91
+ export function composeRow(head, segments, W) {
92
+ const present = segments.filter((x) => x != null && x.text !== "");
93
+ const join = (xs) => [head, ...xs.map((x) => x.text)].join(" · ");
94
+ const full = join(present);
95
+ if (W === undefined || displayWidth(full) <= W)
96
+ return full;
97
+ // 1. elide every label in its middle
98
+ let row = present.map((x) => (x.kind === "label" ? { ...x, text: elideMiddle(x.text, LABEL_ON_ROW) } : x));
99
+ if (displayWidth(join(row)) <= W)
100
+ return join(row);
101
+ // 2. drop hints from the end, one at a time
102
+ for (let i = row.length - 1; i >= 0; i -= 1) {
103
+ if (row[i].kind !== "hint")
104
+ continue;
105
+ row = [...row.slice(0, i), ...row.slice(i + 1)];
106
+ if (displayWidth(join(row)) <= W)
107
+ return join(row);
108
+ }
109
+ // 3. facts are never dropped: past this point the row is over budget,
110
+ // and it is invariant ①'s to cut — which it will do to the LAST
111
+ // segment, so the order of facts is the order of their importance.
112
+ return join(row);
113
+ }
114
+ /** `retrying 3/10 · network · 4s` — ONE fact: the attempt, the budget it
115
+ * counts against, what failed, and how long until it is tried. Whole
116
+ * seconds, rounded UP, so the row never says 0s while still waiting. */
117
+ export function retrySegment(r) {
118
+ const head = `retrying ${r.attempt}/${r.maxRetries} · ${r.code}`;
119
+ return r.remainingMs > 0 ? `${head} · ${Math.ceil(r.remainingMs / 1000)}s` : head;
120
+ }
121
+ export function runningStatus(glyph, since, outTokens, ctxRatio, tokPerSec = null, W, retry) {
99
122
  const out = outTokens !== null ? ` ↓ ${kUnit(outTokens)} tokens` : "";
100
- // TPS-1: after each call SETTLES within the turn, between the tokens
101
- // segment and the stop hint. The default is null and that is the honest
102
- // rule spelled as a default — the recovery flow has no per-call timing
103
- // state, so its row says nothing rather than guessing.
104
- const rate = tokPerSec !== null ? ` · ${tokPerSec} tok/s` : "";
105
123
  const seconds = Math.max(1, Math.round((Date.now() - since) / 1000));
106
- return `${glyph} working ${elapsedLabel(seconds)}${out}${rate} · esc stop · alt+⏎ redirect · ${ctxSegment(ctxRatio)}`;
124
+ return composeRow(`${glyph} working ${elapsedLabel(seconds)}${out}`, [
125
+ // ADR-0005 Amendment 2: a pending retry is a FACT and sits first — it
126
+ // is the one thing on the row that explains why nothing is arriving,
127
+ // and a retry budget of minutes with nothing on screen reads as a
128
+ // hung session.
129
+ retry != null ? { kind: "fact", text: retrySegment(retry) } : null,
130
+ // TPS-1: after each call SETTLES within the turn, between the tokens
131
+ // segment and the stop hint. The default is null and that is the
132
+ // honest rule spelled as a default — the recovery flow has no
133
+ // per-call timing state, so its row says nothing rather than guessing.
134
+ tokPerSec !== null ? { kind: "fact", text: `${tokPerSec} tok/s` } : null,
135
+ { kind: "hint", text: "esc stop" },
136
+ { kind: "hint", text: "alt+⏎ redirect" },
137
+ { kind: "fact", text: ctxSegment(ctxRatio) },
138
+ ], W);
139
+ }
140
+ /**
141
+ * The COMPACTING row (W18): the covered rounds, the pre-call token
142
+ * estimate, and the elapsed seconds — all knowable before the one summary
143
+ * call returns, which has no fraction of its own. Moved here from an
144
+ * inline template in dispatch (0.40.0) so it composes like every other
145
+ * row and has a place for what the launch build adds to it.
146
+ */
147
+ export function compactingStatus(glyph, rounds, tokens, elapsedSeconds, W, retry, progress) {
148
+ // 0.40.0: with a budget to measure against, the covered size and the bar
149
+ // are ONE fact — what went in, and how much of the output budget has
150
+ // come out. Without a budget the row keeps the covered size alone: the
151
+ // bar never invents a denominator.
152
+ const covered = progress != null && progress.budget !== null && progress.budget > 0
153
+ ? `~${kUnit(tokens)} \u2192 ${meterGlyphs(progress.produced / progress.budget, BAR_ON_ROW)} ${kUnit(progress.produced)}/${kUnit(progress.budget)}`
154
+ : `~${kUnit(tokens)} tokens`;
155
+ return composeRow(`${glyph} compacting`, [
156
+ { kind: "fact", text: `${rounds} rounds` },
157
+ { kind: "fact", text: covered },
158
+ // why the figure jumped when the usage landed — a HINT, so a narrow
159
+ // row gives it up before the bar, the seconds or the retry
160
+ progress?.reasoningUnseen === true && progress.budget !== null ? { kind: "hint", text: "incl. unstreamed reasoning" } : null,
161
+ { kind: "fact", text: `${Math.max(0, elapsedSeconds)}s` },
162
+ // ADR-0005 Amendment 2: the summary call retries under the kernel's
163
+ // policy, and a retry here is the same fact it is on the running row.
164
+ retry != null ? { kind: "fact", text: retrySegment(retry) } : null,
165
+ ], W);
107
166
  }
108
- /** DF-0330-F1 — how far the model id may be squeezed on the ROW. Twenty
109
- * visible columns keeps a head and a tail: `deepseek-v…s-on-0910` still
110
- * says which binding is driving, and the tail is where the parts that
111
- * distinguish one id from its neighbours live (`-flash`, `-0910`). */
112
- const MODEL_ON_ROW = 20;
167
+ /** DF-0330-F1 — how far a LABEL may be squeezed on the ROW; the model id
168
+ * is the one there is. Twenty visible columns keeps a head and a tail:
169
+ * `deepseek-v…s-on-0910` still says which binding is driving, and the
170
+ * tail is where the parts that distinguish one id from its neighbours
171
+ * live (`-flash`, `-0910`). Read by `composeRow`. */
172
+ const LABEL_ON_ROW = 20;
113
173
  /** Elide in the MIDDLE, keeping the head and the tail. A string already
114
174
  * within budget is returned untouched, so this is a no-op for every
115
175
  * ordinary model name.
@@ -169,27 +229,19 @@ function elideMiddle(text, max) {
169
229
  * No `W` means no dropping, which is what the callers that do not know
170
230
  * their width should get: today's row, unchanged.
171
231
  */
172
- export function idleStatus(tier, model, ctxRatio, meter, W) {
173
- const compose = (label, hint) => {
174
- const parts = [`▸ ${tier}`];
175
- if (hint)
176
- parts.push("/mode to switch");
177
- parts.push(label);
178
- if (meter?.cacheHitPct != null)
179
- parts.push(`CH ${Math.round(meter.cacheHitPct)}%`);
232
+ export function idleStatus(tier, model, ctxRatio, meter, W, floorOff = false) {
233
+ return composeRow(`▸ ${tier}`, [
234
+ // 0.40.0: the catastrophe floor is on by default and says nothing;
235
+ // OFF is the state worth seeing, and a fact beside the tier it
236
+ // changes the meaning of.
237
+ floorOff ? { kind: "fact", text: "floor off" } : null,
238
+ { kind: "hint", text: "/mode to switch" },
239
+ { kind: "label", text: model },
240
+ meter?.cacheHitPct != null ? { kind: "fact", text: `CH ${Math.round(meter.cacheHitPct)}%` } : null,
180
241
  // costUsd deliberately NOT rendered — see StatusMeter.costUsd.
181
- parts.push(ctxSegment(ctxRatio));
182
- if (meter?.tokPerSec != null)
183
- parts.push(`${meter.tokPerSec} tok/s`); // TPS-1: last, after the ctx estimate
184
- return parts.join(" · ");
185
- };
186
- const full = compose(model, true);
187
- if (W === undefined || displayWidth(full) <= W)
188
- return full;
189
- const squeezed = compose(elideMiddle(model, MODEL_ON_ROW), true);
190
- if (displayWidth(squeezed) <= W)
191
- return squeezed;
192
- return compose(elideMiddle(model, MODEL_ON_ROW), false);
242
+ { kind: "fact", text: ctxSegment(ctxRatio) },
243
+ meter?.tokPerSec != null ? { kind: "fact", text: `${meter.tokPerSec} tok/s` } : null, // TPS-1: last, after the ctx estimate
244
+ ], W);
193
245
  }
194
246
  /** TUI2-R1 (E) — the cache hit rate the status row shows, from the usage
195
247
  * 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.39.2",
3
+ "version": "0.40.0",
4
4
  "description": "kiso tui \u2014 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.39.2"
38
+ "@vincemakes/kiso-tui-cells": "0.40.0"
39
39
  }
40
40
  }