@vincemakes/kiso-tui 0.15.3 → 0.15.5
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/ask-panel.d.ts +5 -0
- package/dist/ask-panel.js +24 -4
- package/dist/editor.js +218 -6
- package/dist/session-picker.d.ts +4 -0
- package/dist/session-picker.js +19 -0
- package/package.json +2 -2
package/dist/ask-panel.d.ts
CHANGED
|
@@ -53,6 +53,11 @@ export interface AskStep {
|
|
|
53
53
|
readonly state: AskRuntime;
|
|
54
54
|
readonly result?: AskResult;
|
|
55
55
|
}
|
|
56
|
+
/** REL-0152-D4: is the cursor on the type-your-own row? The editor asks
|
|
57
|
+
* before it decides what a printable key means — on this row a key is
|
|
58
|
+
* the first character of an answer, everywhere else it is a shortcut.
|
|
59
|
+
* Exported so that rule lives on ONE definition of the row. */
|
|
60
|
+
export declare function askOnCustomRow(spec: AskSpec, state: AskRuntime): boolean;
|
|
56
61
|
export declare function askKey(spec: AskSpec, state: AskRuntime, key: string): AskStep;
|
|
57
62
|
/** The typed answer commits: it becomes THE answer for this question
|
|
58
63
|
* (clearing its picks) and the walk advances. An empty line is a
|
package/dist/ask-panel.js
CHANGED
|
@@ -107,6 +107,14 @@ function toggle(state, option, multi) {
|
|
|
107
107
|
* option, which is where `askBlockRows` draws it. Options and this row
|
|
108
108
|
* are one list to the eye, so they are one list to the cursor. */
|
|
109
109
|
const customRow = (q) => q.options.length;
|
|
110
|
+
/** REL-0152-D4: is the cursor on the type-your-own row? The editor asks
|
|
111
|
+
* before it decides what a printable key means — on this row a key is
|
|
112
|
+
* the first character of an answer, everywhere else it is a shortcut.
|
|
113
|
+
* Exported so that rule lives on ONE definition of the row. */
|
|
114
|
+
export function askOnCustomRow(spec, state) {
|
|
115
|
+
const q = spec.questions[state.qIndex];
|
|
116
|
+
return q !== undefined && state.phase === "options" && state.cursor === customRow(q);
|
|
117
|
+
}
|
|
110
118
|
export function askKey(spec, state, key) {
|
|
111
119
|
const q = spec.questions[state.qIndex];
|
|
112
120
|
const multi = q.multiSelect === true;
|
|
@@ -119,7 +127,11 @@ export function askKey(spec, state, key) {
|
|
|
119
127
|
}
|
|
120
128
|
if (key === "esc")
|
|
121
129
|
return { state, result: askDeclineAll(spec) };
|
|
122
|
-
|
|
130
|
+
// `t` is the shortcut from anywhere in the list; `type` is the
|
|
131
|
+
// REL-0152-D4 gesture — the editor sends it when the cursor is
|
|
132
|
+
// already on the custom row and a printable key arrives, so the
|
|
133
|
+
// keystroke that opened the phase is also its first character.
|
|
134
|
+
if (key === "t" || key === "type")
|
|
123
135
|
return { state: { ...state, phase: "custom" } };
|
|
124
136
|
if (key === "left")
|
|
125
137
|
return { state: state.qIndex === 0 ? state : { ...state, qIndex: state.qIndex - 1, cursor: 0 } };
|
|
@@ -202,9 +214,17 @@ export function askBlockRows(view, state, W, maxRows) {
|
|
|
202
214
|
// look like a footnote.
|
|
203
215
|
const typed = state.custom[state.qIndex];
|
|
204
216
|
const onCustom = state.cursor === customRow(q);
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
217
|
+
// REL-0152-D4: while the phase is OPEN the row becomes the answer's
|
|
218
|
+
// box — a faint placeholder standing where the text will land, so an
|
|
219
|
+
// empty typing phase looks like somewhere to type instead of looking
|
|
220
|
+
// like nothing happened. The placeholder is dim and the answer is
|
|
221
|
+
// not: the two can never be mistaken for each other.
|
|
222
|
+
const typingHere = state.phase === "custom";
|
|
223
|
+
body.push(`${gutter}${cutLine(typed !== null && typed !== undefined
|
|
224
|
+
? `${onCustom || typingHere ? p.bold : ""} t ◉ ${escapeTerminal(typed)}${p.reset}`
|
|
225
|
+
: typingHere
|
|
226
|
+
? `${p.bold} t ▸${p.reset} ${p.dim}type your answer — enter sends, esc backs out${p.reset}`
|
|
227
|
+
: `${onCustom ? p.bold : p.dim} t type your own answer${p.reset}`, Math.max(1, W - 2))}`);
|
|
208
228
|
// the bounded block: the options fold nothing and cut individually,
|
|
209
229
|
// so the cap drops whole rows with the W21 notice row.
|
|
210
230
|
const budget = Math.max(1, maxRows - 5);
|
package/dist/editor.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
* (?2004h) unwraps and inserts its newlines LITERALLY; every newline
|
|
22
22
|
* source funnels through the ONE normalizer in feed() (§3).
|
|
23
23
|
*/
|
|
24
|
+
var _a;
|
|
24
25
|
import { charWidth, displayWidth, leadWidth, widthOf } from "./width.js";
|
|
25
26
|
// the width primitives moved to width.ts (W1, the single width
|
|
26
27
|
// authority) — re-exported so the editor's public surface is unchanged.
|
|
@@ -29,7 +30,7 @@ import { palette } from "./render.js";
|
|
|
29
30
|
import { PICK_MAX, panelOptions, saferDegradedNote, } from "./approval-panel.js";
|
|
30
31
|
// KC3.5: the panel-slot dispatchers — the ask branch folded into the
|
|
31
32
|
// W21 lead/rows, so this file keeps ONE panel and one key owner.
|
|
32
|
-
import { askCommitCustom, askKey, askStart, panelLead } from "./ask-panel.js";
|
|
33
|
+
import { askCommitCustom, askKey, askOnCustomRow, askStart, panelLead } from "./ask-panel.js";
|
|
33
34
|
import { AT_VISIBLE, atFilter } from "./at-picker.js";
|
|
34
35
|
// TUI2-R2 ②: the session picker — the band's THIRD occupant. Its filter
|
|
35
36
|
// is the @ picker's rank aimed at the session id; the editor owns the
|
|
@@ -110,6 +111,48 @@ export class Editor {
|
|
|
110
111
|
// user's next turn.
|
|
111
112
|
#panel = null;
|
|
112
113
|
#pasting = false;
|
|
114
|
+
/**
|
|
115
|
+
* REL-0152-D8 — the paste capsule.
|
|
116
|
+
*
|
|
117
|
+
* A paste large enough to break the composer's layout is held HERE
|
|
118
|
+
* and shown in the buffer as `[Pasted text #N +M lines]`. The buffer
|
|
119
|
+
* is the display; this map is the content; the line that LEAVES the
|
|
120
|
+
* editor is the content again. That ordering is the whole design —
|
|
121
|
+
* the capsule can never truncate what gets sent, because expansion
|
|
122
|
+
* happens on the way out and reads from a map the display cannot
|
|
123
|
+
* edit.
|
|
124
|
+
*
|
|
125
|
+
* A capsule the human deletes is a paste that never happened: the
|
|
126
|
+
* token is gone, the expansion finds nothing to replace, and the
|
|
127
|
+
* entry is simply never read. That is how you take a paste back.
|
|
128
|
+
*
|
|
129
|
+
* The map is per-editor and grows by one entry per large paste in a
|
|
130
|
+
* session — bounded by how many times a human can press cmd-V, and
|
|
131
|
+
* every entry is text they chose to paste and may still submit.
|
|
132
|
+
*/
|
|
133
|
+
#pastes = new Map();
|
|
134
|
+
#pasteSeq = 0;
|
|
135
|
+
/** The buffer index where the in-flight paste began; null outside one. */
|
|
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;
|
|
113
156
|
/** TUI2-R3v2 ①: one-shot — a panel that just closed swallows the
|
|
114
157
|
* habitual trailing enter rather than submitting the restored draft. */
|
|
115
158
|
#swallowEnter = false;
|
|
@@ -760,6 +803,19 @@ export class Editor {
|
|
|
760
803
|
continue;
|
|
761
804
|
}
|
|
762
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
|
+
}
|
|
763
819
|
this.#pickPanelEnter();
|
|
764
820
|
i += 1;
|
|
765
821
|
continue;
|
|
@@ -791,10 +847,39 @@ export class Editor {
|
|
|
791
847
|
continue;
|
|
792
848
|
}
|
|
793
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
|
+
}
|
|
794
863
|
this.#askStep(typing ? "commit" : "enter");
|
|
795
864
|
i += 1;
|
|
796
865
|
continue;
|
|
797
866
|
}
|
|
867
|
+
// REL-0152-D4 — on the custom row a printable key is TEXT.
|
|
868
|
+
// The row names typing as its purpose and then swallowed
|
|
869
|
+
// the first thing you typed; only enter or `t` opened the
|
|
870
|
+
// phase. Now the keystroke opens it AND lands in the
|
|
871
|
+
// buffer, so the character you meant is the character you
|
|
872
|
+
// get. This is checked BEFORE the shortcut branch below on
|
|
873
|
+
// purpose: on this row "3" and "t" are the start of an
|
|
874
|
+
// answer, not a pick and not a mode key. Everywhere else
|
|
875
|
+
// in the list they keep their fast-path meaning exactly.
|
|
876
|
+
if (askOnCustomRow(panel.view.ask, panel.ask) && c !== undefined && c >= " " && c !== "\x7f") {
|
|
877
|
+
this.#askStep("type");
|
|
878
|
+
this.#insert(c.codePointAt(0));
|
|
879
|
+
this.#onRender();
|
|
880
|
+
i += 1;
|
|
881
|
+
continue;
|
|
882
|
+
}
|
|
798
883
|
if (!typing && (c === " " || (c !== undefined && c >= "1" && c <= "4") || c === "t" || c === "T")) {
|
|
799
884
|
this.#askStep(c === " " ? "space" : c === "T" ? "t" : c);
|
|
800
885
|
i += 1;
|
|
@@ -827,6 +912,19 @@ export class Editor {
|
|
|
827
912
|
continue;
|
|
828
913
|
}
|
|
829
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
|
+
}
|
|
830
928
|
this.#panelEnter();
|
|
831
929
|
i += 1;
|
|
832
930
|
continue;
|
|
@@ -1186,10 +1284,14 @@ export class Editor {
|
|
|
1186
1284
|
const n = Number(params);
|
|
1187
1285
|
if (n === 3)
|
|
1188
1286
|
this.#delete();
|
|
1189
|
-
else if (n === 200)
|
|
1287
|
+
else if (n === 200) {
|
|
1190
1288
|
this.#pasting = true;
|
|
1289
|
+
this.#pasteAt = this.#cursor; // REL-0152-D8: where the capsule will go
|
|
1290
|
+
this.#pasteRun = []; // REL-0152-D9: collect, do not insert
|
|
1291
|
+
}
|
|
1191
1292
|
else if (n === 201) {
|
|
1192
1293
|
this.#pasting = false;
|
|
1294
|
+
this.#commitPaste();
|
|
1193
1295
|
this.#onRender();
|
|
1194
1296
|
}
|
|
1195
1297
|
}
|
|
@@ -1568,7 +1670,10 @@ export class Editor {
|
|
|
1568
1670
|
return;
|
|
1569
1671
|
const spec = panel.view.ask;
|
|
1570
1672
|
const before = panel.ask.phase;
|
|
1571
|
-
|
|
1673
|
+
// REL-0152-D8: a typed ask answer is a line leaving the editor too —
|
|
1674
|
+
// pasting a stack trace into "type your own answer" must send the
|
|
1675
|
+
// stack trace, not the capsule that stands for it.
|
|
1676
|
+
const step = key === "commit" ? askCommitCustom(spec, panel.ask, this.#expandPastes(this.line())) : askKey(spec, panel.ask, key);
|
|
1572
1677
|
panel.ask = step.state;
|
|
1573
1678
|
if (step.state.phase !== before) {
|
|
1574
1679
|
this.#chars = [];
|
|
@@ -1664,6 +1769,13 @@ export class Editor {
|
|
|
1664
1769
|
}
|
|
1665
1770
|
// ---- editing ----
|
|
1666
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
|
+
}
|
|
1667
1779
|
if (this.#historyIdx !== null)
|
|
1668
1780
|
this.#historyIdx = null; // editing leaves the browse
|
|
1669
1781
|
this.#queuePopMode = false; // W22: editing leaves the pop-walk too
|
|
@@ -1737,6 +1849,100 @@ export class Editor {
|
|
|
1737
1849
|
* reset together (W22: a departing line ends the pop-walk, so the
|
|
1738
1850
|
* next esc at rest interrupts again). Shared by the submit and the
|
|
1739
1851
|
* redirect — the two doors a line can leave by. */
|
|
1852
|
+
/**
|
|
1853
|
+
* REL-0152-D8 — how big a paste has to be before it is a capsule.
|
|
1854
|
+
*
|
|
1855
|
+
* LINES first, because lines are what actually break the layout: the
|
|
1856
|
+
* composer grows a row per line and walks up the terminal. The
|
|
1857
|
+
* character bound catches the pathological one-liner, which wraps to
|
|
1858
|
+
* the same screenful by another route.
|
|
1859
|
+
*
|
|
1860
|
+
* Below both, the paste is left exactly as it arrived. A four-line
|
|
1861
|
+
* snippet is something you want to SEE in the composer, and a capsule
|
|
1862
|
+
* there would be pure obstruction.
|
|
1863
|
+
*/
|
|
1864
|
+
static #PASTE_LINES = 8;
|
|
1865
|
+
static #PASTE_CHARS = 900;
|
|
1866
|
+
/** The token a capsule shows as. Parsed back by the same regexp on
|
|
1867
|
+
* the way out — one definition, so the two can never drift. */
|
|
1868
|
+
static #capsuleText(id, lines) {
|
|
1869
|
+
return `[Pasted text #${id} +${lines} line${lines === 1 ? "" : "s"}]`;
|
|
1870
|
+
}
|
|
1871
|
+
static #CAPSULE = /\[Pasted text #(\d+) \+\d+ lines?\]/g;
|
|
1872
|
+
/**
|
|
1873
|
+
* Close an in-flight paste: if it was large, swap the pasted run out
|
|
1874
|
+
* of the buffer for its capsule and keep the text.
|
|
1875
|
+
*
|
|
1876
|
+
* The swap is a splice at the recorded start, so a paste in the
|
|
1877
|
+
* MIDDLE of a line leaves the prose on both sides of it untouched —
|
|
1878
|
+
* the capsule is a character run like any other from here on, and
|
|
1879
|
+
* every editing operation in this file works on it without knowing
|
|
1880
|
+
* it exists.
|
|
1881
|
+
*/
|
|
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;
|
|
1908
|
+
this.#pasteAt = null;
|
|
1909
|
+
if (run.length === 0)
|
|
1910
|
+
return;
|
|
1911
|
+
if (this.#historyIdx !== null)
|
|
1912
|
+
this.#historyIdx = null;
|
|
1913
|
+
this.#queuePopMode = false;
|
|
1914
|
+
const pasted = _a.#textOf(run);
|
|
1915
|
+
const lines = pasted.split("\n").length;
|
|
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;
|
|
1928
|
+
this.#reflow();
|
|
1929
|
+
this.#refreshMenu();
|
|
1930
|
+
}
|
|
1931
|
+
/**
|
|
1932
|
+
* The way out: every capsule token becomes its text again.
|
|
1933
|
+
*
|
|
1934
|
+
* Applied to the line the editor HANDS OVER, never to the buffer —
|
|
1935
|
+
* so what the human sees stays short and what the model receives is
|
|
1936
|
+
* what the human pasted. A token whose entry is missing (a stale id
|
|
1937
|
+
* recalled from history after the map moved on) is left standing as
|
|
1938
|
+
* literal text rather than silently becoming an empty string: a
|
|
1939
|
+
* visible oddity beats a silent deletion of someone's paste.
|
|
1940
|
+
*/
|
|
1941
|
+
#expandPastes(line) {
|
|
1942
|
+
if (this.#pastes.size === 0)
|
|
1943
|
+
return line;
|
|
1944
|
+
return line.replace(_a.#CAPSULE, (whole, id) => this.#pastes.get(Number(id)) ?? whole);
|
|
1945
|
+
}
|
|
1740
1946
|
#takeLine() {
|
|
1741
1947
|
const line = String.fromCodePoint(...this.#chars);
|
|
1742
1948
|
this.#chars = [];
|
|
@@ -1850,16 +2056,21 @@ export class Editor {
|
|
|
1850
2056
|
}
|
|
1851
2057
|
}
|
|
1852
2058
|
const line = this.#takeLine();
|
|
2059
|
+
// REL-0152-D8: the capsule expands ON THE WAY OUT. The consumer
|
|
2060
|
+
// gets what was pasted; the HISTORY keeps the short form, so ↑
|
|
2061
|
+
// recalls a readable line that still expands when it is sent
|
|
2062
|
+
// again (the map outlives the buffer, by design).
|
|
2063
|
+
const sent = this.#expandPastes(line);
|
|
1853
2064
|
const cb = this.#questionCb;
|
|
1854
2065
|
this.#questionCb = null;
|
|
1855
2066
|
if (cb !== null) {
|
|
1856
|
-
cb(
|
|
2067
|
+
cb(sent);
|
|
1857
2068
|
}
|
|
1858
2069
|
else if (this.#lineCb !== null) {
|
|
1859
|
-
this.#lineCb(
|
|
2070
|
+
this.#lineCb(sent);
|
|
1860
2071
|
}
|
|
1861
2072
|
else {
|
|
1862
|
-
this.#pendingLines.push(
|
|
2073
|
+
this.#pendingLines.push(sent); // nobody wired yet — hold it
|
|
1863
2074
|
}
|
|
1864
2075
|
if (cb === null && line !== "")
|
|
1865
2076
|
this.#remember(line);
|
|
@@ -1949,3 +2160,4 @@ export class Editor {
|
|
|
1949
2160
|
return end;
|
|
1950
2161
|
}
|
|
1951
2162
|
}
|
|
2163
|
+
_a = Editor;
|
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.5",
|
|
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.5"
|
|
39
39
|
}
|
|
40
40
|
}
|