@vincemakes/kiso-tui 0.14.0 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compositor.d.ts +5 -0
- package/dist/compositor.js +86 -41
- package/dist/editor.d.ts +9 -0
- package/dist/editor.js +32 -1
- package/dist/status.d.ts +6 -0
- package/dist/status.js +1 -2
- package/package.json +2 -2
package/dist/compositor.d.ts
CHANGED
|
@@ -74,6 +74,11 @@ export interface InputState {
|
|
|
74
74
|
readonly cursorCol?: number;
|
|
75
75
|
}
|
|
76
76
|
export interface BodyOptions {
|
|
77
|
+
/** REL-0150-D1 test seam: overrides the TERM_PROGRAM detection for
|
|
78
|
+
* the conservative frame mode — process.env is SHARED across
|
|
79
|
+
* concurrently-running test files in one worker, so a test that
|
|
80
|
+
* mutated the env would bleed 40ms frames into its neighbors. */
|
|
81
|
+
readonly termProgram?: string;
|
|
77
82
|
/** Is the cell renderer live? A color TTY with a real size — checked
|
|
78
83
|
* per mutation (the TIOCSWINSZ can land after main constructs us). */
|
|
79
84
|
active: () => boolean;
|
package/dist/compositor.js
CHANGED
|
@@ -153,9 +153,24 @@ export class Body {
|
|
|
153
153
|
// box top), the live caps shrink by their rows, and the status
|
|
154
154
|
// row's right hint shows the count while any turn waits.
|
|
155
155
|
#queueState = () => [];
|
|
156
|
+
/** REL-0150-D1 — the conservative frame mode. Terminal.app does not
|
|
157
|
+
* support DEC 2026 synchronized output (half-frames paint on its own
|
|
158
|
+
* schedule — the reviewer dogfood watched the tearing live) and its
|
|
159
|
+
* renderer is throughput-weak (typed input lagged seconds behind the
|
|
160
|
+
* stream). Where TERM_PROGRAM says Apple_Terminal: the frame wraps
|
|
161
|
+
* in ?25l/?25h (cursor hidden during the repaint — the classic
|
|
162
|
+
* anti-tearing degrade; every frame re-shows it) instead of the dead
|
|
163
|
+
* 2026 bytes, and the coalesce window widens 16→40ms (fewer, bigger
|
|
164
|
+
* frames: fewer tear opportunities, less renderer pressure). Every
|
|
165
|
+
* other terminal keeps today's bytes exactly. Heuristic on purpose —
|
|
166
|
+
* a DECRQM round-trip would race the editor for stdin at boot. */
|
|
167
|
+
#conservative;
|
|
168
|
+
#frameMs;
|
|
156
169
|
constructor(opts) {
|
|
157
170
|
this.#opts = opts;
|
|
158
171
|
this.#write = opts.write ?? ((s) => process.stdout.write(s));
|
|
172
|
+
this.#conservative = (opts.termProgram ?? process.env.TERM_PROGRAM) === "Apple_Terminal";
|
|
173
|
+
this.#frameMs = this.#conservative ? 40 : FRAME_MS;
|
|
159
174
|
this.#active = opts.active();
|
|
160
175
|
// v6: the single writer — the compositor IS the dock; the CLI's
|
|
161
176
|
// onDock callback (which used to re-pin the dock after a scroll)
|
|
@@ -472,9 +487,6 @@ export class Body {
|
|
|
472
487
|
return;
|
|
473
488
|
}
|
|
474
489
|
this.#endMd();
|
|
475
|
-
const last = this.#cells[this.#cells.length - 1];
|
|
476
|
-
if (last !== undefined && last.kind === "text" && !last.done)
|
|
477
|
-
last.done = true;
|
|
478
490
|
this.#mark();
|
|
479
491
|
}
|
|
480
492
|
/** W14 — the turn boundary's END: the CLI calls this at the run's
|
|
@@ -941,7 +953,7 @@ export class Body {
|
|
|
941
953
|
this.#dirty = false;
|
|
942
954
|
this.render();
|
|
943
955
|
}
|
|
944
|
-
},
|
|
956
|
+
}, this.#frameMs);
|
|
945
957
|
this.#frameTimer.unref();
|
|
946
958
|
}
|
|
947
959
|
/** The spinner: a ONE-SHOT re-armed ONLY while a running tool exists —
|
|
@@ -1168,7 +1180,7 @@ export class Body {
|
|
|
1168
1180
|
panelSpan === null ? null : { top: liveTop + panelSpan.offset, count: panelSpan.count, first: panelSpan.first };
|
|
1169
1181
|
// 5. the frame bytes.
|
|
1170
1182
|
const out = [];
|
|
1171
|
-
out.push("\x1b[?2026h"); //
|
|
1183
|
+
out.push(this.#conservative ? "\x1b[?25l" : "\x1b[?2026h"); // D1: sync ON, or cursor-hide where 2026 is dead bytes
|
|
1172
1184
|
// A8: the bottom-anchored window (the model's last H rows) shifts
|
|
1173
1185
|
// DOWN when the live region SHRINKS — the done-fold, the fold-hold
|
|
1174
1186
|
// release at the terminal event. The steady path's scroll syncs
|
|
@@ -1189,7 +1201,7 @@ export class Body {
|
|
|
1189
1201
|
else {
|
|
1190
1202
|
this.#drawSteady(out, W, H, liveTop, liveLines, queueRows, menuRows, editor);
|
|
1191
1203
|
}
|
|
1192
|
-
out.push("\x1b[?2026l");
|
|
1204
|
+
out.push(this.#conservative ? "\x1b[?25h" : "\x1b[?2026l");
|
|
1193
1205
|
this.#write(out.join(""));
|
|
1194
1206
|
this.#lastLiveTop = liveTop;
|
|
1195
1207
|
this.#lastLiveRows = liveRowsTotal;
|
|
@@ -1688,40 +1700,67 @@ export class Body {
|
|
|
1688
1700
|
// content loss).
|
|
1689
1701
|
if (skip > 0 && !overlay) {
|
|
1690
1702
|
const leaving = Math.max(0, skip - this.#lastSkip);
|
|
1691
|
-
//
|
|
1692
|
-
//
|
|
1693
|
-
//
|
|
1694
|
-
//
|
|
1695
|
-
//
|
|
1696
|
-
//
|
|
1697
|
-
//
|
|
1698
|
-
//
|
|
1699
|
-
//
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1703
|
+
// TT-1B (finding TUI2-MD-1): a burst taller than the screen can
|
|
1704
|
+
// NOT be staged in one pass — rows placed past H are clamped by
|
|
1705
|
+
// the terminal itself (CUP pins to the last row), the pile keeps
|
|
1706
|
+
// only its last member, and the scroll pushes the wreckage: the
|
|
1707
|
+
// short-terminal row loss the finding measured. The transit is
|
|
1708
|
+
// chunked instead: each pass paints <= H leaving rows at rows
|
|
1709
|
+
// 1..chunk and scrolls exactly that many LFs, so every row is ON
|
|
1710
|
+
// the screen when its scroll slot comes up. The <= H path below
|
|
1711
|
+
// keeps its exact pre-round bytes (its staging never exceeds H —
|
|
1712
|
+
// position <= skip - lastSkip = leaving).
|
|
1713
|
+
if (leaving > H) {
|
|
1714
|
+
let p = this.#lastSkip;
|
|
1715
|
+
while (p < skip) {
|
|
1716
|
+
const chunk = Math.min(skip - p, H);
|
|
1717
|
+
for (let k = 0; k < chunk; k += 1) {
|
|
1718
|
+
out.push(`\x1b[${k + 1};1H\x1b[0K${this.#checked(all[p + k], W)}`);
|
|
1719
|
+
}
|
|
1720
|
+
if (chunk < H)
|
|
1721
|
+
out.push(`\x1b[${chunk + 1};1H\x1b[0J`);
|
|
1722
|
+
out.push(`\x1b[${H};1H`);
|
|
1723
|
+
for (let k = 0; k < chunk; k += 1)
|
|
1724
|
+
out.push("\n");
|
|
1725
|
+
p += chunk;
|
|
1705
1726
|
}
|
|
1706
1727
|
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1728
|
+
else {
|
|
1729
|
+
// A8b (the fresh leaving share): a leaving row whose old-screen
|
|
1730
|
+
// copy is stale — the committed-this-frame lines (their old rows
|
|
1731
|
+
// held the previous live/chrome) — is pre-painted at its OLD row
|
|
1732
|
+
// so the LF scroll carries it into the scrollback; the frozen
|
|
1733
|
+
// leaving rows are already on screen and scroll as-is. The first
|
|
1734
|
+
// overflow frame of a batch: the window's top row is the freshly
|
|
1735
|
+
// committed line, NEVER on the old screen — without the
|
|
1736
|
+
// pre-paint the scroll pushes a blank and the line's only paint
|
|
1737
|
+
// (the clamped march at row 1) is overwritten by its neighbor.
|
|
1738
|
+
if (skip > frozen.length) {
|
|
1739
|
+
const fromIdx = Math.max(0, this.#lastSkip - frozen.length);
|
|
1740
|
+
const top = Math.min(skip - frozen.length, all.length - frozen.length);
|
|
1741
|
+
for (let i = fromIdx; i < top; i += 1) {
|
|
1742
|
+
out.push(`\x1b[${Math.max(1, frozen.length + i - this.#lastSkip + 1)};1H\x1b[0K${this.#checked(all[frozen.length + i], W)}`);
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
if (leaving < skip)
|
|
1746
|
+
out.push(`\x1b[${leaving + 1};1H\x1b[0J`);
|
|
1747
|
+
out.push(`\x1b[${H};1H`);
|
|
1748
|
+
// TUI2-R2pre ② — scroll the rows that LEFT THE WINDOW SINCE THE
|
|
1749
|
+
// LAST FRAME (`leaving`), never `skip`, which is the window's
|
|
1750
|
+
// ABSOLUTE top. Scrolling the absolute top re-pushed the whole
|
|
1751
|
+
// history's worth of rows on EVERY full redraw — and a live-region
|
|
1752
|
+
// shrink takes this path, so that was most frames of a real
|
|
1753
|
+
// session. The ED above had just blanked everything below row
|
|
1754
|
+
// `leaving`, so what those surplus LFs carried into the terminal's
|
|
1755
|
+
// scrollback was blank rows: the large blank bands mid-history of
|
|
1756
|
+
// the owner's field report. The SCREEN never showed it because the
|
|
1757
|
+
// repaint below covers every row 1..H (the V6-1 rule), and the
|
|
1758
|
+
// house emulator drops scrolled rows on the floor — so no gate
|
|
1759
|
+
// could see it either. Measured on the 5-turn 80x24 repro: the
|
|
1760
|
+
// scrollback went from 162 blank rows of 168 to 14 of 52.
|
|
1761
|
+
for (let i = 0; i < leaving; i += 1)
|
|
1762
|
+
out.push("\n");
|
|
1763
|
+
}
|
|
1725
1764
|
}
|
|
1726
1765
|
if (!overlay)
|
|
1727
1766
|
this.#lastSkip = skip;
|
|
@@ -1788,6 +1827,15 @@ export class Body {
|
|
|
1788
1827
|
const overlay = this.#overlayFrame;
|
|
1789
1828
|
const skip = overlay ? this.#lastSkip : Math.max(0, this.#committedLines + liveLines.length + CHROME_ROWS + inputExtra + queueRows.length + menuRows.length - H);
|
|
1790
1829
|
const leaving = overlay ? 0 : Math.max(0, skip - this.#lastSkip);
|
|
1830
|
+
// TT-1B (finding TUI2-MD-1): the steady staging has the same
|
|
1831
|
+
// past-H clamp as the full path had — a window that moved more than
|
|
1832
|
+
// a screenful in one frame cannot transit on this path at all.
|
|
1833
|
+
// Delegate the oversized frame to #drawFull's chunked emission (the
|
|
1834
|
+
// full path repaints every row, so the hand-off is self-contained).
|
|
1835
|
+
if (leaving > H) {
|
|
1836
|
+
this.#drawFull(out, W, H, liveTop, liveLines, queueRows, menuRows, editor);
|
|
1837
|
+
return;
|
|
1838
|
+
}
|
|
1791
1839
|
// the jump to the bottom row H, then N real LFs scroll the screen
|
|
1792
1840
|
// exactly N rows — ONE per committed line (the bookkeeping; the
|
|
1793
1841
|
// stale 1B anchor jumped to H−1 and the N LFs scrolled only N−1 —
|
|
@@ -1999,9 +2047,6 @@ export class Body {
|
|
|
1999
2047
|
* tail block closes and its cell becomes commit-eligible. */
|
|
2000
2048
|
#closeOpenText() {
|
|
2001
2049
|
this.#endMd();
|
|
2002
|
-
const last = this.#cells[this.#cells.length - 1];
|
|
2003
|
-
if (last !== undefined && last.kind === "text" && !last.done)
|
|
2004
|
-
last.done = true;
|
|
2005
2050
|
}
|
|
2006
2051
|
/** Close an open thinking cell when a new cell starts. */
|
|
2007
2052
|
#closeOpenThinking() {
|
package/dist/editor.d.ts
CHANGED
|
@@ -60,6 +60,9 @@ export declare class Editor {
|
|
|
60
60
|
onSigint(cb: () => void): void;
|
|
61
61
|
onEot(cb: () => void): void;
|
|
62
62
|
onEscape(cb: () => void): void;
|
|
63
|
+
/** R3a — Shift+Tab: the approval-tier cycle. The MEANING lives in the
|
|
64
|
+
* CLI (which tier follows which); the editor only reports the key. */
|
|
65
|
+
onModeCycle(cb: () => void): void;
|
|
63
66
|
onExpand(cb: () => void): void;
|
|
64
67
|
/** KC2 §2: the redirect chain — the gesture hands the buffer's text
|
|
65
68
|
* over while the run is told to stop. Mirrors onEscape (a list, so
|
|
@@ -153,4 +156,10 @@ export declare class Editor {
|
|
|
153
156
|
count: number;
|
|
154
157
|
first?: number;
|
|
155
158
|
} | null) | null): void;
|
|
159
|
+
/** R3a — cross-session input history: seed the recall buffer and
|
|
160
|
+
* register the append sink. The cap and the adjacent-duplicate
|
|
161
|
+
* collapse are unchanged; the seed takes the TAIL of what the CLI
|
|
162
|
+
* loaded. Never persists question answers (#remember's callers
|
|
163
|
+
* already exclude them). */
|
|
164
|
+
bindHistory(seed: readonly string[], persist: (line: string) => void): void;
|
|
156
165
|
}
|
package/dist/editor.js
CHANGED
|
@@ -55,6 +55,9 @@ export const MENU_ITEMS = [
|
|
|
55
55
|
{ name: "/mode", desc: "switch the approval tier (manual/default/accept-edits/plan/bypass)" },
|
|
56
56
|
{ name: "/model", desc: "list model profiles; switch with /model <name|provider/model>" },
|
|
57
57
|
{ name: "/compact", desc: "summarize the older conversation to free context" },
|
|
58
|
+
// the /resume+/clear mini-spec: the session-navigation pair
|
|
59
|
+
{ name: "/clear", desc: "start a fresh conversation (the old session stays resumable)" },
|
|
60
|
+
{ name: "/resume", desc: "switch to another session; /resume <id> goes directly" },
|
|
58
61
|
{ name: "/think", desc: "show the last full thinking block" },
|
|
59
62
|
{ name: "/last", desc: "show the most recent tool call's input and output" },
|
|
60
63
|
{ name: "/status", desc: "show session id, event count, and context estimate" },
|
|
@@ -223,6 +226,12 @@ export class Editor {
|
|
|
223
226
|
onEscape(cb) {
|
|
224
227
|
this.#escapeCbs.push(cb);
|
|
225
228
|
}
|
|
229
|
+
#onModeCycle = null;
|
|
230
|
+
/** R3a — Shift+Tab: the approval-tier cycle. The MEANING lives in the
|
|
231
|
+
* CLI (which tier follows which); the editor only reports the key. */
|
|
232
|
+
onModeCycle(cb) {
|
|
233
|
+
this.#onModeCycle = cb;
|
|
234
|
+
}
|
|
226
235
|
onExpand(cb) {
|
|
227
236
|
this.#expandCbs.push(cb);
|
|
228
237
|
}
|
|
@@ -1144,6 +1153,14 @@ export class Editor {
|
|
|
1144
1153
|
this.#mouseEvent(params, final === "M");
|
|
1145
1154
|
return;
|
|
1146
1155
|
}
|
|
1156
|
+
// R3a — Shift+Tab (CSI Z, the universal back-tab encoding) cycles
|
|
1157
|
+
// the approval tier. Composer-idle ONLY: a panel, picker, menu,
|
|
1158
|
+
// history browse or question owns its keys first (the W21 gate),
|
|
1159
|
+
// and a mid-word back-tab has no meaning the composer would miss.
|
|
1160
|
+
if (final === "Z" && params === "" && this.#composerIdle()) {
|
|
1161
|
+
this.#onModeCycle?.();
|
|
1162
|
+
return;
|
|
1163
|
+
}
|
|
1147
1164
|
// KC1 §4 — Shift+Enter WHERE THE TERMINAL ENCODES IT: kitty's
|
|
1148
1165
|
// CSI-u (ESC [ 13;2 u) and xterm's modifyOtherKeys (ESC [ 27;2;13 ~).
|
|
1149
1166
|
// Never claimed universal — Ctrl+J is the everywhere baseline; a
|
|
@@ -1736,11 +1753,25 @@ export class Editor {
|
|
|
1736
1753
|
* answers, never empties; adjacent duplicates collapse, the tail
|
|
1737
1754
|
* caps at 100. A redirect is a turn, so it is remembered too. */
|
|
1738
1755
|
#remember(line) {
|
|
1739
|
-
if (this.#history[this.#history.length - 1] !== line)
|
|
1756
|
+
if (this.#history[this.#history.length - 1] !== line) {
|
|
1740
1757
|
this.#history.push(line);
|
|
1758
|
+
// R3a: the persistence seam — the CLI owns the file (the tui
|
|
1759
|
+
// stays I/O-free); adjacent-duplicate collapse already applied
|
|
1760
|
+
this.#persistHistory?.(line);
|
|
1761
|
+
}
|
|
1741
1762
|
if (this.#history.length > 100)
|
|
1742
1763
|
this.#history.shift();
|
|
1743
1764
|
}
|
|
1765
|
+
#persistHistory = null;
|
|
1766
|
+
/** R3a — cross-session input history: seed the recall buffer and
|
|
1767
|
+
* register the append sink. The cap and the adjacent-duplicate
|
|
1768
|
+
* collapse are unchanged; the seed takes the TAIL of what the CLI
|
|
1769
|
+
* loaded. Never persists question answers (#remember's callers
|
|
1770
|
+
* already exclude them). */
|
|
1771
|
+
bindHistory(seed, persist) {
|
|
1772
|
+
this.#history = seed.slice(-100).filter((l) => l !== "");
|
|
1773
|
+
this.#persistHistory = persist;
|
|
1774
|
+
}
|
|
1744
1775
|
/** KC2 §2 — the NORMAL composer state: the redirect gesture is live
|
|
1745
1776
|
* ONLY here. The approval panel, the slash menu, the history browse
|
|
1746
1777
|
* and the queue-pop walk each OWN their keys first (the W21 "the
|
package/dist/status.d.ts
CHANGED
|
@@ -52,6 +52,12 @@ export declare function runningStatus(glyph: string, since: number, outTokens: n
|
|
|
52
52
|
*/
|
|
53
53
|
export interface StatusMeter {
|
|
54
54
|
readonly cacheHitPct: number | null;
|
|
55
|
+
/** RETIRED from the row (the owner's 2026-08-23 directive): live
|
|
56
|
+
* prices fluctuate and the canonical table is "an approximation,
|
|
57
|
+
* not a bill" — a four-decimal figure on the status bar claimed a
|
|
58
|
+
* precision the data never had. The canonical cost STAYS recorded
|
|
59
|
+
* (trace ledger, /context); the field is kept so callers need not
|
|
60
|
+
* change shape, and it renders NOTHING. */
|
|
55
61
|
readonly costUsd: number | null;
|
|
56
62
|
}
|
|
57
63
|
/** The IDLE row: the approval tier as the CALLER names it, the /mode
|
package/dist/status.js
CHANGED
|
@@ -55,8 +55,7 @@ export function idleStatus(tier, model, ctxRatio, meter) {
|
|
|
55
55
|
const parts = [`▸ ${tier}`, "/mode to switch", model];
|
|
56
56
|
if (meter?.cacheHitPct != null)
|
|
57
57
|
parts.push(`CH ${Math.round(meter.cacheHitPct)}%`);
|
|
58
|
-
|
|
59
|
-
parts.push(`$${meter.costUsd.toFixed(4)}`);
|
|
58
|
+
// costUsd deliberately NOT rendered — see StatusMeter.costUsd.
|
|
60
59
|
parts.push(`ctx left ~${ctxLeft(ctxRatio)}%`);
|
|
61
60
|
return parts.join(" · ");
|
|
62
61
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.15.1",
|
|
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.
|
|
38
|
+
"@vincemakes/kiso-tui-cells": "0.15.1"
|
|
39
39
|
}
|
|
40
40
|
}
|