@vincemakes/kiso-tui 0.46.1 → 0.47.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/ask-panel.d.ts +10 -5
- package/dist/ask-panel.js +109 -54
- package/dist/at-picker.d.ts +38 -68
- package/dist/at-picker.js +50 -141
- package/dist/compositor.d.ts +79 -10
- package/dist/compositor.js +339 -57
- package/dist/context-ledger.d.ts +28 -0
- package/dist/context-ledger.js +62 -0
- package/dist/editor.d.ts +17 -0
- package/dist/editor.js +76 -15
- package/dist/index.d.ts +10 -10
- package/dist/index.js +10 -10
- package/dist/lines.d.ts +11 -0
- package/dist/lines.js +38 -0
- package/dist/notice-meta.d.ts +25 -0
- package/dist/notice-meta.js +83 -0
- package/dist/panel-input.d.ts +2 -2
- package/dist/panel-input.js +70 -60
- package/dist/pick-input.d.ts +23 -3
- package/dist/pick-input.js +9 -29
- package/dist/session-picker.d.ts +37 -22
- package/dist/session-picker.js +270 -57
- package/dist/status.d.ts +78 -75
- package/dist/status.js +182 -108
- package/package.json +2 -2
package/dist/session-picker.js
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*/
|
|
18
18
|
import { escapeTerminal, palette } from "./lines.js";
|
|
19
19
|
import { selectionBar, visibleWidth, widthCut } from "./components.js";
|
|
20
|
-
import { atEmbed, bandHeader,
|
|
20
|
+
import { atEmbed, bandHeader, bandVisible, bandWindow, longestRun } from "./at-picker.js";
|
|
21
21
|
/** The scope, as a pure function of the cards: CURRENT is the sessions
|
|
22
22
|
* whose recorded workspace IS the running one; a session with no recorded
|
|
23
23
|
* workspace is never "here" — unknown history is shown under ALL only. */
|
|
@@ -26,13 +26,17 @@ export function scopeSessions(cards, here, wantAll) {
|
|
|
26
26
|
const unknown = cards.filter((c) => c.workspace === null || c.workspace === undefined).length;
|
|
27
27
|
return { cards: wantAll ? cards : inHere, scope: { here, all: wantAll, inHere: inHere.length, total: cards.length, unknown } };
|
|
28
28
|
}
|
|
29
|
-
/** The band's title —
|
|
30
|
-
|
|
29
|
+
/** The band's title — named after the command that opened it, then the
|
|
30
|
+
* scope and its counts. Graphite P1 (owner, 2026-09-30): the key that
|
|
31
|
+
* flips the scope moved to the key row, and a filter says how many of
|
|
32
|
+
* the scope match. */
|
|
33
|
+
export function scopeTitle(scope, filter = null) {
|
|
34
|
+
const match = filter === null ? null : `${filter.matches} of ${filter.of} match`;
|
|
31
35
|
if (scope === null)
|
|
32
|
-
return "
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
+
return match === null ? "resume" : `resume · ${match}`;
|
|
37
|
+
if (scope.all)
|
|
38
|
+
return `resume · every workspace · ${match ?? scope.total}`;
|
|
39
|
+
return `resume · this workspace · ${match ?? `${scope.inHere} of ${scope.total}`}`;
|
|
36
40
|
}
|
|
37
41
|
/** A workspace path as a person reads it: the home directory as `~`. */
|
|
38
42
|
function tildePath(path) {
|
|
@@ -247,68 +251,277 @@ function rowSpans(card, budget, now, idCol, here = null) {
|
|
|
247
251
|
}
|
|
248
252
|
return { text, width: w };
|
|
249
253
|
}
|
|
250
|
-
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
* two-cell marker in an eighty-column row is a selection you have to
|
|
254
|
-
* hunt for.
|
|
254
|
+
/*
|
|
255
|
+
* Graphite P1 — the /resume picker as a table (owner, 2026-09-30: option
|
|
256
|
+
* B, revision 2, after comparing it with the reference picker).
|
|
255
257
|
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
+
* DECLARED CHANGES, each against the ruling it touches:
|
|
259
|
+
* - The row is a TABLE: the title, then a state word, the age and the
|
|
260
|
+
* turns in columns measured over every card in the scope (never the
|
|
261
|
+
* filtered subset — the idColumn rule, kept), so the eye reads down a
|
|
262
|
+
* column and nothing moves while the person types. The facts used to
|
|
263
|
+
* trail the title and start at a different cell on every row.
|
|
264
|
+
* - The state is a word only when it asks for attention. A finished
|
|
265
|
+
* session says nothing; `interrupted` is blue (unfinished work, the
|
|
266
|
+
* running card's colour), an ask or an uncertain side effect gold (it
|
|
267
|
+
* waits for the person), a failed ending red. 0.40.1's "words, never
|
|
268
|
+
* glyphs" stands — the quiet default is an empty cell, not a glyph.
|
|
269
|
+
* - The row under the cursor OPENS into a second row that says the
|
|
270
|
+
* whole note (sessionNote — one definition with `kiso sessions`), when
|
|
271
|
+
* the session started, its profile, and its id where they fit. The
|
|
272
|
+
* note used to be on every row; R2's "the id left the row" stands —
|
|
273
|
+
* it is on the opened card only.
|
|
274
|
+
* - 0.40.1's row counting the sessions with no workspace, above the
|
|
275
|
+
* list, is folded into the key row's `tab N more elsewhere` (N counts
|
|
276
|
+
* them with the rest). The default view still never falls back to
|
|
277
|
+
* every workspace; under tab they are listed, marked `unknown`.
|
|
278
|
+
* - The counter row joins the key row; the band shows eight sessions on
|
|
279
|
+
* a terminal 30 rows or taller, five below (it showed five).
|
|
258
280
|
*/
|
|
259
|
-
|
|
281
|
+
/** How many sessions the band shows: eight on a terminal 30 rows or
|
|
282
|
+
* taller, five below (owner, 2026-09-30). */
|
|
283
|
+
export function resumeVisible(height) {
|
|
284
|
+
return bandVisible(height); // Graphite P2: one window rule for every band
|
|
285
|
+
}
|
|
286
|
+
/** The window over the matches, with a scroll-off of one: while more lies
|
|
287
|
+
* past an edge, the cursor stays a row inside it, so the edge row that
|
|
288
|
+
* carries a more-mark is never the selected one. Stateless, like
|
|
289
|
+
* bandWindow: the same (total, selected) always draws the same window. */
|
|
290
|
+
export function resumeWindow(total, selected, visible) {
|
|
291
|
+
return bandWindow(total, selected, visible);
|
|
292
|
+
}
|
|
293
|
+
/** The row's state word — empty for the quiet default, a finished session. */
|
|
294
|
+
export function sessionStateWord(card) {
|
|
295
|
+
switch (card.badge) {
|
|
296
|
+
case "interrupted":
|
|
297
|
+
return "interrupted";
|
|
298
|
+
case "ask":
|
|
299
|
+
return `${card.asks} ask${card.asks === 1 ? "" : "s"}`;
|
|
300
|
+
case "uncertain":
|
|
301
|
+
return `${card.uncertain} uncertain`;
|
|
302
|
+
case "completed":
|
|
303
|
+
return "";
|
|
304
|
+
default:
|
|
305
|
+
// failed (the outcome's own words) and unknown ("no summary")
|
|
306
|
+
return sessionNote(card);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
/** The state's colour: blue for unfinished work, gold for what waits for
|
|
310
|
+
* the person, red for a failed ending, dim for what is not known. Off a
|
|
311
|
+
* known ground gold falls back to the warn tint and blue to none. */
|
|
312
|
+
function stateTone(card) {
|
|
260
313
|
const p = palette();
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
314
|
+
switch (card.badge) {
|
|
315
|
+
case "interrupted":
|
|
316
|
+
return p.blue;
|
|
317
|
+
case "ask":
|
|
318
|
+
case "uncertain":
|
|
319
|
+
return p.gold !== "" ? p.gold : p.warn;
|
|
320
|
+
case "failed":
|
|
321
|
+
return p.red;
|
|
322
|
+
case "completed":
|
|
323
|
+
return "";
|
|
324
|
+
default:
|
|
325
|
+
return p.dim;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/** Where a row is from, for the workspace column (every-workspace view
|
|
329
|
+
* only): blank for this workspace, `unknown` for a session from before
|
|
330
|
+
* workspaces were recorded, the last directory — or on a wide terminal
|
|
331
|
+
* the path — for another. */
|
|
332
|
+
function whereOf(card, here, wide) {
|
|
333
|
+
if (card.workspace === here)
|
|
334
|
+
return "";
|
|
335
|
+
if (card.workspace === null || card.workspace === undefined)
|
|
336
|
+
return "unknown";
|
|
337
|
+
return wide ? tildePath(card.workspace) : `…/${card.workspace.split("/").filter((x) => x !== "").at(-1) ?? ""}`;
|
|
338
|
+
}
|
|
339
|
+
/** A title fitted to its column: whole when it fits; otherwise cut by
|
|
340
|
+
* CELLS (a CJK character is two) with an ellipsis — on a space when one
|
|
341
|
+
* lies in the last two fifths, so a word is not halved needlessly. */
|
|
342
|
+
function fitTitle(text, room) {
|
|
343
|
+
if (visibleWidth(text) <= room)
|
|
344
|
+
return text;
|
|
345
|
+
if (room <= 1)
|
|
346
|
+
return widthCut(text, Math.max(0, room));
|
|
347
|
+
let cut = widthCut(text, room - 1);
|
|
348
|
+
const sp = cut.lastIndexOf(" ");
|
|
349
|
+
if (sp > cut.length * 0.6)
|
|
350
|
+
cut = cut.slice(0, sp);
|
|
351
|
+
return `${cut.trimEnd()}…`;
|
|
272
352
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
353
|
+
const TITLE_CAP = 60;
|
|
354
|
+
const TITLE_FLOOR = 12;
|
|
355
|
+
function resumeColumns(cards, W, now, where) {
|
|
356
|
+
const wide = W >= 110;
|
|
357
|
+
const width = (xs) => xs.reduce((m, x) => Math.max(m, visibleWidth(x)), 0);
|
|
358
|
+
const longest = Math.max(1, width(cards.map((c) => escapeTerminal(c.title ?? c.id))));
|
|
359
|
+
let parts = {
|
|
360
|
+
state: width(cards.map(sessionStateWord)),
|
|
361
|
+
where: where === null ? 0 : Math.min(wide ? 24 : 12, width(cards.map((c) => whereOf(c, where, wide)))),
|
|
362
|
+
age: width(cards.map((c) => sessionAge(c.updatedAt, now))),
|
|
363
|
+
turns: width(cards.map((c) => (c.turns === null ? "" : turnsOf(c.turns)))),
|
|
364
|
+
};
|
|
365
|
+
const avail = Math.max(0, W - 3); // the two-cell indent and one cell of right margin
|
|
366
|
+
const used = (x) => Object.values(x).reduce((s, v) => s + (v > 0 ? v + 2 : 0), 0);
|
|
367
|
+
const floor = Math.min(longest, TITLE_FLOOR);
|
|
368
|
+
for (const drop of ["turns", "where", "state", "age"]) {
|
|
369
|
+
if (floor + used(parts) <= avail)
|
|
370
|
+
break;
|
|
371
|
+
parts = { ...parts, [drop]: 0 };
|
|
372
|
+
}
|
|
373
|
+
const title = Math.max(0, Math.min(longest, TITLE_CAP, avail - used(parts)));
|
|
374
|
+
return { title, ...parts, table: 2 + title + used(parts) + 1 };
|
|
375
|
+
}
|
|
376
|
+
const turnsOf = (n) => `${n} turn${n === 1 ? "" : "s"}`;
|
|
377
|
+
/** The title with the letters the filter matched in gold — what the
|
|
378
|
+
* person typed, in the colour of what the person says. */
|
|
379
|
+
function markMatches(title, query, base) {
|
|
380
|
+
const p = palette();
|
|
381
|
+
const hit = query === "" ? null : atEmbed(title.toLowerCase(), query.toLowerCase());
|
|
382
|
+
if (hit === null || hit.length === 0)
|
|
383
|
+
return base === "" ? title : `${base}${title}${p.reset}`;
|
|
384
|
+
const on = new Set(hit);
|
|
385
|
+
const gold = p.gold !== "" ? p.gold : p.warn;
|
|
386
|
+
let out = "";
|
|
387
|
+
let i = 0;
|
|
388
|
+
for (const ch of title) {
|
|
389
|
+
out += on.has(i) ? `${p.reset}${p.bold}${gold}${ch}${p.reset}${base}` : ch;
|
|
390
|
+
i += ch.length;
|
|
391
|
+
}
|
|
392
|
+
return `${base}${out}${p.reset}`;
|
|
393
|
+
}
|
|
394
|
+
/** ONE table row. Unselected it is indented two cells, or carries a dim
|
|
395
|
+
* more-mark in column 0 (column 1 stays empty, so the mark never reads
|
|
396
|
+
* as the title's first letter); selected it is the Graphite selection
|
|
397
|
+
* bar with the title in bold, its text still starting in column 2. */
|
|
398
|
+
function resumeRow(card, selected, cols, W, now, query, where, mark) {
|
|
399
|
+
const p = palette();
|
|
400
|
+
const title = fitTitle(escapeTerminal(card.title ?? card.id), cols.title);
|
|
401
|
+
let text = `${markMatches(title, query, selected ? p.bold : "")}${" ".repeat(cols.title - visibleWidth(title))}`;
|
|
402
|
+
if (cols.state > 0) {
|
|
403
|
+
const word = sessionStateWord(card);
|
|
404
|
+
const tone = stateTone(card);
|
|
405
|
+
text += ` ${word === "" || tone === "" ? word : `${tone}${word}${p.reset}`}${" ".repeat(cols.state - visibleWidth(word))}`;
|
|
406
|
+
}
|
|
407
|
+
if (cols.where > 0 && where !== null) {
|
|
408
|
+
const at = fitTitle(whereOf(card, where, W >= 110), cols.where);
|
|
409
|
+
text += ` ${p.dim}${at}${p.reset}${" ".repeat(cols.where - visibleWidth(at))}`;
|
|
410
|
+
}
|
|
411
|
+
if (cols.age > 0)
|
|
412
|
+
text += ` ${p.dim}${sessionAge(card.updatedAt, now).padStart(cols.age)}${p.reset}`;
|
|
413
|
+
if (cols.turns > 0)
|
|
414
|
+
text += ` ${p.dim}${(card.turns === null ? "" : turnsOf(card.turns)).padStart(cols.turns)}${p.reset}`;
|
|
415
|
+
const w = cols.table - 3;
|
|
416
|
+
if (selected)
|
|
417
|
+
return selectionBar(` ${text}`, w + 1, W);
|
|
418
|
+
return mark === null ? ` ${text}` : `${p.dim}${mark}${p.reset} ${text}`;
|
|
419
|
+
}
|
|
420
|
+
const MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
|
|
421
|
+
/** When the session started, read from its id — the id IS the UTC stamp
|
|
422
|
+
* it was made at (apps/cli session-id.ts) — in local time. Null for an
|
|
423
|
+
* id that is not one. */
|
|
424
|
+
export function sessionStarted(id) {
|
|
425
|
+
const m = /^(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})/.exec(id);
|
|
426
|
+
if (m === null)
|
|
427
|
+
return null;
|
|
428
|
+
const t = new Date(Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]), Number(m[4]), Number(m[5]), Number(m[6])));
|
|
429
|
+
if (Number.isNaN(t.getTime()))
|
|
430
|
+
return null;
|
|
431
|
+
const two = (n) => String(n).padStart(2, "0");
|
|
432
|
+
return `${MONTHS[t.getMonth()]} ${t.getDate()} ${two(t.getHours())}:${two(t.getMinutes())}`;
|
|
433
|
+
}
|
|
434
|
+
/** The opened card's second row: the whole note (its state word in the
|
|
435
|
+
* state's colour, the rest dim), then — while they fit — when it
|
|
436
|
+
* started, its profile, where it is from (another workspace only), and
|
|
437
|
+
* its id. On the selection bar, like the row above it. */
|
|
438
|
+
function openedRow(card, W, where) {
|
|
439
|
+
const p = palette();
|
|
440
|
+
const room = Math.max(0, W - 3);
|
|
441
|
+
const note = sessionNote(card);
|
|
442
|
+
const dash = note.indexOf(" — ");
|
|
443
|
+
const head = widthCut(dash < 0 ? note : note.slice(0, dash), room);
|
|
444
|
+
const tone = stateTone(card);
|
|
445
|
+
let text = tone === "" ? head : `${tone}${head}${p.reset}`;
|
|
446
|
+
let w = visibleWidth(head);
|
|
447
|
+
const facts = [];
|
|
448
|
+
if (dash >= 0)
|
|
449
|
+
facts.push(note.slice(dash));
|
|
450
|
+
const started = sessionStarted(card.id);
|
|
451
|
+
if (started !== null)
|
|
452
|
+
facts.push(` · started ${started}`);
|
|
453
|
+
if (typeof card.profileName === "string" && card.profileName !== "")
|
|
454
|
+
facts.push(` · profile ${card.profileName}`);
|
|
455
|
+
if (where !== null && typeof card.workspace === "string" && card.workspace !== where)
|
|
456
|
+
facts.push(` · ${tildePath(card.workspace)}`);
|
|
457
|
+
if (card.title !== undefined && card.title !== card.id)
|
|
458
|
+
facts.push(` · ${card.id}`);
|
|
459
|
+
for (const f of facts) {
|
|
460
|
+
const cells = visibleWidth(escapeTerminal(f));
|
|
461
|
+
if (w + cells > room)
|
|
462
|
+
break;
|
|
463
|
+
text += `${p.dim}${escapeTerminal(f)}${p.reset}`;
|
|
464
|
+
w += cells;
|
|
465
|
+
}
|
|
466
|
+
return selectionBar(` ${text}`, w + 1, W);
|
|
467
|
+
}
|
|
468
|
+
/** The key row: what the keys do, then the selection's place in the whole
|
|
469
|
+
* filtered list, aligned to the table's right edge. The keys give way
|
|
470
|
+
* from the least needed — the scope's, then the arrows' — and enter and
|
|
471
|
+
* esc stay while anything does. */
|
|
472
|
+
function resumeKeyRow(state, W, table) {
|
|
276
473
|
const p = palette();
|
|
277
|
-
|
|
474
|
+
const total = state.matches.length;
|
|
475
|
+
const count = total === 0 ? "0/0" : `${state.selected + 1}/${total}`;
|
|
476
|
+
const scope = state.scope ?? null;
|
|
477
|
+
const elsewhere = scope === null ? 0 : scope.total - scope.inHere;
|
|
478
|
+
const tab = scope === null ? null : scope.all ? "tab this workspace" : elsewhere > 0 ? `tab ${elsewhere} more elsewhere` : null;
|
|
479
|
+
const keys = ["↑↓ move", "⏎ resumes", tab, "esc"].filter((k) => k !== null);
|
|
480
|
+
const edge = Math.min(W, Math.max(table, visibleWidth(keys.join(" · ")) + count.length + 5));
|
|
481
|
+
const fits = (ks) => 2 + visibleWidth(ks.join(" · ")) + 2 + count.length + 1 <= edge;
|
|
482
|
+
let kept = keys;
|
|
483
|
+
for (const drop of [tab, "↑↓ move", "⏎ resumes", "esc"]) {
|
|
484
|
+
if (fits(kept))
|
|
485
|
+
break;
|
|
486
|
+
kept = kept.filter((k) => k !== drop);
|
|
487
|
+
}
|
|
488
|
+
const text = kept.join(" · ");
|
|
489
|
+
const gap = Math.max(1, edge - 1 - 2 - visibleWidth(text) - count.length);
|
|
490
|
+
return `${p.dim}${widthCut(` ${text}${" ".repeat(gap)}${count}`, W)}${p.reset}`;
|
|
278
491
|
}
|
|
492
|
+
/** What the empty input says while the picker is up: typing there filters. */
|
|
493
|
+
export const RESUME_FILTER_HINT = "filter by title or id";
|
|
279
494
|
/**
|
|
280
|
-
* The whole band: the
|
|
281
|
-
*
|
|
282
|
-
* rows,
|
|
283
|
-
*
|
|
284
|
-
*
|
|
285
|
-
*
|
|
495
|
+
* The whole band: the named hairline, the windowed table with the
|
|
496
|
+
* selected row opened, and the key row. Returned as plain strings for the
|
|
497
|
+
* menu-rows channel, which already accounts them in chromeRows — the
|
|
498
|
+
* picker needs no geometry of its own, which is the entire reason it
|
|
499
|
+
* rides that channel. `height` is the terminal's: it sets how many
|
|
500
|
+
* sessions show (resumeVisible).
|
|
286
501
|
*/
|
|
287
|
-
export function sessionPickerRows(state, W, now) {
|
|
502
|
+
export function sessionPickerRows(state, W, now, height = 24) {
|
|
503
|
+
const p = palette();
|
|
288
504
|
const scope = state.scope ?? null;
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
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;
|
|
299
|
-
const col = idColumn(state.cards);
|
|
505
|
+
const query = state.query ?? "";
|
|
506
|
+
const rows = [bandHeader(scopeTitle(scope, query === "" ? null : { matches: state.matches.length, of: state.cards.length }), W)];
|
|
507
|
+
// the workspace column shows only when every workspace is listed
|
|
508
|
+
const where = scope !== null && scope.all ? scope.here : null;
|
|
509
|
+
const cols = resumeColumns(state.cards, W, now, where);
|
|
300
510
|
if (state.matches.length === 0) {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
rows.push(
|
|
305
|
-
rows.push(sessionCounterRow(0, 0, W));
|
|
511
|
+
// 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";
|
|
513
|
+
rows.push(`${p.dim}${widthCut(` ${empty}`, W)}${p.reset}`);
|
|
514
|
+
rows.push(resumeKeyRow(state, W, cols.table));
|
|
306
515
|
return rows;
|
|
307
516
|
}
|
|
308
|
-
const { first, count } =
|
|
309
|
-
for (let i = first; i < first + count; i += 1)
|
|
310
|
-
|
|
311
|
-
|
|
517
|
+
const { first, count } = resumeWindow(state.matches.length, state.selected, resumeVisible(height));
|
|
518
|
+
for (let i = first; i < first + count; i += 1) {
|
|
519
|
+
const mark = i === first && first > 0 ? "↑" : i === first + count - 1 && first + count < state.matches.length ? "↓" : null;
|
|
520
|
+
rows.push(resumeRow(state.matches[i], i === state.selected, cols, W, now, query, where, mark));
|
|
521
|
+
if (i === state.selected)
|
|
522
|
+
rows.push(openedRow(state.matches[i], W, where));
|
|
523
|
+
}
|
|
524
|
+
rows.push(resumeKeyRow(state, W, cols.table));
|
|
312
525
|
return rows;
|
|
313
526
|
}
|
|
314
527
|
/**
|
package/dist/status.d.ts
CHANGED
|
@@ -117,86 +117,17 @@ export interface RetryOnRow {
|
|
|
117
117
|
* counts against, what failed, and how long until it is tried. Whole
|
|
118
118
|
* seconds, rounded UP, so the row never says 0s while still waiting. */
|
|
119
119
|
export declare function retrySegment(r: RetryOnRow): string;
|
|
120
|
-
/** ADR-0058 (3e): the
|
|
120
|
+
/** ADR-0058 (3e, Amendment 8): the tasks kiso manages, as the status rows
|
|
121
|
+
* count them. */
|
|
121
122
|
export interface TaskCountsOnRow {
|
|
122
123
|
/** running or starting */
|
|
123
124
|
readonly running: number;
|
|
124
|
-
/** an outcome that cannot be known, not yet looked at in `/tasks` */
|
|
125
|
-
readonly unknown: number;
|
|
126
125
|
}
|
|
127
|
-
/** `● 2 tasks running
|
|
128
|
-
*
|
|
129
|
-
*
|
|
126
|
+
/** `● 2 tasks running` — or nothing. A task kiso lost track of is not a
|
|
127
|
+
* second count here: it is an event, said once in the transcript
|
|
128
|
+
* (Amendment 8). */
|
|
130
129
|
export declare function tasksSegment(c: TaskCountsOnRow | undefined): string;
|
|
131
|
-
export declare function
|
|
132
|
-
/**
|
|
133
|
-
* The COMPACTING row (W18): the covered rounds, the pre-call token
|
|
134
|
-
* estimate, and the elapsed seconds — all knowable before the one summary
|
|
135
|
-
* call returns, which has no fraction of its own. Moved here from an
|
|
136
|
-
* inline template in dispatch (0.40.0) so it composes like every other
|
|
137
|
-
* row and has a place for what the launch build adds to it.
|
|
138
|
-
*/
|
|
139
|
-
export declare function compactingStatus(glyph: string, rounds: number, tokens: number, elapsedSeconds: number, W?: number, retry?: RetryOnRow | null, progress?: CompactingProgress | null): string;
|
|
140
|
-
/**
|
|
141
|
-
* TUI2-R1 (E) — the idle row's meter: what the session has SPENT, next
|
|
142
|
-
* to what it has left.
|
|
143
|
-
*
|
|
144
|
-
* Both fields are optional and both are omitted when unknown, because
|
|
145
|
-
* the row's job is to be true rather than complete:
|
|
146
|
-
*
|
|
147
|
-
* - `cacheHitPct` is cacheRead / (fresh + cacheRead) — the E2
|
|
148
|
-
* denominator (the pinned sentence: it cannot exceed 100%). A
|
|
149
|
-
* session with no usage yet has no cache hit rate, and an
|
|
150
|
-
* unmeasured cache is NOT a 0% cache, so it renders nothing.
|
|
151
|
-
* - `costUsd` is the CANONICAL cost, which is null whenever the
|
|
152
|
-
* pricing table has no rate for the route. Null renders nothing.
|
|
153
|
-
* No rate table, no number — kiso does not invent a price.
|
|
154
|
-
*/
|
|
155
|
-
export interface StatusMeter {
|
|
156
|
-
readonly cacheHitPct: number | null;
|
|
157
|
-
/** RETIRED from the row (the owner's 2026-08-23 directive): live
|
|
158
|
-
* prices fluctuate and the canonical table is "an approximation,
|
|
159
|
-
* not a bill" — a four-decimal figure on the status bar claimed a
|
|
160
|
-
* precision the data never had. The canonical cost STAYS recorded
|
|
161
|
-
* (trace ledger, /context); the field is kept so callers need not
|
|
162
|
-
* change shape, and it renders NOTHING. */
|
|
163
|
-
readonly costUsd: number | null;
|
|
164
|
-
/** TPS-1 — the decode rate of the LAST settled call, carried into the
|
|
165
|
-
* idle row so the figure a person watched during the turn is still
|
|
166
|
-
* there when the turn ends. Null renders nothing (see `decodeRate`);
|
|
167
|
-
* a new model binding starts with none, exactly as the cache figure
|
|
168
|
-
* does (DF-0311-F1: an unmeasured binding has no measurement). */
|
|
169
|
-
readonly tokPerSec: number | null;
|
|
170
|
-
}
|
|
171
|
-
/**
|
|
172
|
-
* The IDLE row: the approval tier as the CALLER names it, the /mode hint,
|
|
173
|
-
* the model driving the session, the TUI2-R1 meter when there is one, and
|
|
174
|
-
* the ctx estimate. Called without a meter — or with one that knows
|
|
175
|
-
* nothing — the row is byte-identical to the pre-round row.
|
|
176
|
-
*
|
|
177
|
-
* DF-0330-F1 — THE DROP ORDER. `W` is the row's budget; given one, the row
|
|
178
|
-
* gives ground in a fixed order rather than letting invariant ① cut its
|
|
179
|
-
* end off. Found the hard way: at 100 columns the ` · N tok/s` segment
|
|
180
|
-
* never appeared and at 140 it did, because the row was 102 columns wide
|
|
181
|
-
* and the segment TPS-1 added sat last.
|
|
182
|
-
*
|
|
183
|
-
* 1. the MODEL ID is elided in its middle. It is the only segment that
|
|
184
|
-
* varies, it is the one that grew (the owner's is 35 columns of a
|
|
185
|
-
* 90-column row), and eliding it gives the row back a budget instead
|
|
186
|
-
* of re-allocating a deficit;
|
|
187
|
-
* 2. `/mode to switch` is dropped. It teaches a gesture; `/mode` and `?`
|
|
188
|
-
* still exist and the row is not the only place they are taught;
|
|
189
|
-
* 3. the FACTS are never dropped and never cut — the tier, CH, the ctx
|
|
190
|
-
* estimate and the rate. A row that silently drops a measurement is
|
|
191
|
-
* the defect this rule exists to prevent.
|
|
192
|
-
*
|
|
193
|
-
* The elision is ON THE ROW only. `/model`, the session log and the trace
|
|
194
|
-
* ledger all keep the id whole — the row is a view, never the record.
|
|
195
|
-
*
|
|
196
|
-
* No `W` means no dropping, which is what the callers that do not know
|
|
197
|
-
* their width should get: today's row, unchanged.
|
|
198
|
-
*/
|
|
199
|
-
export declare function idleStatus(tier: string, model: string, ctxRatio: number, meter?: StatusMeter, W?: number, floorOff?: boolean, tasks?: TaskCountsOnRow): string;
|
|
130
|
+
export declare function compactingStatus(glyph: string, rounds: number, tokens: number, elapsedSeconds: number, W?: number, retry?: RetryOnRow | null, progress?: CompactingProgress | null, why?: string): string;
|
|
200
131
|
/** TUI2-R1 (E) — the cache hit rate the status row shows, from the usage
|
|
201
132
|
* the CLI already tracks. The denominator is the TOTAL the model was
|
|
202
133
|
* given (fresh + cacheRead), which is the E2 ruling's own: cacheRead
|
|
@@ -206,3 +137,75 @@ export declare function cacheHitPct(usage: {
|
|
|
206
137
|
in: number | null;
|
|
207
138
|
cache: number | null;
|
|
208
139
|
}): number | null;
|
|
140
|
+
/**
|
|
141
|
+
* Graphite §8.9 — what the status bar says: the session and its health.
|
|
142
|
+
* The CLI keeps the state and hands this over; the compositor composes the
|
|
143
|
+
* row at paint time, because the drop order (§8.5) runs across both sides
|
|
144
|
+
* of the row and only the paint knows the width and the `ctrl+o` state.
|
|
145
|
+
*/
|
|
146
|
+
export interface BarInput {
|
|
147
|
+
/** The mode as the chip says it: `default`, `plan · read-only`, … */
|
|
148
|
+
readonly mode: string;
|
|
149
|
+
/** Full access (the old bypass) wears the failure colour (§8.9). */
|
|
150
|
+
readonly modeAlert?: boolean;
|
|
151
|
+
/** The main-sync round (owner, 2026-09-30): the don't-ask switch is on —
|
|
152
|
+
* a second chip, gold (the person's standing choice), beside the tier's. */
|
|
153
|
+
readonly dontAsk?: boolean;
|
|
154
|
+
readonly floorOff: boolean;
|
|
155
|
+
readonly model: string;
|
|
156
|
+
/** The share of the window USED (0..1), and the compaction tiers as
|
|
157
|
+
* shares of the same window; null when the window is not known. */
|
|
158
|
+
readonly ctx: {
|
|
159
|
+
readonly used: number;
|
|
160
|
+
readonly soft: number;
|
|
161
|
+
readonly hard: number;
|
|
162
|
+
} | null;
|
|
163
|
+
readonly tokPerSec: number | null;
|
|
164
|
+
/** The main-sync round (ADR-0058 3e): the session's tasks — running, and
|
|
165
|
+
* unknown not yet looked at in `/tasks`. Absent or all zero, nothing. */
|
|
166
|
+
readonly tasks?: TaskCountsOnRow;
|
|
167
|
+
readonly branch: string | null;
|
|
168
|
+
readonly folder: string | null;
|
|
169
|
+
}
|
|
170
|
+
/** The ctx meter's width in cells (§8.9). */
|
|
171
|
+
/**
|
|
172
|
+
* §8.9 — the ctx meter: ten `▆` cells with the used share filled, then the
|
|
173
|
+
* percentage used. Filled cells take `ink2` below the soft compaction tier,
|
|
174
|
+
* gold from the soft tier to the hard one, the failure colour past the hard
|
|
175
|
+
* tier; the empty cells are `track`. There is no marker inside the bar —
|
|
176
|
+
* the tier shows as colour only (owner, 2026-09-28). Off a known ground
|
|
177
|
+
* the colours carry nothing, so the meter is the percentage alone (§1.2:
|
|
178
|
+
* the fact survives without the colour). `ctx ?` when the window is not
|
|
179
|
+
* known.
|
|
180
|
+
*/
|
|
181
|
+
export declare function ctxMeter(ctx: BarInput["ctx"]): string;
|
|
182
|
+
/**
|
|
183
|
+
* §8.9 — the status bar, composed for `W` cells. Left: the mode chip,
|
|
184
|
+
* `floor off`, `/mode to switch`, the model and its effort, the ctx meter,
|
|
185
|
+
* the decode rate. The cache share is the seal's (owner, 2026-09-29: every
|
|
186
|
+
* run's closing row already says it), not the bar's. Right: the branch, the folder, and the
|
|
187
|
+
* `ctrl+o` switch while a card has rows behind the key.
|
|
188
|
+
*
|
|
189
|
+
* §8.5 — what gives way, in order: the `ctrl+o` hint, the folder (the
|
|
190
|
+
* terminal title names it too), the branch, the model's middle, and last
|
|
191
|
+
* `/mode to switch` — the one place a newcomer meets modes. The facts
|
|
192
|
+
* never drop; past that the row is invariant ①'s to cut.
|
|
193
|
+
*
|
|
194
|
+
* On a known ground the chip is a surface and the segments are spaced; off
|
|
195
|
+
* one the head is today's `▸ <mode>` and the segments are joined with ` · `
|
|
196
|
+
* — the same facts, in words that survive without colour.
|
|
197
|
+
*/
|
|
198
|
+
export declare function statusBar(b: BarInput, W: number, expand: "expand all" | "collapse all" | null): string;
|
|
199
|
+
/**
|
|
200
|
+
* §8.7 — the LIVE ROW's words for a running turn: `working` for the whole
|
|
201
|
+
* turn, whatever the model is doing (it never switches to "thinking" —
|
|
202
|
+
* what the model thinks is in the stream), the elapsed, the output tokens,
|
|
203
|
+
* the decode rate. A pending retry replaces it. The keys ride the row's
|
|
204
|
+
* right end.
|
|
205
|
+
*/
|
|
206
|
+
export declare function workingRow(glyph: string, since: number, outTokens: number | null, tokPerSec: number | null, W: number, retry?: RetryOnRow | null, detachable?: boolean): string;
|
|
207
|
+
/** §8.7 — a live row: its mark in the mark column (column 0), its facts
|
|
208
|
+
* from the content edge, its keys right-aligned; the keys give way from
|
|
209
|
+
* the right when the row is short, the facts never (past them the row is
|
|
210
|
+
* cut). */
|
|
211
|
+
export declare function liveRow(facts: string, keys: readonly string[], W: number): string;
|