archymedes-cli 2.2.0 → 2.3.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 CHANGED
@@ -312,13 +312,23 @@ The startup geometry makes a brief rotation on roomy interactive terminals. Set
312
312
 
313
313
  ## Fixed workspace
314
314
 
315
- Start with `archymedes --layout fixed` for a persistent mode bar, an anchored composer, and menus
316
- that open inside the transcript area. `/layout` switches layouts during a session;
317
- `ARCHYMEDES_LAYOUT=fixed` makes the fixed workspace your launch preference.
318
-
319
- Use `/mode` to choose plan, build, auto, or defender with the arrow keys. Page Up / Page Down
320
- read retained transcript history; Escape returns to live output. Home and End still edit the
321
- composer. Each tab retains up to 5,000 output lines. Resize reflows the live transcript.
315
+ Interactive sessions open in the fixed workspace: a persistent mode bar, an anchored composer, and
316
+ menus that open inside the transcript area. `/layout` switches layouts during a session;
317
+ `archymedes --layout scrollback` or `ARCHYMEDES_LAYOUT=scrollback` keeps the plain terminal log.
318
+
319
+ Use `/mode` to choose plan, build, auto, or defender with the arrow keys. To read history, use the
320
+ mouse wheel or Page Up / Page Down. Alt+Up / Alt+Down (or Ctrl) move one line, and Ctrl+Home jumps
321
+ to the top. Escape, Ctrl+End, or scrolling back to the bottom returns to live output. History keeps
322
+ its colours, a scrollbar shows your position, and the header counts output that arrived while you
323
+ were reading. Arrow keys, Home and End still edit the composer. Wheel scrolling uses mouse
324
+ reporting, so hold Shift to select text, or set `ARCHYMEDES_MOUSE=0` to keep the terminal's own
325
+ selection. Each tab retains up to 5,000 output lines, and resizing reflows the transcript.
326
+
327
+ `/find <text>` searches the tab's history and jumps to the first match; `/find` again moves to the
328
+ next one, `/find prev` to the previous, and `/find off` or Escape returns to live output. `/pager`
329
+ opens the whole transcript in `$PAGER` (`less -R` by default). `/cat image.png` draws a PNG in the
330
+ transcript with coloured half-blocks; in the scrollback layout, `ARCHYMEDES_IMAGES=kitty` uses Kitty
331
+ graphics for real pixels, and `/clear` removes them.
322
332
 
323
333
  The opening geometry rotates while the workspace is idle and settles as soon as you type.
324
334
  `ARCHYMEDES_NO_MOTION=1` disables this motion and menu focus animation. `--ascii`, `NO_COLOR`,
@@ -1,13 +1,14 @@
1
1
  import {
2
2
  SpringAnimator,
3
3
  paginator
4
- } from "./archymedes-r54qthg5.mjs";
4
+ } from "./archymedes-g8y3rt6t.mjs";
5
5
  import {
6
6
  UNICODE_GLYPHS,
7
+ clipTo,
7
8
  visibleWidth
8
- } from "./archymedes-5zn4zp84.mjs";
9
+ } from "./archymedes-a10k8yrk.mjs";
9
10
 
10
- // packages/archymedes-cli/src/chooser.ts
11
+ // src/ui/chooser.ts
11
12
  var ALL_CHROME = { title: true, status: true, pagination: true, help: true };
12
13
  function chromeOf(chrome) {
13
14
  return { ...ALL_CHROME, ...chrome };
@@ -150,22 +151,6 @@ function renderChooser(state, items, options) {
150
151
  return lines.join(`
151
152
  `);
152
153
  }
153
- function clipTo(text, width, glyphs = UNICODE_GLYPHS) {
154
- if (width <= 0)
155
- return "";
156
- if (visibleWidth(text) <= width)
157
- return text;
158
- const mark = visibleWidth(glyphs.ellipsis) < width ? glyphs.ellipsis : "";
159
- const budget = width - visibleWidth(mark);
160
- let out = "";
161
- const segments = typeof Intl.Segmenter === "function" ? [...new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(text)].map((entry) => entry.segment) : [...text];
162
- for (const segment of segments) {
163
- if (visibleWidth(out + segment) > budget)
164
- break;
165
- out += segment;
166
- }
167
- return `${out}${mark}`;
168
- }
169
154
  async function runChooser(keys, items, paint, options) {
170
155
  let state = { selected: Math.max(0, Math.min(options.initialIndex ?? 0, Math.max(0, items.length - 1))), query: "" };
171
156
  const liveOptions = () => {
@@ -221,4 +206,4 @@ async function runChooser(keys, items, paint, options) {
221
206
  return;
222
207
  }
223
208
 
224
- export { INITIAL_CHOOSER_STATE, windowStart, terminalColumns, renderChooser, clipTo, runChooser };
209
+ export { INITIAL_CHOOSER_STATE, filterItems, windowStart, terminalColumns, renderChooser, runChooser };
@@ -1,4 +1,4 @@
1
- // packages/archymedes-cli/src/theme.ts
1
+ // src/theme/theme.ts
2
2
  var BLUEPRINT = `
3
3
  /* Blueprint navy, chalk-white ink and an amber pencil — the original drafting table. */
4
4
  @theme blueprint {
@@ -296,9 +296,30 @@ function buildPalette(theme, depth) {
296
296
  tokens: theme.tokens
297
297
  };
298
298
  }
299
+ var EXTERNAL_MARK = "\x1B[35m";
300
+ var ANSI_PALETTE = buildPalette({ name: "ansi", description: "", tokens: DEFAULT_TOKENS }, "ansi256");
301
+ function roleCode(role, palette, depth) {
302
+ if (depth === "none")
303
+ return "";
304
+ return palette?.[role] || ANSI_PALETTE[role];
305
+ }
299
306
  var NO_COLOR_PALETTE = buildPalette(findBuiltinTheme(DEFAULT_THEME_NAME) ?? { name: DEFAULT_THEME_NAME, description: "", tokens: DEFAULT_TOKENS }, "none");
300
307
 
301
- // packages/archymedes-cli/src/glyphs.ts
308
+ // src/text/ansi.ts
309
+ var RESET = "\x1B[0m";
310
+ var BOLD = "\x1B[1m";
311
+ var DIM = "\x1B[2m";
312
+ var ITALIC = "\x1B[3m";
313
+ var STRIKE = "\x1B[9m";
314
+ var REVERSE = "\x1B[7m";
315
+ function paint(text, code, depth) {
316
+ return depth === "none" || code === "" ? text : `${code}${text}${RESET}`;
317
+ }
318
+ function paintAll(text, codes, depth) {
319
+ return depth === "none" || codes.length === 0 ? text : `${codes.join("")}${text}${RESET}`;
320
+ }
321
+
322
+ // src/text/glyphs.ts
302
323
  var UNICODE_GLYPHS = {
303
324
  starGlyphs: [".", "·", "✧", "✦", "✶"],
304
325
  spinnerFrames: [" ◜ ", " ◠ ", " ◝ ", " ◞ ", " ◡ ", " ◟ "],
@@ -405,20 +426,14 @@ function borderGlyphsFor(style, glyphs) {
405
426
  return ROUND_BORDER;
406
427
  }
407
428
 
408
- // packages/archymedes-cli/src/markdown.ts
409
- var RESET = "\x1B[0m";
410
- var BOLD = "\x1B[1m";
411
- var DIM = "\x1B[2m";
412
- var ITALIC = "\x1B[3m";
413
- var STRIKE = "\x1B[9m";
414
- var CYAN = "\x1B[36m";
415
- var BLUE = "\x1B[34m";
416
- var GREEN = "\x1B[32m";
417
- var YELLOW = "\x1B[33m";
429
+ // src/text/text-width.ts
418
430
  var ANSI = /\x1b\[[0-9;]*m/g;
431
+ var PRINTABLE_ASCII = /^[\x20-\x7e]*$/;
419
432
  function visibleWidth(text) {
433
+ if (PRINTABLE_ASCII.test(text))
434
+ return text.length;
420
435
  let width = 0;
421
- for (const character of text.replace(ANSI, "")) {
436
+ for (const character of text.includes("\x1B") ? text.replace(ANSI, "") : text) {
422
437
  const code = character.codePointAt(0) ?? 0;
423
438
  if (code >= 768 && code <= 879)
424
439
  continue;
@@ -429,218 +444,21 @@ function visibleWidth(text) {
429
444
  function isWide(code) {
430
445
  return code >= 4352 && code <= 4447 || code >= 11904 && code <= 42191 || code >= 44032 && code <= 55203 || code >= 63744 && code <= 64255 || code >= 65072 && code <= 65135 || code >= 65280 && code <= 65376 || code >= 127744 && code <= 129535;
431
446
  }
432
- var INLINE_PATTERN = /(?<!\\)(`[^`\n]+`)|(?<!\\)(\*\*[^*\n]+\*\*)|(?<!\\)(__[^_\n]+__)|(?<!\\)(~~[^~\n]+~~)|(?<!\\)(\*[^*\n]+\*)|(?<!\\)(_[^_\n]+_)|(!?\[([^\]\n]+)\]\((https?:\/\/[^\s)\n]+)\))|(<(https?:\/\/[^>\s]+)>)/g;
433
- var unescapeMarkdown = (text) => text.replace(/\\([\\`*_[\]{}()#+.!~>-])/g, "$1");
434
- function parseInline(line) {
435
- const tokens = [];
436
- let lastIndex = 0;
437
- INLINE_PATTERN.lastIndex = 0;
438
- for (let match = INLINE_PATTERN.exec(line);match !== null; match = INLINE_PATTERN.exec(line)) {
439
- if (match.index > lastIndex)
440
- tokens.push({ text: unescapeMarkdown(line.slice(lastIndex, match.index)), code: "" });
441
- const [whole, code, boldStars, boldUnderscores, strike, italicStars, italicUnderscores, link, linkLabel, linkUrl, autolink, autolinkUrl] = match;
442
- if (code !== undefined)
443
- tokens.push({ text: code.slice(1, -1), code: CYAN });
444
- else if (boldStars !== undefined)
445
- tokens.push({ text: boldStars.slice(2, -2), code: BOLD });
446
- else if (boldUnderscores !== undefined)
447
- tokens.push({ text: boldUnderscores.slice(2, -2), code: BOLD });
448
- else if (strike !== undefined)
449
- tokens.push({ text: strike.slice(2, -2), code: STRIKE });
450
- else if (italicStars !== undefined)
451
- tokens.push({ text: italicStars.slice(1, -1), code: ITALIC });
452
- else if (italicUnderscores !== undefined)
453
- tokens.push({ text: italicUnderscores.slice(1, -1), code: ITALIC });
454
- else if (link !== undefined)
455
- tokens.push({ text: `${link.startsWith("!") ? `image: ${linkLabel}` : linkLabel} (${linkUrl})`, code: CYAN });
456
- else if (autolink !== undefined)
457
- tokens.push({ text: autolinkUrl, code: CYAN });
458
- lastIndex = match.index + whole.length;
459
- }
460
- if (lastIndex < line.length)
461
- tokens.push({ text: unescapeMarkdown(line.slice(lastIndex)), code: "" });
462
- return tokens.filter((token) => token.text.length > 0);
463
- }
464
- function paint(text, code, depth) {
465
- return depth === "none" || code === "" ? text : `${code}${text}${RESET}`;
466
- }
467
- function wrapTokens(tokens, width, options) {
468
- const firstPrefix = options.firstPrefix ?? "";
469
- const continuationPrefix = options.continuationPrefix ?? "";
470
- const lines = [];
471
- let current = "";
472
- let currentWidth = 0;
473
- let prefix = firstPrefix;
474
- let prefixWidth = visibleWidth(firstPrefix);
475
- const limit = Math.max(1, width);
476
- const flush = () => {
477
- lines.push(`${prefix}${current}`.replace(/[ \t]+$/, ""));
478
- current = "";
479
- currentWidth = 0;
480
- prefix = continuationPrefix;
481
- prefixWidth = visibleWidth(continuationPrefix);
482
- };
483
- for (const token of tokens) {
484
- for (const word of token.text.split(/(\s+)/)) {
485
- if (word === "")
486
- continue;
487
- const wordWidth = visibleWidth(word);
488
- if (/^\s+$/.test(word)) {
489
- if (currentWidth > 0 && prefixWidth + currentWidth + wordWidth <= limit) {
490
- current += word;
491
- currentWidth += wordWidth;
492
- }
493
- continue;
494
- }
495
- if (currentWidth > 0 && prefixWidth + currentWidth + wordWidth > limit)
496
- flush();
497
- if (prefixWidth + wordWidth > limit) {
498
- let remaining = word;
499
- while (remaining.length > 0) {
500
- const available = Math.max(1, limit - prefixWidth - currentWidth);
501
- if (visibleWidth(remaining) <= available)
502
- break;
503
- const head = [...remaining].slice(0, available).join("");
504
- current += paint(head, token.code, options.depth);
505
- remaining = [...remaining].slice(available).join("");
506
- flush();
507
- }
508
- if (remaining.length > 0) {
509
- current += paint(remaining, token.code, options.depth);
510
- currentWidth += visibleWidth(remaining);
511
- }
512
- continue;
513
- }
514
- current += paint(word, token.code, options.depth);
515
- currentWidth += wordWidth;
516
- }
517
- }
518
- if (current !== "" || lines.length === 0)
519
- lines.push(`${prefix}${current}`.replace(/[ \t]+$/, ""));
520
- return lines;
521
- }
522
- function newMarkdownState() {
523
- return { inFence: false, fenceLanguage: "" };
524
- }
525
- var HEADING = /^(#{1,6})\s+(.*)$/;
526
- var BULLET = /^(\s*)([-*+])\s+(.*)$/;
527
- var TASK = /^(\s*)[-*+]\s+\[([ xX])\]\s+(.*)$/;
528
- var NUMBERED = /^(\s*)(\d+[.)])\s+(.*)$/;
529
- var BLOCKQUOTE = /^>\s?(.*)$/;
530
- var RULE = /^\s*([-*_])\s*(?:\1\s*){2,}$/;
531
- var FENCE = /^\s*```(.*)$/;
532
- var TABLE_SEPARATOR_CELL = /^:?-{3,}:?$/;
533
- function tableCells(line) {
534
- if (!line.includes("|"))
535
- return null;
536
- const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, "");
537
- const cells = trimmed.split("|").map((cell) => cell.trim());
538
- return cells.length >= 2 ? cells : null;
539
- }
540
- function fitCell(text, width, glyphs = UNICODE_GLYPHS) {
541
- let result = "";
542
- for (const character of text) {
543
- if (visibleWidth(result + character) > width) {
544
- while (visibleWidth(result) > Math.max(0, width - visibleWidth(glyphs.ellipsis)))
545
- result = [...result].slice(0, -1).join("");
546
- result += glyphs.ellipsis;
547
- return result + " ".repeat(Math.max(0, width - visibleWidth(result)));
548
- }
549
- result += character;
550
- }
551
- return result + " ".repeat(Math.max(0, width - visibleWidth(result)));
552
- }
553
- function renderTableLine(cells, separator, width, depth, glyphs) {
554
- const cellWidth = Math.max(3, Math.floor((Math.max(width, 12) - cells.length - 1) / cells.length));
555
- if (separator)
556
- return paint(`${glyphs.boxTeeLeft}${cells.map(() => glyphs.boxHorizontal.repeat(cellWidth)).join(glyphs.boxCross)}${glyphs.boxTeeRight}`, DIM, depth);
557
- const contentWidth = Math.max(1, cellWidth - 2);
558
- const rendered = cells.map((cell) => fitCell(parseInline(cell).map((token) => token.text).join(""), contentWidth, glyphs));
559
- return `${glyphs.boxVertical}${rendered.map((cell) => ` ${cell} `).join(glyphs.boxVertical)}${glyphs.boxVertical}`;
560
- }
561
- function renderMarkdownLine(line, state, options) {
562
- const { width, depth } = options;
563
- const glyphs = options.glyphs ?? UNICODE_GLYPHS;
564
- const fence = FENCE.exec(line);
565
- if (fence) {
566
- const ruleWidth = Math.max(4, Math.min(width - 4, 40));
567
- if (state.inFence) {
568
- state.inFence = false;
569
- state.fenceLanguage = "";
570
- return [paint(` ${glyphs.boxBottomLeft}${glyphs.boxHorizontal.repeat(ruleWidth)}`, DIM, depth)];
571
- }
572
- state.inFence = true;
573
- state.fenceLanguage = fence[1].trim();
574
- const label = state.fenceLanguage ? ` ${state.fenceLanguage} ` : "";
575
- const drawn = 1 + visibleWidth(label);
576
- return [paint(` ${glyphs.boxTopLeft}${glyphs.boxHorizontal}${label}${glyphs.boxHorizontal.repeat(Math.max(0, ruleWidth - drawn))}`, DIM, depth)];
577
- }
578
- if (state.inFence) {
579
- return [`${paint(` ${glyphs.boxVertical} `, DIM, depth)}${paint(line, GREEN, depth)}`];
580
- }
581
- const table = tableCells(line);
582
- if (table) {
583
- const separator = table.every((cell) => TABLE_SEPARATOR_CELL.test(cell));
584
- return [renderTableLine(table, separator, width, depth, glyphs)];
585
- }
586
- if (RULE.test(line) && line.trim().length > 0) {
587
- return [paint(" " + glyphs.boxHorizontal.repeat(Math.max(4, Math.min(width - 4, 40))), DIM, depth)];
588
- }
589
- const heading = HEADING.exec(line);
590
- if (heading) {
591
- const level = heading[1].length;
592
- const code = level <= 2 ? `${BOLD}${CYAN}` : BOLD;
593
- const tokens = parseInline(heading[2]).map((token) => ({ text: token.text, code }));
594
- return wrapTokens(tokens, width, { depth });
595
- }
596
- const task = TASK.exec(line);
597
- if (task) {
598
- return wrapTokens(parseInline(task[3]), width, {
599
- firstPrefix: `${task[1]}${paint(task[2].trim() ? glyphs.checkboxDone : glyphs.checkbox, task[2].trim() ? GREEN : BLUE, depth)} `,
600
- continuationPrefix: `${task[1]} `,
601
- depth
602
- });
603
- }
604
- const bullet = BULLET.exec(line);
605
- if (bullet) {
606
- const indent = bullet[1];
607
- return wrapTokens(parseInline(bullet[3]), width, {
608
- firstPrefix: `${indent}${paint(glyphs.bullet, BLUE, depth)} `,
609
- continuationPrefix: `${indent} `,
610
- depth
611
- });
612
- }
613
- const numbered = NUMBERED.exec(line);
614
- if (numbered) {
615
- const indent = numbered[1];
616
- const marker = numbered[2];
617
- return wrapTokens(parseInline(numbered[3]), width, {
618
- firstPrefix: `${indent}${paint(marker, BLUE, depth)} `,
619
- continuationPrefix: `${indent}${" ".repeat(marker.length + 1)}`,
620
- depth
621
- });
622
- }
623
- const quote = BLOCKQUOTE.exec(line);
624
- if (quote) {
625
- return wrapTokens([{ text: quote[1], code: DIM }], width, {
626
- firstPrefix: paint(`${glyphs.boxVertical} `, YELLOW, depth),
627
- continuationPrefix: paint(`${glyphs.boxVertical} `, YELLOW, depth),
628
- depth
629
- });
447
+ function clipTo(text, width, glyphs = UNICODE_GLYPHS) {
448
+ if (width <= 0)
449
+ return "";
450
+ if (visibleWidth(text) <= width)
451
+ return text;
452
+ const mark = visibleWidth(glyphs.ellipsis) < width ? glyphs.ellipsis : "";
453
+ const budget = width - visibleWidth(mark);
454
+ let out = "";
455
+ const segments = typeof Intl.Segmenter === "function" ? [...new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(text)].map((entry) => entry.segment) : [...text];
456
+ for (const segment of segments) {
457
+ if (visibleWidth(out + segment) > budget)
458
+ break;
459
+ out += segment;
630
460
  }
631
- if (line.trim() === "")
632
- return [""];
633
- return wrapTokens(parseInline(line), width, { depth });
634
- }
635
- function renderMarkdown(text, options) {
636
- const state = newMarkdownState();
637
- const glyphs = options.glyphs ?? UNICODE_GLYPHS;
638
- const rendered = text.split(`
639
- `).flatMap((line) => renderMarkdownLine(line, state, options));
640
- if (state.inFence)
641
- rendered.push(paint(` ${glyphs.boxBottomLeft}${glyphs.boxHorizontal.repeat(4)}`, DIM, options.depth));
642
- return rendered.join(`
643
- `);
461
+ return `${out}${mark}`;
644
462
  }
645
463
 
646
- export { UNICODE_GLYPHS, ASCII_GLYPHS, borderGlyphsFor, visibleWidth, newMarkdownState, renderMarkdownLine, renderMarkdown, BUILTIN_THEME_SOURCES, rgbTo256, rainbowText, parseThemeSource, NO_COLOR_PALETTE };
464
+ export { RESET, BOLD, DIM, ITALIC, STRIKE, REVERSE, paint, paintAll, UNICODE_GLYPHS, ASCII_GLYPHS, borderGlyphsFor, visibleWidth, clipTo, BUILTIN_THEME_SOURCES, parseColor, rgbTo256, rainbowText, parseThemeSource, EXTERNAL_MARK, ANSI_PALETTE, roleCode, NO_COLOR_PALETTE };