archymedes-cli 2.1.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
@@ -309,3 +309,28 @@ TUI dependency resolution. The reviewed archive and SHA256 checksum are written
309
309
  See [the release assessment](https://github.com/chrisnkuno/archymedes-cli/blob/main/docs/RELEASE_ASSESSMENT.md) for scope and remaining release proof.
310
310
 
311
311
  The startup geometry makes a brief rotation on roomy interactive terminals. Set `ARCHYMEDES_NO_MOTION=1` to keep it still. Preview it with `bun run preview:tui archymedes --animate`.
312
+
313
+ ## Fixed workspace
314
+
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.
332
+
333
+ The opening geometry rotates while the workspace is idle and settles as soon as you type.
334
+ `ARCHYMEDES_NO_MOTION=1` disables this motion and menu focus animation. `--ascii`, `NO_COLOR`,
335
+ and the existing themes remain supported. Try the real interface offline from the repository
336
+ with `bun run preview:workspace`; the preview uses a temporary project and a local model fixture.
@@ -1,11 +1,14 @@
1
1
  import {
2
2
  SpringAnimator,
3
+ paginator
4
+ } from "./archymedes-g8y3rt6t.mjs";
5
+ import {
3
6
  UNICODE_GLYPHS,
4
- paginator,
7
+ clipTo,
5
8
  visibleWidth
6
- } from "./archymedes-dwvn4c51.mjs";
9
+ } from "./archymedes-a10k8yrk.mjs";
7
10
 
8
- // packages/archymedes-cli/src/chooser.ts
11
+ // src/ui/chooser.ts
9
12
  var ALL_CHROME = { title: true, status: true, pagination: true, help: true };
10
13
  function chromeOf(chrome) {
11
14
  return { ...ALL_CHROME, ...chrome };
@@ -129,7 +132,11 @@ function renderChooser(state, items, options) {
129
132
  const shownLabel = clip(item.label, labelBudget);
130
133
  const padded = shownLabel + " ".repeat(Math.max(0, labelBudget - visibleWidth(shownLabel)));
131
134
  const marker = active ? paint.green(glyphs.prompt) : fadingOut ? paint.dim(glyphs.prompt) : " ";
132
- const head = `${marker} ${paint.dim(number)} ${padded} `;
135
+ const focus = Math.max(0, Math.min(1, options.focusProgress ?? 1));
136
+ const graphemes = [...new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(padded)].map((part) => part.segment);
137
+ const illuminated = Math.round(graphemes.length * focus);
138
+ const label = active ? paint.cyan(graphemes.slice(0, illuminated).join("")) + graphemes.slice(illuminated).join("") : padded;
139
+ const head = `${marker} ${paint.dim(number)} ${label} `;
133
140
  const tail = `${item.hint ? ` ${item.hint}` : ""}${item.description ? ` ${item.description}` : ""}`;
134
141
  lines.push(` ${head}${paint.dim(clip(tail, Math.max(0, room - visibleWidth(padded) - 2)))}`);
135
142
  }
@@ -144,22 +151,6 @@ function renderChooser(state, items, options) {
144
151
  return lines.join(`
145
152
  `);
146
153
  }
147
- function clipTo(text, width, glyphs = UNICODE_GLYPHS) {
148
- if (width <= 0)
149
- return "";
150
- if (visibleWidth(text) <= width)
151
- return text;
152
- const mark = visibleWidth(glyphs.ellipsis) < width ? glyphs.ellipsis : "";
153
- const budget = width - visibleWidth(mark);
154
- let out = "";
155
- const segments = typeof Intl.Segmenter === "function" ? [...new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(text)].map((entry) => entry.segment) : [...text];
156
- for (const segment of segments) {
157
- if (visibleWidth(out + segment) > budget)
158
- break;
159
- out += segment;
160
- }
161
- return `${out}${mark}`;
162
- }
163
154
  async function runChooser(keys, items, paint, options) {
164
155
  let state = { selected: Math.max(0, Math.min(options.initialIndex ?? 0, Math.max(0, items.length - 1))), query: "" };
165
156
  const liveOptions = () => {
@@ -173,40 +164,46 @@ async function runChooser(keys, items, paint, options) {
173
164
  glide?.stop();
174
165
  glide = undefined;
175
166
  };
176
- for await (const input of keys) {
177
- const current = liveOptions();
178
- const previousSelected = state.selected;
179
- const step = advanceChooser(state, items, input, {
180
- filter: options.filter ?? false,
181
- page: options.page ?? current.height ?? 8,
182
- ...current.height === undefined ? {} : { height: current.height }
183
- });
184
- state = step.state;
185
- if (step.done) {
167
+ try {
168
+ for await (const input of keys) {
169
+ const current = liveOptions();
170
+ const previousSelected = state.selected;
171
+ const step = advanceChooser(state, items, input, {
172
+ filter: options.filter ?? false,
173
+ page: options.page ?? current.height ?? 8,
174
+ ...current.height === undefined ? {} : { height: current.height }
175
+ });
176
+ state = step.state;
177
+ if (step.done) {
178
+ settleGlide();
179
+ if (step.done.index === undefined)
180
+ return;
181
+ return filterItems(items, state.query)[step.done.index]?.value;
182
+ }
186
183
  settleGlide();
187
- if (step.done.index === undefined)
188
- return;
189
- return filterItems(items, state.query)[step.done.index]?.value;
184
+ const isSingleStep = (input.key.name === "up" || input.key.name === "down") && Math.abs(state.selected - previousSelected) === 1;
185
+ const motion = options.motion ?? (process.env.ARCHYMEDES_NO_MOTION !== "1" && process.env.NO_COLOR === undefined && process.env.TERM !== "dumb");
186
+ if (!motion || !isSingleStep || previousSelected === state.selected) {
187
+ paint(renderChooser(state, items, liveOptions()));
188
+ continue;
189
+ }
190
+ paint(renderChooser(state, items, { ...liveOptions(), transitionFrom: previousSelected, focusProgress: 0 }));
191
+ const animator = new SpringAnimator(0, (value) => {
192
+ if (value < 0.95) {
193
+ paint(renderChooser(state, items, { ...liveOptions(), transitionFrom: value < 0.5 ? previousSelected : undefined, focusProgress: value }));
194
+ return;
195
+ }
196
+ animator.stop();
197
+ glide = undefined;
198
+ paint(renderChooser(state, items, liveOptions()));
199
+ }, { intervalMs: 30 });
200
+ glide = animator;
201
+ animator.retarget(1);
190
202
  }
203
+ } finally {
191
204
  settleGlide();
192
- const isSingleStep = (input.key.name === "up" || input.key.name === "down") && Math.abs(state.selected - previousSelected) === 1;
193
- if (!isSingleStep || previousSelected === state.selected) {
194
- paint(renderChooser(state, items, liveOptions()));
195
- continue;
196
- }
197
- paint(renderChooser(state, items, { ...liveOptions(), transitionFrom: previousSelected }));
198
- const animator = new SpringAnimator(0, (value) => {
199
- if (value < 0.6)
200
- return;
201
- animator.stop();
202
- glide = undefined;
203
- paint(renderChooser(state, items, liveOptions()));
204
- }, { intervalMs: 30 });
205
- glide = animator;
206
- animator.retarget(1);
207
205
  }
208
- settleGlide();
209
206
  return;
210
207
  }
211
208
 
212
- 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 {
@@ -89,12 +89,31 @@ var ARCHYMEDES = `
89
89
  --border-focus: #e7bb78;
90
90
  }
91
91
  `;
92
+ var RAINBOW = `
93
+ /* Every hue at once — the opening art and the thinking spinner sweep the full spectrum. */
94
+ @theme rainbow {
95
+ --primary: #ff5f5f;
96
+ --secondary: #5fd7ff;
97
+ --accent: #ffd75f;
98
+ --bg: #0c0c0c;
99
+ --surface: #161616;
100
+ --text: #f2f2f2;
101
+ --text-muted: #9a9a9a;
102
+ --success: #5fff87;
103
+ --warning: #ffaf5f;
104
+ --error: #ff3b3b;
105
+ --border: double;
106
+ --border-color: #8a8a8a;
107
+ --border-focus: #d75fff;
108
+ }
109
+ `;
92
110
  var BUILTIN_THEME_SOURCES = {
93
111
  archymedes: ARCHYMEDES,
94
112
  blueprint: BLUEPRINT,
95
113
  parchment: PARCHMENT,
96
114
  chalkboard: CHALKBOARD,
97
- "high-contrast": HIGH_CONTRAST
115
+ "high-contrast": HIGH_CONTRAST,
116
+ rainbow: RAINBOW
98
117
  };
99
118
  var DEFAULT_THEME_NAME = "archymedes";
100
119
  var NAMED_COLORS = {
@@ -163,6 +182,39 @@ function colorCode(value, depth) {
163
182
  return parsed < 8 ? `\x1B[${30 + parsed}m` : `\x1B[${90 + parsed - 8}m`;
164
183
  return depth === "truecolor" ? `\x1B[38;2;${parsed.r};${parsed.g};${parsed.b}m` : `\x1B[38;5;${rgbTo256(parsed)}m`;
165
184
  }
185
+ function rainbowHex(t) {
186
+ const hue = (t % 1 + 1) % 1 * 360;
187
+ const c = 0.5;
188
+ const x = c * (1 - Math.abs(hue / 60 % 2 - 1));
189
+ const m = 0.6 - c / 2;
190
+ const [r1, g1, b1] = hue < 60 ? [c, x, 0] : hue < 120 ? [x, c, 0] : hue < 180 ? [0, c, x] : hue < 240 ? [0, x, c] : hue < 300 ? [x, 0, c] : [c, 0, x];
191
+ const channel = (value) => Math.round((value + m) * 255).toString(16).padStart(2, "0");
192
+ return `#${channel(r1)}${channel(g1)}${channel(b1)}`;
193
+ }
194
+ function rainbowText(text, depth, phase = 0) {
195
+ if (depth === "none")
196
+ return text;
197
+ const characters = [...text];
198
+ let out = "";
199
+ let open;
200
+ characters.forEach((character, index) => {
201
+ if (character === " ") {
202
+ if (open !== undefined) {
203
+ out += "\x1B[0m";
204
+ open = undefined;
205
+ }
206
+ out += character;
207
+ return;
208
+ }
209
+ const code = colorCode(rainbowHex(phase + index / 12), depth);
210
+ if (code !== open) {
211
+ out += code;
212
+ open = code;
213
+ }
214
+ out += character;
215
+ });
216
+ return open === undefined ? out : `${out}\x1B[0m`;
217
+ }
166
218
  var DEFAULT_TOKENS = {
167
219
  primary: "cyan",
168
220
  secondary: "blue",
@@ -244,6 +296,169 @@ function buildPalette(theme, depth) {
244
296
  tokens: theme.tokens
245
297
  };
246
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
+ }
247
306
  var NO_COLOR_PALETTE = buildPalette(findBuiltinTheme(DEFAULT_THEME_NAME) ?? { name: DEFAULT_THEME_NAME, description: "", tokens: DEFAULT_TOKENS }, "none");
248
307
 
249
- export { BUILTIN_THEME_SOURCES, rgbTo256, parseThemeSource, NO_COLOR_PALETTE };
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
323
+ var UNICODE_GLYPHS = {
324
+ starGlyphs: [".", "·", "✧", "✦", "✶"],
325
+ spinnerFrames: [" ◜ ", " ◠ ", " ◝ ", " ◞ ", " ◡ ", " ◟ "],
326
+ star: "✦",
327
+ starDim: "✧",
328
+ check: "✓",
329
+ cross: "✗",
330
+ pending: "⋯",
331
+ prompt: "❯",
332
+ caret: "›",
333
+ bullet: "•",
334
+ ellipsis: "…",
335
+ middot: "·",
336
+ elbow: "⎿",
337
+ boxTopLeft: "╭",
338
+ boxTopRight: "╮",
339
+ boxBottomLeft: "╰",
340
+ boxBottomRight: "╯",
341
+ boxHorizontal: "─",
342
+ boxVertical: "│",
343
+ boxTeeLeft: "├",
344
+ boxTeeRight: "┤",
345
+ boxCross: "┼",
346
+ circleEmpty: "○",
347
+ circleHalf: "◐",
348
+ circleFull: "●",
349
+ checkbox: "☐",
350
+ checkboxDone: "☑",
351
+ paused: "⏸",
352
+ cancelled: "⊘",
353
+ treeBranch: "├─",
354
+ treeLast: "╰─",
355
+ treeTrunk: "│ ",
356
+ collapsed: "▸",
357
+ expanded: "▾",
358
+ arrowUp: "↑",
359
+ arrowLeft: "←",
360
+ arrowRight: "→",
361
+ arrowDown: "↓",
362
+ plus: "+",
363
+ minus: "-",
364
+ warning: "!",
365
+ effectWorkspace: "✎",
366
+ effectExternal: "◎",
367
+ sparkLevels: ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"]
368
+ };
369
+ var ASCII_GLYPHS = {
370
+ starGlyphs: [".", ".", "+", "*", "*"],
371
+ spinnerFrames: ["-", "\\", "|", "/"],
372
+ star: "*",
373
+ starDim: "+",
374
+ check: "+",
375
+ cross: "x",
376
+ pending: "~",
377
+ prompt: ">",
378
+ caret: ">",
379
+ bullet: "-",
380
+ ellipsis: "...",
381
+ middot: "-",
382
+ elbow: "\\",
383
+ boxTopLeft: "+",
384
+ boxTopRight: "+",
385
+ boxBottomLeft: "+",
386
+ boxBottomRight: "+",
387
+ boxHorizontal: "-",
388
+ boxVertical: "|",
389
+ boxTeeLeft: "+",
390
+ boxTeeRight: "+",
391
+ boxCross: "+",
392
+ circleEmpty: "o",
393
+ circleHalf: "*",
394
+ circleFull: "*",
395
+ checkbox: "[ ]",
396
+ checkboxDone: "[x]",
397
+ paused: "=",
398
+ cancelled: "x",
399
+ treeBranch: "|-",
400
+ treeLast: "`-",
401
+ treeTrunk: "| ",
402
+ collapsed: ">",
403
+ expanded: "v",
404
+ arrowUp: "^",
405
+ arrowLeft: "<-",
406
+ arrowRight: "->",
407
+ arrowDown: "v",
408
+ plus: "+",
409
+ minus: "-",
410
+ warning: "!",
411
+ effectWorkspace: "w",
412
+ effectExternal: "e",
413
+ sparkLevels: [" ", ".", ":", "-", "=", "+", "*", "#"]
414
+ };
415
+ var ROUND_BORDER = { topLeft: "╭", topRight: "╮", bottomLeft: "╰", bottomRight: "╯", horizontal: "─", vertical: "│", teeLeft: "├", teeRight: "┤", cross: "┼" };
416
+ var SINGLE_BORDER = { topLeft: "┌", topRight: "┐", bottomLeft: "└", bottomRight: "┘", horizontal: "─", vertical: "│", teeLeft: "├", teeRight: "┤", cross: "┼" };
417
+ var DOUBLE_BORDER = { topLeft: "╔", topRight: "╗", bottomLeft: "╚", bottomRight: "╝", horizontal: "═", vertical: "║", teeLeft: "╠", teeRight: "╣", cross: "╬" };
418
+ var ASCII_BORDER = { topLeft: "+", topRight: "+", bottomLeft: "+", bottomRight: "+", horizontal: "-", vertical: "|", teeLeft: "+", teeRight: "+", cross: "+" };
419
+ function borderGlyphsFor(style, glyphs) {
420
+ if (glyphs === ASCII_GLYPHS)
421
+ return ASCII_BORDER;
422
+ if (style === "single")
423
+ return SINGLE_BORDER;
424
+ if (style === "double")
425
+ return DOUBLE_BORDER;
426
+ return ROUND_BORDER;
427
+ }
428
+
429
+ // src/text/text-width.ts
430
+ var ANSI = /\x1b\[[0-9;]*m/g;
431
+ var PRINTABLE_ASCII = /^[\x20-\x7e]*$/;
432
+ function visibleWidth(text) {
433
+ if (PRINTABLE_ASCII.test(text))
434
+ return text.length;
435
+ let width = 0;
436
+ for (const character of text.includes("\x1B") ? text.replace(ANSI, "") : text) {
437
+ const code = character.codePointAt(0) ?? 0;
438
+ if (code >= 768 && code <= 879)
439
+ continue;
440
+ width += isWide(code) ? 2 : 1;
441
+ }
442
+ return width;
443
+ }
444
+ function isWide(code) {
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;
446
+ }
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;
460
+ }
461
+ return `${out}${mark}`;
462
+ }
463
+
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 };