@vincemakes/kiso-tui 0.8.0 → 0.10.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 +8 -7
- package/dist/ask-panel.js +31 -12
- package/dist/at-picker.d.ts +4 -0
- package/dist/at-picker.js +41 -8
- package/dist/compositor.d.ts +28 -10
- package/dist/compositor.js +405 -53
- package/dist/context-ledger.d.ts +66 -0
- package/dist/context-ledger.js +87 -0
- package/dist/editor.d.ts +11 -1
- package/dist/editor.js +301 -17
- package/dist/index.d.ts +7 -5
- package/dist/index.js +16 -5
- package/dist/session-picker.d.ts +113 -0
- package/dist/session-picker.js +249 -0
- package/dist/status.d.ts +32 -2
- package/dist/status.js +24 -3
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -9,25 +9,36 @@ export { Body, Dock, CURSOR_MARKER } from "./compositor.js";
|
|
|
9
9
|
// W21 (the v8 approval round): the approval panel — the bounded block
|
|
10
10
|
// that replaces the running tool's live window while a human-chain
|
|
11
11
|
// approval is pending (the shape authority is the committed preview).
|
|
12
|
-
export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus,
|
|
13
|
-
|
|
12
|
+
export { panelAffordance, panelBlockRows, panelLead, panelLeadPlain, panelLeadWidth, panelStatus,
|
|
13
|
+
// TUI2-R2 ④: the pick payload — the panel slot's third occupant.
|
|
14
|
+
PICK_MAX, modelPickView, pickAffordance, pickBlockRows, pickLeadPlain, } from "./approval-panel.js";
|
|
15
|
+
export { Container, foldLine, foldWords, visibleWidth, SPINNER } from "./components.js";
|
|
14
16
|
export { Editor, MENU_ITEMS, PROMPT, PROMPT_WIDTH, displayWidth, charWidth, widthOf, } from "./editor.js";
|
|
15
17
|
export { bannerLines, COLOR_OFF, COLOR_ON, escapeTerminal, foldResult, foldThinking, kUnit, palette, renderEvent, renderRecap, renderResumeList, renderSessionLine, renderStatusLine, relativeTime, renderTerminalGap, renderToolSummary, TAGLINE, toolTarget, truncateRow, } from "./render.js";
|
|
16
18
|
export { editFileDiff, truncateDiff, writeFileDiff } from "./diff.js";
|
|
17
19
|
// KC2 §5: the status rows' formatters — the CLI keeps the state and the
|
|
18
20
|
// repaint, the terminal layer owns what the row says.
|
|
19
|
-
export { STATUS_GLYPHS, idleStatus, runningStatus } from "./status.js";
|
|
21
|
+
export { STATUS_GLYPHS, cacheHitPct, idleStatus, runningStatus } from "./status.js";
|
|
22
|
+
// TUI2-R1 (E): /context's attribution rows — a pure function of the
|
|
23
|
+
// counts the trace sidecar already records (the CLI reads, this renders).
|
|
24
|
+
export { contextRows, contextUnavailableRows } from "./context-ledger.js";
|
|
20
25
|
// KC3 §1 (the extraction): the human-facing strings — the prompt, the
|
|
21
26
|
// project-trust listing/view/note, the uncertain execution's view. The
|
|
22
27
|
// FLOW (who is asked, what a verdict means) stays in the cli.
|
|
23
28
|
export { interactivePrompt, projectTrustRows, projectTrustView, projectUntrustedNote, uncertainView } from "./strings.js";
|
|
24
29
|
// KC3 §3/§5: the @ file picker's pure half — the subsequence filter, the
|
|
25
30
|
// deterministic rank, and the ONE cap the CLI's file source shares.
|
|
26
|
-
export { AT_CAP, AT_SKIP, AT_VISIBLE, atEmbed, atFilter, atPanelRows, atWindow, longestRun } from "./at-picker.js";
|
|
31
|
+
export { AT_CAP, AT_SKIP, AT_VISIBLE, atEmbed, atFilter, atPanelRows, atWindow, bandHeader, longestRun } from "./at-picker.js";
|
|
32
|
+
// TUI2-R2 ①–③: the session picker's pure half — the durability badge,
|
|
33
|
+
// the row (picked or printed), the band, and the filter. The CARDS are
|
|
34
|
+
// the cli's projection (session-cards.ts); this turns them into bytes.
|
|
35
|
+
export { BADGE_GLYPH, idColumn, sessionAge, sessionBadge, sessionCounterRow, sessionFilter, sessionListFooter, sessionListRow, sessionNote, sessionPickerRows, sessionRow, } from "./session-picker.js";
|
|
27
36
|
// KC3.5 (the ask round): the ask view — the panel machinery generalized.
|
|
28
37
|
// The cli composes the view and hands the answers to the tool; the keys,
|
|
29
38
|
// the rows and the walk are the terminal layer's.
|
|
30
39
|
export { ASK_HEADER_CAP, ASK_MAX_OPTIONS, ASK_MAX_QUESTIONS, ASK_MIN_OPTIONS, askAffordance, askAnswers, askBlockRows, askCommitCustom, askDeclineAll, askDeclineList, askKey, askLeadPlain, askStart, askStatus, askView, } from "./ask-panel.js";
|
|
31
40
|
// KC3.5 §4: the interrupted-ask copy — the SAME uncertainty gate, said
|
|
32
41
|
// honestly for a question nobody answered (the ① probe's surface).
|
|
33
|
-
|
|
42
|
+
// TUI2-R1 (D): the keys sheet + THE key table — one source for the ?
|
|
43
|
+
// overlay and /help's keys row.
|
|
44
|
+
export { KEY_BINDINGS, PANEL_KEYS_ROW, displayVerb, extensionsBannerText, helpRows, keysHelpRow, keysSheetRows, unansweredAskView } from "./strings.js";
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI2-R2 slices ①–③ — the session picker's PURE half: the durability
|
|
3
|
+
* badge, the row, the band, and the filter.
|
|
4
|
+
*
|
|
5
|
+
* The badge is the round's whole argument. kiso's claim is that a
|
|
6
|
+
* session survives kill -9 and resumes from its durable prefix; until
|
|
7
|
+
* now that claim was a sentence in a README. A badge per row makes it a
|
|
8
|
+
* thing you can SEE before you pick: this one completed, this one was
|
|
9
|
+
* cut mid-run and will resume exactly, this one is holding a question
|
|
10
|
+
* for you.
|
|
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
|
|
19
|
+
*
|
|
20
|
+
* Purity, as everywhere in this package: the cards are DATA the CLI
|
|
21
|
+
* projects (session-cards.ts) and this module turns them into bytes. It
|
|
22
|
+
* never reads a session, never asks the runtime anything, and holds no
|
|
23
|
+
* state — which is what lets the picker band and the `kiso sessions`
|
|
24
|
+
* listing render from ONE definition instead of two that drift.
|
|
25
|
+
*/
|
|
26
|
+
/** The projected card — structurally what apps/cli/src/session-cards.ts
|
|
27
|
+
* produces. Declared here as the tui's INPUT contract (the package
|
|
28
|
+
* imports nothing from the runtime, by rule). */
|
|
29
|
+
export interface SessionCardView {
|
|
30
|
+
readonly id: string;
|
|
31
|
+
readonly badge: "uncertain" | "ask" | "interrupted" | "completed" | "failed";
|
|
32
|
+
readonly turns: number;
|
|
33
|
+
readonly updatedAt: number;
|
|
34
|
+
readonly uncertain: number;
|
|
35
|
+
readonly asks: number;
|
|
36
|
+
readonly outcome: string | null;
|
|
37
|
+
}
|
|
38
|
+
/** The glyph per state — one cell each, so the badge column never
|
|
39
|
+
* shifts the id column (a column that moves per row reads as damage). */
|
|
40
|
+
export declare const BADGE_GLYPH: Readonly<Record<SessionCardView["badge"], string>>;
|
|
41
|
+
/** The badge, styled. The colour IS the meaning here (the mono
|
|
42
|
+
* discipline's three functional exceptions), so NO_COLOR degrades to
|
|
43
|
+
* the glyph alone — which is why the glyphs are distinct shapes and
|
|
44
|
+
* not three coloured dots. */
|
|
45
|
+
export declare function sessionBadge(badge: SessionCardView["badge"]): string;
|
|
46
|
+
/**
|
|
47
|
+
* What the row SAYS about the state. The interrupted note is the
|
|
48
|
+
* product's promise stated in the place the promise matters: the run
|
|
49
|
+
* continues from its durable prefix, so picking this row costs nothing
|
|
50
|
+
* that was already paid for.
|
|
51
|
+
*
|
|
52
|
+
* The ✗ note names the OUTCOME rather than flattening six endings into
|
|
53
|
+
* one word — "aborted" and "max turns" are different things to have
|
|
54
|
+
* happened, and a picker that calls both "failed" teaches the user
|
|
55
|
+
* nothing.
|
|
56
|
+
*/
|
|
57
|
+
export declare function sessionNote(card: SessionCardView): string;
|
|
58
|
+
/** The compact age — the picker's column, not the banner's sentence.
|
|
59
|
+
* `relativeTime` says "3d ago"; a column of ages does not need the
|
|
60
|
+
* word repeated on every row. */
|
|
61
|
+
export declare function sessionAge(updatedAt: number, now: number): string;
|
|
62
|
+
/** The id column's width — computed over EVERY card, never over the
|
|
63
|
+
* filtered subset, so the columns do not jump while the user types
|
|
64
|
+
* (the whole reason a filter-as-you-type picker is usable). */
|
|
65
|
+
export declare function idColumn(cards: readonly SessionCardView[]): number;
|
|
66
|
+
/**
|
|
67
|
+
* The filter — the @ picker's muscle, aimed at the session id: a
|
|
68
|
+
* case-insensitive SUBSEQUENCE, ranked by the longest contiguous run,
|
|
69
|
+
* then by id length, then lexically. Identical determinism, identical
|
|
70
|
+
* feel; a row under the cursor never moves because two ids tied.
|
|
71
|
+
*
|
|
72
|
+
* An empty query matches everything and keeps the caller's order (the
|
|
73
|
+
* listing's newest-first), because "no query" is not a search — it is
|
|
74
|
+
* the list.
|
|
75
|
+
*/
|
|
76
|
+
export declare function sessionFilter(cards: readonly SessionCardView[], query: string): SessionCardView[];
|
|
77
|
+
/**
|
|
78
|
+
* ONE picker row. The selection is a FULL-ROW reverse bar — the R1.5
|
|
79
|
+
* ⑧ ruling's shape, shared with the @ picker and the user chip: a
|
|
80
|
+
* two-cell marker in an eighty-column row is a selection you have to
|
|
81
|
+
* hunt for.
|
|
82
|
+
*
|
|
83
|
+
* The inner spans close with rvEnd inside the bar (never SGR 0, which
|
|
84
|
+
* would punch a hole in it) — the same composition atRow uses.
|
|
85
|
+
*/
|
|
86
|
+
export declare function sessionRow(card: SessionCardView, selected: boolean, W: number, now: number, idCol: number): string;
|
|
87
|
+
/** The counter row — the SELECTION's 1-based place in the whole
|
|
88
|
+
* filtered list, which the visible window cannot tell the user. */
|
|
89
|
+
export declare function sessionCounterRow(selected: number, total: number, W: number): string;
|
|
90
|
+
/** The picker's bound state — the editor owns it, the compositor reads
|
|
91
|
+
* it (the @ picker's contract, one surface over). */
|
|
92
|
+
export interface SessionPickState {
|
|
93
|
+
readonly cards: readonly SessionCardView[];
|
|
94
|
+
readonly matches: readonly SessionCardView[];
|
|
95
|
+
readonly selected: number;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The whole band: the `sessions` header (R1.5 ⑦(b) — a band names
|
|
99
|
+
* itself or it reads as more scrollback), at most AT_VISIBLE windowed
|
|
100
|
+
* rows, then the counter. Returned as plain strings for the menu-rows
|
|
101
|
+
* channel, which already accounts them in chromeRows — the picker needs
|
|
102
|
+
* no geometry of its own, which is the entire reason it rides that
|
|
103
|
+
* channel.
|
|
104
|
+
*/
|
|
105
|
+
export declare function sessionPickerRows(state: SessionPickState, W: number, now: number): string[];
|
|
106
|
+
/** Slice ③ — the `kiso sessions` TTY row: the SAME projection, printed
|
|
107
|
+
* rather than picked. No selection bar (nothing is selected on a
|
|
108
|
+
* listing) and no leading indent: this row starts at column 1 like
|
|
109
|
+
* every other line a shell command prints. */
|
|
110
|
+
export declare function sessionListRow(card: SessionCardView, W: number, now: number, idCol: number): string;
|
|
111
|
+
/** Slice ③ — the listing's last line: the count, and the one thing the
|
|
112
|
+
* user can do next. */
|
|
113
|
+
export declare function sessionListFooter(count: number, W: number): string;
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI2-R2 slices ①–③ — the session picker's PURE half: the durability
|
|
3
|
+
* badge, the row, the band, and the filter.
|
|
4
|
+
*
|
|
5
|
+
* The badge is the round's whole argument. kiso's claim is that a
|
|
6
|
+
* session survives kill -9 and resumes from its durable prefix; until
|
|
7
|
+
* now that claim was a sentence in a README. A badge per row makes it a
|
|
8
|
+
* thing you can SEE before you pick: this one completed, this one was
|
|
9
|
+
* cut mid-run and will resume exactly, this one is holding a question
|
|
10
|
+
* for you.
|
|
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
|
|
19
|
+
*
|
|
20
|
+
* Purity, as everywhere in this package: the cards are DATA the CLI
|
|
21
|
+
* projects (session-cards.ts) and this module turns them into bytes. It
|
|
22
|
+
* never reads a session, never asks the runtime anything, and holds no
|
|
23
|
+
* state — which is what lets the picker band and the `kiso sessions`
|
|
24
|
+
* listing render from ONE definition instead of two that drift.
|
|
25
|
+
*/
|
|
26
|
+
import { escapeTerminal, palette } from "./render.js";
|
|
27
|
+
import { visibleWidth, widthCut } from "./components.js";
|
|
28
|
+
import { atEmbed, bandHeader, longestRun, AT_VISIBLE, atWindow } from "./at-picker.js";
|
|
29
|
+
/** The glyph per state — one cell each, so the badge column never
|
|
30
|
+
* shifts the id column (a column that moves per row reads as damage). */
|
|
31
|
+
export const BADGE_GLYPH = {
|
|
32
|
+
completed: "✓", // ✓
|
|
33
|
+
failed: "✗", // ✗
|
|
34
|
+
interrupted: "▌", // ▌ — the input brick: this session is mid-sentence
|
|
35
|
+
uncertain: "?",
|
|
36
|
+
ask: "◌", // ◌ — the dotted circle: a question with no answer in it yet
|
|
37
|
+
};
|
|
38
|
+
/** The badge, styled. The colour IS the meaning here (the mono
|
|
39
|
+
* discipline's three functional exceptions), so NO_COLOR degrades to
|
|
40
|
+
* the glyph alone — which is why the glyphs are distinct shapes and
|
|
41
|
+
* not three coloured dots. */
|
|
42
|
+
export function sessionBadge(badge) {
|
|
43
|
+
const p = palette();
|
|
44
|
+
const g = BADGE_GLYPH[badge];
|
|
45
|
+
if (badge === "completed")
|
|
46
|
+
return `${p.green}${g}${p.reset}`;
|
|
47
|
+
if (badge === "failed")
|
|
48
|
+
return `${p.red}${g}${p.reset}`;
|
|
49
|
+
if (badge === "interrupted")
|
|
50
|
+
return `${p.bold}${g}${p.reset}`;
|
|
51
|
+
if (badge === "uncertain")
|
|
52
|
+
return `${p.warn}${g}${p.reset}`;
|
|
53
|
+
return `${p.dim}${g}${p.reset}`;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* What the row SAYS about the state. The interrupted note is the
|
|
57
|
+
* product's promise stated in the place the promise matters: the run
|
|
58
|
+
* continues from its durable prefix, so picking this row costs nothing
|
|
59
|
+
* that was already paid for.
|
|
60
|
+
*
|
|
61
|
+
* The ✗ note names the OUTCOME rather than flattening six endings into
|
|
62
|
+
* one word — "aborted" and "max turns" are different things to have
|
|
63
|
+
* happened, and a picker that calls both "failed" teaches the user
|
|
64
|
+
* nothing.
|
|
65
|
+
*/
|
|
66
|
+
export function sessionNote(card) {
|
|
67
|
+
switch (card.badge) {
|
|
68
|
+
case "uncertain":
|
|
69
|
+
return `${card.uncertain} uncertain — needs your verdict`;
|
|
70
|
+
case "ask":
|
|
71
|
+
return `${card.asks} ask${card.asks === 1 ? "" : "s"} pending`;
|
|
72
|
+
case "interrupted":
|
|
73
|
+
return "interrupted mid-run — resumes exactly";
|
|
74
|
+
case "completed":
|
|
75
|
+
return "completed clean";
|
|
76
|
+
default:
|
|
77
|
+
return card.outcome === null || card.outcome === "error" ? "failed" : card.outcome.replaceAll("_", " ");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
/** The compact age — the picker's column, not the banner's sentence.
|
|
81
|
+
* `relativeTime` says "3d ago"; a column of ages does not need the
|
|
82
|
+
* word repeated on every row. */
|
|
83
|
+
export function sessionAge(updatedAt, now) {
|
|
84
|
+
const s = Math.max(0, now - updatedAt) / 1000;
|
|
85
|
+
if (s < 60)
|
|
86
|
+
return "now";
|
|
87
|
+
const m = Math.floor(s / 60);
|
|
88
|
+
if (m < 60)
|
|
89
|
+
return `${m}m`;
|
|
90
|
+
const h = Math.floor(m / 60);
|
|
91
|
+
if (h < 24)
|
|
92
|
+
return `${h}h`;
|
|
93
|
+
const d = Math.floor(h / 24);
|
|
94
|
+
if (d < 7)
|
|
95
|
+
return `${d}d`;
|
|
96
|
+
return `${Math.floor(d / 7)}w`;
|
|
97
|
+
}
|
|
98
|
+
/** The id column's width — computed over EVERY card, never over the
|
|
99
|
+
* filtered subset, so the columns do not jump while the user types
|
|
100
|
+
* (the whole reason a filter-as-you-type picker is usable). */
|
|
101
|
+
export function idColumn(cards) {
|
|
102
|
+
let w = 0;
|
|
103
|
+
for (const c of cards)
|
|
104
|
+
w = Math.max(w, visibleWidth(escapeTerminal(c.id)));
|
|
105
|
+
return Math.min(Math.max(w, 1), 24);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* The filter — the @ picker's muscle, aimed at the session id: a
|
|
109
|
+
* case-insensitive SUBSEQUENCE, ranked by the longest contiguous run,
|
|
110
|
+
* then by id length, then lexically. Identical determinism, identical
|
|
111
|
+
* feel; a row under the cursor never moves because two ids tied.
|
|
112
|
+
*
|
|
113
|
+
* An empty query matches everything and keeps the caller's order (the
|
|
114
|
+
* listing's newest-first), because "no query" is not a search — it is
|
|
115
|
+
* the list.
|
|
116
|
+
*/
|
|
117
|
+
export function sessionFilter(cards, query) {
|
|
118
|
+
if (query === "")
|
|
119
|
+
return [...cards];
|
|
120
|
+
const lower = query.toLowerCase();
|
|
121
|
+
const scored = [];
|
|
122
|
+
for (const card of cards) {
|
|
123
|
+
const hit = atEmbed(card.id.toLowerCase(), lower);
|
|
124
|
+
if (hit === null)
|
|
125
|
+
continue;
|
|
126
|
+
scored.push({ card, run: longestRun(hit) });
|
|
127
|
+
}
|
|
128
|
+
scored.sort((a, b) => {
|
|
129
|
+
if (a.run !== b.run)
|
|
130
|
+
return b.run - a.run;
|
|
131
|
+
if (a.card.id.length !== b.card.id.length)
|
|
132
|
+
return a.card.id.length - b.card.id.length;
|
|
133
|
+
return a.card.id < b.card.id ? -1 : a.card.id > b.card.id ? 1 : 0;
|
|
134
|
+
});
|
|
135
|
+
return scored.map((s) => s.card);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* The row's spans, built against a HARD budget — the badge, the id
|
|
139
|
+
* column, the metadata, the note.
|
|
140
|
+
*
|
|
141
|
+
* The spans are appended in order of what the row is FOR and each one
|
|
142
|
+
* is dropped whole rather than half-drawn when the budget runs out:
|
|
143
|
+
* the badge and the id are the row's identity, the age/turns say
|
|
144
|
+
* whether it is the one, and the note is the sentence that explains
|
|
145
|
+
* the badge. A narrow terminal loses them from the right.
|
|
146
|
+
*
|
|
147
|
+
* The running width is the authority — never a formula computed up
|
|
148
|
+
* front. That is what the invariant-① sweep across five widths in the
|
|
149
|
+
* gate is for: a row that overflows does not truncate quietly here, it
|
|
150
|
+
* CRASHES the compositor, so the arithmetic has to be provably right at
|
|
151
|
+
* every width rather than right at eighty.
|
|
152
|
+
*/
|
|
153
|
+
function rowSpans(card, budget, now, idCol) {
|
|
154
|
+
const p = palette();
|
|
155
|
+
let text = "";
|
|
156
|
+
let w = 0;
|
|
157
|
+
/** append a styled span iff its VISIBLE cells still fit */
|
|
158
|
+
const put = (plain, styled) => {
|
|
159
|
+
const cells = visibleWidth(plain);
|
|
160
|
+
if (w + cells > budget)
|
|
161
|
+
return;
|
|
162
|
+
text += styled;
|
|
163
|
+
w += cells;
|
|
164
|
+
};
|
|
165
|
+
// the badge: one glyph + one space, styled as a unit (the glyph's own
|
|
166
|
+
// SGR spans make it unmeasurable by `put`'s plain/styled pair)
|
|
167
|
+
if (w + 2 <= budget) {
|
|
168
|
+
text += `${sessionBadge(card.badge)} `;
|
|
169
|
+
w += 2;
|
|
170
|
+
}
|
|
171
|
+
const id = widthCut(escapeTerminal(card.id), Math.max(1, Math.min(idCol, budget - w)));
|
|
172
|
+
put(id, id);
|
|
173
|
+
// the column pad only survives while there is room for what follows
|
|
174
|
+
const pad = Math.max(0, Math.min(idCol - visibleWidth(id), budget - w));
|
|
175
|
+
put(" ".repeat(pad), " ".repeat(pad));
|
|
176
|
+
const meta = ` ${sessionAge(card.updatedAt, now)} · ${card.turns} turn${card.turns === 1 ? "" : "s"}`;
|
|
177
|
+
put(meta, `${p.dim}${meta}${p.reset}`);
|
|
178
|
+
const note = widthCut(sessionNote(card), Math.max(0, budget - w - 3));
|
|
179
|
+
if (note !== "") {
|
|
180
|
+
// the ? note carries the warn tint — the row's own words are what
|
|
181
|
+
// the user acts on, and the one that demands an action says so
|
|
182
|
+
put(" · ", `${p.dim} · ${p.reset}`);
|
|
183
|
+
put(note, card.badge === "uncertain" ? `${p.warn}${note}${p.reset}` : `${p.dim}${note}${p.reset}`);
|
|
184
|
+
}
|
|
185
|
+
return { text, width: w };
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* ONE picker row. The selection is a FULL-ROW reverse bar — the R1.5
|
|
189
|
+
* ⑧ ruling's shape, shared with the @ picker and the user chip: a
|
|
190
|
+
* two-cell marker in an eighty-column row is a selection you have to
|
|
191
|
+
* hunt for.
|
|
192
|
+
*
|
|
193
|
+
* The inner spans close with rvEnd inside the bar (never SGR 0, which
|
|
194
|
+
* would punch a hole in it) — the same composition atRow uses.
|
|
195
|
+
*/
|
|
196
|
+
export function sessionRow(card, selected, W, now, idCol) {
|
|
197
|
+
const p = palette();
|
|
198
|
+
// both forms spend two cells of the width on their frame — the
|
|
199
|
+
// unselected row's indent, the bar's own leading/trailing cell — so
|
|
200
|
+
// the spans are built against the same budget either way and the
|
|
201
|
+
// selection cannot change the columns
|
|
202
|
+
const { text, width } = rowSpans(card, Math.max(0, W - 2), now, idCol);
|
|
203
|
+
if (!selected)
|
|
204
|
+
return ` ${text}`;
|
|
205
|
+
const inner = text.replaceAll(p.reset, `${p.reset}${p.rv}`);
|
|
206
|
+
return `${p.rv} ${inner}${" ".repeat(Math.max(0, W - width - 2))} ${p.rvEnd}`;
|
|
207
|
+
}
|
|
208
|
+
/** The counter row — the SELECTION's 1-based place in the whole
|
|
209
|
+
* filtered list, which the visible window cannot tell the user. */
|
|
210
|
+
export function sessionCounterRow(selected, total, W) {
|
|
211
|
+
const p = palette();
|
|
212
|
+
return `${p.dim}${widthCut(total === 0 ? " (0/0)" : ` (${selected + 1}/${total})`, W)}${p.reset}`;
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The whole band: the `sessions` header (R1.5 ⑦(b) — a band names
|
|
216
|
+
* itself or it reads as more scrollback), at most AT_VISIBLE windowed
|
|
217
|
+
* rows, then the counter. Returned as plain strings for the menu-rows
|
|
218
|
+
* channel, which already accounts them in chromeRows — the picker needs
|
|
219
|
+
* no geometry of its own, which is the entire reason it rides that
|
|
220
|
+
* channel.
|
|
221
|
+
*/
|
|
222
|
+
export function sessionPickerRows(state, W, now) {
|
|
223
|
+
const rows = [bandHeader("sessions", W)];
|
|
224
|
+
const col = idColumn(state.cards);
|
|
225
|
+
if (state.matches.length === 0) {
|
|
226
|
+
const p = palette();
|
|
227
|
+
rows.push(`${p.dim}${widthCut(" no session matches", W)}${p.reset}`);
|
|
228
|
+
rows.push(sessionCounterRow(0, 0, W));
|
|
229
|
+
return rows;
|
|
230
|
+
}
|
|
231
|
+
const { first, count } = atWindow(state.matches.length, state.selected, AT_VISIBLE);
|
|
232
|
+
for (let i = first; i < first + count; i += 1)
|
|
233
|
+
rows.push(sessionRow(state.matches[i], i === state.selected, W, now, col));
|
|
234
|
+
rows.push(sessionCounterRow(state.selected, state.matches.length, W));
|
|
235
|
+
return rows;
|
|
236
|
+
}
|
|
237
|
+
/** Slice ③ — the `kiso sessions` TTY row: the SAME projection, printed
|
|
238
|
+
* rather than picked. No selection bar (nothing is selected on a
|
|
239
|
+
* listing) and no leading indent: this row starts at column 1 like
|
|
240
|
+
* every other line a shell command prints. */
|
|
241
|
+
export function sessionListRow(card, W, now, idCol) {
|
|
242
|
+
return rowSpans(card, W, now, idCol).text;
|
|
243
|
+
}
|
|
244
|
+
/** Slice ③ — the listing's last line: the count, and the one thing the
|
|
245
|
+
* user can do next. */
|
|
246
|
+
export function sessionListFooter(count, W) {
|
|
247
|
+
const p = palette();
|
|
248
|
+
return `${p.dim}${widthCut(`${count} session${count === 1 ? "" : "s"} · kiso resume picks interactively`, W)}${p.reset}`;
|
|
249
|
+
}
|
package/dist/status.d.ts
CHANGED
|
@@ -33,6 +33,36 @@ export declare const STATUS_GLYPHS: readonly ["▖", "▘", "▝", "▗"];
|
|
|
33
33
|
* because it is on screen exactly when the gesture is useful.
|
|
34
34
|
*/
|
|
35
35
|
export declare function runningStatus(glyph: string, since: number, outTokens: number | null, ctxRatio: number): string;
|
|
36
|
+
/**
|
|
37
|
+
* TUI2-R1 (E) — the idle row's meter: what the session has SPENT, next
|
|
38
|
+
* to what it has left.
|
|
39
|
+
*
|
|
40
|
+
* Both fields are optional and both are omitted when unknown, because
|
|
41
|
+
* the row's job is to be true rather than complete:
|
|
42
|
+
*
|
|
43
|
+
* - `cacheHitPct` is cacheRead / (fresh + cacheRead) — the E2
|
|
44
|
+
* denominator (the pinned sentence: it cannot exceed 100%). A
|
|
45
|
+
* session with no usage yet has no cache hit rate, and an
|
|
46
|
+
* unmeasured cache is NOT a 0% cache, so it renders nothing.
|
|
47
|
+
* - `costUsd` is the CANONICAL cost, which is null whenever the
|
|
48
|
+
* pricing table has no rate for the route. Null renders nothing.
|
|
49
|
+
* No rate table, no number — kiso does not invent a price.
|
|
50
|
+
*/
|
|
51
|
+
export interface StatusMeter {
|
|
52
|
+
readonly cacheHitPct: number | null;
|
|
53
|
+
readonly costUsd: number | null;
|
|
54
|
+
}
|
|
36
55
|
/** The IDLE row: the approval tier as the CALLER names it, the /mode
|
|
37
|
-
* hint, the model driving the session,
|
|
38
|
-
|
|
56
|
+
* hint, the model driving the session, the TUI2-R1 meter when there is
|
|
57
|
+
* one, and the ctx estimate. Called without a meter — or with one that
|
|
58
|
+
* knows nothing — the row is byte-identical to the pre-round row. */
|
|
59
|
+
export declare function idleStatus(tier: string, model: string, ctxRatio: number, meter?: StatusMeter): string;
|
|
60
|
+
/** TUI2-R1 (E) — the cache hit rate the status row shows, from the usage
|
|
61
|
+
* the CLI already tracks. The denominator is the TOTAL the model was
|
|
62
|
+
* given (fresh + cacheRead), which is the E2 ruling's own: cacheRead
|
|
63
|
+
* over fresh alone once rendered 923%. No input at all → null, never a
|
|
64
|
+
* zero. */
|
|
65
|
+
export declare function cacheHitPct(usage: {
|
|
66
|
+
in: number | null;
|
|
67
|
+
cache: number | null;
|
|
68
|
+
}): number | null;
|
package/dist/status.js
CHANGED
|
@@ -46,7 +46,28 @@ export function runningStatus(glyph, since, outTokens, ctxRatio) {
|
|
|
46
46
|
return `${glyph} working ${seconds}s${out} · esc stop · alt+⏎ redirect · ctx left ~${ctxLeft(ctxRatio)}%`;
|
|
47
47
|
}
|
|
48
48
|
/** The IDLE row: the approval tier as the CALLER names it, the /mode
|
|
49
|
-
* hint, the model driving the session,
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
* hint, the model driving the session, the TUI2-R1 meter when there is
|
|
50
|
+
* one, and the ctx estimate. Called without a meter — or with one that
|
|
51
|
+
* knows nothing — the row is byte-identical to the pre-round row. */
|
|
52
|
+
export function idleStatus(tier, model, ctxRatio, meter) {
|
|
53
|
+
const parts = [`▸ ${tier}`, "/mode to switch", model];
|
|
54
|
+
if (meter?.cacheHitPct != null)
|
|
55
|
+
parts.push(`CH ${Math.round(meter.cacheHitPct)}%`);
|
|
56
|
+
if (meter?.costUsd != null)
|
|
57
|
+
parts.push(`$${meter.costUsd.toFixed(4)}`);
|
|
58
|
+
parts.push(`ctx left ~${ctxLeft(ctxRatio)}%`);
|
|
59
|
+
return parts.join(" · ");
|
|
60
|
+
}
|
|
61
|
+
/** TUI2-R1 (E) — the cache hit rate the status row shows, from the usage
|
|
62
|
+
* the CLI already tracks. The denominator is the TOTAL the model was
|
|
63
|
+
* given (fresh + cacheRead), which is the E2 ruling's own: cacheRead
|
|
64
|
+
* over fresh alone once rendered 923%. No input at all → null, never a
|
|
65
|
+
* zero. */
|
|
66
|
+
export function cacheHitPct(usage) {
|
|
67
|
+
const fresh = usage.in;
|
|
68
|
+
const cached = usage.cache;
|
|
69
|
+
if (fresh === null || cached === null)
|
|
70
|
+
return null;
|
|
71
|
+
const total = fresh + cached;
|
|
72
|
+
return total > 0 ? (cached / total) * 100 : null;
|
|
52
73
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincemakes/kiso-tui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "kiso tui — 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.10.0"
|
|
39
39
|
}
|
|
40
40
|
}
|