@lardexx/arenacode 0.0.0-stage → 2.5.2

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.
@@ -0,0 +1,246 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Palettes copied from opencode's bundled theme assets (packages/tui/src/theme/assets/*.json,
5
+ * v1.19.5) — same role names, same hex values, truecolor output.
6
+ *
7
+ * Roles: primary, secondary, accent, error, warning, success, info,
8
+ * text, textMuted, background, backgroundPanel, backgroundElement,
9
+ * border, borderActive, borderSubtle, diffAdded, diffRemoved
10
+ */
11
+
12
+ const ANSI_16 = {
13
+ 0: [0, 0, 0],
14
+ 1: [128, 0, 0],
15
+ 2: [0, 128, 0],
16
+ 3: [128, 128, 0],
17
+ 4: [0, 0, 128],
18
+ 5: [128, 0, 128],
19
+ 6: [0, 128, 128],
20
+ 7: [192, 192, 192],
21
+ 8: [128, 128, 128],
22
+ 9: [255, 0, 0],
23
+ 10: [0, 255, 0],
24
+ 11: [255, 255, 0],
25
+ 12: [0, 0, 255],
26
+ 13: [255, 0, 255],
27
+ 14: [0, 255, 255],
28
+ 15: [255, 255, 255],
29
+ };
30
+
31
+ function rgb(hex) {
32
+ const h = String(hex).replace('#', '');
33
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
34
+ }
35
+
36
+ function toRgb(color) {
37
+ if (Array.isArray(color)) return color;
38
+ if (typeof color === 'number') return ANSI_16[color] || [255, 255, 255];
39
+ if (color && color.hex) return rgb(color.hex);
40
+ if (typeof color === 'string' && color.startsWith('#')) return rgb(color);
41
+ return [255, 255, 255];
42
+ }
43
+
44
+ const truecolor = process.env.COLORTERM === 'truecolor' || process.env.TERM_PROGRAM === 'iTerm.app' || true;
45
+
46
+ function fgSeq(color) {
47
+ const [r, g, b] = toRgb(color);
48
+ return truecolor ? `\x1b[38;2;${r};${g};${b}m` : color256(r, g, b, false);
49
+ }
50
+
51
+ function bgSeq(color) {
52
+ const [r, g, b] = toRgb(color);
53
+ return truecolor ? `\x1b[48;2;${r};${g};${b}m` : color256(r, g, b, true);
54
+ }
55
+
56
+ function color256(r, g, b, isBg) {
57
+ const q = (v) => Math.round((v / 255) * 5);
58
+ const n = 16 + 36 * q(r) + 6 * q(g) + q(b);
59
+ return `\x1b[${isBg ? 48 : 38};5;${n}m`;
60
+ }
61
+
62
+ const RESET = '\x1b[0m';
63
+
64
+ function mix(a, b, amount) {
65
+ const [r1, g1, b1] = toRgb(a);
66
+ const [r2, g2, b2] = toRgb(b);
67
+ const m = (x, y) => Math.round(x + (y - x) * amount);
68
+ return `#${[m(r1, r2), m(g1, g2), m(b1, b2)].map((v) => v.toString(16).padStart(2, '0')).join('')}`;
69
+ }
70
+
71
+ function makeTheme(name, c) {
72
+ const paintFg = (color) => (s) => `${fgSeq(color)}${s}${RESET}`;
73
+ const paintBg = (color) => (s) => `${bgSeq(color)}${s}${RESET}`;
74
+
75
+ const t = {
76
+ name,
77
+ c,
78
+
79
+ // text roles -------------------------------------------------------------
80
+ text: (s) => `${fgSeq(c.text)}${s}${RESET}`,
81
+ fg: (s) => `${fgSeq(c.text)}${s}${RESET}`,
82
+ muted: (s) => `${fgSeq(c.textMuted)}${s}${RESET}`,
83
+ dim: (s) => `\x1b[2m${s}\x1b[22m`,
84
+ primary: (s) => `${fgSeq(c.primary)}${s}${RESET}`,
85
+ secondary: (s) => `${fgSeq(c.secondary)}${s}${RESET}`,
86
+ accent: (s) => `${fgSeq(c.accent)}${s}${RESET}`,
87
+ accent2: (s) => `${fgSeq(c.secondary)}${s}${RESET}`,
88
+ ok: (s) => `${fgSeq(c.success)}${s}${RESET}`,
89
+ success: (s) => `${fgSeq(c.success)}${s}${RESET}`,
90
+ warn: (s) => `${fgSeq(c.warning)}${s}${RESET}`,
91
+ warning: (s) => `${fgSeq(c.warning)}${s}${RESET}`,
92
+ err: (s) => `${fgSeq(c.error)}${s}${RESET}`,
93
+ error: (s) => `${fgSeq(c.error)}${s}${RESET}`,
94
+ info: (s) => `${fgSeq(c.info)}${s}${RESET}`,
95
+ border: (s) => `${fgSeq(c.border)}${s}${RESET}`,
96
+ borderActive: (s) => `${fgSeq(c.borderActive)}${s}${RESET}`,
97
+ user: (s) => `\x1b[1m${fgSeq(c.primary)}${s}${RESET}`,
98
+ title: (s) => `\x1b[1m${fgSeq(c.primary)}${s}${RESET}`,
99
+ bold: (s) => `\x1b[1m${s}\x1b[22m`,
100
+ italic: (s) => `\x1b[3m${s}\x1b[23m`,
101
+ strike: (s) => `\x1b[9m${s}\x1b[29m`,
102
+ inverse: (s) => `\x1b[7m${s}\x1b[27m`,
103
+ diffAdded: (s) => `${fgSeq(c.diffAdded)}${s}${RESET}`,
104
+ diffRemoved: (s) => `${fgSeq(c.diffRemoved)}${s}${RESET}`,
105
+
106
+ // backgrounds ------------------------------------------------------------
107
+ bgPanel: paintBg(c.backgroundPanel),
108
+ bgElement: paintBg(c.backgroundElement),
109
+ bgPrimary: paintBg(c.primary),
110
+ bgBg: paintBg(c.background),
111
+
112
+ // any role by name -------------------------------------------------------
113
+ fgRole(role) {
114
+ return paintFg(c[role] ?? c.text);
115
+ },
116
+ bgRole(role) {
117
+ return paintBg(c[role] ?? c.background);
118
+ },
119
+ hex: c,
120
+ };
121
+
122
+ /** foreground + background at once, used by the prompt box and dialogs */
123
+ t.both = (color, on) => (s) => `${fgSeq(color)}${bgSeq(on)}${s}${RESET}`;
124
+ /** selection row: primary background, background foreground (opencode's active item) */
125
+ t.selected = (s) => `${fgSeq(c.background)}${bgSeq(c.primary)}${s}${RESET}`;
126
+ /** muted selection used for group headers */
127
+ t.mixBorder = (amount) => mix(c.border, c.primary, amount);
128
+
129
+ return t;
130
+ }
131
+
132
+ const PALETTES = {
133
+ arenacode: {
134
+ primary: '#fab283',
135
+ secondary: '#5c9cf5',
136
+ accent: '#9d7cd8',
137
+ error: '#e06c75',
138
+ warning: '#f5a742',
139
+ success: '#7fd88f',
140
+ info: '#56b6c2',
141
+ text: '#eeeeee',
142
+ textMuted: '#808080',
143
+ background: '#0a0a0a',
144
+ backgroundPanel: '#141414',
145
+ backgroundElement: '#1e1e1e',
146
+ border: '#484848',
147
+ borderActive: '#606060',
148
+ borderSubtle: '#3c3c3c',
149
+ diffAdded: '#4fd6be',
150
+ diffRemoved: '#c53b53',
151
+ },
152
+ tokyonight: {
153
+ primary: '#82aaff',
154
+ secondary: '#c099ff',
155
+ accent: '#ff966c',
156
+ error: '#ff757f',
157
+ warning: '#ff966c',
158
+ success: '#c3e88d',
159
+ info: '#82aaff',
160
+ text: '#c8d3f5',
161
+ textMuted: '#828bb8',
162
+ background: '#1a1b26',
163
+ backgroundPanel: '#1e2030',
164
+ backgroundElement: '#222436',
165
+ border: '#737aa2',
166
+ borderActive: '#9099b2',
167
+ borderSubtle: '#545c7e',
168
+ diffAdded: '#4fd6be',
169
+ diffRemoved: '#c53b53',
170
+ },
171
+ catppuccin: {
172
+ primary: '#89b4fa',
173
+ secondary: '#cba6f7',
174
+ accent: '#f5c2e7',
175
+ error: '#f38ba8',
176
+ warning: '#f9e2af',
177
+ success: '#a6e3a1',
178
+ info: '#94e2d5',
179
+ text: '#cdd6f4',
180
+ textMuted: '#9399b2',
181
+ background: '#1e1e2e',
182
+ backgroundPanel: '#181825',
183
+ backgroundElement: '#11111b',
184
+ border: '#313244',
185
+ borderActive: '#45475a',
186
+ borderSubtle: '#585b70',
187
+ diffAdded: '#a6e3a1',
188
+ diffRemoved: '#f38ba8',
189
+ },
190
+ gruvbox: {
191
+ primary: '#83a598',
192
+ secondary: '#d3869b',
193
+ accent: '#8ec07c',
194
+ error: '#fb4934',
195
+ warning: '#fe8019',
196
+ success: '#b8bb26',
197
+ info: '#fabd2f',
198
+ text: '#ebdbb2',
199
+ textMuted: '#928374',
200
+ background: '#282828',
201
+ backgroundPanel: '#3c3836',
202
+ backgroundElement: '#504945',
203
+ border: '#665c54',
204
+ borderActive: '#ebdbb2',
205
+ borderSubtle: '#504945',
206
+ diffAdded: '#98971a',
207
+ diffRemoved: '#cc241d',
208
+ },
209
+ nord: {
210
+ primary: '#88c0d0',
211
+ secondary: '#81a1c1',
212
+ accent: '#8fbcbb',
213
+ error: '#bf616a',
214
+ warning: '#d08770',
215
+ success: '#a3be8c',
216
+ info: '#88c0d0',
217
+ text: '#eceff4',
218
+ textMuted: '#8b95a7',
219
+ background: '#2e3440',
220
+ backgroundPanel: '#3b4252',
221
+ backgroundElement: '#434c5e',
222
+ border: '#434c5e',
223
+ borderActive: '#4c566a',
224
+ borderSubtle: '#434c5e',
225
+ diffAdded: '#a3be8c',
226
+ diffRemoved: '#bf616a',
227
+ },
228
+ };
229
+
230
+ const THEMES = {};
231
+ for (const [name, colors] of Object.entries(PALETTES)) THEMES[name] = makeTheme(name, colors);
232
+
233
+ function themeNames() {
234
+ return Object.keys(THEMES);
235
+ }
236
+
237
+ function getTheme(name) {
238
+ return THEMES[name] || THEMES.arenacode;
239
+ }
240
+
241
+ /** opencode's `tint()`: blend `fg` towards `bg` by `amount` (used for the prompt border) */
242
+ function tint(bgColor, fgColor, amount) {
243
+ return mix(bgColor, fgColor, amount);
244
+ }
245
+
246
+ module.exports = { THEMES, themeNames, getTheme, tint, mix, toRgb, fgSeq, bgSeq, RESET };
@@ -0,0 +1,374 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Widgets copied from opencode's TUI (v1.19.5, packages/tui/src):
5
+ *
6
+ * prompt box → component/prompt/index.tsx (left border ┃, backgroundElement fill,
7
+ * meta row, ╹▀▀▀ underline)
8
+ * user message → routes/session/index.tsx (left border in the agent colour, panel fill)
9
+ * tool rows → routes/session/index.tsx (inline: 2-cell icon + text, block: panel with ┃)
10
+ * footer → feature-plugins/home/footer.tsx
11
+ * tips → feature-plugins/home/tips-view.tsx
12
+ */
13
+
14
+ const R = require('./render');
15
+ const { bgSeq, fgSeq, RESET, tint } = require('./theme');
16
+ const { logoLines, LOGO_WIDTH } = require('./logo');
17
+
18
+ // ------------------------------------------------------------------ helpers
19
+
20
+ /** paint `text` padded to `width` cells on top of a background colour */
21
+ function bgWrap(bg, text, width) {
22
+ const pad = Math.max(0, width - R.vlen(text));
23
+ const inner = String(text).replace(/\x1b\[0m/g, '\x1b[0m' + bg);
24
+ return bg + inner + ' '.repeat(pad) + RESET;
25
+ }
26
+
27
+ function spaces(n) {
28
+ return ' '.repeat(Math.max(0, n));
29
+ }
30
+
31
+ function padEnd(text, width) {
32
+ return text + spaces(width - R.vlen(text));
33
+ }
34
+
35
+ /** blocks spinner, the one opencode uses next to a running tool / prompt */
36
+ const SPINNER_BLOCKS = ['■■■■⬝⬝⬝⬝', '■■■■■⬝⬝⬝', '⬝■■■■■⬝⬝', '⬝⬝■■■■■⬝', '⬝⬝⬝■■■■■', '⬝⬝⬝⬝■■■■', '⬝⬝⬝⬝⬝■■■', '⬝⬝⬝⬝⬝⬝■■'];
37
+ const SPINNER_BRAILLE = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
38
+
39
+ function spinnerText(frame) {
40
+ return SPINNER_BRAILLE[frame % SPINNER_BRAILLE.length];
41
+ }
42
+
43
+ // ------------------------------------------------------------------ prompt
44
+
45
+ /**
46
+ * The prompt box: `┃` on the left, backgroundElement fill, meta row, `╹▀▀…` underline.
47
+ * Returns { lines, cursor } where cursor is { row, col } relative to `lines`.
48
+ */
49
+ function promptBlock(t, opts) {
50
+ const {
51
+ width,
52
+ input = '',
53
+ placeholder = '',
54
+ agent = 'Build',
55
+ agentColor = t.c.secondary,
56
+ model = '',
57
+ provider = '',
58
+ hints = [],
59
+ busy = false,
60
+ statusText = '',
61
+ frame = 0,
62
+ multi = '',
63
+ } = opts;
64
+
65
+ const innerW = Math.max(8, width - 1);
66
+ const bodyW = Math.max(4, innerW - 4); // 2 cells padding on each side
67
+ const bg = bgSeq(t.c.backgroundElement);
68
+ const borderPaint = (s) => `${fgSeq(agentColor)}${s}${RESET}`;
69
+ const hintText = hints.map(([k, label]) => `${fgSeq(t.c.text)}${k}${RESET} ${fgSeq(t.c.textMuted)}${label}${RESET}`).join(' ');
70
+ const lines = [];
71
+ let cursor = null;
72
+
73
+ const rows = textRows(input, bodyW);
74
+ const before = textRows(String(input).slice(0, opts.cursor ?? String(input).length), bodyW);
75
+ const cursorRow = { row: before.length - 1, col: R.vlen(before[before.length - 1] || '') };
76
+
77
+ // top padding row
78
+ lines.push(borderPaint('┃') + bgWrap(bg, '', innerW));
79
+
80
+ const showPlaceholder = !input && placeholder;
81
+ const displayRows = showPlaceholder ? [placeholder] : rows;
82
+ for (let i = 0; i < displayRows.length; i++) {
83
+ const content = showPlaceholder ? `${fgSeq(t.c.textMuted)}${displayRows[i]}${RESET}` : `${fgSeq(t.c.text)}${displayRows[i]}${RESET}`;
84
+ lines.push(borderPaint('┃') + bgWrap(bg, spaces(2) + content, innerW));
85
+ }
86
+ lines.push(borderPaint('┃') + bgWrap(bg, '', innerW));
87
+
88
+ // meta row: agent · model provider [multi] hints
89
+ const metaLeft =
90
+ spaces(2) +
91
+ `${fgSeq(agentColor)}${agent}${RESET}` +
92
+ (model ? ` ${fgSeq(t.c.textMuted)}·${RESET} ${fgSeq(t.c.text)}${model}${RESET}` : '') +
93
+ (provider ? ` ${fgSeq(t.c.textMuted)}${provider}${RESET}` : '') +
94
+ (multi ? ` ${fgSeq(t.c.textMuted)}${multi}${RESET}` : '');
95
+
96
+ const meta = metaLeft + spaces(Math.max(1, innerW - R.vlen(metaLeft)));
97
+ lines.push(borderPaint('┃') + bgWrap(bg, meta, innerW));
98
+
99
+ // underline: ╹ then half blocks in the element colour
100
+ lines.push(borderPaint('╹') + `${fgSeq(t.c.backgroundElement)}${'▀'.repeat(innerW)}${RESET}`);
101
+
102
+ // status row(s) under the box
103
+ if (busy) {
104
+ const spin = `${fgSeq(agentColor)}${spinnerText(frame)}${RESET}`;
105
+ const text = statusText ? `${spaces(1)}${fgSeq(t.c.textMuted)}${statusText}${RESET}` : '';
106
+ const right = `${fgSeq(t.c.text)}esc${RESET} ${fgSeq(t.c.textMuted)}interrupt${RESET}`;
107
+ const left = spaces(1) + spin + text;
108
+ lines.push(left + spaces(Math.max(1, width - R.vlen(left) - R.vlen(right) - 2)) + right);
109
+ } else if (hints.length) {
110
+ lines.push(spaces(Math.max(0, width - R.vlen(hintText) - 2)) + hintText + spaces(2));
111
+ }
112
+
113
+ if (showPlaceholder) cursor = null;
114
+ else cursor = { row: 1 + cursorRow.row, col: 3 + cursorRow.col };
115
+
116
+ return { lines, cursor };
117
+ }
118
+
119
+ /** soft-wrap the input on newlines and on the body width */
120
+ function textRows(text, width) {
121
+ const out = [];
122
+ for (const raw of String(text).split('\n')) {
123
+ if (!raw.length) {
124
+ out.push('');
125
+ continue;
126
+ }
127
+ let line = '';
128
+ for (const word of raw.split(/(\s+)/)) {
129
+ if (R.vlen(line) + R.vlen(word) <= width) {
130
+ line += word;
131
+ continue;
132
+ }
133
+ if (line) out.push(line);
134
+ if (R.vlen(word) > width) {
135
+ let rest = word;
136
+ while (R.vlen(rest) > width) {
137
+ out.push(rest.slice(0, width));
138
+ rest = rest.slice(width);
139
+ }
140
+ line = rest;
141
+ } else {
142
+ line = word.trimStart();
143
+ }
144
+ }
145
+ out.push(line);
146
+ }
147
+ return out.length ? out : [''];
148
+ }
149
+
150
+ // ------------------------------------------------------------------ chat
151
+
152
+ const TOOL_ICONS = {
153
+ terminal: '$',
154
+ globe: '%',
155
+ search: '✱',
156
+ eye: '→',
157
+ pencil: '←',
158
+ folder: '✱',
159
+ check: '✓',
160
+ x: '✗',
161
+ bolt: '⚙',
162
+ plan: '◈',
163
+ };
164
+
165
+ function toolIcon(name) {
166
+ return TOOL_ICONS[name] || '⚙';
167
+ }
168
+
169
+ /**
170
+ * Render the session timeline: user blocks with a left border, assistant parts
171
+ * indented by three cells, tool rows, thinking rows and the closing `▣` line.
172
+ */
173
+ function chatLines(t, items, opts = {}) {
174
+ const { width, frame = 0, agent = 'Build', model = '', details = true } = opts;
175
+ const out = [];
176
+ let prevKind = null;
177
+
178
+ for (const item of items) {
179
+ switch (item.kind) {
180
+ case 'user': {
181
+ if (out.length) out.push('');
182
+ const bg = bgSeq(t.c.backgroundPanel);
183
+ const color = item.color || t.c.secondary;
184
+ const bar = `${fgSeq(color)}┃${RESET}`;
185
+ out.push(bar + bgWrap(bg, '', width - 1));
186
+ for (const line of R.wrap(item.text || '', width - 5)) {
187
+ out.push(bar + bgWrap(bg, spaces(2) + `${fgSeq(t.c.text)}${line}${RESET}`, width - 1));
188
+ }
189
+ if (item.files && item.files.length) {
190
+ const tags = item.files.map((f) => `${t.selected(` File `)}${bgSeq(t.c.backgroundElement)}${fgSeq(t.c.textMuted)} ${f} ${RESET}`).join(' ');
191
+ out.push(bar + bgWrap(bg, spaces(2) + tags, width - 1));
192
+ }
193
+ out.push(bar + bgWrap(bg, '', width - 1));
194
+ prevKind = 'user';
195
+ break;
196
+ }
197
+ case 'text': {
198
+ if (prevKind === 'user') out.push('');
199
+ for (const line of R.wrap(item.text || '', Math.max(10, width - 3))) {
200
+ out.push(spaces(3) + `${fgSeq(t.c.text)}${line}${RESET}`);
201
+ }
202
+ out.push('');
203
+ prevKind = 'text';
204
+ break;
205
+ }
206
+ case 'thought': {
207
+ const done = item.state === 'done';
208
+ const label = item.label || 'Thinking';
209
+ if (done) {
210
+ out.push(spaces(3) + `${fgSeq(t.c.warning)}- Thought${label ? `: ${label}` : ''}${item.duration ? ` · ${item.duration}` : ''}${RESET}`);
211
+ } else {
212
+ out.push(
213
+ spaces(3) +
214
+ `${fgSeq(t.c.warning)}${spinnerText(frame)} Thinking${label && label !== '…' ? `: ${label}` : ''}${RESET}`,
215
+ );
216
+ }
217
+ prevKind = 'thought';
218
+ break;
219
+ }
220
+ case 'tool': {
221
+ const icon = toolIcon(item.icon);
222
+ const running = item.state !== 'done' && item.state !== 'failed';
223
+ const color = item.state === 'failed' ? t.c.error : running ? t.c.text : t.c.textMuted;
224
+ const label = item.label || 'step';
225
+ const meta = item.meta ? ` (${item.meta})` : '';
226
+ if (item.detail && details) {
227
+ // block tool: left border, panel background, command then output
228
+ out.push('');
229
+ const bg = bgSeq(t.c.backgroundPanel);
230
+ const bar = `${fgSeq(t.c.background)}┃${RESET}`;
231
+ out.push(bar + bgWrap(bg, '', width - 1));
232
+ const head = item.command
233
+ ? `${fgSeq(color)}$${RESET} ${fgSeq(t.c.text)}${R.vclip(item.command, Math.max(8, width - 8))}${RESET}`
234
+ : `${fgSeq(color)}${icon}${RESET} ${fgSeq(t.c.text)}${label}${RESET}`;
235
+ out.push(bar + bgWrap(bg, spaces(2) + head + `${fgSeq(t.c.textMuted)}${meta}${RESET}`, width - 1));
236
+ const raw = R.strip(item.detail)
237
+ .split('\n')
238
+ .filter((line) => !/^\s*```/.test(line))
239
+ .filter((line, i, all) => !(i === 0 && /^(shell|bash|sh|text|json)$/i.test(line.trim())));
240
+ for (const line of raw.slice(0, details === 'full' ? 40 : 8)) {
241
+ out.push(bar + bgWrap(bg, spaces(2) + `${fgSeq(t.c.textMuted)}${R.vclip(line, width - 3)}${RESET}`, width - 1));
242
+ }
243
+ } else {
244
+ const head = running
245
+ ? `${fgSeq(t.c.text)}${label}${RESET}`
246
+ : `${fgSeq(t.c.textMuted)}${label}${RESET}`;
247
+ const iconPaint = running
248
+ ? `${fgSeq(t.c.text)}${spinnerText(frame)}${RESET} `
249
+ : `${fgSeq(item.state === 'failed' ? t.c.error : t.c.textMuted)}${icon}${RESET} `;
250
+ out.push(spaces(3) + iconPaint + head + `${fgSeq(t.c.textMuted)}${meta}${RESET}`);
251
+ }
252
+ prevKind = 'tool';
253
+ break;
254
+ }
255
+ case 'status':
256
+ // the bridge's own task id is chrome, not conversation: it lives in the footer
257
+ if (item.label && !/^task\b/i.test(item.label)) {
258
+ out.push(spaces(3) + `${fgSeq(t.c.textMuted)}~ ${item.label}${RESET}`);
259
+ }
260
+ break;
261
+ case 'system':
262
+ out.push(spaces(3) + `${fgSeq(t.c.textMuted)}${R.vclip(item.text || '', width - 4)}${RESET}`);
263
+ break;
264
+ case 'error':
265
+ out.push(spaces(3) + `${fgSeq(t.c.error)}${R.vclip(item.text || 'error', width - 4)}${RESET}`);
266
+ break;
267
+ case 'done': {
268
+ const tone = item.state === 'error' ? t.c.error : item.state === 'cancelled' ? t.c.textMuted : t.c.primary;
269
+ const secs = item.secs ? ` · ${Number(item.secs).toFixed(1)}s` : '';
270
+ out.push(
271
+ spaces(3) +
272
+ `${fgSeq(tone)}▣${RESET} ${fgSeq(t.c.text)}${agent}${RESET}${fgSeq(t.c.textMuted)} · ${model}${secs}${RESET}`,
273
+ );
274
+ out.push('');
275
+ prevKind = 'done';
276
+ break;
277
+ }
278
+ default:
279
+ if (item.text) out.push(spaces(3) + `${fgSeq(t.c.textMuted)}${item.text}${RESET}`);
280
+ }
281
+ }
282
+ return out;
283
+ }
284
+
285
+ // ------------------------------------------------------------------ chrome
286
+
287
+ function footerLine(t, { width, left = '', right = '' }) {
288
+ const l = spaces(2) + `${fgSeq(t.c.textMuted)}${left}${RESET}`;
289
+ const r = `${fgSeq(t.c.textMuted)}${right}${RESET}` + spaces(2);
290
+ return l + spaces(Math.max(1, width - R.vlen(l) - R.vlen(r))) + r;
291
+ }
292
+
293
+ function tipLine(t, { width, text, command }) {
294
+ const body =
295
+ `${fgSeq(t.c.warning)}● Tip ${RESET}` +
296
+ `${fgSeq(t.c.textMuted)}${text}${RESET}` +
297
+ (command ? `${fgSeq(t.c.text)}${command}${RESET}` : '');
298
+ const w = R.vlen(body);
299
+ const left = Math.max(0, Math.floor((width - w) / 2));
300
+ return spaces(left) + body;
301
+ }
302
+
303
+ function notifyLine(t, { width, command, text }) {
304
+ const body = `${fgSeq(t.c.primary)}${command}${RESET}${fgSeq(t.c.textMuted)}${text}${RESET}`;
305
+ const left = Math.max(0, Math.floor((width - R.vlen(body)) / 2));
306
+ return spaces(left) + body;
307
+ }
308
+
309
+ function centerLines(lines, width) {
310
+ return lines.map((line) => {
311
+ const left = Math.max(0, Math.floor((width - R.vlen(line)) / 2));
312
+ return spaces(left) + line;
313
+ });
314
+ }
315
+
316
+ function homeScreen(t, opts) {
317
+ const { width, height, promptLines, tip, notify, footer, brand } = opts;
318
+ const out = [];
319
+ const logo = centerLines(logoLines(t), width);
320
+ const promptH = promptLines.length;
321
+ const chromeH = 1 + 1 + 1 + 1; // notify + tip + blank + footer
322
+ const blockH = logo.length + 1 + promptH + chromeH;
323
+ const top = Math.max(1, Math.floor((height - blockH) / 2));
324
+
325
+ for (let i = 0; i < top; i++) out.push('');
326
+ out.push(...logo);
327
+ out.push('');
328
+ if (notify) out.push(centerLines([notify], width)[0]);
329
+ out.push(...centerLines(promptLines, width).map((line, i) => (i === 0 ? line : line)));
330
+ out.push('');
331
+ if (tip) out.push(tip);
332
+ while (out.length < height - 2) out.push('');
333
+ out.push('');
334
+ out.push(footer);
335
+ while (out.length < height) out.push('');
336
+ return out.slice(0, height);
337
+ }
338
+
339
+ function sessionScreen(t, opts) {
340
+ const { width, height, chat, promptLines, footer, statusLine } = opts;
341
+ const out = [];
342
+ const footerH = 2;
343
+ const statusH = statusLine ? 1 : 0;
344
+ const bodyH = Math.max(1, height - promptLines.length - footerH - statusH);
345
+ const maxScroll = Math.max(0, chat.length - bodyH);
346
+ const scroll = Math.min(Math.max(0, opts.scroll || 0), maxScroll);
347
+ const start = Math.max(0, chat.length - bodyH - scroll);
348
+ const visible = chat.slice(start, start + bodyH);
349
+ while (visible.length < bodyH) visible.unshift('');
350
+ for (const line of visible) out.push(line);
351
+ out.push(...promptLines);
352
+ if (statusLine) out.push(statusLine);
353
+ out.push('');
354
+ out.push(footer);
355
+ return out.slice(0, height);
356
+ }
357
+
358
+ module.exports = {
359
+ promptBlock,
360
+ chatLines,
361
+ footerLine,
362
+ tipLine,
363
+ notifyLine,
364
+ homeScreen,
365
+ sessionScreen,
366
+ centerLines,
367
+ bgWrap,
368
+ spaces,
369
+ spinnerText,
370
+ SPINNER_BLOCKS,
371
+ toolIcon,
372
+ textRows,
373
+ LOGO_WIDTH,
374
+ };