@vincemakes/kiso-tui 0.46.1 → 0.47.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.
@@ -17,6 +17,8 @@
17
17
  * Every number is a count the ledger already carries. Nothing here
18
18
  * estimates, projects, or predicts.
19
19
  */
20
+ import { SHEET_CLOSE, bandHeader } from "@vincemakes/kiso-tui-cells/strings";
21
+ import { cutLine } from "@vincemakes/kiso-tui-cells/render";
20
22
  import { palette } from "./lines.js";
21
23
  const BAR_CELLS = 12;
22
24
  /** k-units for the ledger's columns: 25700 → 25.7k, 300 → 300, 11 → 11.
@@ -94,3 +96,63 @@ export function contextUnavailableRows(reason) {
94
96
  const p = palette();
95
97
  return [`${p.bold}context — no ledger yet${p.reset}`, ` ${p.dim}${reason}${p.reset}`];
96
98
  }
99
+ /** §8.9 — the meter's cells, ONE rule for the status bar's ctx meter and
100
+ * the `/context` sheet (the sheets round, owner 2026-10-06: the two
101
+ * meters cannot disagree). Ten `▆` cells filled to the percentage SHOWN
102
+ * (`0%` is empty; from 1% at least one cell), `ink2` below the soft
103
+ * compaction tier, gold to the hard one, the failure colour past it; the
104
+ * empty cells `track`. "" off a known ground: the colours carry nothing
105
+ * there, and the percentage says it alone. `used`, `soft` and `hard`
106
+ * are shares of the window. */
107
+ export const METER_CELLS = 10;
108
+ export function meterCells(used, soft, hard) {
109
+ const p = palette();
110
+ if (p.track === "")
111
+ return "";
112
+ const shown = Math.round(Math.max(0, used) * 100);
113
+ const filled = shown <= 0 ? 0 : Math.min(METER_CELLS, Math.max(1, Math.round(shown / 10)));
114
+ const tone = used >= hard ? p.fail : used >= soft ? p.gold : p.ink2;
115
+ return `${tone}${"\u2586".repeat(filled)}${p.track}${"\u2586".repeat(METER_CELLS - filled)}${p.fgEnd}`;
116
+ }
117
+ /**
118
+ * The sheets round (owner, 2026-10-06) — `/context` on a dock is a sheet
119
+ * over the input, the shape `/status` has (§8.16): the band names the
120
+ * total (`context · 2.7k of 200k · 1%`), the bar's meter with when
121
+ * compaction happens beside it, the surfaces as a table — the label, the
122
+ * count right-aligned, the detail dim — the free remainder dim, and the
123
+ * closing row. No blank rows: the printed form spent a cell per row and
124
+ * came out double-spaced. On a pipe `/context` prints `contextRows`,
125
+ * unchanged. `tiers` are the compaction thresholds as shares of the
126
+ * window, or null when the window is not known.
127
+ */
128
+ export function contextSheetRows(ledger, tiers, W) {
129
+ const p = palette();
130
+ const used = ledger.systemPrompt + ledger.toolTable + ledger.skillsIndex + ledger.envelope + ledger.messages;
131
+ const free = Math.max(0, ledger.window - used);
132
+ const ratio = ledger.window > 0 ? Math.min(1, used / ledger.window) : 1;
133
+ const rows = [bandHeader(`context \u00b7 ${k(used)} of ${k(ledger.window)} \u00b7 ${Math.round(ratio * 100)}%`, W)];
134
+ const cells = meterCells(ratio, tiers?.soft ?? Number.POSITIVE_INFINITY, tiers?.hard ?? Number.POSITIVE_INFINITY);
135
+ const when = tiers === null ? "" : `compaction past ${Math.round(tiers.soft * 100)}% at a phase end, past ${Math.round(tiers.hard * 100)}% at once`;
136
+ if (cells !== "" || when !== "")
137
+ rows.push(` ${cells}${cells !== "" && when !== "" ? " " : ""}${when === "" ? "" : `${p.dim}${when}${p.reset}`}`);
138
+ const row = (label, value, detail) => ` ${label.padEnd(15)}${k(value).padStart(6)}${detail === "" ? "" : ` ${p.dim}${detail}${p.reset}`}`;
139
+ if (ledger.systemPrompt > 0)
140
+ rows.push(row("system prompt", ledger.systemPrompt, `base ${k(ledger.systemBase)}${ledger.appends > 0 ? ` + ${ledger.appends} extension append${ledger.appends === 1 ? "" : "s"}` : ""}`));
141
+ if (ledger.toolTable > 0)
142
+ rows.push(row("tool table", ledger.toolTable, `${ledger.tools} tool${ledger.tools === 1 ? "" : "s"}`));
143
+ if (ledger.skillsIndex > 0)
144
+ rows.push(row("skills index", ledger.skillsIndex, `${ledger.skills > 0 ? `${ledger.skills} skill${ledger.skills === 1 ? "" : "s"}, ` : ""}tier-1 lines only`));
145
+ if (ledger.envelope > 0)
146
+ rows.push(row("envelope", ledger.envelope, ""));
147
+ if (ledger.messages > 0)
148
+ rows.push(row("messages", ledger.messages, `${ledger.turns} turn${ledger.turns === 1 ? "" : "s"}`));
149
+ rows.push(` ${p.dim}${"free".padEnd(15)}${k(free).padStart(6)}${p.reset}`);
150
+ rows.push(` ${p.dim}${SHEET_CLOSE}${p.reset}`);
151
+ return rows.map((r) => cutLine(r, W));
152
+ }
153
+ /** The sheets round — `/context` before any request, as a sheet: the band
154
+ * says there is no ledger yet, the row says what produces one. */
155
+ export function contextUnavailableSheetRows(reason, W) {
156
+ const p = palette();
157
+ return [bandHeader("context \u00b7 no ledger yet", W), ` ${p.dim}${reason}${p.reset}`, ` ${p.dim}${SHEET_CLOSE}${p.reset}`].map((r) => cutLine(r, W));
158
+ }
package/dist/editor.d.ts CHANGED
@@ -142,6 +142,17 @@ export declare class Editor {
142
142
  /** TUI2-R1 (D): whether the keys sheet is up — the compositor's slot
143
143
  * read (bound like the menu and the picker). */
144
144
  sheetOpen(): boolean;
145
+ /** Graphite R3e: what the sheet shows — false when it is closed, true for
146
+ * the keys sheet, the caller's rows otherwise. */
147
+ sheetContent(): boolean | ((W: number) => string[]);
148
+ /** Graphite R3e: open a read-only sheet over the input with the caller's
149
+ * rows (`/status`, `/context`, `/skills`). Any key closes it, and what
150
+ * is typed goes to the input; esc is eaten. */
151
+ openSheet(rows: (W: number) => string[]): void;
152
+ /** The sheets round (owner, 2026-10-06): `/help` opens the command
153
+ * list — the band a typed `/` opens, filtered as the person types. On
154
+ * an empty composer only: a line already being written is theirs. */
155
+ openCommands(): void;
145
156
  clearLine(): void;
146
157
  /** The dock's input-row state — ADDITIVE (§5): `line` + `cursor` keep
147
158
  * their legacy meaning (the CURSOR LINE's visible slice and the
@@ -165,6 +176,9 @@ export declare class Editor {
165
176
  menuState(): {
166
177
  items: readonly MenuItem[];
167
178
  selected: number;
179
+ total?: number;
180
+ query?: string;
181
+ nameCol?: number;
168
182
  } | null;
169
183
  /** 0.40.1 — bind the menu's extra entries (the CLI binds the installed
170
184
  * skills). A function, read per keystroke, never a snapshot. */
@@ -182,6 +196,9 @@ export declare class Editor {
182
196
  matches: readonly AtMatch[];
183
197
  selected: number;
184
198
  capped: boolean;
199
+ query: string;
200
+ total: number;
201
+ nameCol: number;
185
202
  } | null;
186
203
  /** Open the picker on a bound card source (PickInput, S5). */
187
204
  beginPick(cards: () => readonly SessionCardView[], onPick: (id: string | null) => void, here?: string): void;
package/dist/editor.js CHANGED
@@ -31,7 +31,7 @@ import {} from "./approval-panel.js";
31
31
  // KC3.5: the panel-slot dispatchers — the ask branch folded into the
32
32
  // W21 lead/rows, so this file keeps ONE panel and one key owner.
33
33
  import { panelLead } from "./ask-panel.js";
34
- import { AT_VISIBLE, atFilter } from "./at-picker.js";
34
+ import { AT_CAP, atFilter, atNameWidth, bandVisible } from "./at-picker.js";
35
35
  // TUI2-R2 ②: the session picker — the band's THIRD occupant. Its filter
36
36
  // is the @ picker's rank aimed at the session id; the editor owns the
37
37
  // keys, the compositor draws the rows.
@@ -137,6 +137,8 @@ export const MENU_ITEMS = [
137
137
  // never be re-wrapped in place (ADR-0046); this appends it re-folded.
138
138
  { name: "/rewrap", desc: "re-print the recent prose at the current width" },
139
139
  { name: "/status", desc: "show session id, event count, and context estimate" },
140
+ // Graphite R3d: the session's own name (the title, the /resume row)
141
+ { name: "/name", desc: "name this session · /name shows it · /name - clears it" },
140
142
  { name: "/settings", desc: "show the settings in force, each value's source, and how to change it" },
141
143
  // TUI2-R1 (E): the rent-ledger attribution — where the context went
142
144
  { name: "/context", desc: "show where the context went — the last request's rent ledger" },
@@ -374,6 +376,9 @@ export class Editor {
374
376
  * exists for interactions (a lead, a status, a reducer, a stashed
375
377
  * buffer), and the sheet has no interaction to speak of. */
376
378
  #sheetOpen = false;
379
+ /** Graphite R3e: a sheet with the CALLER's rows (`/status`), null for the
380
+ * keys sheet. Any key closes either. */
381
+ #sheetRows = null;
377
382
  #menuOpen = false; // v3 §04: the slash-command menu
378
383
  /** 0.40.1: the menu's extra entries (the installed skills), read LIVE on
379
384
  * every keystroke — a skill added by /reload appears without rebinding. */
@@ -394,6 +399,9 @@ export class Editor {
394
399
  // bit with no token under the cursor is inert by construction — the
395
400
  // next open re-snapshots, so a stale list can never be shown.
396
401
  #atList = null;
402
+ /** Graphite P2: the widest file name in the list, in cells — measured
403
+ * once per open, as the list is listed */
404
+ #atNameCol = 0;
397
405
  #atItems = null;
398
406
  // TUI2-R2 ② — the session picker. Two fields: the bound source (its
399
407
  // presence IS "the picker is up") and the selection. The query, like
@@ -618,6 +626,30 @@ export class Editor {
618
626
  sheetOpen() {
619
627
  return this.#sheetOpen;
620
628
  }
629
+ /** Graphite R3e: what the sheet shows — false when it is closed, true for
630
+ * the keys sheet, the caller's rows otherwise. */
631
+ sheetContent() {
632
+ return !this.#sheetOpen ? false : (this.#sheetRows ?? true);
633
+ }
634
+ /** Graphite R3e: open a read-only sheet over the input with the caller's
635
+ * rows (`/status`, `/context`, `/skills`). Any key closes it, and what
636
+ * is typed goes to the input; esc is eaten. */
637
+ openSheet(rows) {
638
+ this.#sheetRows = rows;
639
+ this.#sheetOpen = true;
640
+ this.#onRender();
641
+ }
642
+ /** The sheets round (owner, 2026-10-06): `/help` opens the command
643
+ * list — the band a typed `/` opens, filtered as the person types. On
644
+ * an empty composer only: a line already being written is theirs. */
645
+ openCommands() {
646
+ if (this.#chars.length > 0)
647
+ return;
648
+ this.#chars = ["/".codePointAt(0)];
649
+ this.#cursor = 1;
650
+ this.#verticalGoalCol = null;
651
+ this.#refreshMenu();
652
+ }
621
653
  clearLine() {
622
654
  this.#undoStack.length = 0; // UD-1
623
655
  this.#redoStack.length = 0;
@@ -783,7 +815,7 @@ export class Editor {
783
815
  * the frame's clamp is the authority. */
784
816
  #visibleRows(lineCount) {
785
817
  const H = process.stdout.rows ?? 24;
786
- const bands = (this.#menuOpen ? this.#menuFiltered().length : 0) + this.#atRows() + this.#pickInput.rows() + this.#queueState().length;
818
+ const bands = (this.#menuOpen ? Math.min(this.#menuFiltered().length, bandVisible(H)) + 2 : 0) + this.#atRows() + this.#pickInput.rows(H) + this.#queueState().length;
787
819
  return Math.max(1, Math.min(lineCount, N_MAX, Math.max(1, H - 3 - bands)));
788
820
  }
789
821
  /** The dock's input-row state — ADDITIVE (§5): `line` + `cursor` keep
@@ -825,7 +857,16 @@ export class Editor {
825
857
  menuState() {
826
858
  if (!this.#menuOpen)
827
859
  return null;
828
- return { items: this.#menuFiltered(), selected: this.#menuSel };
860
+ // Graphite P2: the whole list's size and its widest name (the column
861
+ // never moves while the person types), and the typed prefix
862
+ const all = this.#menuAll();
863
+ return { items: this.#menuFiltered(), selected: this.#menuSel, total: all.length, query: this.line().slice(1), nameCol: all.reduce((n, m) => Math.max(n, m.name.length - 1), 0) };
864
+ }
865
+ /** Every entry the menu can show: the built-ins, then the extras that do
866
+ * not shadow one (0.40.1's rule, shared with #menuFiltered). */
867
+ #menuAll() {
868
+ const builtins = new Set(MENU_ITEMS.map((m) => m.name));
869
+ return [...MENU_ITEMS, ...this.#menuExtras().filter((m) => !builtins.has(m.name))];
829
870
  }
830
871
  /** v3 §04: the filtered command list for the current buffer.
831
872
  *
@@ -845,9 +886,13 @@ export class Editor {
845
886
  // 0.40.1: the built-ins first, then the extras (skills) — and a
846
887
  // built-in WINS a shared name, so a skill named like a command is
847
888
  // never listed twice and never shadows it (the dispatcher's rule)
848
- const builtins = new Set(MENU_ITEMS.map((m) => m.name));
849
- const all = [...MENU_ITEMS, ...this.#menuExtras().filter((m) => !builtins.has(m.name))];
850
- return all.filter((m) => m.name.startsWith(line));
889
+ const matches = this.#menuAll().filter((m) => m.name.startsWith(line));
890
+ // the last sweep (owner, 2026-10-06): a line that IS a command's name
891
+ // lists that command first, so ⏎ runs it. `/skill` listed `/skills`
892
+ // first (the table's order) and ⏎ completed the line to it — the
893
+ // command typed in full never ran. The rest keep the table's order.
894
+ const exact = matches.findIndex((m) => m.name === line);
895
+ return exact <= 0 ? matches : [matches[exact], ...matches.slice(0, exact), ...matches.slice(exact + 1)];
851
896
  }
852
897
  /** 0.40.1 — bind the menu's extra entries (the CLI binds the installed
853
898
  * skills). A function, read per keystroke, never a snapshot. */
@@ -936,7 +981,10 @@ export class Editor {
936
981
  const view = this.#atView();
937
982
  if (view === null)
938
983
  return null;
939
- return { matches: view.matches, selected: view.selected, capped: view.capped };
984
+ // Graphite P2: the band counts the whole list and lays its name column
985
+ // over it, so the folder column never moves while the person types
986
+ const total = Math.min(this.#atList?.length ?? 0, AT_CAP);
987
+ return { matches: view.matches, selected: view.selected, capped: view.capped, query: this.#atToken()?.query ?? "", total, nameCol: this.#atNameCol };
940
988
  }
941
989
  /** KC3 §3 — arm the picker at a freshly typed `@`. The gate is the
942
990
  * KC2 precedence pattern: the approval panel, the slash menu and a
@@ -959,12 +1007,16 @@ export class Editor {
959
1007
  this.#atOpen = true;
960
1008
  this.#atSel = 0;
961
1009
  this.#atList = this.#atItems(); // §5: listed per OPEN, never per keystroke
1010
+ this.#atNameCol = 0;
1011
+ for (const item of this.#atList.slice(0, AT_CAP))
1012
+ this.#atNameCol = Math.max(this.#atNameCol, atNameWidth(item.path));
962
1013
  this.#syncMouse();
963
1014
  }
964
1015
  #atClose() {
965
1016
  this.#atOpen = false;
966
1017
  this.#atSel = 0;
967
1018
  this.#atList = null;
1019
+ this.#atNameCol = 0;
968
1020
  this.#syncMouse();
969
1021
  }
970
1022
  /**
@@ -999,7 +1051,8 @@ export class Editor {
999
1051
  * plus the counter row. */
1000
1052
  #atRows() {
1001
1053
  const view = this.#atView();
1002
- return view === null ? 0 : Math.min(view.matches.length, AT_VISIBLE) + 1;
1054
+ // Graphite P2: the band's name, its window, its key row
1055
+ return view === null ? 0 : Math.min(view.matches.length, bandVisible(process.stdout.rows ?? 24)) + 2;
1003
1056
  }
1004
1057
  // ── TUI2-R2 ② — the session picker ───────────────────────────────
1005
1058
  /** Open the picker on a bound card source (PickInput, S5). */
@@ -1153,16 +1206,23 @@ export class Editor {
1153
1206
  #feedKeys(raw) {
1154
1207
  const text = this.#pending + this.#decoder.decode(raw, { stream: true });
1155
1208
  this.#pending = "";
1156
- // TUI2-R1 (D): the sheet is up — ANY key closes it, and the key
1157
- // that closed it is CONSUMED. The whole chunk goes, deliberately:
1158
- // an arrow key is three bytes, and closing on the first while
1159
- // letting `[A` fall through as literal text would be a sheet that
1160
- // types into your composer on the way out. A dismissal costs one
1161
- // keystroke; that is the entire contract.
1209
+ // TUI2-R1 (D): the sheet is up — ANY key closes it. Graphite R3e: a
1210
+ // sheet is read and then typed past — the whole chunk is typed as it
1211
+ // would have been (it is parsed whole, so an arrow is an arrow, never
1212
+ // `[A`); only esc, which means nothing else here, is eaten.
1213
+ //
1214
+ // DECLARED REVERSAL (the sheets round, owner, 2026-10-06): the keys
1215
+ // sheet ate the key that closed it, where `/status` let it through;
1216
+ // every sheet now closes the same way, and says so on its last row.
1217
+ // The `?` that opens the keys sheet is eaten too: typed onto an
1218
+ // empty composer it would open the sheet it just closed.
1162
1219
  if (this.#sheetOpen) {
1220
+ const keys = this.#sheetRows === null;
1163
1221
  this.#sheetOpen = false;
1222
+ this.#sheetRows = null;
1164
1223
  this.#onRender();
1165
- return;
1224
+ if (text === "\x1b" || (keys && text === "?"))
1225
+ return;
1166
1226
  }
1167
1227
  // R5 — while the viewer is up it OWNS the keyboard. Unlike the
1168
1228
  // sheet (which any key dismisses) this surface is INTERACTIVE, so
@@ -1723,6 +1783,7 @@ export class Editor {
1723
1783
  // already encodes "no panel, no menu, no picker, no browse".
1724
1784
  // The precedence can only ever ADD: every state that used to
1725
1785
  // insert a `?` still inserts one.
1786
+ this.#sheetRows = null;
1726
1787
  this.#sheetOpen = true;
1727
1788
  this.#onRender();
1728
1789
  i += 1;
package/dist/index.d.ts CHANGED
@@ -7,17 +7,17 @@
7
7
  * editor, the diff renderer, and the palette.
8
8
  */
9
9
  export { Body, Dock, CURSOR_MARKER, type BodyOptions } from "./compositor.js";
10
- export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus, PICK_MAX, modePickView, modelPickView, namedPickView, pickAffordance, pickBlockRows, pickLeadPlain, type PickOption, type PickResult, type PickRuntime, type PickSpec, type PanelArgs, type PanelFlavor, type PanelPhase, deletionRiskHint, SAFER_BACK, SAFER_DEGRADED, SAFER_DEGRADED_TRUNCATED, saferDegradedNote, type SaferAnswer, type SaferFailure, type SaferOption, type SaferRuntime, panelOptions, type PanelOption, type PanelOptionKind, type PanelState, type PanelVerdict, type PanelView, } from "./approval-panel.js";
11
- export { Container, foldLine, foldWords, visibleWidth, SPINNER, type Component, type FrameCtx } from "./components.js";
10
+ export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus, PICK_MAX, modePickView, modelPickView, settingsPickView, namedPickView, pickAffordance, pickBlockRows, pickLeadPlain, type PickOption, type PickResult, type PickRuntime, type PickSpec, type PanelArgs, type PanelFlavor, type PanelPhase, deletionRiskHint, SAFER_BACK, SAFER_DEGRADED, SAFER_DEGRADED_TRUNCATED, saferDegradedNote, type SaferAnswer, type SaferFailure, type SaferOption, type SaferRuntime, panelOptions, type PanelOption, type PanelOptionKind, type PanelState, type PanelVerdict, type PanelView, } from "./approval-panel.js";
11
+ export { Container, foldLine, foldWords, infoSheetRows, visibleWidth, SPINNER, type Component, type FrameCtx, type NoticeMark, type RecallSection } from "./components.js";
12
12
  export { Editor, MENU_ITEMS, PROMPT, PROMPT_WIDTH, displayWidth, charWidth, widthOf, type MenuItem, } from "./editor.js";
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
- export { editFileDiff, truncateDiff, writeFileDiff, type DiffLine, type DiffResult } from "./diff.js";
15
- export { STATUS_GLYPHS, cacheHitPct, compactingStatus, composeRow, decodeRate, idleStatus, retrySegment, runningStatus, tasksSegment, type CompactingProgress, type RetryOnRow, type RowSegment, type StatusMeter, type TaskCountsOnRow } from "./status.js";
16
- export { contextRows, contextUnavailableRows, type ContextLedger } from "./context-ledger.js";
13
+ export { bannerLines, COLOR_OFF, COLOR_ON, currentGround, setGround, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, sealTiers, renderResumeList, renderSessionLine, renderStatusLine, relativeTime, renderTerminalGap, renderToolSummary, TAGLINE, MOTTO, WORDMARK_W, type BannerFact, type BannerMeta, toolTarget, truncateRow, type Palette, type PathResolver, type RecapStats, type ResumeMeta, type RenderInput, type RenderResult, type RunUsage, } from "./lines.js";
14
+ export { editFileDiff, editFileHunksDiff, hunksDiff, hunksOf, truncateDiff, writeFileDiff, type DiffLine, type DiffResult, type Hunk } from "./diff.js";
15
+ export { STATUS_GLYPHS, cacheHitPct, compactingStatus, composeRow, ctxMeter, decodeRate, liveRow, retrySegment, statusBar, tasksSegment, workingRow, type BarInput, type CompactingProgress, type RetryOnRow, type RowSegment, type TaskCountsOnRow } from "./status.js";
16
+ export { contextRows, contextSheetRows, contextUnavailableRows, contextUnavailableSheetRows, meterCells, type ContextLedger } from "./context-ledger.js";
17
17
  export { interactivePrompt, projectTrustRows, projectTrustView, projectUntrustedNote, uncertainView, type TrustArtifact } from "./strings.js";
18
- export { AT_CAP, AT_SKIP, AT_VISIBLE, atEmbed, atFilter, atPanelRows, atWindow, bandHeader, longestRun, type AtItem, type AtMatch } from "./at-picker.js";
19
- export { idColumn, sessionAge, sessionCounterRow, sessionFilter, sessionListFooter, sessionListHeader, sessionListUnknownLine, sessionListRow, sessionNote, sessionPickerRows, sessionRow, scopeSessions, scopeTitle, type PickScopeState, type SessionCardView, type SessionPickState, } from "./session-picker.js";
18
+ export { AT_CAP, AT_SKIP, AT_VISIBLE, atEmbed, atFilter, atPanelRows, bandHeader, longestRun, type AtItem, type AtMatch } from "./at-picker.js";
19
+ export { idColumn, sessionAge, sessionFilter, sessionListFooter, sessionListHeader, sessionListUnknownLine, sessionListRow, sessionNote, sessionPickerRows, scopeSessions, scopeTitle, type PickScopeState, type SessionCardView, type SessionPickState, } from "./session-picker.js";
20
20
  export { ASK_HEADER_CAP, ASK_MAX_OPTIONS, ASK_MAX_QUESTIONS, ASK_MIN_OPTIONS, askAffordance, askAnswers, askBlockRows, askCommitCustom, askDeclineAll, askDeclineList, askKey, askLeadPlain, askStart, askStatus, askView, type AskAnswer, type AskOption, type AskQuestion, type AskResult, type AskRuntime, type AskSpec, type AskStep, } from "./ask-panel.js";
21
- export { resolveGround, type Ground } from "@vincemakes/kiso-tui-cells";
21
+ export { currentGroundRgb, parseOscColor, resolveGround, type Ground, type Rgb } from "@vincemakes/kiso-tui-cells";
22
22
  export { settledLabel } from "@vincemakes/kiso-tui-cells";
23
- export { KEY_BINDINGS, PANEL_KEYS_ROW, coldResumeLine, coldResumeView, displayVerb, extensionsBannerText, helpRows, keysHelpRow, keysSheetRows, slashCommandNames, unansweredAskView, type BannerExtension, type KeyBinding } from "./strings.js";
23
+ export { KEY_BINDINGS, SHEET_CLOSE, coldResumeLine, coldResumeView, displayVerb, extensionsBannerText, extensionsFact, helpRows, keysHelpRow, keysSheetRows, slashCommandNames, unansweredAskView, type BannerExtension, type KeyBinding } from "./strings.js";
package/dist/index.js CHANGED
@@ -12,28 +12,28 @@ export { Body, Dock, CURSOR_MARKER } from "./compositor.js";
12
12
  // approval is pending (the shape authority is the committed preview).
13
13
  export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus,
14
14
  // TUI2-R2 ④: the pick payload — the panel slot's third occupant.
15
- PICK_MAX, modePickView, modelPickView, namedPickView, pickAffordance, pickBlockRows, pickLeadPlain, deletionRiskHint, SAFER_BACK, SAFER_DEGRADED, SAFER_DEGRADED_TRUNCATED, saferDegradedNote, panelOptions, } from "./approval-panel.js";
16
- export { Container, foldLine, foldWords, visibleWidth, SPINNER } from "./components.js";
15
+ PICK_MAX, modePickView, modelPickView, settingsPickView, namedPickView, pickAffordance, pickBlockRows, pickLeadPlain, deletionRiskHint, SAFER_BACK, SAFER_DEGRADED, SAFER_DEGRADED_TRUNCATED, saferDegradedNote, panelOptions, } from "./approval-panel.js";
16
+ export { Container, foldLine, foldWords, infoSheetRows, visibleWidth, SPINNER } from "./components.js";
17
17
  export { Editor, MENU_ITEMS, PROMPT, PROMPT_WIDTH, displayWidth, charWidth, widthOf, } from "./editor.js";
18
- export { bannerLines, COLOR_OFF, COLOR_ON, currentGround, setGround, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderResumeList, renderSessionLine, renderStatusLine, relativeTime, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, } from "./lines.js";
19
- export { editFileDiff, truncateDiff, writeFileDiff } from "./diff.js";
18
+ export { bannerLines, COLOR_OFF, COLOR_ON, currentGround, setGround, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, sealTiers, renderResumeList, renderSessionLine, renderStatusLine, relativeTime, renderTerminalGap, renderToolSummary, TAGLINE, MOTTO, WORDMARK_W, toolTarget, truncateRow, } from "./lines.js";
19
+ export { editFileDiff, editFileHunksDiff, hunksDiff, hunksOf, 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, compactingStatus, composeRow, decodeRate, idleStatus, retrySegment, runningStatus, tasksSegment } from "./status.js";
22
+ export { STATUS_GLYPHS, cacheHitPct, compactingStatus, composeRow, ctxMeter, decodeRate, liveRow, retrySegment, statusBar, tasksSegment, workingRow } 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
- export { contextRows, contextUnavailableRows } from "./context-ledger.js";
25
+ export { contextRows, contextSheetRows, contextUnavailableRows, contextUnavailableSheetRows, meterCells } from "./context-ledger.js";
26
26
  // KC3 §1 (the extraction): the human-facing strings — the prompt, the
27
27
  // project-trust listing/view/note, the uncertain execution's view. The
28
28
  // FLOW (who is asked, what a verdict means) stays in the cli.
29
29
  export { interactivePrompt, projectTrustRows, projectTrustView, projectUntrustedNote, uncertainView } from "./strings.js";
30
30
  // KC3 §3/§5: the @ file picker's pure half — the subsequence filter, the
31
31
  // deterministic rank, and the ONE cap the CLI's file source shares.
32
- export { AT_CAP, AT_SKIP, AT_VISIBLE, atEmbed, atFilter, atPanelRows, atWindow, bandHeader, longestRun } from "./at-picker.js";
32
+ export { AT_CAP, AT_SKIP, AT_VISIBLE, atEmbed, atFilter, atPanelRows, bandHeader, longestRun } from "./at-picker.js";
33
33
  // TUI2-R2 ①–③: the session picker's pure half — the durability badge,
34
34
  // the row (picked or printed), the band, and the filter. The CARDS are
35
35
  // the cli's projection (session-cards.ts); this turns them into bytes.
36
- export { idColumn, sessionAge, sessionCounterRow, sessionFilter, sessionListFooter, sessionListHeader, sessionListUnknownLine, sessionListRow, sessionNote, sessionPickerRows, sessionRow,
36
+ export { idColumn, sessionAge, sessionFilter, sessionListFooter, sessionListHeader, sessionListUnknownLine, sessionListRow, sessionNote, sessionPickerRows,
37
37
  // 0.40.0: the workspace scope — pure, so `kiso sessions` and the picker
38
38
  // scope by one rule
39
39
  scopeSessions, scopeTitle, } from "./session-picker.js";
@@ -45,8 +45,8 @@ export { ASK_HEADER_CAP, ASK_MAX_OPTIONS, ASK_MAX_QUESTIONS, ASK_MIN_OPTIONS, as
45
45
  // honestly for a question nobody answered (the ① probe's surface).
46
46
  // TUI2-R1 (D): the keys sheet + THE key table — one source for the ?
47
47
  // overlay and /help's keys row.
48
- export { resolveGround } from "@vincemakes/kiso-tui-cells";
48
+ export { currentGroundRgb, parseOscColor, resolveGround } from "@vincemakes/kiso-tui-cells";
49
49
  // one duration form: the CLI's own surfaces label a settled duration the
50
50
  // way a settled card does, rather than writing a second one.
51
51
  export { settledLabel } from "@vincemakes/kiso-tui-cells";
52
- export { KEY_BINDINGS, PANEL_KEYS_ROW, coldResumeLine, coldResumeView, displayVerb, extensionsBannerText, helpRows, keysHelpRow, keysSheetRows, slashCommandNames, unansweredAskView } from "./strings.js";
52
+ export { KEY_BINDINGS, SHEET_CLOSE, coldResumeLine, coldResumeView, displayVerb, extensionsBannerText, extensionsFact, helpRows, keysHelpRow, keysSheetRows, slashCommandNames, unansweredAskView } from "./strings.js";
package/dist/lines.d.ts CHANGED
@@ -181,7 +181,18 @@ export interface RecapStats {
181
181
  * the plan, not the tool count — the timing and tool-count parts
182
182
  * drop, and the two /mode hints replace them. */
183
183
  readonly mode?: string;
184
+ /** Graphite §7.11 — the person stopped the turn (esc): the seal says
185
+ * so, and that everything before the stop is on disk. */
186
+ readonly stopped?: boolean;
184
187
  }
188
+ /**
189
+ * Graphite §7.11 — THE SEAL's forms, widest first, without the `✦`: the
190
+ * compositor draws the mark and picks the widest form that fits. The
191
+ * same facts as `renderRecap`, in its words, with two differences: no
192
+ * context share — the status bar's meter carries it — and a stopped turn
193
+ * says so. The pipe keeps `renderRecap`'s line.
194
+ */
195
+ export declare function sealTiers(s: RecapStats): string[];
185
196
  export declare function renderRecap(s: RecapStats): string;
186
197
  /** One-line summary of a session, for `kiso sessions`. */
187
198
  export declare function renderSessionLine(meta: {
package/dist/lines.js CHANGED
@@ -155,6 +155,44 @@ export function renderStatusLine(turn, usage, ctxRatio, faux = false) {
155
155
  return null;
156
156
  return `[turn ${turn} · ${parts.join(" · ")}]`;
157
157
  }
158
+ /**
159
+ * Graphite §7.11 — THE SEAL's forms, widest first, without the `✦`: the
160
+ * compositor draws the mark and picks the widest form that fits. The
161
+ * same facts as `renderRecap`, in its words, with two differences: no
162
+ * context share — the status bar's meter carries it — and a stopped turn
163
+ * says so. The pipe keeps `renderRecap`'s line.
164
+ */
165
+ export function sealTiers(s) {
166
+ if (s.mode === "plan")
167
+ return [["plan ready", "/mode default executes", "/mode accept-edits auto-approves edits"].join(" \u00b7 ")];
168
+ const time = s.stopped === true ? `stopped by you after ${elapsedLabel(s.seconds)}` : `took ${elapsedLabel(s.seconds)}`;
169
+ const parts = [time];
170
+ const cold = s.coldAfterMinutes !== undefined && s.usage.known && s.usage.in !== null;
171
+ let coldLabel = null;
172
+ if (cold) {
173
+ coldLabel = `cache cold after ${s.coldAfterMinutes} min`;
174
+ parts.push(coldLabel, `re-read ${kUnit(s.missed ?? s.usage.in ?? 0)}`);
175
+ if (s.usage.out !== null)
176
+ parts.push(`out ${kUnit(s.usage.out)}`);
177
+ }
178
+ else if (s.usage.known) {
179
+ const seg = `${s.usage.in !== null ? `fresh ${kUnit(s.usage.in)}` : ""}${s.usage.in !== null && s.usage.out !== null ? " " : ""}${s.usage.out !== null ? `out ${kUnit(s.usage.out)}` : ""}`;
180
+ if (seg !== "")
181
+ parts.push(seg);
182
+ if (s.usage.cache !== null && s.usage.in !== null && (s.usage.in > 0 || s.usage.cache > 0)) {
183
+ const hit = `cache ${Math.round((s.usage.cache / (s.usage.in + s.usage.cache)) * 100)}%`;
184
+ parts.push(s.missed !== undefined && s.missed > 0 ? `${hit} \u00b7 miss ${kUnit(s.missed)}` : hit);
185
+ }
186
+ }
187
+ if (s.stopped === true)
188
+ parts.push("everything up to here is saved");
189
+ const full = parts.join(" \u00b7 ");
190
+ if (coldLabel === null)
191
+ return [full];
192
+ // R3g: a cold turn sheds its LABEL before anything is cut — the gap and
193
+ // the re-read are the facts.
194
+ return [full, parts.map((x) => (x === coldLabel ? `cold ${s.coldAfterMinutes} min` : x)).join(" \u00b7 ")];
195
+ }
158
196
  /** R3d — the per-tool terms of a settled turn (`read 4 files · ran 1
159
197
  * shell command`, zero terms dropped). One wording for "what a run
160
198
  * did", wherever it is said. */
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Graphite §7.12 — a notice's META ROW: the label and the sentence.
3
+ *
4
+ * kiso's notices are sentences the CLI writes in a handful of shapes, and
5
+ * a pipe prints them exactly as written. On a terminal each becomes a meta
6
+ * row — a word label and the sentence beside it — so this maps the shapes
7
+ * to their labels in ONE place, and the call sites keep their text.
8
+ *
9
+ * The `✦` some notices open with retires on the terminal: `✦` is the
10
+ * turn's seal (§4), and a label says what the row is.
11
+ */
12
+ import type { NoticeMark } from "./components.js";
13
+ export interface NoticeMeta {
14
+ /** The row's label; absent for a sentence that has no kind of its own
15
+ * (a command's confirmation — `mode → plan`, `[/compact] …` without
16
+ * its brackets), which stays whole at the content edge. `""`
17
+ * continues the row above. */
18
+ readonly label?: string;
19
+ readonly sentence: string;
20
+ /** The word the row marks, and its colour — a lost task's gold. */
21
+ readonly mark?: NoticeMark;
22
+ /** One row, cut with `…` — a row that names a thing (§7.12). */
23
+ readonly oneRow?: true;
24
+ }
25
+ export declare function noticeMeta(text: string): NoticeMeta;
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Graphite §7.12 — a notice's META ROW: the label and the sentence.
3
+ *
4
+ * kiso's notices are sentences the CLI writes in a handful of shapes, and
5
+ * a pipe prints them exactly as written. On a terminal each becomes a meta
6
+ * row — a word label and the sentence beside it — so this maps the shapes
7
+ * to their labels in ONE place, and the call sites keep their text.
8
+ *
9
+ * The `✦` some notices open with retires on the terminal: `✦` is the
10
+ * turn's seal (§4), and a label says what the row is.
11
+ */
12
+ /** kiso's session events — the notices that are a KIND of thing, first
13
+ * match wins. A command's own confirmation is not one of them: its words
14
+ * are read as a whole, and a label would only split them. */
15
+ const SHAPES = [
16
+ [/^✦ compacted (.*)$/s, "COMPACTED", (m) => m[1]],
17
+ [/^\[\/compact\] ✦ compacted(?: · )?(.*)$/s, "COMPACTED", (m) => m[1]],
18
+ [/^✦ pruned (.*)$/s, "PRUNED", (m) => m[1]],
19
+ [/^✦ window learned — (.*)$/s, "WINDOW", (m) => `learned — ${m[1]}`],
20
+ // the main-sync round: a background task's delivery (ADR-0058, main's
21
+ // task-notice.ts) is a session event like the others
22
+ [/^✦ task (.*)$/s, "TASK", (m) => m[1]],
23
+ [/^run failed — (.*)$/s, "FAILED", (m) => m[1]],
24
+ [/^(\S+) FAILED — (.*)$/s, "UNCERTAIN", (m) => `${m[1]} — ${m[2]}`],
25
+ // Graphite P1b (owner, 2026-09-30): dontAsk leaves an interrupted call
26
+ // undecided — said as what it is, and when it will be asked
27
+ [/^\[dontAsk\] (\d+) uncertain executions? left unresolved — resolve them in an asking mode$/s, "UNCERTAIN", (m) => `${m[1]} interrupted command${m[1] === "1" ? "" : "s"} left undecided — asked once don't ask is off`],
28
+ // the main-sync round (owner, 2026-09-30): a call refused because the
29
+ // don't-ask switch is on — the person set this up, so the label is dim,
30
+ // not a failure's red
31
+ [/^\[dontAsk\] (\S+) would ask — denied$/s, "DENIED", (m) => `${m[1]} would ask — denied`],
32
+ [/^(stopped at the .*-turn limit.*)$/s, "LIMIT", (m) => m[1]],
33
+ [/^(answer truncated at max_tokens.*)$/s, "LIMIT", (m) => m[1]],
34
+ [/^stream interrupted — (.*)$/s, "INTERRUPTED", (m) => m[1]],
35
+ [/^(no reply recorded — .*)$/s, "INTERRUPTED", (m) => m[1]],
36
+ [/^verification pass$/s, "VERIFY", () => ""],
37
+ // the last sweep (owner, 2026-10-06): a session switch (`/clear`,
38
+ // `/resume <id>`) is a session event — the id it opened and the way back
39
+ [/^session (\S+) \(switched — previous: (\S+), \/resume \S+ returns\)\n?$/s, "SESSION", (m) => `${m[1]} · /resume ${m[2]} returns`],
40
+ [/^--- re-wrapped (\d+ blocks?) at the current width \(appended — the history above is unchanged\) ---$/s, "REWRAPPED", (m) => `${m[1]} at the current width · the history above is unchanged`],
41
+ [/^\[dontAsk\] the model's question was declined — nothing asks in dontAsk$/s, "DENIED", () => "the model's question — nothing asks while don't ask is on"],
42
+ // …and a command's own reply. It wore its command in brackets
43
+ // (`[/compact] …`, `[reload] …`), or brackets alone (`[no thinking
44
+ // yet]`), or `--- … ---` rules; on the terminal the person has just
45
+ // typed the command, so the reply is a sentence at the content edge.
46
+ // A pipe keeps every bracket.
47
+ [/^\[reload\] (\d+ extensions?\b.*)$/s, null, (m) => `reloaded ${m[1]}`],
48
+ [/^\[reload\] (.*)$/s, null, (m) => `reload failed: ${m[1]}`],
49
+ [/^\[\/([a-z-]+)\] failed: (.*)$/s, null, (m) => `/${m[1]} failed: ${m[2]}`],
50
+ [/^\[\/[a-z-]+\] (.*)$/s, null, (m) => m[1]],
51
+ [/^\[(turn held — [^\]]+)\] (.*)$/s, null, (m) => `${m[1]} · ${m[2]}`],
52
+ [/^\[([^[\]\n]+)\]$/s, null, (m) => m[1]],
53
+ [/^no such mode: (.*)\ntiers: (.*)$/s, null, (m) => `no such mode: ${m[1]} · tiers: ${m[2]}`],
54
+ [/^--- (\d+ earlier blocks?) not re-wrapped \(bounded at two screens\) ---$/s, null, (m) => `${m[1]} not re-wrapped — bounded at two screens`],
55
+ ];
56
+ /** The main sync (0.46.2, Amendment 8) — a task kiso lost track of, said
57
+ * the moment it is concluded: `✦ lost track of <id> (<what>) — it may
58
+ * still be running · /tasks shows it` (the parenthesis only when the
59
+ * command is known; the last ` — ` opens the tail — both pinned by the
60
+ * line's own test). On the terminal it is the TASK row a delivery draws,
61
+ * `lost track — may still be running` in gold. */
62
+ const LOST = /^✦ lost track of (\S+)(?: \((.*)\))? — (it may still be running.*)$/s;
63
+ export function noticeMeta(text) {
64
+ const lost = LOST.exec(text);
65
+ if (lost !== null) {
66
+ const word = "lost track — may still be running";
67
+ // one row, as every row that names a thing (§7.12): where to look before
68
+ // what ran, so a cut takes the command, never the way to it
69
+ return { label: "TASK", sentence: `${lost[1]} ${word} · /tasks shows it${lost[2] !== undefined ? ` · ${lost[2]}` : ""}`, mark: { text: word, tone: "gold" }, oneRow: true };
70
+ }
71
+ // a notice that opens with spaces continues the one above it (the
72
+ // verification pass's content): no label of its own
73
+ if (/^ {2}\S/.test(text))
74
+ return { label: "", sentence: text.trim() };
75
+ for (const [re, label, sentence] of SHAPES) {
76
+ const m = re.exec(text);
77
+ if (m !== null)
78
+ return label === null ? { sentence: sentence(m) } : { label, sentence: sentence(m) };
79
+ }
80
+ // no kind of its own: the sentence stays whole — only the `✦`, which is
81
+ // the seal's mark on a terminal (§4), comes off
82
+ return { sentence: text.replace(/^✦ /, "") };
83
+ }
@@ -6,8 +6,8 @@
6
6
  * one band, in an order the editor keeps in one place: the panel first,
7
7
  * then the session picker, then the composer's own bands. What the
8
8
  * panel may do to the composer is the `BandHost` surface and nothing
9
- * else: it types its amend reason, its custom ask answer and its custom
10
- * pick into the composer, stashes the composer at open, restores it at
9
+ * else: it types its amend reason, its custom ask answer and a pick's
10
+ * filter into the composer, stashes the composer at open, restores it at
11
11
  * close — that is the whole coupling, and it is visible here.
12
12
  *
13
13
  * DECLARED MOVE (S5, 2026-09-06): every method below stood in editor.ts