@vincemakes/kiso-tui 0.15.4 → 0.15.6
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.js +38 -1
- package/dist/editor.js +111 -12
- package/dist/session-picker.d.ts +4 -0
- package/dist/session-picker.js +19 -0
- package/package.json +2 -2
package/dist/compositor.js
CHANGED
|
@@ -803,8 +803,14 @@ export class Body {
|
|
|
803
803
|
// "[?1000l[?1006l" into piped stdout and four gates caught it
|
|
804
804
|
// (compact-cli, tui-modes, tui-v7-planmode, tui2-r2-resume-picker):
|
|
805
805
|
// the invariant is about terminals, and a pipe is not one.
|
|
806
|
+
// REL-0152-D14: autowrap goes back on with the mouse, and for the
|
|
807
|
+
// same reason — the terminal outlives kiso, and a mode kiso turned
|
|
808
|
+
// off inside a frame must never be what the shell inherits. The
|
|
809
|
+
// frame restores it itself; this is the second belt, on the same
|
|
810
|
+
// TTY gate, because a half-torn-down compositor is exactly where a
|
|
811
|
+
// mode leak survives.
|
|
806
812
|
if (process.stdout.isTTY === true)
|
|
807
|
-
this.#write(MOUSE_OFF);
|
|
813
|
+
this.#write(`${MOUSE_OFF}\x1b[?7h`);
|
|
808
814
|
if (!this.#docked) {
|
|
809
815
|
// TUI2-R2pre ③: an un-docked compositor can still hold a listener
|
|
810
816
|
// (it was superseded, or enter() ran and the dock was torn down by
|
|
@@ -1180,6 +1186,36 @@ export class Body {
|
|
|
1180
1186
|
panelSpan === null ? null : { top: liveTop + panelSpan.offset, count: panelSpan.count, first: panelSpan.first };
|
|
1181
1187
|
// 5. the frame bytes.
|
|
1182
1188
|
const out = [];
|
|
1189
|
+
// REL-0152-D14 — AUTOWRAP OFF for the frame's duration.
|
|
1190
|
+
//
|
|
1191
|
+
// Found in the owner's own capture: 97 of the rows kiso emitted at
|
|
1192
|
+
// 80 columns are EXACTLY 80 cells wide — the box top, the box
|
|
1193
|
+
// bottom, the composer row, every gap row the chrome pads out. A
|
|
1194
|
+
// character printed into the last column does not move the cursor
|
|
1195
|
+
// past it; it sets the terminal's PENDING WRAP flag, and the next
|
|
1196
|
+
// printed character goes to column 1 of the following row. What
|
|
1197
|
+
// terminals do with that flag when a cursor MOVE arrives instead
|
|
1198
|
+
// of a character is not agreed on — some clear it, some keep it —
|
|
1199
|
+
// and this frame then makes 65 relative cursor-ups through exactly
|
|
1200
|
+
// that state.
|
|
1201
|
+
//
|
|
1202
|
+
// So the frame's layout depended on a behaviour terminals disagree
|
|
1203
|
+
// about, on every frame, at every width where a row happens to
|
|
1204
|
+
// fill the screen. That is enough to explain a layout that is
|
|
1205
|
+
// correct on one terminal and damaged on another, and damaged only
|
|
1206
|
+
// while frames are being painted.
|
|
1207
|
+
//
|
|
1208
|
+
// With autowrap off, a character in the last column simply stays
|
|
1209
|
+
// there: no pending flag, no disagreement, and relative moves mean
|
|
1210
|
+
// what they say. kiso can never lose content to it either, because
|
|
1211
|
+
// #checked already refuses to emit a row wider than the screen —
|
|
1212
|
+
// the invariant that makes turning wrapping off safe was in place
|
|
1213
|
+
// long before this.
|
|
1214
|
+
//
|
|
1215
|
+
// Restored at the end of every frame: prose written OUTSIDE a
|
|
1216
|
+
// frame (bodyLog, an error) is ordinary output and must still
|
|
1217
|
+
// wrap, and the shell inherits the terminal when kiso exits.
|
|
1218
|
+
out.push("\x1b[?7l");
|
|
1183
1219
|
out.push(this.#conservative ? "\x1b[?25l" : "\x1b[?2026h"); // D1: sync ON, or cursor-hide where 2026 is dead bytes
|
|
1184
1220
|
// A8: the bottom-anchored window (the model's last H rows) shifts
|
|
1185
1221
|
// DOWN when the live region SHRINKS — the done-fold, the fold-hold
|
|
@@ -1202,6 +1238,7 @@ export class Body {
|
|
|
1202
1238
|
this.#drawSteady(out, W, H, liveTop, liveLines, queueRows, menuRows, editor);
|
|
1203
1239
|
}
|
|
1204
1240
|
out.push(this.#conservative ? "\x1b[?25h" : "\x1b[?2026l");
|
|
1241
|
+
out.push("\x1b[?7h"); // REL-0152-D14: autowrap back on for everything outside the frame
|
|
1205
1242
|
this.#write(out.join(""));
|
|
1206
1243
|
this.#lastLiveTop = liveTop;
|
|
1207
1244
|
this.#lastLiveRows = liveRowsTotal;
|
package/dist/editor.js
CHANGED
|
@@ -134,6 +134,25 @@ export class Editor {
|
|
|
134
134
|
#pasteSeq = 0;
|
|
135
135
|
/** The buffer index where the in-flight paste began; null outside one. */
|
|
136
136
|
#pasteAt = null;
|
|
137
|
+
/**
|
|
138
|
+
* REL-0152-D9 — the in-flight paste's characters, held OUT of the
|
|
139
|
+
* buffer until the paste ends.
|
|
140
|
+
*
|
|
141
|
+
* Every character used to go through #insert, which splices one code
|
|
142
|
+
* point and then reflows — and a reflow scans the line to find the
|
|
143
|
+
* cursor's bounds and measures its width. That is linear work per
|
|
144
|
+
* character, so a paste cost time in the SQUARE of its size: measured
|
|
145
|
+
* on the shipped build, 10k characters took 33ms and 30k took 278ms,
|
|
146
|
+
* with a 100k paste heading for three seconds of a frozen composer.
|
|
147
|
+
* The owner's report: the capsule appears, but only after a long wait.
|
|
148
|
+
*
|
|
149
|
+
* Held here, the whole run splices in ONCE and reflows ONCE, so the
|
|
150
|
+
* cost is linear and the arithmetic is done on a finished string
|
|
151
|
+
* rather than re-done at every character of it. It survives across
|
|
152
|
+
* chunks by construction — a terminal delivers a large paste in many
|
|
153
|
+
* reads, and this is a field, not a local.
|
|
154
|
+
*/
|
|
155
|
+
#pasteRun = null;
|
|
137
156
|
/** TUI2-R3v2 ①: one-shot — a panel that just closed swallows the
|
|
138
157
|
* habitual trailing enter rather than submitting the restored draft. */
|
|
139
158
|
#swallowEnter = false;
|
|
@@ -784,6 +803,19 @@ export class Editor {
|
|
|
784
803
|
continue;
|
|
785
804
|
}
|
|
786
805
|
if (c === "\x0d" || c === "\x0a") {
|
|
806
|
+
// REL-0152-D10: a newline inside a PASTE is content, never a
|
|
807
|
+
// commit. Bracketed paste marks its own boundaries, so a
|
|
808
|
+
// \n between them is a line of the pasted text and nothing
|
|
809
|
+
// else. Without this, pasting a stack trace into a typed
|
|
810
|
+
// panel phase submitted the first line and dropped the
|
|
811
|
+
// rest into the composer behind the closed panel — the
|
|
812
|
+
// owner asked whether the type-your-own box takes a paste,
|
|
813
|
+
// and the answer was no, in the worst way.
|
|
814
|
+
if (this.#pasting) {
|
|
815
|
+
this.#insert(NEWLINE);
|
|
816
|
+
i += c === "\x0d" && text[i + 1] === "\x0a" ? 2 : 1;
|
|
817
|
+
continue;
|
|
818
|
+
}
|
|
787
819
|
this.#pickPanelEnter();
|
|
788
820
|
i += 1;
|
|
789
821
|
continue;
|
|
@@ -815,6 +847,19 @@ export class Editor {
|
|
|
815
847
|
continue;
|
|
816
848
|
}
|
|
817
849
|
if (c === "\x0d" || c === "\x0a") {
|
|
850
|
+
// REL-0152-D10: a newline inside a PASTE is content, never a
|
|
851
|
+
// commit. Bracketed paste marks its own boundaries, so a
|
|
852
|
+
// \n between them is a line of the pasted text and nothing
|
|
853
|
+
// else. Without this, pasting a stack trace into a typed
|
|
854
|
+
// panel phase submitted the first line and dropped the
|
|
855
|
+
// rest into the composer behind the closed panel — the
|
|
856
|
+
// owner asked whether the type-your-own box takes a paste,
|
|
857
|
+
// and the answer was no, in the worst way.
|
|
858
|
+
if (this.#pasting) {
|
|
859
|
+
this.#insert(NEWLINE);
|
|
860
|
+
i += c === "\x0d" && text[i + 1] === "\x0a" ? 2 : 1;
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
818
863
|
this.#askStep(typing ? "commit" : "enter");
|
|
819
864
|
i += 1;
|
|
820
865
|
continue;
|
|
@@ -867,6 +912,19 @@ export class Editor {
|
|
|
867
912
|
continue;
|
|
868
913
|
}
|
|
869
914
|
if (c === "\x0d" || c === "\x0a") {
|
|
915
|
+
// REL-0152-D10: a newline inside a PASTE is content, never a
|
|
916
|
+
// commit. Bracketed paste marks its own boundaries, so a
|
|
917
|
+
// \n between them is a line of the pasted text and nothing
|
|
918
|
+
// else. Without this, pasting a stack trace into a typed
|
|
919
|
+
// panel phase submitted the first line and dropped the
|
|
920
|
+
// rest into the composer behind the closed panel — the
|
|
921
|
+
// owner asked whether the type-your-own box takes a paste,
|
|
922
|
+
// and the answer was no, in the worst way.
|
|
923
|
+
if (this.#pasting) {
|
|
924
|
+
this.#insert(NEWLINE);
|
|
925
|
+
i += c === "\x0d" && text[i + 1] === "\x0a" ? 2 : 1;
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
870
928
|
this.#panelEnter();
|
|
871
929
|
i += 1;
|
|
872
930
|
continue;
|
|
@@ -1229,10 +1287,11 @@ export class Editor {
|
|
|
1229
1287
|
else if (n === 200) {
|
|
1230
1288
|
this.#pasting = true;
|
|
1231
1289
|
this.#pasteAt = this.#cursor; // REL-0152-D8: where the capsule will go
|
|
1290
|
+
this.#pasteRun = []; // REL-0152-D9: collect, do not insert
|
|
1232
1291
|
}
|
|
1233
1292
|
else if (n === 201) {
|
|
1234
1293
|
this.#pasting = false;
|
|
1235
|
-
this.#
|
|
1294
|
+
this.#commitPaste();
|
|
1236
1295
|
this.#onRender();
|
|
1237
1296
|
}
|
|
1238
1297
|
}
|
|
@@ -1710,6 +1769,13 @@ export class Editor {
|
|
|
1710
1769
|
}
|
|
1711
1770
|
// ---- editing ----
|
|
1712
1771
|
#insert(cp) {
|
|
1772
|
+
// REL-0152-D9: inside a paste the character is COLLECTED, not
|
|
1773
|
+
// inserted — see #pasteRun. Every other caller (a typed key, a
|
|
1774
|
+
// ctrl+J newline) is outside a paste and lands below unchanged.
|
|
1775
|
+
if (this.#pasteRun !== null) {
|
|
1776
|
+
this.#pasteRun.push(cp);
|
|
1777
|
+
return;
|
|
1778
|
+
}
|
|
1713
1779
|
if (this.#historyIdx !== null)
|
|
1714
1780
|
this.#historyIdx = null; // editing leaves the browse
|
|
1715
1781
|
this.#queuePopMode = false; // W22: editing leaves the pop-walk too
|
|
@@ -1813,21 +1879,54 @@ export class Editor {
|
|
|
1813
1879
|
* every editing operation in this file works on it without knowing
|
|
1814
1880
|
* it exists.
|
|
1815
1881
|
*/
|
|
1816
|
-
|
|
1817
|
-
|
|
1882
|
+
/** REL-0152-D9: code points to a string WITHOUT spreading the whole
|
|
1883
|
+
* array into one call. `String.fromCodePoint(...run)` throws
|
|
1884
|
+
* RangeError on a large paste — the argument list is the stack — and
|
|
1885
|
+
* a composer that crashes on a big paste is worse than one that is
|
|
1886
|
+
* slow. Chunked, it is linear and bounded. */
|
|
1887
|
+
static #textOf(run) {
|
|
1888
|
+
const CHUNK = 4096;
|
|
1889
|
+
let out = "";
|
|
1890
|
+
for (let i = 0; i < run.length; i += CHUNK)
|
|
1891
|
+
out += String.fromCodePoint(...run.slice(i, i + CHUNK));
|
|
1892
|
+
return out;
|
|
1893
|
+
}
|
|
1894
|
+
/**
|
|
1895
|
+
* Close an in-flight paste: the collected run goes into the buffer in
|
|
1896
|
+
* ONE splice — as itself when it is small, as its capsule when it is
|
|
1897
|
+
* not (REL-0152-D8).
|
|
1898
|
+
*
|
|
1899
|
+
* The splice is at the recorded start, so a paste in the MIDDLE of a
|
|
1900
|
+
* line leaves the prose on both sides untouched — what lands is a
|
|
1901
|
+
* character run like any other from here on, and every editing
|
|
1902
|
+
* operation in this file works on it without knowing it exists.
|
|
1903
|
+
*/
|
|
1904
|
+
#commitPaste() {
|
|
1905
|
+
const run = this.#pasteRun ?? [];
|
|
1906
|
+
const start = this.#pasteAt ?? this.#cursor;
|
|
1907
|
+
this.#pasteRun = null;
|
|
1818
1908
|
this.#pasteAt = null;
|
|
1819
|
-
if (
|
|
1909
|
+
if (run.length === 0)
|
|
1820
1910
|
return;
|
|
1821
|
-
|
|
1911
|
+
if (this.#historyIdx !== null)
|
|
1912
|
+
this.#historyIdx = null;
|
|
1913
|
+
this.#queuePopMode = false;
|
|
1914
|
+
const pasted = _a.#textOf(run);
|
|
1822
1915
|
const lines = pasted.split("\n").length;
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1916
|
+
const small = lines < _a.#PASTE_LINES && run.length < _a.#PASTE_CHARS;
|
|
1917
|
+
let placed;
|
|
1918
|
+
if (small) {
|
|
1919
|
+
placed = run;
|
|
1920
|
+
}
|
|
1921
|
+
else {
|
|
1922
|
+
this.#pasteSeq += 1;
|
|
1923
|
+
this.#pastes.set(this.#pasteSeq, pasted);
|
|
1924
|
+
placed = [..._a.#capsuleText(this.#pasteSeq, lines)].map((ch) => ch.codePointAt(0));
|
|
1925
|
+
}
|
|
1926
|
+
this.#chars.splice(start, 0, ...placed);
|
|
1927
|
+
this.#cursor = start + placed.length;
|
|
1830
1928
|
this.#reflow();
|
|
1929
|
+
this.#refreshMenu();
|
|
1831
1930
|
}
|
|
1832
1931
|
/**
|
|
1833
1932
|
* The way out: every capsule token becomes its text again.
|
package/dist/session-picker.d.ts
CHANGED
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
* imports nothing from the runtime, by rule). */
|
|
29
29
|
export interface SessionCardView {
|
|
30
30
|
readonly id: string;
|
|
31
|
+
/** REL-0152-D6b: the session's first substantive prompt. Optional so
|
|
32
|
+
* a caller that has not got one still renders — the row simply
|
|
33
|
+
* carries no title, which is where this picker started. */
|
|
34
|
+
readonly title?: string;
|
|
31
35
|
readonly badge: "uncertain" | "ask" | "interrupted" | "completed" | "failed";
|
|
32
36
|
readonly turns: number;
|
|
33
37
|
readonly updatedAt: number;
|
package/dist/session-picker.js
CHANGED
|
@@ -150,6 +150,11 @@ export function sessionFilter(cards, query) {
|
|
|
150
150
|
* CRASHES the compositor, so the arithmetic has to be provably right at
|
|
151
151
|
* every width rather than right at eighty.
|
|
152
152
|
*/
|
|
153
|
+
/** REL-0152-D6b: what the title may take, and what it must leave. The
|
|
154
|
+
* reserve is the widest note this picker writes ("N uncertain
|
|
155
|
+
* executions"), so an actionable row keeps saying so. */
|
|
156
|
+
const TITLE_MAX = 44;
|
|
157
|
+
const NOTE_RESERVE = 22;
|
|
153
158
|
function rowSpans(card, budget, now, idCol) {
|
|
154
159
|
const p = palette();
|
|
155
160
|
let text = "";
|
|
@@ -175,6 +180,20 @@ function rowSpans(card, budget, now, idCol) {
|
|
|
175
180
|
put(" ".repeat(pad), " ".repeat(pad));
|
|
176
181
|
const meta = ` ${sessionAge(card.updatedAt, now)} · ${card.turns} turn${card.turns === 1 ? "" : "s"}`;
|
|
177
182
|
put(meta, `${p.dim}${meta}${p.reset}`);
|
|
183
|
+
// REL-0152-D6b: the TITLE — the only span on this row that answers
|
|
184
|
+
// "which conversation is this?". It goes after the meta and before
|
|
185
|
+
// the note, and it is bounded so the note (which can be the one that
|
|
186
|
+
// demands an action) still has room at ordinary widths; on a narrow
|
|
187
|
+
// terminal `put` drops whichever no longer fits, in that order.
|
|
188
|
+
const title = card.title ?? "";
|
|
189
|
+
if (title !== "") {
|
|
190
|
+
const room = Math.max(0, Math.min(budget - w - 3 - NOTE_RESERVE, TITLE_MAX));
|
|
191
|
+
const cut = widthCut(escapeTerminal(title), room);
|
|
192
|
+
if (cut !== "") {
|
|
193
|
+
put(" ", " ");
|
|
194
|
+
put(cut, `${p.bold}${cut}${p.reset}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
178
197
|
const note = widthCut(sessionNote(card), Math.max(0, budget - w - 3));
|
|
179
198
|
if (note !== "") {
|
|
180
199
|
// the ? note carries the warn tint — the row's own words are what
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.6",
|
|
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.15.
|
|
38
|
+
"@vincemakes/kiso-tui-cells": "0.15.6"
|
|
39
39
|
}
|
|
40
40
|
}
|