@vincemakes/kiso-tui 0.39.2 → 0.40.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 +39 -3
- package/dist/compositor.js +133 -8
- package/dist/context-ledger.d.ts +5 -0
- package/dist/context-ledger.js +11 -2
- package/dist/editor.d.ts +22 -7
- package/dist/editor.js +111 -40
- package/dist/index.d.ts +3 -3
- package/dist/index.js +6 -3
- package/dist/lines.d.ts +5 -0
- package/dist/lines.js +19 -4
- package/dist/panel-input.d.ts +8 -0
- package/dist/panel-input.js +17 -4
- package/dist/pick-input.d.ts +5 -1
- package/dist/pick-input.js +33 -5
- package/dist/session-picker.d.ts +54 -26
- package/dist/session-picker.js +109 -54
- package/dist/status.d.ts +62 -2
- package/dist/status.js +94 -42
- package/package.json +2 -2
package/dist/session-picker.d.ts
CHANGED
|
@@ -2,20 +2,12 @@
|
|
|
2
2
|
* TUI2-R2 slices ①–③ — the session picker's PURE half: the durability
|
|
3
3
|
* badge, the row, the band, and the filter.
|
|
4
4
|
*
|
|
5
|
-
* The
|
|
6
|
-
* session survives kill -9 and resumes from its durable prefix;
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* The vocabulary (the palette's functional set — no new colour):
|
|
13
|
-
*
|
|
14
|
-
* ✓ green the run's terminal event says completed
|
|
15
|
-
* ✗ red the terminal says anything else
|
|
16
|
-
* ▌ bold no terminal event — interrupted mid-run
|
|
17
|
-
* ? warn the uncertain ledger is not empty (overrides ▌)
|
|
18
|
-
* ◌ dim a permission request nobody has answered
|
|
5
|
+
* The row's STATE is the round's whole argument. kiso's claim is that a
|
|
6
|
+
* session survives kill -9 and resumes from its durable prefix; the note
|
|
7
|
+
* column makes it a thing you can READ before you pick: this one completed,
|
|
8
|
+
* this one was cut mid-run and will resume exactly, this one is holding a
|
|
9
|
+
* question for you. (0.40.1, owner's ruling: words, never glyphs — the
|
|
10
|
+
* ✓ ✗ ▌ ? ◌ column that stood here said nothing the words did not.)
|
|
19
11
|
*
|
|
20
12
|
* Purity, as everywhere in this package: the cards are DATA the CLI
|
|
21
13
|
* projects (session-cards.ts) and this module turns them into bytes. It
|
|
@@ -32,21 +24,46 @@ export interface SessionCardView {
|
|
|
32
24
|
* a caller that has not got one still renders — the row simply
|
|
33
25
|
* carries no title, which is where this picker started. */
|
|
34
26
|
readonly title?: string;
|
|
35
|
-
readonly badge: "uncertain" | "ask" | "interrupted" | "completed" | "failed";
|
|
36
|
-
|
|
27
|
+
readonly badge: "uncertain" | "ask" | "interrupted" | "completed" | "failed" | "unknown";
|
|
28
|
+
/** null when unknown — the row then says nothing about turns */
|
|
29
|
+
readonly turns: number | null;
|
|
37
30
|
readonly updatedAt: number;
|
|
38
31
|
readonly uncertain: number;
|
|
39
32
|
readonly asks: number;
|
|
40
33
|
readonly outcome: string | null;
|
|
34
|
+
/** 0.40.0: the realpath the session STARTED in, from its profile; null
|
|
35
|
+
* when unknown (a legacy session). Absent = the caller did not say. */
|
|
36
|
+
readonly workspace?: string | null;
|
|
37
|
+
/** 0.40.0: the config profile its latest revision names, for a dim tag. */
|
|
38
|
+
readonly profileName?: string | null;
|
|
39
|
+
/** 0.40.0: its project was inferred by the one-time migration, never
|
|
40
|
+
* recorded — the row says so. */
|
|
41
|
+
readonly inferred?: boolean;
|
|
41
42
|
}
|
|
42
|
-
/**
|
|
43
|
-
*
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
|
|
43
|
+
/** 0.40.0 — which sessions the picker shows. `here` is the running
|
|
44
|
+
* workspace's realpath; `all` is the person's choice; `unknown` counts
|
|
45
|
+
* the sessions with no recorded workspace (0.40.1), which the default view
|
|
46
|
+
* hides behind one header row.
|
|
47
|
+
*
|
|
48
|
+
* 0.40.1 (owner's ruling): the default view NEVER falls back to all. The
|
|
49
|
+
* fallback made every directory list every older session — 118 of them —
|
|
50
|
+
* which is the view the scope exists to prevent. */
|
|
51
|
+
export interface PickScopeState {
|
|
52
|
+
readonly here: string;
|
|
53
|
+
readonly all: boolean;
|
|
54
|
+
readonly inHere: number;
|
|
55
|
+
readonly total: number;
|
|
56
|
+
readonly unknown: number;
|
|
57
|
+
}
|
|
58
|
+
/** The scope, as a pure function of the cards: CURRENT is the sessions
|
|
59
|
+
* whose recorded workspace IS the running one; a session with no recorded
|
|
60
|
+
* workspace is never "here" — unknown history is shown under ALL only. */
|
|
61
|
+
export declare function scopeSessions(cards: readonly SessionCardView[], here: string, wantAll: boolean): {
|
|
62
|
+
readonly cards: readonly SessionCardView[];
|
|
63
|
+
readonly scope: PickScopeState;
|
|
64
|
+
};
|
|
65
|
+
/** The band's title — the scope and both counts, and the key that flips it. */
|
|
66
|
+
export declare function scopeTitle(scope: PickScopeState | null): string;
|
|
50
67
|
/**
|
|
51
68
|
* What the row SAYS about the state. The interrupted note is the
|
|
52
69
|
* product's promise stated in the place the promise matters: the run
|
|
@@ -100,7 +117,7 @@ export declare function sessionFilter(cards: readonly SessionCardView[], query:
|
|
|
100
117
|
* The inner spans close with rvEnd inside the bar (never SGR 0, which
|
|
101
118
|
* would punch a hole in it) — the same composition atRow uses.
|
|
102
119
|
*/
|
|
103
|
-
export declare function sessionRow(card: SessionCardView, selected: boolean, W: number, now: number, idCol: number): string;
|
|
120
|
+
export declare function sessionRow(card: SessionCardView, selected: boolean, W: number, now: number, idCol: number, here?: string | null): string;
|
|
104
121
|
/** The counter row — the SELECTION's 1-based place in the whole
|
|
105
122
|
* filtered list, which the visible window cannot tell the user. */
|
|
106
123
|
export declare function sessionCounterRow(selected: number, total: number, W: number): string;
|
|
@@ -110,6 +127,8 @@ export interface SessionPickState {
|
|
|
110
127
|
readonly cards: readonly SessionCardView[];
|
|
111
128
|
readonly matches: readonly SessionCardView[];
|
|
112
129
|
readonly selected: number;
|
|
130
|
+
/** 0.40.0: null when the picker was opened without a workspace. */
|
|
131
|
+
readonly scope?: PickScopeState | null;
|
|
113
132
|
}
|
|
114
133
|
/**
|
|
115
134
|
* The whole band: the `sessions` header (R1.5 ⑦(b) — a band names
|
|
@@ -137,7 +156,16 @@ export declare function sessionPickerRows(state: SessionPickState, W: number, no
|
|
|
137
156
|
* id goes LAST and dim — present for the hand that needs it, out of the
|
|
138
157
|
* way of the eye that does not.
|
|
139
158
|
*/
|
|
140
|
-
export declare function sessionListRow(card: SessionCardView, W: number, now: number, idCol: number): string;
|
|
159
|
+
export declare function sessionListRow(card: SessionCardView, W: number, now: number, idCol: number, here?: string | null): string;
|
|
141
160
|
/** Slice ③ — the listing's last line: the count, and the one thing the
|
|
142
161
|
* user can do next. */
|
|
143
162
|
export declare function sessionListFooter(count: number, W: number): string;
|
|
163
|
+
/** 0.40.1 — the `kiso sessions` TTY listing's line for the sessions with no
|
|
164
|
+
* recorded workspace: counted, never listed, and the flag that lists them.
|
|
165
|
+
* Empty when there are none. */
|
|
166
|
+
export declare function sessionListUnknownLine(unknown: number, W: number): string;
|
|
167
|
+
/** 0.40.0 — the `kiso sessions` TTY listing's FIRST line: which sessions
|
|
168
|
+
* follow, and both counts. The listing never falls back the way the
|
|
169
|
+
* picker does: a listing that says "0 of 5 from this workspace" and how
|
|
170
|
+
* to see the rest is already the honest answer. */
|
|
171
|
+
export declare function sessionListHeader(inHere: number, total: number, all: boolean, W: number): string;
|
package/dist/session-picker.js
CHANGED
|
@@ -2,20 +2,12 @@
|
|
|
2
2
|
* TUI2-R2 slices ①–③ — the session picker's PURE half: the durability
|
|
3
3
|
* badge, the row, the band, and the filter.
|
|
4
4
|
*
|
|
5
|
-
* The
|
|
6
|
-
* session survives kill -9 and resumes from its durable prefix;
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* The vocabulary (the palette's functional set — no new colour):
|
|
13
|
-
*
|
|
14
|
-
* ✓ green the run's terminal event says completed
|
|
15
|
-
* ✗ red the terminal says anything else
|
|
16
|
-
* ▌ bold no terminal event — interrupted mid-run
|
|
17
|
-
* ? warn the uncertain ledger is not empty (overrides ▌)
|
|
18
|
-
* ◌ dim a permission request nobody has answered
|
|
5
|
+
* The row's STATE is the round's whole argument. kiso's claim is that a
|
|
6
|
+
* session survives kill -9 and resumes from its durable prefix; the note
|
|
7
|
+
* column makes it a thing you can READ before you pick: this one completed,
|
|
8
|
+
* this one was cut mid-run and will resume exactly, this one is holding a
|
|
9
|
+
* question for you. (0.40.1, owner's ruling: words, never glyphs — the
|
|
10
|
+
* ✓ ✗ ▌ ? ◌ column that stood here said nothing the words did not.)
|
|
19
11
|
*
|
|
20
12
|
* Purity, as everywhere in this package: the cards are DATA the CLI
|
|
21
13
|
* projects (session-cards.ts) and this module turns them into bytes. It
|
|
@@ -26,31 +18,53 @@
|
|
|
26
18
|
import { escapeTerminal, palette } from "./lines.js";
|
|
27
19
|
import { selectionBar, visibleWidth, widthCut } from "./components.js";
|
|
28
20
|
import { atEmbed, bandHeader, longestRun, AT_VISIBLE, atWindow } from "./at-picker.js";
|
|
29
|
-
/** The
|
|
30
|
-
*
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
21
|
+
/** The scope, as a pure function of the cards: CURRENT is the sessions
|
|
22
|
+
* whose recorded workspace IS the running one; a session with no recorded
|
|
23
|
+
* workspace is never "here" — unknown history is shown under ALL only. */
|
|
24
|
+
export function scopeSessions(cards, here, wantAll) {
|
|
25
|
+
const inHere = cards.filter((c) => c.workspace === here);
|
|
26
|
+
const unknown = cards.filter((c) => c.workspace === null || c.workspace === undefined).length;
|
|
27
|
+
return { cards: wantAll ? cards : inHere, scope: { here, all: wantAll, inHere: inHere.length, total: cards.length, unknown } };
|
|
28
|
+
}
|
|
29
|
+
/** The band's title — the scope and both counts, and the key that flips it. */
|
|
30
|
+
export function scopeTitle(scope) {
|
|
31
|
+
if (scope === null)
|
|
32
|
+
return "sessions";
|
|
33
|
+
return scope.all
|
|
34
|
+
? `sessions \u00b7 all ${scope.total} \u00b7 tab this workspace (${scope.inHere})`
|
|
35
|
+
: `sessions \u00b7 this workspace ${scope.inHere} of ${scope.total} \u00b7 tab all`;
|
|
36
|
+
}
|
|
37
|
+
/** A workspace path as a person reads it: the home directory as `~`. */
|
|
38
|
+
function tildePath(path) {
|
|
39
|
+
const home = process.env.HOME;
|
|
40
|
+
return home !== undefined && home !== "" && (path === home || path.startsWith(`${home}/`)) ? `~${path.slice(home.length)}` : path;
|
|
41
|
+
}
|
|
42
|
+
/** 0.40.0 — the row's dim tags: the profile the session last ran under,
|
|
43
|
+
* and — only when the row is from ANOTHER workspace than `here` — where
|
|
44
|
+
* it came from. `here === null` means the listing is not scoped at all,
|
|
45
|
+
* so no row is foreign.
|
|
46
|
+
*
|
|
47
|
+
* Fitted to `room`, degrading by the DC-2 rule (drop or shorten a whole
|
|
48
|
+
* part, never cut one mid-word): the full path, then `…/<last dir>`, then
|
|
49
|
+
* the path alone without the profile, then nothing. An inferred row keeps
|
|
50
|
+
* its "inferred" mark longest. */
|
|
51
|
+
function fitTags(card, here, room) {
|
|
52
|
+
const profile = typeof card.profileName === "string" && card.profileName !== "" ? card.profileName : null;
|
|
53
|
+
const foreign = here !== null && card.workspace !== undefined && card.workspace !== here;
|
|
54
|
+
const where = !foreign ? null : card.workspace === null || card.workspace === undefined ? "workspace unknown" : tildePath(card.workspace);
|
|
55
|
+
const short = where === null || card.workspace === null || card.workspace === undefined ? where : `\u2026/${card.workspace.split("/").filter((x) => x !== "").at(-1) ?? ""}`;
|
|
56
|
+
const join = (parts) => {
|
|
57
|
+
const kept = parts.filter((x) => x !== null);
|
|
58
|
+
return kept.length === 0 ? "" : ` \u00b7 ${kept.join(" \u00b7 ")}`;
|
|
59
|
+
};
|
|
60
|
+
// 0.40.0: an inferred project is a guess, and the row never presents a
|
|
61
|
+
// guess as a record; the mark outlives the profile when room is short
|
|
62
|
+
const mark = card.inferred === true ? "inferred" : null;
|
|
63
|
+
for (const candidate of [join([profile, where, mark]), join([profile, short, mark]), join([short, mark]), join([profile, mark]), join([mark]), join([profile])]) {
|
|
64
|
+
if (candidate !== "" && visibleWidth(candidate) <= room)
|
|
65
|
+
return candidate;
|
|
66
|
+
}
|
|
67
|
+
return "";
|
|
54
68
|
}
|
|
55
69
|
/**
|
|
56
70
|
* What the row SAYS about the state. The interrupted note is the
|
|
@@ -73,6 +87,10 @@ export function sessionNote(card) {
|
|
|
73
87
|
return "interrupted mid-run — resumes exactly";
|
|
74
88
|
case "completed":
|
|
75
89
|
return "completed clean";
|
|
90
|
+
case "unknown":
|
|
91
|
+
// 0.40.0 dogfood: no summary, or a log that could not be read —
|
|
92
|
+
// said, never guessed
|
|
93
|
+
return card.outcome ?? "no summary";
|
|
76
94
|
default:
|
|
77
95
|
return card.outcome === null || card.outcome === "error" ? "failed" : card.outcome.replaceAll("_", " ");
|
|
78
96
|
}
|
|
@@ -173,7 +191,7 @@ export function sessionFilter(cards, query) {
|
|
|
173
191
|
* executions"), so an actionable row keeps saying so. */
|
|
174
192
|
const TITLE_MAX = 44;
|
|
175
193
|
const NOTE_RESERVE = 22;
|
|
176
|
-
function rowSpans(card, budget, now, idCol) {
|
|
194
|
+
function rowSpans(card, budget, now, idCol, here = null) {
|
|
177
195
|
const p = palette();
|
|
178
196
|
let text = "";
|
|
179
197
|
let w = 0;
|
|
@@ -185,12 +203,9 @@ function rowSpans(card, budget, now, idCol) {
|
|
|
185
203
|
text += styled;
|
|
186
204
|
w += cells;
|
|
187
205
|
};
|
|
188
|
-
//
|
|
189
|
-
//
|
|
190
|
-
|
|
191
|
-
text += `${sessionBadge(card.badge)} `;
|
|
192
|
-
w += 2;
|
|
193
|
-
}
|
|
206
|
+
// 0.40.1 (owner's ruling): NO status glyph. The ✓ ✗ ▌ ? ◌ column is
|
|
207
|
+
// gone; the state is a WORD, in the note column (sessionNote), where it
|
|
208
|
+
// already said everything the glyph did.
|
|
194
209
|
// R2 (owner, 2026-08-27) — the TITLE LEADS and the id is gone.
|
|
195
210
|
//
|
|
196
211
|
// The id was four characters of machine identity sitting in the column
|
|
@@ -212,8 +227,17 @@ function rowSpans(card, budget, now, idCol) {
|
|
|
212
227
|
if (cut !== "")
|
|
213
228
|
put(cut, `${p.bold}${cut}${p.reset}`);
|
|
214
229
|
}
|
|
215
|
-
const meta = ` ${sessionAge(card.updatedAt, now)} · ${card.turns} turn${card.turns === 1 ? "" : "s"}`;
|
|
230
|
+
const meta = ` ${sessionAge(card.updatedAt, now)}${card.turns === null ? "" : ` · ${card.turns} turn${card.turns === 1 ? "" : "s"}`}`;
|
|
216
231
|
put(meta, `${p.dim}${meta}${p.reset}`);
|
|
232
|
+
// 0.40.0: the tags are their OWN span, after the meta and before the
|
|
233
|
+
// note, and they give way first — a long workspace path must never take
|
|
234
|
+
// the age and the turn count down with it, nor the note's reserve.
|
|
235
|
+
// the note is what the person acts on ("needs your verdict"), so it keeps
|
|
236
|
+
// its WHOLE width — a tag that cut it would trade an action for a label
|
|
237
|
+
const noteCells = sessionNote(card) === "" ? 0 : visibleWidth(sessionNote(card)) + 3;
|
|
238
|
+
const tags = fitTags(card, here, Math.max(0, budget - w - noteCells));
|
|
239
|
+
if (tags !== "")
|
|
240
|
+
put(tags, `${p.dim}${tags}${p.reset}`);
|
|
217
241
|
const note = widthCut(sessionNote(card), Math.max(0, budget - w - 3));
|
|
218
242
|
if (note !== "") {
|
|
219
243
|
// the ? note carries the warn tint — the row's own words are what
|
|
@@ -232,13 +256,13 @@ function rowSpans(card, budget, now, idCol) {
|
|
|
232
256
|
* The inner spans close with rvEnd inside the bar (never SGR 0, which
|
|
233
257
|
* would punch a hole in it) — the same composition atRow uses.
|
|
234
258
|
*/
|
|
235
|
-
export function sessionRow(card, selected, W, now, idCol) {
|
|
259
|
+
export function sessionRow(card, selected, W, now, idCol, here = null) {
|
|
236
260
|
const p = palette();
|
|
237
261
|
// both forms spend two cells of the width on their frame — the
|
|
238
262
|
// unselected row's indent, the bar's own leading/trailing cell — so
|
|
239
263
|
// the spans are built against the same budget either way and the
|
|
240
264
|
// selection cannot change the columns
|
|
241
|
-
const { text, width } = rowSpans(card, Math.max(0, W - 2), now, idCol);
|
|
265
|
+
const { text, width } = rowSpans(card, Math.max(0, W - 2), now, idCol, here);
|
|
242
266
|
if (!selected)
|
|
243
267
|
return ` ${text}`;
|
|
244
268
|
// R2: one bar, in one place — and with it §2.1's rule that dim never
|
|
@@ -261,17 +285,29 @@ export function sessionCounterRow(selected, total, W) {
|
|
|
261
285
|
* channel.
|
|
262
286
|
*/
|
|
263
287
|
export function sessionPickerRows(state, W, now) {
|
|
264
|
-
const
|
|
288
|
+
const scope = state.scope ?? null;
|
|
289
|
+
const rows = [bandHeader(scopeTitle(scope), W)];
|
|
290
|
+
// 0.40.1: the sessions without a workspace, as ONE row under CURRENT —
|
|
291
|
+
// counted, never listed (tab shows them, labelled)
|
|
292
|
+
if (scope !== null && !scope.all && scope.unknown > 0) {
|
|
293
|
+
const p = palette();
|
|
294
|
+
rows.push(`${p.dim}${widthCut(` ${scope.unknown} older session${scope.unknown === 1 ? "" : "s"} without a workspace \u00b7 tab all`, W)}${p.reset}`);
|
|
295
|
+
}
|
|
296
|
+
// a row is tagged with its workspace only when ALL is showing — under
|
|
297
|
+
// CURRENT every row is from here, and saying so eight times is noise
|
|
298
|
+
const here = scope !== null && scope.all ? scope.here : null;
|
|
265
299
|
const col = idColumn(state.cards);
|
|
266
300
|
if (state.matches.length === 0) {
|
|
267
301
|
const p = palette();
|
|
268
|
-
|
|
302
|
+
// an empty CURRENT view says why, rather than an empty band
|
|
303
|
+
const empty = scope !== null && !scope.all && scope.inHere === 0 ? " no session from this workspace yet" : " no session matches";
|
|
304
|
+
rows.push(`${p.dim}${widthCut(empty, W)}${p.reset}`);
|
|
269
305
|
rows.push(sessionCounterRow(0, 0, W));
|
|
270
306
|
return rows;
|
|
271
307
|
}
|
|
272
308
|
const { first, count } = atWindow(state.matches.length, state.selected, AT_VISIBLE);
|
|
273
309
|
for (let i = first; i < first + count; i += 1)
|
|
274
|
-
rows.push(sessionRow(state.matches[i], i === state.selected, W, now, col));
|
|
310
|
+
rows.push(sessionRow(state.matches[i], i === state.selected, W, now, col, here));
|
|
275
311
|
rows.push(sessionCounterRow(state.selected, state.matches.length, W));
|
|
276
312
|
return rows;
|
|
277
313
|
}
|
|
@@ -292,10 +328,10 @@ export function sessionPickerRows(state, W, now) {
|
|
|
292
328
|
* id goes LAST and dim — present for the hand that needs it, out of the
|
|
293
329
|
* way of the eye that does not.
|
|
294
330
|
*/
|
|
295
|
-
export function sessionListRow(card, W, now, idCol) {
|
|
331
|
+
export function sessionListRow(card, W, now, idCol, here = null) {
|
|
296
332
|
const p = palette();
|
|
297
333
|
const tail = ` ${card.id}`;
|
|
298
|
-
const { text, width } = rowSpans(card, Math.max(1, W - visibleWidth(tail)), now, idCol);
|
|
334
|
+
const { text, width } = rowSpans(card, Math.max(1, W - visibleWidth(tail)), now, idCol, here);
|
|
299
335
|
if (width + visibleWidth(tail) > W)
|
|
300
336
|
return text; // a terminal too narrow for both keeps the words
|
|
301
337
|
return `${text}${p.dim}${tail}${p.reset}`;
|
|
@@ -306,3 +342,22 @@ export function sessionListFooter(count, W) {
|
|
|
306
342
|
const p = palette();
|
|
307
343
|
return `${p.dim}${widthCut(`${count} session${count === 1 ? "" : "s"} · kiso resume picks interactively`, W)}${p.reset}`;
|
|
308
344
|
}
|
|
345
|
+
/** 0.40.1 — the `kiso sessions` TTY listing's line for the sessions with no
|
|
346
|
+
* recorded workspace: counted, never listed, and the flag that lists them.
|
|
347
|
+
* Empty when there are none. */
|
|
348
|
+
export function sessionListUnknownLine(unknown, W) {
|
|
349
|
+
if (unknown === 0)
|
|
350
|
+
return "";
|
|
351
|
+
const p = palette();
|
|
352
|
+
return `${p.dim}${widthCut(`${unknown} older session${unknown === 1 ? "" : "s"} without a workspace \u00b7 --all`, W)}${p.reset}`;
|
|
353
|
+
}
|
|
354
|
+
/** 0.40.0 — the `kiso sessions` TTY listing's FIRST line: which sessions
|
|
355
|
+
* follow, and both counts. The listing never falls back the way the
|
|
356
|
+
* picker does: a listing that says "0 of 5 from this workspace" and how
|
|
357
|
+
* to see the rest is already the honest answer. */
|
|
358
|
+
export function sessionListHeader(inHere, total, all, W) {
|
|
359
|
+
const p = palette();
|
|
360
|
+
const plural = (n) => `${n} session${n === 1 ? "" : "s"}`;
|
|
361
|
+
const text = all ? `all ${plural(total)}` : `${inHere} of ${plural(total)} from this workspace \u00b7 --all lists every one`;
|
|
362
|
+
return `${p.dim}${widthCut(text, W)}${p.reset}`;
|
|
363
|
+
}
|
package/dist/status.d.ts
CHANGED
|
@@ -21,6 +21,14 @@
|
|
|
21
21
|
* deliberate exception: the running row's interrupt hint, which KC2 §2
|
|
22
22
|
* widens to name the new gesture.
|
|
23
23
|
*/
|
|
24
|
+
/** 0.40.0 — the compacting row's bar: output produced so far against the
|
|
25
|
+
* summary call's output budget (text AND reasoning — see the runtime's
|
|
26
|
+
* SummaryProgress), and whether reasoning was billed without streaming. */
|
|
27
|
+
export interface CompactingProgress {
|
|
28
|
+
readonly produced: number;
|
|
29
|
+
readonly budget: number | null;
|
|
30
|
+
readonly reasoningUnseen: boolean;
|
|
31
|
+
}
|
|
24
32
|
/**
|
|
25
33
|
* R3 (design §5.2) — the working glyph family is the TWINKLE, and the
|
|
26
34
|
* CLI's 200ms spinner walks it exactly as it walked the four quadrant
|
|
@@ -55,6 +63,37 @@ export declare const STATUS_GLYPHS: readonly ["✧", "✦", "✶", "✸", "✺",
|
|
|
55
63
|
* the same reason.
|
|
56
64
|
*/
|
|
57
65
|
export declare function decodeRate(outputTokens: number | null, elapsedMs: number): number | null;
|
|
66
|
+
/**
|
|
67
|
+
* THE ROW SEAM (0.40.0). One composer for every status row.
|
|
68
|
+
*
|
|
69
|
+
* Three rows grew their own string-building — the running row, the idle
|
|
70
|
+
* row, and the compacting row inline in dispatch — and three features of
|
|
71
|
+
* the launch build want to add to them at once (a retry state, a mode
|
|
72
|
+
* and floor indicator, a compaction progress bar). Each splicing its own
|
|
73
|
+
* segment into its own template is how a row ends up with two of its
|
|
74
|
+
* facts cut by invariant ① at 80 columns, because nobody decided what
|
|
75
|
+
* gives way first. So the decision is made HERE, once, and every row is
|
|
76
|
+
* a list of typed segments:
|
|
77
|
+
*
|
|
78
|
+
* - `fact` — a measurement or a state (the tier, ctx, a rate, a retry
|
|
79
|
+
* count). NEVER dropped and never cut; a row that silently
|
|
80
|
+
* drops a measurement is the defect DF-0330-F1 fixed;
|
|
81
|
+
* - `hint` — teaches a gesture (`esc stop`, `/mode to switch`). Dropped
|
|
82
|
+
* first, from the END, because the row is not the only
|
|
83
|
+
* place a gesture is taught;
|
|
84
|
+
* - `label` — a name that may be ELIDED in its middle (the model id),
|
|
85
|
+
* tried before any hint is dropped, because eliding gives
|
|
86
|
+
* the row its budget back instead of re-allocating a deficit.
|
|
87
|
+
*
|
|
88
|
+
* The first entry is the row's HEAD and is never touched. Everything is
|
|
89
|
+
* joined with ` · `. With no `W` the row is the full composition — every
|
|
90
|
+
* caller that does not know its width gets exactly the row it had.
|
|
91
|
+
*/
|
|
92
|
+
export interface RowSegment {
|
|
93
|
+
readonly text: string;
|
|
94
|
+
readonly kind: "fact" | "hint" | "label";
|
|
95
|
+
}
|
|
96
|
+
export declare function composeRow(head: string, segments: readonly (RowSegment | null | undefined)[], W?: number): string;
|
|
58
97
|
/**
|
|
59
98
|
* The RUNNING row: the rotating glyph, the wall seconds since `since`
|
|
60
99
|
* (never below 1 — a run that just started still reads "1s", so the row
|
|
@@ -65,7 +104,28 @@ export declare function decodeRate(outputTokens: number | null, elapsedMs: numbe
|
|
|
65
104
|
* — stop, and do THIS instead. The row is where the gesture is taught,
|
|
66
105
|
* because it is on screen exactly when the gesture is useful.
|
|
67
106
|
*/
|
|
68
|
-
|
|
107
|
+
/** ADR-0005 Amendment 2 — a retry the kernel is waiting on, as the row
|
|
108
|
+
* shows it. `remainingMs` is how much of the wait is left; at or below
|
|
109
|
+
* zero the attempt is in flight and the countdown is gone. */
|
|
110
|
+
export interface RetryOnRow {
|
|
111
|
+
readonly attempt: number;
|
|
112
|
+
readonly maxRetries: number;
|
|
113
|
+
readonly code: string;
|
|
114
|
+
readonly remainingMs: number;
|
|
115
|
+
}
|
|
116
|
+
/** `retrying 3/10 · network · 4s` — ONE fact: the attempt, the budget it
|
|
117
|
+
* counts against, what failed, and how long until it is tried. Whole
|
|
118
|
+
* seconds, rounded UP, so the row never says 0s while still waiting. */
|
|
119
|
+
export declare function retrySegment(r: RetryOnRow): string;
|
|
120
|
+
export declare function runningStatus(glyph: string, since: number, outTokens: number | null, ctxRatio: number, tokPerSec?: number | null, W?: number, retry?: RetryOnRow | null): string;
|
|
121
|
+
/**
|
|
122
|
+
* The COMPACTING row (W18): the covered rounds, the pre-call token
|
|
123
|
+
* estimate, and the elapsed seconds — all knowable before the one summary
|
|
124
|
+
* call returns, which has no fraction of its own. Moved here from an
|
|
125
|
+
* inline template in dispatch (0.40.0) so it composes like every other
|
|
126
|
+
* row and has a place for what the launch build adds to it.
|
|
127
|
+
*/
|
|
128
|
+
export declare function compactingStatus(glyph: string, rounds: number, tokens: number, elapsedSeconds: number, W?: number, retry?: RetryOnRow | null, progress?: CompactingProgress | null): string;
|
|
69
129
|
/**
|
|
70
130
|
* TUI2-R1 (E) — the idle row's meter: what the session has SPENT, next
|
|
71
131
|
* to what it has left.
|
|
@@ -125,7 +185,7 @@ export interface StatusMeter {
|
|
|
125
185
|
* No `W` means no dropping, which is what the callers that do not know
|
|
126
186
|
* their width should get: today's row, unchanged.
|
|
127
187
|
*/
|
|
128
|
-
export declare function idleStatus(tier: string, model: string, ctxRatio: number, meter?: StatusMeter, W?: number): string;
|
|
188
|
+
export declare function idleStatus(tier: string, model: string, ctxRatio: number, meter?: StatusMeter, W?: number, floorOff?: boolean): string;
|
|
129
189
|
/** TUI2-R1 (E) — the cache hit rate the status row shows, from the usage
|
|
130
190
|
* the CLI already tracks. The denominator is the TOTAL the model was
|
|
131
191
|
* given (fresh + cacheRead), which is the E2 ruling's own: cacheRead
|
package/dist/status.js
CHANGED
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
* widens to name the new gesture.
|
|
23
23
|
*/
|
|
24
24
|
import { kUnit } from "./lines.js";
|
|
25
|
+
import { meterGlyphs } from "./context-ledger.js";
|
|
26
|
+
/** The compacting row's bar width — short: it shares a row. */
|
|
27
|
+
const BAR_ON_ROW = 6;
|
|
25
28
|
import { elapsedLabel } from "@vincemakes/kiso-tui-cells";
|
|
26
29
|
import { TWINKLE } from "@vincemakes/kiso-tui-cells/render";
|
|
27
30
|
import { displayWidth } from "@vincemakes/kiso-tui-cells/width";
|
|
@@ -85,31 +88,88 @@ export function decodeRate(outputTokens, elapsedMs) {
|
|
|
85
88
|
const rate = Math.round(outputTokens / (elapsedMs / 1000));
|
|
86
89
|
return rate > 0 ? rate : null;
|
|
87
90
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
91
|
+
export function composeRow(head, segments, W) {
|
|
92
|
+
const present = segments.filter((x) => x != null && x.text !== "");
|
|
93
|
+
const join = (xs) => [head, ...xs.map((x) => x.text)].join(" · ");
|
|
94
|
+
const full = join(present);
|
|
95
|
+
if (W === undefined || displayWidth(full) <= W)
|
|
96
|
+
return full;
|
|
97
|
+
// 1. elide every label in its middle
|
|
98
|
+
let row = present.map((x) => (x.kind === "label" ? { ...x, text: elideMiddle(x.text, LABEL_ON_ROW) } : x));
|
|
99
|
+
if (displayWidth(join(row)) <= W)
|
|
100
|
+
return join(row);
|
|
101
|
+
// 2. drop hints from the end, one at a time
|
|
102
|
+
for (let i = row.length - 1; i >= 0; i -= 1) {
|
|
103
|
+
if (row[i].kind !== "hint")
|
|
104
|
+
continue;
|
|
105
|
+
row = [...row.slice(0, i), ...row.slice(i + 1)];
|
|
106
|
+
if (displayWidth(join(row)) <= W)
|
|
107
|
+
return join(row);
|
|
108
|
+
}
|
|
109
|
+
// 3. facts are never dropped: past this point the row is over budget,
|
|
110
|
+
// and it is invariant ①'s to cut — which it will do to the LAST
|
|
111
|
+
// segment, so the order of facts is the order of their importance.
|
|
112
|
+
return join(row);
|
|
113
|
+
}
|
|
114
|
+
/** `retrying 3/10 · network · 4s` — ONE fact: the attempt, the budget it
|
|
115
|
+
* counts against, what failed, and how long until it is tried. Whole
|
|
116
|
+
* seconds, rounded UP, so the row never says 0s while still waiting. */
|
|
117
|
+
export function retrySegment(r) {
|
|
118
|
+
const head = `retrying ${r.attempt}/${r.maxRetries} · ${r.code}`;
|
|
119
|
+
return r.remainingMs > 0 ? `${head} · ${Math.ceil(r.remainingMs / 1000)}s` : head;
|
|
120
|
+
}
|
|
121
|
+
export function runningStatus(glyph, since, outTokens, ctxRatio, tokPerSec = null, W, retry) {
|
|
99
122
|
const out = outTokens !== null ? ` ↓ ${kUnit(outTokens)} tokens` : "";
|
|
100
|
-
// TPS-1: after each call SETTLES within the turn, between the tokens
|
|
101
|
-
// segment and the stop hint. The default is null and that is the honest
|
|
102
|
-
// rule spelled as a default — the recovery flow has no per-call timing
|
|
103
|
-
// state, so its row says nothing rather than guessing.
|
|
104
|
-
const rate = tokPerSec !== null ? ` · ${tokPerSec} tok/s` : "";
|
|
105
123
|
const seconds = Math.max(1, Math.round((Date.now() - since) / 1000));
|
|
106
|
-
return `${glyph} working ${elapsedLabel(seconds)}${out}
|
|
124
|
+
return composeRow(`${glyph} working ${elapsedLabel(seconds)}${out}`, [
|
|
125
|
+
// ADR-0005 Amendment 2: a pending retry is a FACT and sits first — it
|
|
126
|
+
// is the one thing on the row that explains why nothing is arriving,
|
|
127
|
+
// and a retry budget of minutes with nothing on screen reads as a
|
|
128
|
+
// hung session.
|
|
129
|
+
retry != null ? { kind: "fact", text: retrySegment(retry) } : null,
|
|
130
|
+
// TPS-1: after each call SETTLES within the turn, between the tokens
|
|
131
|
+
// segment and the stop hint. The default is null and that is the
|
|
132
|
+
// honest rule spelled as a default — the recovery flow has no
|
|
133
|
+
// per-call timing state, so its row says nothing rather than guessing.
|
|
134
|
+
tokPerSec !== null ? { kind: "fact", text: `${tokPerSec} tok/s` } : null,
|
|
135
|
+
{ kind: "hint", text: "esc stop" },
|
|
136
|
+
{ kind: "hint", text: "alt+⏎ redirect" },
|
|
137
|
+
{ kind: "fact", text: ctxSegment(ctxRatio) },
|
|
138
|
+
], W);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The COMPACTING row (W18): the covered rounds, the pre-call token
|
|
142
|
+
* estimate, and the elapsed seconds — all knowable before the one summary
|
|
143
|
+
* call returns, which has no fraction of its own. Moved here from an
|
|
144
|
+
* inline template in dispatch (0.40.0) so it composes like every other
|
|
145
|
+
* row and has a place for what the launch build adds to it.
|
|
146
|
+
*/
|
|
147
|
+
export function compactingStatus(glyph, rounds, tokens, elapsedSeconds, W, retry, progress) {
|
|
148
|
+
// 0.40.0: with a budget to measure against, the covered size and the bar
|
|
149
|
+
// are ONE fact — what went in, and how much of the output budget has
|
|
150
|
+
// come out. Without a budget the row keeps the covered size alone: the
|
|
151
|
+
// bar never invents a denominator.
|
|
152
|
+
const covered = progress != null && progress.budget !== null && progress.budget > 0
|
|
153
|
+
? `~${kUnit(tokens)} \u2192 ${meterGlyphs(progress.produced / progress.budget, BAR_ON_ROW)} ${kUnit(progress.produced)}/${kUnit(progress.budget)}`
|
|
154
|
+
: `~${kUnit(tokens)} tokens`;
|
|
155
|
+
return composeRow(`${glyph} compacting`, [
|
|
156
|
+
{ kind: "fact", text: `${rounds} rounds` },
|
|
157
|
+
{ kind: "fact", text: covered },
|
|
158
|
+
// why the figure jumped when the usage landed — a HINT, so a narrow
|
|
159
|
+
// row gives it up before the bar, the seconds or the retry
|
|
160
|
+
progress?.reasoningUnseen === true && progress.budget !== null ? { kind: "hint", text: "incl. unstreamed reasoning" } : null,
|
|
161
|
+
{ kind: "fact", text: `${Math.max(0, elapsedSeconds)}s` },
|
|
162
|
+
// ADR-0005 Amendment 2: the summary call retries under the kernel's
|
|
163
|
+
// policy, and a retry here is the same fact it is on the running row.
|
|
164
|
+
retry != null ? { kind: "fact", text: retrySegment(retry) } : null,
|
|
165
|
+
], W);
|
|
107
166
|
}
|
|
108
|
-
/** DF-0330-F1 — how far
|
|
109
|
-
* visible columns keeps a head and a tail:
|
|
110
|
-
* says which binding is driving, and the
|
|
111
|
-
* distinguish one id from its neighbours
|
|
112
|
-
|
|
167
|
+
/** DF-0330-F1 — how far a LABEL may be squeezed on the ROW; the model id
|
|
168
|
+
* is the one there is. Twenty visible columns keeps a head and a tail:
|
|
169
|
+
* `deepseek-v…s-on-0910` still says which binding is driving, and the
|
|
170
|
+
* tail is where the parts that distinguish one id from its neighbours
|
|
171
|
+
* live (`-flash`, `-0910`). Read by `composeRow`. */
|
|
172
|
+
const LABEL_ON_ROW = 20;
|
|
113
173
|
/** Elide in the MIDDLE, keeping the head and the tail. A string already
|
|
114
174
|
* within budget is returned untouched, so this is a no-op for every
|
|
115
175
|
* ordinary model name.
|
|
@@ -169,27 +229,19 @@ function elideMiddle(text, max) {
|
|
|
169
229
|
* No `W` means no dropping, which is what the callers that do not know
|
|
170
230
|
* their width should get: today's row, unchanged.
|
|
171
231
|
*/
|
|
172
|
-
export function idleStatus(tier, model, ctxRatio, meter, W) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
232
|
+
export function idleStatus(tier, model, ctxRatio, meter, W, floorOff = false) {
|
|
233
|
+
return composeRow(`▸ ${tier}`, [
|
|
234
|
+
// 0.40.0: the catastrophe floor is on by default and says nothing;
|
|
235
|
+
// OFF is the state worth seeing, and a fact beside the tier it
|
|
236
|
+
// changes the meaning of.
|
|
237
|
+
floorOff ? { kind: "fact", text: "floor off" } : null,
|
|
238
|
+
{ kind: "hint", text: "/mode to switch" },
|
|
239
|
+
{ kind: "label", text: model },
|
|
240
|
+
meter?.cacheHitPct != null ? { kind: "fact", text: `CH ${Math.round(meter.cacheHitPct)}%` } : null,
|
|
180
241
|
// costUsd deliberately NOT rendered — see StatusMeter.costUsd.
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
return parts.join(" · ");
|
|
185
|
-
};
|
|
186
|
-
const full = compose(model, true);
|
|
187
|
-
if (W === undefined || displayWidth(full) <= W)
|
|
188
|
-
return full;
|
|
189
|
-
const squeezed = compose(elideMiddle(model, MODEL_ON_ROW), true);
|
|
190
|
-
if (displayWidth(squeezed) <= W)
|
|
191
|
-
return squeezed;
|
|
192
|
-
return compose(elideMiddle(model, MODEL_ON_ROW), false);
|
|
242
|
+
{ kind: "fact", text: ctxSegment(ctxRatio) },
|
|
243
|
+
meter?.tokPerSec != null ? { kind: "fact", text: `${meter.tokPerSec} tok/s` } : null, // TPS-1: last, after the ctx estimate
|
|
244
|
+
], W);
|
|
193
245
|
}
|
|
194
246
|
/** TUI2-R1 (E) — the cache hit rate the status row shows, from the usage
|
|
195
247
|
* the CLI already tracks. The denominator is the TOTAL the model was
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.40.1",
|
|
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.40.1"
|
|
39
39
|
}
|
|
40
40
|
}
|