@zenera/cli 1.1.18 → 1.1.20

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.
@@ -1,10 +1,17 @@
1
- export type Appearance = 'dark' | 'light';
2
- /** The roles a line can play in the transcript. */
3
- export type Kind = 'you' | 'agent' | 'tool' | 'note' | 'error';
1
+ import { type Appearance } from '../term.ts';
2
+ export type { Appearance };
3
+ /**
4
+ * The roles a line can play in the transcript.
5
+ *
6
+ * `text` is what the agent said on its way somewhere — prose between one batch
7
+ * of calls and the next — and `agent` is the answer it finished on, which is
8
+ * the only one that gets a box. Reasoning is not among them: it is progress,
9
+ * and progress that has finished is worth no rows at all.
10
+ */
11
+ export type Kind = 'you' | 'text' | 'agent' | 'tool' | 'note' | 'error';
4
12
  export interface LineStyle {
5
13
  /** `undefined` means the terminal's own foreground. */
6
14
  readonly color?: string;
7
- readonly dim?: boolean;
8
15
  }
9
16
  export interface Theme {
10
17
  readonly appearance: Appearance;
@@ -13,13 +20,35 @@ export interface Theme {
13
20
  readonly accent: string;
14
21
  /** Read-only badge, busy label. */
15
22
  readonly warn: string;
23
+ /**
24
+ * Box rules, clocks, the footer's numbers, the call rows. One step quieter
25
+ * than the prose and never two — stated as a grey because `dim` is a
26
+ * request the terminal is free to over-honour, and some do.
27
+ */
28
+ readonly chrome: LineStyle;
16
29
  /**
17
30
  * Reasoning. It is not a tool call and it is not the answer, and drawn in
18
- * the same dim foreground as the rows around it there was nothing to say
19
- * which — a wall of one weight reads as one thing. Its own hue is what
20
- * separates the model's thinking from the work it went on to do.
31
+ * the same grey as the rows around it there was nothing to say which — a
32
+ * wall of one weight reads as one thing. Its own hue is what separates the
33
+ * model's thinking from the work it went on to do.
21
34
  */
22
35
  readonly thinking: LineStyle;
36
+ /**
37
+ * Inline code inside prose — `zen check`, `--theme`, a path.
38
+ *
39
+ * The one role added for markdown, and it is added because the alternative
40
+ * is worse: bold is already what `**strong**` means, and grey would say a
41
+ * command name matters less than the sentence around it when it is usually
42
+ * the only part worth copying. A hue says *this is literal* without
43
+ * ranking it.
44
+ */
45
+ readonly code: LineStyle;
46
+ /**
47
+ * The three things a turn spends time on. One word in the footer says which,
48
+ * and it is read at a glance from across a desk — so the word changing is
49
+ * backed by the colour changing, not left to be spelled out.
50
+ */
51
+ readonly phase: Record<'reasoning' | 'waiting' | 'working', string>;
23
52
  /**
24
53
  * Branch colours, cycled in order of first sight. A fan-out is the one
25
54
  * place where colour carries information rather than decoration: eight
package/dist/tui/theme.js CHANGED
@@ -13,47 +13,75 @@
13
13
  // unreadable on dark and invisible on paper. What is left is the handful of
14
14
  // accents that ANSI *does* let a light theme get wrong, so those swap.
15
15
  //
16
- // Dim is spent on *chrome* — box rules, clocks, the footer's numbers — and no
17
- // longer on content. Tool rows are most of what is on screen, and dimming them
18
- // put the bulk of the transcript one step from unreadable: the whole thing read
19
- // as too dark. They are content, so they get the foreground like content does.
16
+ // A second weight is spent on *chrome* and on the machinery: box rules,
17
+ // clocks, the footer's numbers, and the call rows that say how an answer was
18
+ // arrived at. What the agent actually said is drawn at full weight in the
19
+ // terminal's own foreground, so a transcript read top to bottom reads as prose
20
+ // with the work annotated beside it — not as one undifferentiated wall.
20
21
  //
21
- // Note what is *not* here: no hex, no 256-colour ramps, no attempt at a brand.
22
- // A palette that ignores the user's scheme is worse on both schemes than one
23
- // that mostly defers to it.
22
+ // That weight used to be SGR 2 (`dim`), and it was the third version of the
23
+ // same bug. `dim` is not a colour, it is an instruction, and how far it is
24
+ // obeyed is the terminal's business: iTerm2 takes a step, Apple Terminal
25
+ // blends most of the way to the background. On Terminal.app the entire margin
26
+ // — every rule, every call row, every number — went out with it.
27
+ //
28
+ // So there is exactly one ramp here, `chrome`, and it is a stated grey rather
29
+ // than an attribute. It is the only number in the file, and it buys the one
30
+ // thing deference cannot: a floor under the quiet half of the screen.
31
+ // Everything else is still named by role and still defers to the user's own
32
+ // scheme — no brand, no second hue, no 256-colour palette.
24
33
  // ---------------------------------------------------------------------------
34
+ import { appearance, CHROME } from "../term.js";
35
+ // The two greys, shared with every one-shot command so a `zen check` table and
36
+ // a TUI call row are the same weight. Chalk steps them down for a 16-colour
37
+ // terminal, which is the only place the old `gray` problem is still reachable.
38
+ const DARK_CHROME = { color: `ansi256(${CHROME.dark})` };
39
+ const LIGHT_CHROME = { color: `ansi256(${CHROME.light})` };
25
40
  const DARK = {
26
41
  appearance: 'dark',
27
42
  line: {
28
43
  you: { color: 'cyan' },
44
+ text: {},
29
45
  agent: {},
30
- // Never dim, and never `gray`: bright black is already the faintest
31
- // colour a terminal has, and either one puts the bulk of the
32
- // transcript a step from unreadable.
33
- tool: {},
46
+ // A call is the machinery, not the work: the transcript is read for
47
+ // what the agent said, and the rows saying how it found out are a
48
+ // margin note beside that.
49
+ tool: DARK_CHROME,
34
50
  note: { color: 'cyan' },
35
51
  error: { color: 'red' },
36
52
  },
37
53
  accent: 'cyan',
38
54
  warn: 'yellow',
39
- thinking: { color: 'magenta' },
55
+ chrome: DARK_CHROME,
56
+ // Bright: the gist is one line among many and plain `magenta` on black is
57
+ // the darkest of the six.
58
+ thinking: { color: 'magentaBright' },
59
+ // Green is the one hue with no other job here: it is not the accent, not
60
+ // the warning, and not the thinking.
61
+ code: { color: 'green' },
62
+ phase: { reasoning: 'magenta', waiting: 'yellow', working: 'cyan' },
40
63
  lanes: ['cyan', 'green', 'yellow', 'blue', 'red', 'magenta'],
41
64
  };
42
65
  // On a light background `cyan` and `yellow` are barely darker than the paper.
43
- // Dimmed default foreground and `blue`/`magenta` are the same information,
44
- // still legible.
66
+ // Grey and `blue`/`magenta` are the same information, still legible.
45
67
  const LIGHT = {
46
68
  appearance: 'light',
47
69
  line: {
48
70
  you: { color: 'blue' },
71
+ text: {},
49
72
  agent: {},
50
- tool: {},
73
+ tool: LIGHT_CHROME,
51
74
  note: { color: 'blue' },
52
75
  error: { color: 'red' },
53
76
  },
54
77
  accent: 'blue',
55
78
  warn: 'magenta',
79
+ chrome: LIGHT_CHROME,
56
80
  thinking: { color: 'magenta' },
81
+ code: { color: 'green' },
82
+ // No yellow: `waiting` takes blue and `working` green, which are the two
83
+ // that survive paper.
84
+ phase: { reasoning: 'magenta', waiting: 'blue', working: 'green' },
57
85
  // No cyan or yellow: on paper they are barely darker than the paper.
58
86
  lanes: ['blue', 'green', 'red', 'magenta'],
59
87
  };
@@ -67,23 +95,17 @@ export async function resolveTheme(choice) {
67
95
  if (asked !== 'auto') {
68
96
  return THEMES[asked];
69
97
  }
70
- return THEMES[(await queryBackground()) ?? fromColorFgBg() ?? 'dark'];
71
- }
72
- /**
73
- * `COLORFGBG` is `fg;bg` or `fg;<something>;bg`; the background is the last
74
- * field. Anything non-numeric (notably `default`) tells us nothing.
75
- */
76
- function fromColorFgBg() {
77
- const parts = process.env['COLORFGBG']?.split(';');
78
- const bg = parts?.[parts.length - 1];
79
- if (bg === undefined || !/^\d+$/.test(bg)) {
80
- return undefined;
81
- }
82
- const n = Number(bg);
83
- return n === 7 || n >= 9 ? 'light' : 'dark';
98
+ return THEMES[(await queryBackground()) ?? appearance()];
84
99
  }
85
- const QUERY = '\u001b]11;?\u0007';
100
+ // The DA1 behind the colour query is what makes the answer conclusive. Every
101
+ // terminal replies to DA1, and none replies to it ahead of an OSC 11 it
102
+ // supports — so a DA1 arriving alone means "I do not do that", known in one
103
+ // round trip instead of guessed at the end of a timeout. What is left for the
104
+ // timeout is the terminal that answers neither, which is the only case worth
105
+ // waiting on and now the only one that does.
106
+ const QUERY = '\u001b]11;?\u0007\u001b[c';
86
107
  const REPLY = /\u001b\]11;rgb:([\da-f]{1,4})\/([\da-f]{1,4})\/([\da-f]{1,4})/i;
108
+ const DA1 = /\u001b\[\?[\d;]*c/;
87
109
  /**
88
110
  * Ask the terminal for its background colour and read the answer off stdin.
89
111
  *
@@ -93,7 +115,7 @@ const REPLY = /\u001b\]11;rgb:([\da-f]{1,4})\/([\da-f]{1,4})\/([\da-f]{1,4})/i;
93
115
  * and nothing else. Anything the user typed in that window is put back, so
94
116
  * the first keystroke of a fast start is not eaten by the handshake.
95
117
  */
96
- async function queryBackground(timeoutMs = 120) {
118
+ async function queryBackground(timeoutMs = 300) {
97
119
  const { stdin, stdout } = process;
98
120
  if (!stdin.isTTY || !stdout.isTTY || typeof stdin.setRawMode !== 'function') {
99
121
  return undefined;
@@ -102,6 +124,7 @@ async function queryBackground(timeoutMs = 120) {
102
124
  let timer;
103
125
  let settled = false;
104
126
  let seen = '';
127
+ let found;
105
128
  return await new Promise((resolve) => {
106
129
  const finish = (result) => {
107
130
  if (settled) {
@@ -114,26 +137,33 @@ async function queryBackground(timeoutMs = 120) {
114
137
  if (!wasRaw) {
115
138
  stdin.pause();
116
139
  }
117
- const typed = seen.replace(REPLY, '').replace(/\u001b\\|\u0007/g, '');
140
+ const typed = seen
141
+ .replace(REPLY, '')
142
+ .replace(DA1, '')
143
+ .replace(/\u001b\\|\u0007/g, '');
118
144
  if (typed) {
119
145
  stdin.unshift(Buffer.from(typed, 'latin1'));
120
146
  }
121
147
  resolve(result);
122
148
  };
149
+ // The DA1 is sent second and answered second, so it is the end of the
150
+ // conversation whether or not the colour came back. Settling on the
151
+ // colour alone leaves the DA1 in the pipe, and it surfaces a moment
152
+ // later as `[?64;1;2;...c` typed into the prompt.
123
153
  const onData = (chunk) => {
124
154
  seen += chunk.toString('latin1');
125
155
  const m = REPLY.exec(seen);
126
156
  if (m) {
127
- finish(appearanceOf(m[1], m[2], m[3]));
157
+ found = appearanceOf(m[1], m[2], m[3]);
128
158
  }
129
- else if (seen.length > 256) {
130
- finish(undefined);
159
+ if (DA1.test(seen) || seen.length > 256) {
160
+ finish(found);
131
161
  }
132
162
  };
133
163
  stdin.setRawMode(true);
134
164
  stdin.resume();
135
165
  stdin.on('data', onData);
136
- timer = setTimeout(() => finish(undefined), timeoutMs);
166
+ timer = setTimeout(() => finish(found), timeoutMs);
137
167
  timer.unref?.();
138
168
  stdout.write(QUERY);
139
169
  });
@@ -17,26 +17,33 @@ export declare function clip(text: string, width: number): string;
17
17
  export declare function wrap(text: string, width: number): string[];
18
18
  /** The three footer rows, its margin, the prompt, and a row in hand. */
19
19
  export declare const CHROME_ROWS = 7;
20
- /** How much of the reasoning stream is worth showing. It is a progress bar. */
21
- export declare const THINKING_ROWS = 6;
20
+ /**
21
+ * How much of the reasoning stream is worth showing.
22
+ *
23
+ * One row. A reasoning summary is not a document to be read while it arrives —
24
+ * it is the model saying what it is about to do, and the sentence that says so
25
+ * is the heading it writes for itself. Six rows of it pushed the answer and the
26
+ * work down the screen to make room for a paragraph nobody finishes reading,
27
+ * and the whole chain is in the trajectory either way.
28
+ */
29
+ export declare const THINKING_ROWS = 1;
22
30
  /** How much of the frame the work in flight may take. A branch is a box now,
23
- * not a row, so a fan-out of two costs fourteen of these. */
24
- export declare const ACTIVITY_ROWS = 16;
25
- /** The rule that opens a reasoning block and the one that closes it. */
26
- export declare const THINKING_CHROME = 2;
31
+ * not a row, so a fan-out of two costs twenty-two of these. */
32
+ export declare const ACTIVITY_ROWS = 24;
33
+ /** The width of the elapsed-time column every call row is drawn behind. */
34
+ export declare const TIME_COL = 5;
27
35
  export interface Budget {
28
36
  /** rows for the list of what is in flight */
29
37
  activity: number;
30
- /** rows for the tail of the reasoning stream */
38
+ /** rows for the gist of what the model is reasoning about */
31
39
  thinking: number;
32
40
  /** rows for the answer as it arrives */
33
41
  live: number;
34
42
  /**
35
- * What the three of them occupy together, rules included — a constant for
36
- * a given terminal, whatever is happening inside it. The region grows to
37
- * this and stops, and never shrinks back while the turn runs, which is what
38
- * keeps the footer on one row instead of riding up and down on the
39
- * reasoning block.
43
+ * What the three of them occupy together — a constant for a given terminal,
44
+ * whatever is happening inside it. The region grows to this and stops, and
45
+ * never shrinks back while the turn runs, which is what keeps the footer on
46
+ * one row instead of riding up and down on the work.
40
47
  */
41
48
  total: number;
42
49
  }
@@ -47,15 +54,22 @@ export interface Budget {
47
54
  * arrives matters more than the machinery producing it. Reasoning yields to it
48
55
  * in turn, because it is a progress indicator and the answer is the point.
49
56
  *
50
- * `thinking` is how many rows of reasoning are wanted, counting text only: none
51
- * when there is no reasoning, fewer than `THINKING_ROWS` once the stream has
52
- * settled and its tail is history rather than progress. The two rules are
53
- * charged on top, so a block too cramped to be worth boxing is not drawn at all
54
- * rather than drawn as a border with a line in it.
57
+ * `thinking` is how many rows of reasoning are wanted — one while the model is
58
+ * reasoning, none otherwise — and it never takes the answer's last row.
55
59
  */
56
60
  export declare function budgetOf(rows: number, activity: number, thinking: number): Budget;
57
- /** The rows a branch box may spend on its own calls and its reasoning. */
58
- export declare const BRANCH_ROWS = 5;
61
+ /** The rows a branch box may spend on its own calls, prose and reasoning. */
62
+ export declare const BRANCH_ROWS = 9;
63
+ /**
64
+ * The fewest a box is worth drawing at.
65
+ *
66
+ * One row per branch fit more of them on screen and said nothing about any of
67
+ * them: a box holding a single call is a label, and the reason it is a box at
68
+ * all is that what a branch just did and said is why it is doing this. Below
69
+ * six rows there is no *what it just did* — so a fork too wide to give every
70
+ * branch six is cut to the ones that fit and counted, which is `fitActivity`.
71
+ */
72
+ export declare const BRANCH_MIN = 6;
59
73
  /** Rows a branch box spends on chrome: the title rule and the closing one. */
60
74
  export declare const BOX_CHROME = 2;
61
75
  /**
@@ -63,18 +77,27 @@ export declare const BOX_CHROME = 2;
63
77
  * activity region has to divide between them.
64
78
  *
65
79
  * Every branch gets the same number, because they are the same kind of thing
66
- * and a fan-out is read across, not down. A wide fork spends its rows on being
67
- * complete rather than on being detailed: eight branches showing one call each
68
- * is a picture of the fork, eight rows of one branch is not.
80
+ * and a fan-out is read across, not down.
69
81
  */
70
82
  export declare function branchRows(count: number, allowance: number): number;
71
83
  /**
72
- * How wide the answer is drawn. A line of prose spanning a 200-column terminal
73
- * is measurably harder to read than one that stops, which is why every demo in
84
+ * How wide *prose* is drawn. A line spanning a 200-column terminal is
85
+ * measurably harder to read than one that stops, which is why every demo in
74
86
  * `examples/sdk/` puts its answer in a box of bounded width — the terminal is the
75
87
  * page, not the paragraph.
88
+ *
89
+ * `boxWidth` is what a whole answer is drawn at; this is the floor it starts from.
76
90
  */
77
91
  export declare function answerWidth(columns: number): number;
92
+ /**
93
+ * How wide the box holding `text` has to be.
94
+ *
95
+ * Prose stops at the comfort width, but a fence and a table cannot be
96
+ * reflowed — fold a table row and the columns stop lining up, which is the one
97
+ * thing the table was for — so the box grows to hold the widest of them, as far
98
+ * as the terminal allows and no further.
99
+ */
100
+ export declare function boxWidth(text: string, columns: number): number;
78
101
  /**
79
102
  * A tool payload as a person would read it, rather than as it was serialised.
80
103
  *
@@ -87,18 +110,102 @@ export declare function answerWidth(columns: number): number;
87
110
  * that got cut.
88
111
  */
89
112
  export declare function readable(preview: string): string;
90
- /** A run of lines from an answer, and whether it was fenced as code. */
91
- export interface Segment {
92
- code: boolean;
113
+ /** Every field of a preview, first mention winning. */
114
+ export declare function fieldsOf(preview: string): Map<string, string>;
115
+ /** What a call is doing, and what it is doing it to. */
116
+ export interface CallView {
117
+ /** The verb, drawn first: what kind of thing this is. */
118
+ verb: string;
119
+ /** What identifies this call: a path, a command, a query. May be empty. */
120
+ subject: string;
121
+ }
122
+ export declare function describeCall(name: string, preview: string): CallView;
123
+ export declare function summarise(name: string, preview: string): string;
124
+ /**
125
+ * One line saying where the model has got to.
126
+ *
127
+ * Reasoning summaries arrive as markdown and are structured by their own bold
128
+ * headings — Gemini emits one every few sentences, and they are the shortest
129
+ * true account of the step. The last one is where the model is now. Failing
130
+ * that the raw text is clipped, which is a fragment, but a moving fragment is
131
+ * still proof that something is happening.
132
+ */
133
+ export declare function gistOf(text: string, width: number): string;
134
+ /** A run of inline text and what it is. Style rides beside the text, never in it. */
135
+ export interface Span {
136
+ text: string;
137
+ bold?: boolean;
138
+ italic?: boolean;
139
+ /** an inline code span, taken verbatim and never scanned again */
140
+ code?: boolean;
141
+ /** where a link points, when its own text does not say */
142
+ href?: string;
143
+ }
144
+ export type BlockKind = 'paragraph' | 'heading' | 'item' | 'quote' | 'code' | 'table' | 'rule';
145
+ /** How a table column is set, when its rule row said. */
146
+ export type Align = 'left' | 'right' | 'center';
147
+ /**
148
+ * One block of an answer.
149
+ *
150
+ * `lines`, `cells` and `spans` are exclusive and say how the block may be
151
+ * drawn: `lines` is verbatim and must be cut rather than wrapped, `cells` is a
152
+ * grid to be laid out by `gridOf`, `spans` is prose and the renderer may do as
153
+ * it likes with it.
154
+ */
155
+ export interface Block {
156
+ kind: BlockKind;
157
+ /** `heading`: its depth, 1–6. `item`: how deep it is nested. */
158
+ level?: number;
93
159
  /** the fence's info string, when it had one */
94
160
  title?: string;
95
- lines: string[];
161
+ /** what a list item is drawn behind — a bullet, or the number that was written */
162
+ marker?: string;
163
+ /** `code` only, whose indentation is its meaning */
164
+ lines?: string[];
165
+ /** `table` only: every row but the rule, as cells, with their markup resolved */
166
+ cells?: Span[][][];
167
+ /** `table` only: how each column is set, and that the first row is a header */
168
+ align?: Align[];
169
+ /** every other kind, with its inline markup resolved */
170
+ spans?: Span[];
96
171
  }
97
172
  /**
98
- * Splits an answer on ``` fences. Prose is left exactly as it was — the common
99
- * answer has no fence in it and comes back in one piece — but a fenced block is
100
- * the one thing a terminal must not reflow: its indentation is its meaning, and
101
- * wrapping it as prose destroys it.
173
+ * The inline markup of one block, resolved.
174
+ *
175
+ * Order is the correctness argument. Code is settled first and never entered
176
+ * again, which is why this is a scanner and not a chain of replaces: in
177
+ * "use `[a](b)` syntax" the brackets are prose, and no regex over the whole
178
+ * string can know that. Everything else recurses on the text between its own
179
+ * markers, so `**bold with `code` in it**` keeps both.
180
+ */
181
+ export declare function inlineOf(text: string, style?: Omit<Span, 'text'>, depth?: number): Span[];
182
+ /**
183
+ * An answer, as the blocks it is made of.
184
+ *
185
+ * Read top down, one line at a time, because that is the order the text
186
+ * arrives in and a stream must be readable before it is finished. The two
187
+ * verbatim kinds win over everything: a pipe inside a fence is whatever the
188
+ * code says it is, and a `#` inside one is a comment, not a heading.
189
+ */
190
+ export declare function blocksOf(text: string): Block[];
191
+ /**
192
+ * A table as its rows, each one a single run of spans.
193
+ *
194
+ * Columns are laid out here and nowhere else, so the drawn table and the plain
195
+ * one are the same table: a cell's width is the length of its text, which is
196
+ * what a `Span` measures whatever styling rides beside it. Nothing a model
197
+ * writes in a cell can move the cell after it.
198
+ */
199
+ export declare function gridOf(block: Block): Span[][];
200
+ /**
201
+ * A block as the row or rows it reads as, with its markers resolved rather
202
+ * than shown, and nothing styled.
203
+ *
204
+ * This is what a sink that cannot style gets — the pre-answer stream, and the
205
+ * plain box `zen meta` draws — so that the text does not visibly change shape
206
+ * at the moment it settles.
102
207
  */
103
- export declare function segmentsOf(text: string): Segment[];
208
+ export declare function textOf(block: Block, width?: number): string;
209
+ /** A whole answer with its markup resolved instead of shown. Still plain text. */
210
+ export declare function unmarked(text: string, width?: number): string;
104
211
  //# sourceMappingURL=wrap.d.ts.map