@lardexx/arenacode 0.0.0-stage → 2.5.3
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/LICENSE +21 -0
- package/README.md +288 -2
- package/bin/arenacode.js +19 -0
- package/docs/ANALYSIS.md +201 -0
- package/docs/PUBLISHING.md +65 -0
- package/docs/TUI.md +114 -0
- package/docs/tui-approve.html +28 -0
- package/docs/tui-approve.png +0 -0
- package/docs/tui-home.html +28 -0
- package/docs/tui-home.png +0 -0
- package/docs/tui-palette.html +28 -0
- package/docs/tui-palette.png +0 -0
- package/docs/tui-plan.html +28 -0
- package/docs/tui-plan.png +0 -0
- package/docs/tui-preview.html +28 -0
- package/docs/tui-session.png +0 -0
- package/docs/tui-slash.html +28 -0
- package/package.json +32 -4
- package/src/bridge.js +135 -0
- package/src/cli.js +720 -0
- package/src/config.js +141 -0
- package/src/local.js +507 -0
- package/src/meta.js +143 -0
- package/src/prompts/agent-prompt.md +237 -0
- package/src/tui/app.js +1368 -0
- package/src/tui/dialogs.js +136 -0
- package/src/tui/keys.js +134 -0
- package/src/tui/logo.js +54 -0
- package/src/tui/render.js +256 -0
- package/src/tui/term.js +93 -0
- package/src/tui/theme.js +246 -0
- package/src/tui/widgets.js +374 -0
- package/src/turn.js +213 -0
- package/src/ui.js +130 -0
package/src/tui/theme.js
ADDED
|
@@ -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
|
+
};
|