@vincemakes/kiso-tui 0.52.0 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compositor.js +2 -2
- package/dist/editor.d.ts +2 -1
- package/dist/editor.js +34 -2
- package/dist/pick-input.d.ts +1 -1
- package/dist/pick-input.js +4 -2
- package/dist/session-picker.d.ts +7 -0
- package/dist/session-picker.js +12 -4
- package/package.json +2 -2
package/dist/compositor.js
CHANGED
|
@@ -55,7 +55,7 @@ import { MOUSE_OFF } from "./editor.js";
|
|
|
55
55
|
import { pickWindowOf } from "./approval-panel.js";
|
|
56
56
|
import { atPanelRows, bandHeader, bandKeyRow, bandVisible, bandWindow, moreMark } from "./at-picker.js";
|
|
57
57
|
// TUI2-R2 ②: the session picker's rows — the band's third occupant.
|
|
58
|
-
import { RESUME_FILTER_HINT, sessionPickerRows } from "./session-picker.js";
|
|
58
|
+
import { FORK_FILTER_HINT, RESUME_FILTER_HINT, sessionPickerRows } from "./session-picker.js";
|
|
59
59
|
import { CAP_PREVIEW, thinkingRow, Container, MOTION_FRAMES, MdStream, bodySpacing, boxBottom, boxTop, cellComponent, cutLine, pendingQueueRows, selectionBar, statusLine, visibleWidth, breathFrame, widthCut, } from "./components.js";
|
|
60
60
|
import { bannerLines, escapeTerminal, foldResult, foldThinking, palette, renderTerminalGap, renderToolSummary } from "./lines.js";
|
|
61
61
|
import { oneRow } from "@vincemakes/kiso-tui-cells/render";
|
|
@@ -2657,7 +2657,7 @@ export class Body {
|
|
|
2657
2657
|
// in the row's pad, so the cursor and the columns do not move.
|
|
2658
2658
|
// Graphite P3: the same exception for a pick that filters (/model) —
|
|
2659
2659
|
// its input is the filter too, and its hint is the caller's words.
|
|
2660
|
-
const hint = panel === null ? (this.#pickState?.() != null ? RESUME_FILTER_HINT : null) : panelHintOf(panel);
|
|
2660
|
+
const hint = panel === null ? (this.#pickState?.() != null ? (this.#pickState?.()?.kind === "fork" ? FORK_FILTER_HINT : RESUME_FILTER_HINT) : null) : panelHintOf(panel);
|
|
2661
2661
|
if (hint !== null && rows.length === 1 && rows[0] === "") {
|
|
2662
2662
|
const pad = / +$/.exec(out[0]);
|
|
2663
2663
|
const hintW = visibleWidth(hint);
|
package/dist/editor.d.ts
CHANGED
|
@@ -132,6 +132,7 @@ export declare class Editor {
|
|
|
132
132
|
* pop walks them (each pop leaves the queue, cancelling the turn);
|
|
133
133
|
* esc ends the walk after one more pop. */
|
|
134
134
|
bindQueue(state: () => readonly string[], pop: () => string | null): void;
|
|
135
|
+
setGhost(text: string | null): void;
|
|
135
136
|
/** ADR-0057: put text back into the buffer — the steers a stop handed
|
|
136
137
|
* back before they landed. It goes AHEAD of a draft already there, on
|
|
137
138
|
* its own line, and the draft stays recoverable (undo). */
|
|
@@ -201,7 +202,7 @@ export declare class Editor {
|
|
|
201
202
|
nameCol: number;
|
|
202
203
|
} | null;
|
|
203
204
|
/** Open the picker on a bound card source (PickInput, S5). */
|
|
204
|
-
beginPick(cards: () => readonly SessionCardView[], onPick: (id: string | null) => void, here?: string): void;
|
|
205
|
+
beginPick(cards: () => readonly SessionCardView[], onPick: (id: string | null) => void, here?: string, kind?: "resume" | "fork"): void;
|
|
205
206
|
pickState(): SessionPickState | null;
|
|
206
207
|
/** One-shot question mode: the NEXT submit answers, not a turn. */
|
|
207
208
|
question(_query: string, cb: (answer: string) => void): void;
|
package/dist/editor.js
CHANGED
|
@@ -123,6 +123,8 @@ export const MENU_ITEMS = [
|
|
|
123
123
|
// the /resume+/clear mini-spec: the session-navigation pair
|
|
124
124
|
{ name: "/clear", desc: "start a fresh conversation (the old session stays resumable)" },
|
|
125
125
|
{ name: "/resume", desc: "switch to another session; /resume <id> goes directly" },
|
|
126
|
+
// owner 2026-11-03: the web app's fork, one rule — after a reply, then switch
|
|
127
|
+
{ name: "/fork", desc: "a new session from after one of this session's replies, then switch to it" },
|
|
126
128
|
// 0.39.2: `/reload` has been dispatchable since §2.5 and `/help` lists
|
|
127
129
|
// it, and typing `/` never offered it — the one place a person who does
|
|
128
130
|
// not know a command exists goes to find one. Caught with `/copy` by the
|
|
@@ -589,6 +591,16 @@ export class Editor {
|
|
|
589
591
|
this.#queueState = state;
|
|
590
592
|
this.#queuePop = pop;
|
|
591
593
|
}
|
|
594
|
+
/** The follow-up (owner 2026-11-03): a line the person may send next, drawn dim in the EMPTY box; Tab takes it into
|
|
595
|
+
* the buffer as real text. Null clears it. Typing hides it (the box is no longer empty); it is never sent by itself. */
|
|
596
|
+
#ghost = null;
|
|
597
|
+
setGhost(text) {
|
|
598
|
+
if (this.#ghost === text)
|
|
599
|
+
return;
|
|
600
|
+
this.#ghost = text === "" ? null : text;
|
|
601
|
+
if (this.#chars.length === 0)
|
|
602
|
+
this.#onRender();
|
|
603
|
+
}
|
|
592
604
|
/** ADR-0057: put text back into the buffer — the steers a stop handed
|
|
593
605
|
* back before they landed. It goes AHEAD of a draft already there, on
|
|
594
606
|
* its own line, and the draft stays recoverable (undo). */
|
|
@@ -834,6 +846,21 @@ export class Editor {
|
|
|
834
846
|
// the window, the markers and the cursor all count them the same
|
|
835
847
|
// way — N_MAX has always been about how tall the box may get, and
|
|
836
848
|
// a folded line is as tall as a pasted one.
|
|
849
|
+
if (this.#chars.length === 0 && this.#ghost !== null) {
|
|
850
|
+
// the follow-up stands in the empty box, dim, cut to one row; the cursor waits at its start
|
|
851
|
+
const budget = this.#budget();
|
|
852
|
+
let shownGhost = "";
|
|
853
|
+
for (const ch of this.#ghost) {
|
|
854
|
+
if (widthOf([...`${shownGhost}${ch}`].map((x) => x.codePointAt(0))) > budget - 1) {
|
|
855
|
+
shownGhost += "\u2026";
|
|
856
|
+
break;
|
|
857
|
+
}
|
|
858
|
+
shownGhost += ch;
|
|
859
|
+
}
|
|
860
|
+
const p = palette();
|
|
861
|
+
const line = `${p.dim}${shownGhost}${p.reset}`;
|
|
862
|
+
return { line, cursor: 0, lines: [line], cursorRow: 0, cursorCol: 0 };
|
|
863
|
+
}
|
|
837
864
|
const rows = this.#visualRows();
|
|
838
865
|
const cursorLine = this.#cursorVisualRow(rows);
|
|
839
866
|
const n = this.#visibleRows(rows.length);
|
|
@@ -1056,8 +1083,8 @@ export class Editor {
|
|
|
1056
1083
|
}
|
|
1057
1084
|
// ── TUI2-R2 ② — the session picker ───────────────────────────────
|
|
1058
1085
|
/** Open the picker on a bound card source (PickInput, S5). */
|
|
1059
|
-
beginPick(cards, onPick, here) {
|
|
1060
|
-
this.#pickInput.begin(cards, onPick, here);
|
|
1086
|
+
beginPick(cards, onPick, here, kind) {
|
|
1087
|
+
this.#pickInput.begin(cards, onPick, here, kind);
|
|
1061
1088
|
}
|
|
1062
1089
|
pickState() {
|
|
1063
1090
|
return this.#pickInput.state();
|
|
@@ -1659,6 +1686,11 @@ export class Editor {
|
|
|
1659
1686
|
this.#atAccept();
|
|
1660
1687
|
i += 1;
|
|
1661
1688
|
}
|
|
1689
|
+
else if (c === "\t" && this.#chars.length === 0 && this.#ghost !== null) {
|
|
1690
|
+
// the follow-up: Tab on the empty box takes it as real text
|
|
1691
|
+
this.restore(this.#ghost);
|
|
1692
|
+
i += 1;
|
|
1693
|
+
}
|
|
1662
1694
|
else if (c === "\x16") {
|
|
1663
1695
|
// REL-0152-D15: ctrl+V asks for the clipboard. Inert with no
|
|
1664
1696
|
// hook wired, and 0x16 must NEVER reach the buffer — an
|
package/dist/pick-input.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export declare class PickInput {
|
|
|
28
28
|
* (the buffer becomes the filter query) and `onPick` receives the
|
|
29
29
|
* chosen id — or null when the human leaves without picking, which
|
|
30
30
|
* is a first-class outcome and not an error. */
|
|
31
|
-
begin(cards: () => readonly SessionCardView[], onPick: (id: string | null) => void, here?: string): void;
|
|
31
|
+
begin(cards: () => readonly SessionCardView[], onPick: (id: string | null) => void, here?: string, kind?: "resume" | "fork"): void;
|
|
32
32
|
/** The picker's state, derived: the full card list (the id column
|
|
33
33
|
* measures over ALL of them, so the columns never jump), the
|
|
34
34
|
* filtered matches, and the selection CLAMPED at read time — the
|
package/dist/pick-input.js
CHANGED
|
@@ -8,6 +8,7 @@ export class PickInput {
|
|
|
8
8
|
* whether the person flipped to ALL. */
|
|
9
9
|
#here = undefined;
|
|
10
10
|
#all = false;
|
|
11
|
+
#kind = "resume";
|
|
11
12
|
constructor(host) {
|
|
12
13
|
this.host = host;
|
|
13
14
|
}
|
|
@@ -18,8 +19,9 @@ export class PickInput {
|
|
|
18
19
|
* (the buffer becomes the filter query) and `onPick` receives the
|
|
19
20
|
* chosen id — or null when the human leaves without picking, which
|
|
20
21
|
* is a first-class outcome and not an error. */
|
|
21
|
-
begin(cards, onPick, here) {
|
|
22
|
+
begin(cards, onPick, here, kind = "resume") {
|
|
22
23
|
this.#cards = cards;
|
|
24
|
+
this.#kind = kind;
|
|
23
25
|
this.#commit = onPick;
|
|
24
26
|
this.#sel = 0;
|
|
25
27
|
this.#here = here;
|
|
@@ -43,7 +45,7 @@ export class PickInput {
|
|
|
43
45
|
const cards = scoped === null ? this.#cards() : scoped.cards;
|
|
44
46
|
const query = this.host.line();
|
|
45
47
|
const matches = sessionFilter(cards, query);
|
|
46
|
-
return { cards, matches, selected: Math.max(0, Math.min(this.#sel, matches.length - 1)), scope: scoped?.scope ?? null, query };
|
|
48
|
+
return { cards, matches, selected: Math.max(0, Math.min(this.#sel, matches.length - 1)), scope: scoped?.scope ?? null, query, kind: this.#kind };
|
|
47
49
|
}
|
|
48
50
|
/** 0.40.0 — tab flips CURRENT ↔ ALL. The filter owns every printable
|
|
49
51
|
* key (the buffer IS the query), so the toggle cannot be one. True when
|
package/dist/session-picker.d.ts
CHANGED
|
@@ -39,6 +39,9 @@ export interface SessionCardView {
|
|
|
39
39
|
/** 0.40.0: its project was inferred by the one-time migration, never
|
|
40
40
|
* recorded — the row says so. */
|
|
41
41
|
readonly inferred?: boolean;
|
|
42
|
+
/** /fork's card: the opened row says this instead of a session's facts
|
|
43
|
+
* (the line the reply answered). */
|
|
44
|
+
readonly detail?: string;
|
|
42
45
|
}
|
|
43
46
|
/** 0.40.0 — which sessions the picker shows. `here` is the running
|
|
44
47
|
* workspace's realpath; `all` is the person's choice; `unknown` counts
|
|
@@ -125,6 +128,9 @@ export interface SessionPickState {
|
|
|
125
128
|
/** Graphite P1: the filter as typed — the title lights the letters it
|
|
126
129
|
* matched, and the band says how many match. Absent = no filter. */
|
|
127
130
|
readonly query?: string;
|
|
131
|
+
/** /fork (owner 2026-11-03): the same band picks a REPLY to fork after —
|
|
132
|
+
* its name, its enter and its empty line say so. Absent = /resume. */
|
|
133
|
+
readonly kind?: "resume" | "fork";
|
|
128
134
|
}
|
|
129
135
|
/** How many sessions the band shows: eight on a terminal 30 rows or
|
|
130
136
|
* taller, five below (owner, 2026-09-30). */
|
|
@@ -145,6 +151,7 @@ export declare function sessionStateWord(card: SessionCardView): string;
|
|
|
145
151
|
export declare function sessionStarted(id: string): string | null;
|
|
146
152
|
/** What the empty input says while the picker is up: typing there filters. */
|
|
147
153
|
export declare const RESUME_FILTER_HINT = "filter by title or id";
|
|
154
|
+
export declare const FORK_FILTER_HINT = "filter replies";
|
|
148
155
|
/**
|
|
149
156
|
* The whole band: the named hairline, the windowed table with the
|
|
150
157
|
* selected row opened, and the key row. Returned as plain strings for the
|
package/dist/session-picker.js
CHANGED
|
@@ -438,6 +438,10 @@ export function sessionStarted(id) {
|
|
|
438
438
|
function openedRow(card, W, where) {
|
|
439
439
|
const p = palette();
|
|
440
440
|
const room = Math.max(0, W - 3);
|
|
441
|
+
if (card.detail !== undefined) {
|
|
442
|
+
const said = widthCut(escapeTerminal(card.detail), room);
|
|
443
|
+
return selectionBar(` ${p.dim}${said}${p.reset}`, visibleWidth(said) + 1, W);
|
|
444
|
+
}
|
|
441
445
|
const note = sessionNote(card);
|
|
442
446
|
const dash = note.indexOf(" — ");
|
|
443
447
|
const head = widthCut(dash < 0 ? note : note.slice(0, dash), room);
|
|
@@ -476,11 +480,12 @@ function resumeKeyRow(state, W, table) {
|
|
|
476
480
|
const scope = state.scope ?? null;
|
|
477
481
|
const elsewhere = scope === null ? 0 : scope.total - scope.inHere;
|
|
478
482
|
const tab = scope === null ? null : scope.all ? "tab this workspace" : elsewhere > 0 ? `tab ${elsewhere} more elsewhere` : null;
|
|
479
|
-
const
|
|
483
|
+
const enter = state.kind === "fork" ? "⏎ forks" : "⏎ resumes";
|
|
484
|
+
const keys = ["↑↓ move", enter, tab, "esc"].filter((k) => k !== null);
|
|
480
485
|
const edge = Math.min(W, Math.max(table, visibleWidth(keys.join(" · ")) + count.length + 5));
|
|
481
486
|
const fits = (ks) => 2 + visibleWidth(ks.join(" · ")) + 2 + count.length + 1 <= edge;
|
|
482
487
|
let kept = keys;
|
|
483
|
-
for (const drop of [tab, "↑↓ move",
|
|
488
|
+
for (const drop of [tab, "↑↓ move", enter, "esc"]) {
|
|
484
489
|
if (fits(kept))
|
|
485
490
|
break;
|
|
486
491
|
kept = kept.filter((k) => k !== drop);
|
|
@@ -491,6 +496,7 @@ function resumeKeyRow(state, W, table) {
|
|
|
491
496
|
}
|
|
492
497
|
/** What the empty input says while the picker is up: typing there filters. */
|
|
493
498
|
export const RESUME_FILTER_HINT = "filter by title or id";
|
|
499
|
+
export const FORK_FILTER_HINT = "filter replies";
|
|
494
500
|
/**
|
|
495
501
|
* The whole band: the named hairline, the windowed table with the
|
|
496
502
|
* selected row opened, and the key row. Returned as plain strings for the
|
|
@@ -503,13 +509,15 @@ export function sessionPickerRows(state, W, now, height = 24) {
|
|
|
503
509
|
const p = palette();
|
|
504
510
|
const scope = state.scope ?? null;
|
|
505
511
|
const query = state.query ?? "";
|
|
506
|
-
const
|
|
512
|
+
const filter = query === "" ? null : { matches: state.matches.length, of: state.cards.length };
|
|
513
|
+
const title = state.kind === "fork" ? (filter === null ? "fork after a reply" : `fork after a reply · ${filter.matches} of ${filter.of} match`) : scopeTitle(scope, filter);
|
|
514
|
+
const rows = [bandHeader(title, W)];
|
|
507
515
|
// the workspace column shows only when every workspace is listed
|
|
508
516
|
const where = scope !== null && scope.all ? scope.here : null;
|
|
509
517
|
const cols = resumeColumns(state.cards, W, now, where);
|
|
510
518
|
if (state.matches.length === 0) {
|
|
511
519
|
// an empty view says why, rather than an empty band
|
|
512
|
-
const empty = query !== "" ? `nothing matches “${escapeTerminal(query)}”` : scope !== null && !scope.all && scope.inHere === 0 ? "no session from this workspace yet" : "nothing to resume yet";
|
|
520
|
+
const empty = query !== "" ? `nothing matches “${escapeTerminal(query)}”` : state.kind === "fork" ? "no finished reply to fork after yet" : scope !== null && !scope.all && scope.inHere === 0 ? "no session from this workspace yet" : "nothing to resume yet";
|
|
513
521
|
rows.push(`${p.dim}${widthCut(` ${empty}`, W)}${p.reset}`);
|
|
514
522
|
rows.push(resumeKeyRow(state, W, cols.table));
|
|
515
523
|
return rows;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.54.0",
|
|
4
4
|
"description": "kiso tui \u2014 the pure terminal layer (cell renderer, dock, raw editor, diff, palette). Zero runtime dependencies: input is data, output is bytes.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -35,6 +35,6 @@
|
|
|
35
35
|
},
|
|
36
36
|
"homepage": "https://github.com/vincemakes/kiso/tree/main/packages/tui#readme",
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@vincemakes/kiso-tui-cells": "0.
|
|
38
|
+
"@vincemakes/kiso-tui-cells": "0.54.0"
|
|
39
39
|
}
|
|
40
40
|
}
|