lazyclaw 5.0.9 → 5.1.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/README.md +29 -8
- package/cli.mjs +1 -1
- package/daemon.mjs +377 -4
- package/package.json +1 -1
- package/tui/splash.mjs +229 -32
- package/web/dashboard.html +383 -24
package/tui/splash.mjs
CHANGED
|
@@ -1,29 +1,12 @@
|
|
|
1
1
|
// tui/splash.mjs — Hermes-style hero splash with gradient wordmark,
|
|
2
2
|
// subcommand catalog, tool registry, skill index, and a bottom status bar.
|
|
3
3
|
//
|
|
4
|
-
// Layout (terminal-width responsive):
|
|
4
|
+
// Layout (terminal-width responsive across four tiers):
|
|
5
5
|
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// │ core / workflow / config / state / ... │
|
|
11
|
-
// │ Available Tools │
|
|
12
|
-
// │ fs / exec / git / ... │
|
|
13
|
-
// │ Available Skills │
|
|
14
|
-
// │ N subcommands · M tool groups · K skills · /help │
|
|
15
|
-
// ╰──────────────────────────────────────────────────────────────────────╯
|
|
16
|
-
//
|
|
17
|
-
// provider · X · Y · trainer · A · B
|
|
18
|
-
// /cwd
|
|
19
|
-
// Session: ...
|
|
20
|
-
//
|
|
21
|
-
// Welcome to lazyclaw. Type your message or /help for commands.
|
|
22
|
-
// + Tip: ...
|
|
23
|
-
//
|
|
24
|
-
// ───────────────────────────────────────────────────────────────────────
|
|
25
|
-
// provider · model | ctx -- | [progress] | 0s
|
|
26
|
-
// ───────────────────────────────────────────────────────────────────────
|
|
6
|
+
// WIDE (cols >= 140) — full wordmark + panel + sloth + 2-col right side
|
|
7
|
+
// MEDIUM ( 90 <= cols < 140) — compact headline, panel + sloth + wrapped right column
|
|
8
|
+
// NARROW ( 60 <= cols < 90) — single column, no sloth/panel, truncated verb lists
|
|
9
|
+
// MINIMAL (cols < 60) — headline + provider + cwd + /help line only
|
|
27
10
|
import React from 'react';
|
|
28
11
|
import { Box, Text } from 'ink';
|
|
29
12
|
import stringWidth from 'string-width';
|
|
@@ -34,6 +17,14 @@ import { wordmark } from './wordmark.mjs';
|
|
|
34
17
|
const LMARGIN = ' ';
|
|
35
18
|
const TITLE = ' trainer-split · FTS5 recall · 6-backend sandbox ';
|
|
36
19
|
|
|
20
|
+
// Tier breakpoints. Wordmark is 120 cols wide + LMARGIN(2)*2 = 124 minimum;
|
|
21
|
+
// the user constraint pins WIDE at >=140 to give comfortable slack. Below
|
|
22
|
+
// 90 the sloth (48 cols) leaves <40 cols for the right column, so we drop
|
|
23
|
+
// it and go single-column. Below 60 we emit only a minimal headline.
|
|
24
|
+
const WORDMARK_BREAKPOINT = 140; // drop wordmark below this
|
|
25
|
+
const PANEL_BREAKPOINT = 90; // drop sloth+panel below this
|
|
26
|
+
const MINIMAL_BREAKPOINT = 60; // drop everything but headline below this
|
|
27
|
+
|
|
37
28
|
// Subcommand catalog — grouped for the splash so a new user sees the
|
|
38
29
|
// surface area at a glance. Mirrors SUBCOMMANDS in cli.mjs.
|
|
39
30
|
export const SUBCOMMAND_GROUPS = [
|
|
@@ -79,10 +70,48 @@ function subcommandRow([label, verbs]) {
|
|
|
79
70
|
return `${label.padEnd(12)} ${verbs.join(' · ')}`;
|
|
80
71
|
}
|
|
81
72
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
73
|
+
// Wrap a labeled verb list onto multiple rows when it would overflow maxWidth.
|
|
74
|
+
// First row: '<label.padEnd(12)> verb · verb'; continuations: ' verb · verb'.
|
|
75
|
+
function wrapVerbs(label, verbs, maxWidth) {
|
|
76
|
+
const pad = ' '.repeat(13); // label.padEnd(12) + ' ' = 13 cells
|
|
77
|
+
const rows = [];
|
|
78
|
+
let current = label.padEnd(12) + ' ';
|
|
79
|
+
let firstOnRow = true;
|
|
80
|
+
for (const v of verbs) {
|
|
81
|
+
const candidate = firstOnRow ? current + v : current + ' · ' + v;
|
|
82
|
+
if (stringWidth(candidate) > maxWidth) {
|
|
83
|
+
if (firstOnRow) {
|
|
84
|
+
// even a single verb overflows — emit it anyway (truncated) to make progress.
|
|
85
|
+
rows.push(fit(candidate, maxWidth).trimEnd());
|
|
86
|
+
current = pad;
|
|
87
|
+
firstOnRow = true;
|
|
88
|
+
} else {
|
|
89
|
+
rows.push(current.trimEnd());
|
|
90
|
+
current = pad + v;
|
|
91
|
+
firstOnRow = false;
|
|
92
|
+
}
|
|
93
|
+
} else {
|
|
94
|
+
current = candidate;
|
|
95
|
+
firstOnRow = false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
if (current.trim()) rows.push(current.trimEnd());
|
|
99
|
+
return rows;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Crush-style truncation for NARROW tier — take first N verbs, append '…' if more.
|
|
103
|
+
function truncateRow(label, verbs, maxWidth, take = 3) {
|
|
104
|
+
const head = label.padEnd(12) + ' ';
|
|
105
|
+
const tail = verbs.slice(0, take).join(' · ');
|
|
106
|
+
let line = head + tail;
|
|
107
|
+
if (verbs.length > take) line += ' …';
|
|
108
|
+
if (stringWidth(line) <= maxWidth) return line;
|
|
109
|
+
return fit(line, maxWidth).trimEnd();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Wide tier — original v5.0.9 layout, kept verbatim.
|
|
113
|
+
function renderWide(props, cols) {
|
|
114
|
+
const PANEL_W = cols - LMARGIN.length * 2;
|
|
86
115
|
const INNER = PANEL_W - 4;
|
|
87
116
|
const SLOTH_W = banner.width;
|
|
88
117
|
const RIGHT_W = Math.max(40, INNER - SLOTH_W - 2);
|
|
@@ -99,7 +128,7 @@ export function renderSplashToString(props, opts = {}) {
|
|
|
99
128
|
const dashRight = '─'.repeat(Math.max(2, PANEL_W - 2 - dashLeft.length - stringWidth(versionLabel)));
|
|
100
129
|
lines.push(`${LMARGIN}╭${dashLeft}${versionLabel}${dashRight}╮`);
|
|
101
130
|
|
|
102
|
-
// 3) right column content
|
|
131
|
+
// 3) right column content
|
|
103
132
|
const { tools = [], skills = [] } = props;
|
|
104
133
|
const right = [];
|
|
105
134
|
right.push('Subcommands');
|
|
@@ -143,7 +172,97 @@ export function renderSplashToString(props, opts = {}) {
|
|
|
143
172
|
lines.push(`${LMARGIN}+ Tip: trainer learns from your Claude Pro subscription at $0.`);
|
|
144
173
|
lines.push('');
|
|
145
174
|
|
|
146
|
-
// 5)
|
|
175
|
+
// 5) status bar (Hermes-style separator)
|
|
176
|
+
const sep = '─'.repeat(PANEL_W);
|
|
177
|
+
lines.push(LMARGIN + sep);
|
|
178
|
+
const ctx = props.ctxUsed != null && props.ctxTotal != null
|
|
179
|
+
? `[${'░'.repeat(20)}] ${props.ctxUsed}/${props.ctxTotal}`
|
|
180
|
+
: `[${'░'.repeat(20)}] --`;
|
|
181
|
+
lines.push(`${LMARGIN} ${provider} · ${model} | ctx -- | ${ctx} | 0s`);
|
|
182
|
+
lines.push(LMARGIN + sep);
|
|
183
|
+
|
|
184
|
+
return lines;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// Medium tier — no wordmark, compact panel title, sloth + wrapped right column.
|
|
188
|
+
function renderMedium(props, cols) {
|
|
189
|
+
const PANEL_W = cols - LMARGIN.length * 2;
|
|
190
|
+
const INNER = PANEL_W - 4;
|
|
191
|
+
const SLOTH_W = banner.width;
|
|
192
|
+
const RIGHT_W = Math.max(20, INNER - SLOTH_W - 2);
|
|
193
|
+
|
|
194
|
+
const lines = [];
|
|
195
|
+
|
|
196
|
+
// 1) compact headline (no wordmark)
|
|
197
|
+
lines.push(`${LMARGIN}lazyclaw ${props.version || ''}`.trimEnd());
|
|
198
|
+
lines.push('');
|
|
199
|
+
|
|
200
|
+
// 2) compact panel top — drop the TITLE chain, just version
|
|
201
|
+
const versionLabel = ` lazyclaw ${props.version || ''} `;
|
|
202
|
+
const dashLeft = '─'.repeat(4);
|
|
203
|
+
const dashRight = '─'.repeat(Math.max(2, PANEL_W - 2 - dashLeft.length - stringWidth(versionLabel)));
|
|
204
|
+
lines.push(`${LMARGIN}╭${dashLeft}${versionLabel}${dashRight}╮`);
|
|
205
|
+
|
|
206
|
+
// 3) build right column with wrapping
|
|
207
|
+
const { tools = [], skills = [] } = props;
|
|
208
|
+
const right = [];
|
|
209
|
+
right.push('Subcommands');
|
|
210
|
+
for (const [label, verbs] of SUBCOMMAND_GROUPS) {
|
|
211
|
+
for (const r of wrapVerbs(label, verbs, RIGHT_W)) right.push(r);
|
|
212
|
+
}
|
|
213
|
+
right.push('');
|
|
214
|
+
right.push('Available Tools');
|
|
215
|
+
for (const t of tools.slice(0, 14)) {
|
|
216
|
+
const label = t.sensitive ? `${t.category}*` : t.category;
|
|
217
|
+
for (const r of wrapVerbs(label, t.verbs.slice(0, 6), RIGHT_W)) right.push(r);
|
|
218
|
+
}
|
|
219
|
+
if (tools.length > 14) right.push(`(and ${tools.length - 14} more...)`);
|
|
220
|
+
right.push('');
|
|
221
|
+
right.push('Available Skills');
|
|
222
|
+
if (skills.length === 0) right.push('(none installed)');
|
|
223
|
+
else {
|
|
224
|
+
for (const s of skills.slice(0, 8)) {
|
|
225
|
+
for (const r of wrapVerbs(s.group, s.names.slice(0, 6), RIGHT_W)) right.push(r);
|
|
226
|
+
}
|
|
227
|
+
if (skills.length > 8) right.push(`(and ${skills.length - 8} more skill groups...)`);
|
|
228
|
+
}
|
|
229
|
+
right.push('');
|
|
230
|
+
const subcmdCount = SUBCOMMAND_GROUPS.reduce((n, [, v]) => n + v.length, 0);
|
|
231
|
+
const summary = `${subcmdCount} subcommands · ${tools.length} tool groups · ${skills.length} skills · /help`;
|
|
232
|
+
if (stringWidth(summary) > RIGHT_W) {
|
|
233
|
+
right.push(`${subcmdCount} subcmds · ${tools.length} tools · ${skills.length} skills`);
|
|
234
|
+
right.push('/help for commands');
|
|
235
|
+
} else {
|
|
236
|
+
right.push(summary);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
const sloth = banner.rows.slice();
|
|
240
|
+
while (sloth.length < right.length) sloth.push(' '.repeat(SLOTH_W));
|
|
241
|
+
while (right.length < sloth.length) right.push('');
|
|
242
|
+
|
|
243
|
+
for (let i = 0; i < sloth.length; i++) {
|
|
244
|
+
const l = sloth[i] || ' '.repeat(SLOTH_W);
|
|
245
|
+
// pad (no ellipsis) — wrapVerbs already guarantees width <= RIGHT_W
|
|
246
|
+
const raw = right[i] || '';
|
|
247
|
+
const r = raw + ' '.repeat(Math.max(0, RIGHT_W - stringWidth(raw)));
|
|
248
|
+
lines.push(`${LMARGIN}│ ${l} ${r} │`);
|
|
249
|
+
}
|
|
250
|
+
lines.push(`${LMARGIN}╰${'─'.repeat(PANEL_W - 2)}╯`);
|
|
251
|
+
lines.push('');
|
|
252
|
+
|
|
253
|
+
// 4) provider / session info
|
|
254
|
+
const { provider, model, trainer = {}, sessionId, cwd } = props;
|
|
255
|
+
const tProv = trainer.provider || provider;
|
|
256
|
+
const tModel = trainer.model || model;
|
|
257
|
+
lines.push(`${LMARGIN}${provider} · ${model} · trainer ${tProv} · ${tModel}`);
|
|
258
|
+
lines.push(`${LMARGIN}${shortCwd(cwd || process.cwd())}`);
|
|
259
|
+
if (sessionId) lines.push(`${LMARGIN}Session: ${sessionId}`);
|
|
260
|
+
lines.push('');
|
|
261
|
+
lines.push(`${LMARGIN}Welcome to lazyclaw. Type your message or /help for commands.`);
|
|
262
|
+
lines.push(`${LMARGIN}+ Tip: trainer learns from your Claude Pro subscription at $0.`);
|
|
263
|
+
lines.push('');
|
|
264
|
+
|
|
265
|
+
// 5) status bar
|
|
147
266
|
const sep = '─'.repeat(PANEL_W);
|
|
148
267
|
lines.push(LMARGIN + sep);
|
|
149
268
|
const ctx = props.ctxUsed != null && props.ctxTotal != null
|
|
@@ -152,25 +271,103 @@ export function renderSplashToString(props, opts = {}) {
|
|
|
152
271
|
lines.push(`${LMARGIN} ${provider} · ${model} | ctx -- | ${ctx} | 0s`);
|
|
153
272
|
lines.push(LMARGIN + sep);
|
|
154
273
|
|
|
274
|
+
return lines;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Narrow tier — single column, no sloth, no panel, truncated verb lists.
|
|
278
|
+
function renderNarrow(props, cols) {
|
|
279
|
+
const W = cols - LMARGIN.length * 2;
|
|
280
|
+
const lines = [];
|
|
281
|
+
|
|
282
|
+
// 1) headline
|
|
283
|
+
lines.push(`${LMARGIN}lazyclaw ${props.version || ''}`.trimEnd());
|
|
284
|
+
lines.push('');
|
|
285
|
+
|
|
286
|
+
const { tools = [], skills = [], provider, model, trainer = {}, sessionId, cwd } = props;
|
|
287
|
+
|
|
288
|
+
// 2) subcommands
|
|
289
|
+
lines.push(`${LMARGIN}Subcommands`);
|
|
290
|
+
for (const [label, verbs] of SUBCOMMAND_GROUPS) {
|
|
291
|
+
lines.push(LMARGIN + truncateRow(label, verbs, W));
|
|
292
|
+
}
|
|
293
|
+
lines.push('');
|
|
294
|
+
|
|
295
|
+
// 3) tools
|
|
296
|
+
lines.push(`${LMARGIN}Available Tools`);
|
|
297
|
+
for (const t of tools.slice(0, 14)) {
|
|
298
|
+
const label = t.sensitive ? `${t.category}*` : t.category;
|
|
299
|
+
lines.push(LMARGIN + truncateRow(label, t.verbs, W));
|
|
300
|
+
}
|
|
301
|
+
if (tools.length > 14) lines.push(`${LMARGIN}(and ${tools.length - 14} more...)`);
|
|
302
|
+
lines.push('');
|
|
303
|
+
|
|
304
|
+
// 4) skills
|
|
305
|
+
lines.push(`${LMARGIN}Available Skills`);
|
|
306
|
+
if (skills.length === 0) lines.push(`${LMARGIN}(none installed)`);
|
|
307
|
+
else {
|
|
308
|
+
for (const s of skills.slice(0, 8)) {
|
|
309
|
+
lines.push(LMARGIN + truncateRow(s.group, s.names, W));
|
|
310
|
+
}
|
|
311
|
+
if (skills.length > 8) lines.push(`${LMARGIN}(and ${skills.length - 8} more skill groups...)`);
|
|
312
|
+
}
|
|
313
|
+
lines.push('');
|
|
314
|
+
|
|
315
|
+
// 5) provider / session info
|
|
316
|
+
const tProv = trainer.provider || provider;
|
|
317
|
+
const tModel = trainer.model || model;
|
|
318
|
+
lines.push(fit(`${LMARGIN}${provider} · ${model} · trainer ${tProv} · ${tModel}`, cols).trimEnd());
|
|
319
|
+
lines.push(fit(`${LMARGIN}${shortCwd(cwd || process.cwd())}`, cols).trimEnd());
|
|
320
|
+
if (sessionId) lines.push(fit(`${LMARGIN}Session: ${sessionId}`, cols).trimEnd());
|
|
321
|
+
lines.push('');
|
|
322
|
+
lines.push(fit(`${LMARGIN}Welcome to lazyclaw. /help for commands.`, cols).trimEnd());
|
|
323
|
+
lines.push('');
|
|
324
|
+
|
|
325
|
+
// 6) compact status — single line, no separator dashes
|
|
326
|
+
const ctx = props.ctxUsed != null && props.ctxTotal != null
|
|
327
|
+
? `${props.ctxUsed}/${props.ctxTotal}`
|
|
328
|
+
: '--';
|
|
329
|
+
lines.push(fit(`${LMARGIN}${provider} · ${model} · ctx ${ctx}`, cols).trimEnd());
|
|
330
|
+
|
|
331
|
+
return lines;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Minimal tier — bare-bones fallback for cols < 60.
|
|
335
|
+
function renderMinimal(props) {
|
|
336
|
+
const { version, provider, model, sessionId, cwd } = props;
|
|
337
|
+
const lines = [];
|
|
338
|
+
lines.push(`lazyclaw ${version || ''}`.trimEnd());
|
|
339
|
+
lines.push(`${provider} · ${model}`);
|
|
340
|
+
lines.push(shortCwd(cwd || process.cwd()));
|
|
341
|
+
if (sessionId) lines.push(`Session: ${sessionId}`);
|
|
342
|
+
lines.push('/help for commands');
|
|
343
|
+
return lines;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export function renderSplashToString(props, opts = {}) {
|
|
347
|
+
const cols = opts.columns || process.stdout.columns || 100;
|
|
348
|
+
let lines;
|
|
349
|
+
if (cols < MINIMAL_BREAKPOINT) lines = renderMinimal(props);
|
|
350
|
+
else if (cols < PANEL_BREAKPOINT) lines = renderNarrow(props, cols);
|
|
351
|
+
else if (cols < WORDMARK_BREAKPOINT) lines = renderMedium(props, cols);
|
|
352
|
+
else lines = renderWide(props, cols);
|
|
155
353
|
return lines.join('\n');
|
|
156
354
|
}
|
|
157
355
|
|
|
158
356
|
export function Splash(props) {
|
|
159
357
|
const cols = process.stdout.columns || 100;
|
|
160
|
-
const TERM = Math.max(80, cols);
|
|
161
|
-
const PANEL_W = TERM - LMARGIN.length * 2;
|
|
162
358
|
const out = renderSplashToString(props, { columns: cols });
|
|
163
359
|
const lines = out.split('\n');
|
|
164
360
|
const palette = wordmark.palette;
|
|
165
361
|
const gradient = wordmark.gradient;
|
|
362
|
+
const showWordmark = cols >= WORDMARK_BREAKPOINT;
|
|
166
363
|
|
|
167
364
|
return React.createElement(
|
|
168
365
|
Box,
|
|
169
366
|
{ flexDirection: 'column' },
|
|
170
367
|
lines.map((line, i) => {
|
|
171
368
|
let color;
|
|
172
|
-
if (i < wordmark.height) color = palette[gradient[i] ?? 1];
|
|
173
|
-
else if (i < wordmark.height + 1 + 1 + banner.height) color = theme.fg;
|
|
369
|
+
if (showWordmark && i < wordmark.height) color = palette[gradient[i] ?? 1];
|
|
370
|
+
else if (showWordmark && i < wordmark.height + 1 + 1 + banner.height) color = theme.fg;
|
|
174
371
|
return React.createElement(Text, { key: i, color }, line);
|
|
175
372
|
})
|
|
176
373
|
);
|