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/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
- // LAZYCLAW wordmark (top, per-row gradient orange)
7
- //
8
- // ╭─ lazyclaw vX.Y.Z · trainer-split · FTS5 recall · 6-backend sandbox ─╮
9
- // [sloth braille] Subcommands │
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
- export function renderSplashToString(props, opts = {}) {
83
- const cols = opts.columns || process.stdout.columns || 100;
84
- const TERM = Math.max(80, cols);
85
- const PANEL_W = TERM - LMARGIN.length * 2;
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 (subcommands + tools + skills)
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) bottom border + status bar + border (Hermes-style separator)
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; // panel + sloth tinted
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
  );