lazyclaw 5.0.8 → 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/tui/splash.mjs CHANGED
@@ -1,23 +1,12 @@
1
- // tui/splash.mjs — Hermes-style hero splash.
1
+ // tui/splash.mjs — Hermes-style hero splash with gradient wordmark,
2
+ // subcommand catalog, tool registry, skill index, and a bottom status bar.
2
3
  //
3
- // Layout (terminal-width responsive):
4
+ // Layout (terminal-width responsive across four tiers):
4
5
  //
5
- // ██╗ █████╗ ... ← wordmark (top, single-tone orange)
6
- // ...
7
- //
8
- // ╭───── lazyclaw vX.Y.Z · trainer-split + FTS5 recall ─────────────╮
9
- // │ [sloth braille] Available Tools │
10
- // │ fs read · write · ... │
11
- // │ Available Skills │
12
- // │ N tools · M skills · /help for commands │
13
- // ╰──────────────────────────────────────────────────────────────────╯
14
- //
15
- // provider · X · Y trainer · A · B
16
- // /Users/o/cwd
17
- // Session: 20260605_180543_2e0351
18
- //
19
- // Welcome to lazyclaw. Type your message or /help for commands.
20
- // + Tip: ...
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
21
10
  import React from 'react';
22
11
  import { Box, Text } from 'ink';
23
12
  import stringWidth from 'string-width';
@@ -28,6 +17,27 @@ import { wordmark } from './wordmark.mjs';
28
17
  const LMARGIN = ' ';
29
18
  const TITLE = ' trainer-split · FTS5 recall · 6-backend sandbox ';
30
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
+
28
+ // Subcommand catalog — grouped for the splash so a new user sees the
29
+ // surface area at a glance. Mirrors SUBCOMMANDS in cli.mjs.
30
+ export const SUBCOMMAND_GROUPS = [
31
+ ['core', ['chat', 'agent', 'orchestrator', 'dashboard', 'menu']],
32
+ ['workflow', ['run', 'resume', 'inspect', 'clear', 'validate', 'graph']],
33
+ ['config', ['config', 'auth', 'rates', 'providers', 'setup', 'onboard']],
34
+ ['state', ['sessions', 'skills', 'workspace', 'memory', 'status', 'doctor']],
35
+ ['runtime', ['daemon', 'cron', 'loop', 'loops', 'goal']],
36
+ ['channels', ['slack', 'telegram', 'matrix', 'channels', 'message', 'pairing']],
37
+ ['v5', ['sandbox', 'personality', 'migrate', 'hermes', 'openclaw', 'trajectories']],
38
+ ['utility', ['browse', 'version', 'completion', 'help', 'export', 'import', 'nodes']],
39
+ ];
40
+
31
41
  function fit(text, max) {
32
42
  if (stringWidth(text) <= max) return text.padEnd(max);
33
43
  let lo = 0, hi = text.length;
@@ -56,38 +66,87 @@ function skillRow({ group, names }) {
56
66
  return `${group.padEnd(12)} ${tail}`;
57
67
  }
58
68
 
59
- export function renderSplashToString(props, opts = {}) {
60
- const cols = opts.columns || process.stdout.columns || 100;
61
- const TERM = Math.max(80, cols);
62
- const PANEL_W = TERM - LMARGIN.length * 2;
63
- const INNER = PANEL_W - 4; // 2 border + 2 padding
69
+ function subcommandRow([label, verbs]) {
70
+ return `${label.padEnd(12)} ${verbs.join(' · ')}`;
71
+ }
72
+
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;
115
+ const INNER = PANEL_W - 4;
64
116
  const SLOTH_W = banner.width;
65
117
  const RIGHT_W = Math.max(40, INNER - SLOTH_W - 2);
66
118
 
67
119
  const lines = [];
68
120
 
69
- // Wordmark
121
+ // 1) wordmark
70
122
  for (const r of wordmark.rows) lines.push(LMARGIN + r);
71
123
  lines.push('');
72
124
 
73
- // Panel top with title
125
+ // 2) panel top with inset title
74
126
  const versionLabel = ` lazyclaw ${props.version || ''} ·${TITLE} `;
75
127
  const dashLeft = '─'.repeat(8);
76
128
  const dashRight = '─'.repeat(Math.max(2, PANEL_W - 2 - dashLeft.length - stringWidth(versionLabel)));
77
129
  lines.push(`${LMARGIN}╭${dashLeft}${versionLabel}${dashRight}╮`);
78
130
 
79
- // Right column content
131
+ // 3) right column content
80
132
  const { tools = [], skills = [] } = props;
81
133
  const right = [];
134
+ right.push('Subcommands');
135
+ for (const g of SUBCOMMAND_GROUPS) right.push(subcommandRow(g));
136
+ right.push('');
82
137
  right.push('Available Tools');
83
- for (const t of tools.slice(0, 8)) right.push(toolRow(t));
84
- if (tools.length > 8) right.push(`(and ${tools.length - 8} more tool groups...)`);
138
+ for (const t of tools.slice(0, 14)) right.push(toolRow(t));
139
+ if (tools.length > 14) right.push(`(and ${tools.length - 14} more...)`);
85
140
  right.push('');
86
141
  right.push('Available Skills');
87
- for (const s of skills.slice(0, 8)) right.push(skillRow(s));
88
- if (skills.length > 8) right.push(`(and ${skills.length - 8} more skill groups...)`);
142
+ if (skills.length === 0) right.push('(none installed)');
143
+ else {
144
+ for (const s of skills.slice(0, 8)) right.push(skillRow(s));
145
+ if (skills.length > 8) right.push(`(and ${skills.length - 8} more skill groups...)`);
146
+ }
89
147
  right.push('');
90
- right.push(`${tools.length} tools · ${skills.length} skills · /help for commands`);
148
+ const subcmdCount = SUBCOMMAND_GROUPS.reduce((n, [, v]) => n + v.length, 0);
149
+ right.push(`${subcmdCount} subcommands · ${tools.length} tool groups · ${skills.length} skills · /help for commands`);
91
150
 
92
151
  const sloth = banner.rows.slice();
93
152
  while (sloth.length < right.length) sloth.push(' '.repeat(SLOTH_W));
@@ -101,7 +160,97 @@ export function renderSplashToString(props, opts = {}) {
101
160
  lines.push(`${LMARGIN}╰${'─'.repeat(PANEL_W - 2)}╯`);
102
161
  lines.push('');
103
162
 
104
- // Provider / session info
163
+ // 4) provider / session info
164
+ const { provider, model, trainer = {}, sessionId, cwd } = props;
165
+ const tProv = trainer.provider || provider;
166
+ const tModel = trainer.model || model;
167
+ lines.push(`${LMARGIN}${provider} · ${model} · trainer ${tProv} · ${tModel}`);
168
+ lines.push(`${LMARGIN}${shortCwd(cwd || process.cwd())}`);
169
+ if (sessionId) lines.push(`${LMARGIN}Session: ${sessionId}`);
170
+ lines.push('');
171
+ lines.push(`${LMARGIN}Welcome to lazyclaw. Type your message or /help for commands.`);
172
+ lines.push(`${LMARGIN}+ Tip: trainer learns from your Claude Pro subscription at $0.`);
173
+ lines.push('');
174
+
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
105
254
  const { provider, model, trainer = {}, sessionId, cwd } = props;
106
255
  const tProv = trainer.provider || provider;
107
256
  const tModel = trainer.model || model;
@@ -111,19 +260,115 @@ export function renderSplashToString(props, opts = {}) {
111
260
  lines.push('');
112
261
  lines.push(`${LMARGIN}Welcome to lazyclaw. Type your message or /help for commands.`);
113
262
  lines.push(`${LMARGIN}+ Tip: trainer learns from your Claude Pro subscription at $0.`);
263
+ lines.push('');
264
+
265
+ // 5) status bar
266
+ const sep = '─'.repeat(PANEL_W);
267
+ lines.push(LMARGIN + sep);
268
+ const ctx = props.ctxUsed != null && props.ctxTotal != null
269
+ ? `[${'░'.repeat(20)}] ${props.ctxUsed}/${props.ctxTotal}`
270
+ : `[${'░'.repeat(20)}] --`;
271
+ lines.push(`${LMARGIN} ${provider} · ${model} | ctx -- | ${ctx} | 0s`);
272
+ lines.push(LMARGIN + sep);
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;
114
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);
115
353
  return lines.join('\n');
116
354
  }
117
355
 
118
356
  export function Splash(props) {
119
- const lines = renderSplashToString(props).split('\n');
120
- // Color the wordmark and sloth rows; everything else stays default.
121
- const heroRowCount = wordmark.height + 1 + 1 + banner.height + 1; // word + blank + top + sloth + bottom
357
+ const cols = process.stdout.columns || 100;
358
+ const out = renderSplashToString(props, { columns: cols });
359
+ const lines = out.split('\n');
360
+ const palette = wordmark.palette;
361
+ const gradient = wordmark.gradient;
362
+ const showWordmark = cols >= WORDMARK_BREAKPOINT;
363
+
122
364
  return React.createElement(
123
365
  Box,
124
366
  { flexDirection: 'column' },
125
- lines.map((line, i) =>
126
- React.createElement(Text, { key: i, color: i < heroRowCount ? theme.fg : undefined }, line)
127
- )
367
+ lines.map((line, i) => {
368
+ let color;
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;
371
+ return React.createElement(Text, { key: i, color }, line);
372
+ })
128
373
  );
129
374
  }
package/tui/wordmark.mjs CHANGED
@@ -1,5 +1,21 @@
1
- // User-supplied "Larry 3D"-style LAZYCLAW wordmark — 13 rows × 120 cols.
2
- // Replaces the v5.0.6 ANSI Shadow version on operator request.
1
+ // "Larry 3D"-style LAZYCLAW wordmark — 13 rows × 120 cols, shared between
2
+ // chat splash and launcher. Gradient palette maps each row to one of four
3
+ // warm-orange shades; top rows = brightest, bottom rows = shadow-dark.
4
+ //
5
+ // The string-output renderer (renderSplashToString) emits the raw rows;
6
+ // the ink/launcher color path picks PALETTE[GRADIENT[row]] when ANSI is
7
+ // available.
8
+
9
+ export const PALETTE = [
10
+ '#FFD580', // 0 top — warm white-gold
11
+ '#FFB347', // 1 highlight (current single-tone reference)
12
+ '#E08020', // 2 midtone — amber
13
+ '#A05010', // 3 bottom — burnt shadow
14
+ ];
15
+
16
+ // 13 rows; bias toward bottom for shadow depth.
17
+ export const GRADIENT = [0, 0, 0, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3];
18
+
3
19
  export const wordmark = {
4
20
  rows: [
5
21
  " ____ ____ _____ _____ _____ _____ ____ ____ _____ ",
@@ -18,5 +34,7 @@ export const wordmark = {
18
34
  ],
19
35
  width: 120,
20
36
  height: 13,
21
- fg: "#FFB347"
37
+ fg: "#FFB347",
38
+ gradient: GRADIENT,
39
+ palette: PALETTE,
22
40
  };