archymedes-cli 2.3.0 → 2.6.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.
Files changed (25) hide show
  1. package/dist/{archymedes-aj30app4.mjs → archymedes-06fh0z84.mjs} +370 -159
  2. package/dist/{archymedes-sf9t1hz9.mjs → archymedes-086xsz64.mjs} +176 -77
  3. package/dist/{archymedes-d5wqcmdr.mjs → archymedes-0jxzsh4z.mjs} +97 -97
  4. package/dist/{archymedes-h13ap0za.mjs → archymedes-1g1y6q74.mjs} +1 -1
  5. package/dist/{archymedes-4kx65anz.mjs → archymedes-1jcn326m.mjs} +43 -12
  6. package/dist/{archymedes-g8y3rt6t.mjs → archymedes-kn6wtq32.mjs} +332 -9
  7. package/dist/archymedes-main.mjs +31010 -27842
  8. package/dist/archymedes-s4qcy0q4.mjs +1250 -0
  9. package/dist/{archymedes-twbks7g3.mjs → archymedes-t1jtzkmp.mjs} +214 -5
  10. package/dist/{archymedes-tsne2m7p.mjs → archymedes-twanftqk.mjs} +2 -2
  11. package/dist/archymedes-w4bjfye7.mjs +280 -0
  12. package/dist/{archymedes-dz59nvw7.mjs → archymedes-zvcs68bv.mjs} +21 -11
  13. package/dist/{editor-screen-g08zzt70.mjs → editor-screen-pf252j2r.mjs} +49 -14
  14. package/dist/{factory-7vtm2fed.mjs → factory-hyv8agr0.mjs} +326 -326
  15. package/dist/{file-screen-ma9yxq2b.mjs → file-screen-1j65f7b4.mjs} +11 -9
  16. package/dist/{guide-screen-gd129txd.mjs → guide-screen-9vxsk83x.mjs} +10 -8
  17. package/dist/{index-qvgcwsvm.mjs → index-teq1cta4.mjs} +1 -1
  18. package/dist/{node-mdgzx68h.mjs → node-0fh63p8z.mjs} +7 -7
  19. package/dist/pager-screen-aamt6xv0.mjs +155 -0
  20. package/dist/projection-x6x6hm9t.mjs +248 -0
  21. package/dist/{sandbox-templates-9jcjws23.mjs → sandbox-templates-8sd1psez.mjs} +2 -2
  22. package/dist/workspace-screen-ntdcgfq9.mjs +70 -0
  23. package/package.json +2 -2
  24. package/dist/archymedes-a10k8yrk.mjs +0 -464
  25. package/dist/workspace-screen-8cwk6jz9.mjs +0 -241
@@ -4,7 +4,7 @@ import {
4
4
  note,
5
5
  panel,
6
6
  rule
7
- } from "./archymedes-tsne2m7p.mjs";
7
+ } from "./archymedes-twanftqk.mjs";
8
8
  import {
9
9
  BOLD,
10
10
  DIM,
@@ -12,9 +12,62 @@ import {
12
12
  paint,
13
13
  paintAll,
14
14
  visibleWidth
15
- } from "./archymedes-a10k8yrk.mjs";
15
+ } from "./archymedes-s4qcy0q4.mjs";
16
16
 
17
- // src/platform/i18n.ts
17
+ // packages/archymedes-cli/src/terminal/viewport.ts
18
+ function newViewport(lines, height) {
19
+ return clamp({ lines, height: Math.max(1, Math.floor(height)), top: 0 });
20
+ }
21
+ function maxTop(state) {
22
+ return Math.max(0, state.lines.length - state.height);
23
+ }
24
+ function clamp(state) {
25
+ const height = Math.max(1, Math.floor(state.height));
26
+ const bounded = { ...state, height };
27
+ const top = Math.max(0, Math.min(Math.floor(bounded.top) || 0, maxTop(bounded)));
28
+ return { ...bounded, top };
29
+ }
30
+ function applyViewport(state, action) {
31
+ switch (action.kind) {
32
+ case "up":
33
+ return clamp({ ...state, top: state.top - Math.max(1, action.rows ?? 1) });
34
+ case "down":
35
+ return clamp({ ...state, top: state.top + Math.max(1, action.rows ?? 1) });
36
+ case "halfUp":
37
+ return clamp({ ...state, top: state.top - Math.max(1, Math.floor(state.height / 2)) });
38
+ case "halfDown":
39
+ return clamp({ ...state, top: state.top + Math.max(1, Math.floor(state.height / 2)) });
40
+ case "pageUp":
41
+ return clamp({ ...state, top: state.top - Math.max(1, state.height - 1) });
42
+ case "pageDown":
43
+ return clamp({ ...state, top: state.top + Math.max(1, state.height - 1) });
44
+ case "top":
45
+ return clamp({ ...state, top: 0 });
46
+ case "bottom":
47
+ return clamp({ ...state, top: maxTop(state) });
48
+ case "resize":
49
+ return clamp({ ...state, height: action.height });
50
+ case "content": {
51
+ const next = clamp({ ...state, lines: action.lines });
52
+ return action.follow ? clamp({ ...next, top: maxTop(next) }) : next;
53
+ }
54
+ }
55
+ }
56
+ function visibleLines(state) {
57
+ return state.lines.slice(state.top, state.top + state.height);
58
+ }
59
+ function atTop(state) {
60
+ return state.top <= 0;
61
+ }
62
+ function atBottom(state) {
63
+ return state.top >= maxTop(state);
64
+ }
65
+ function scrollFraction(state) {
66
+ const furthest = maxTop(state);
67
+ return furthest === 0 ? 1 : state.top / furthest;
68
+ }
69
+
70
+ // packages/archymedes-cli/src/platform/i18n.ts
18
71
  var CONTROL_LANGUAGES = {
19
72
  en: "English",
20
73
  zh: "中文 (Mandarin)",
@@ -501,7 +554,7 @@ function t(language, key) {
501
554
  return MESSAGES[key][language] ?? MESSAGES[key].en;
502
555
  }
503
556
 
504
- // src/catalog/commands.ts
557
+ // packages/archymedes-cli/src/catalog/commands.ts
505
558
  function defineCommands(table) {
506
559
  return Object.entries(table).map(([name, command]) => ({ name, ...command }));
507
560
  }
@@ -524,19 +577,22 @@ var COMMANDS2 = defineCommands({
524
577
  "/route": { args: "[all|summary|plan]", description: "The hosted exchange's routing decision: chosen model, alternatives, policy, est vs actual cost. `plan` forecasts the next turn before spending" },
525
578
  "/clear": { description: "Start a fresh thread" },
526
579
  "/memory": { args: "[add|replace|recall|forget]", description: "Bounded, relevant memory across sessions — # <fact> remembers for this project" },
527
- "/history": { args: "[<id> | search <text> | resume | status]", description: "Browse, search or pick up durable conversation history" },
580
+ "/history": { args: "[list | <id> | search <text> | delete <id>|--all | status]", description: "Go back to an earlier conversation — opens a picker (Del deletes one); list prints them, search finds one" },
581
+ "/resume": { args: "[<id> | latest]", description: "Continue an earlier conversation in this folder (picker, or straight to one)" },
528
582
  "/export": { args: "[markdown|json|support]", description: "Write a redacted session transcript or compact support artifact" },
529
583
  "/expand": { args: "[N | all | list]", description: "Show a folded block — written code, a test run, a long result — in full" },
530
584
  "/slow": { args: "[on|strict|off]", description: "Spend at a slower pace: fewer model rounds, smaller replies, a pause between turns" },
531
585
  "/workspace": { description: "Open the control panel: every tab and watched job, live, on one screen" },
532
586
  "/watch": { args: "[id|show <id>|stop <id>]", description: "Follow a background job's output without giving up the prompt" },
533
- "/theme": { args: "[name|list|where]", description: "Change the colours — blueprint, parchment, chalkboard, high-contrast, rainbow, or a .tss file of your own" },
587
+ "/theme": { args: "[name|list|where]", description: "Pick the colours from a list, or name one — blueprint, parchment, chalkboard, high-contrast, rainbow, or a .tss file of your own" },
534
588
  "/tab": { args: "[new|next|prev|close|rename|N]", description: "Several pieces of work, one at a time — only the tab in front runs (/detach for parallel)" },
535
589
  "/pull": { args: "[dir]", description: "Copy sandbox files here" },
536
590
  "/where": { description: "Show the current workspace" },
591
+ "/diagnostics": { args: "[glob]", description: "Language server diagnostics for the workspace — errors, warnings, info from installed language servers" },
537
592
  "/tools": { description: "What the agent can call, and which skills, plugins, MCP servers or hooks a project added" },
538
593
  "/providers": { description: "Which model providers are configured" },
539
- "/settings": { description: "Configure API keys, URLs, models and voice input" },
594
+ "/settings": { description: "Theme, code colours, simple mode, startup, free-mode savings, keys and models" },
595
+ "/upgrade": { description: "Use your own OpenRouter API key instead of the shared free gateway" },
540
596
  "/update": { args: "[auto|check|off]", description: "Update Archymedes now, or choose whether it updates itself" },
541
597
  "/voice": { args: "[audio-file]", description: "Record or transcribe an editable voice prompt" },
542
598
  "/wander": { args: "[topic|random|daily|weekly]", description: "Run a bounded research lab and grade what it finds" },
@@ -546,7 +602,7 @@ var COMMANDS2 = defineCommands({
546
602
  "/cost": { description: "Token and cost breakdown for this session" },
547
603
  "/balance": { args: "[amount] [currency] | clear", description: "Your Archymedes credit balance on the exchange, or a locally tracked spend limit on a direct provider" },
548
604
  "/scan": { args: "[glob]", description: "Deterministic secret scan of the workspace, worst severity first — no model turn needed" },
549
- "/sessions": { description: "List sessions in this project (an alias for /history)" },
605
+ "/sessions": { description: "Pick an earlier conversation in this project (an alias for /history)" },
550
606
  "/palette": { description: "Search every command by name or by what it does" },
551
607
  "/keys": { description: "Every shortcut in full, plus line-editing keys and which chords this terminal may not deliver — /help lists them inline, next to each command" },
552
608
  "/guide": { args: "[topic|search <text>|all]", description: "The user guide, as a screen you can browse — or print one topic with /guide <topic>" },
@@ -570,9 +626,55 @@ function renderCommandHelp(language = "en", shortcuts = new Map, options = {}) {
570
626
  }).join(`
571
627
  `);
572
628
  }
629
+ function suggestionsFor(line, models = []) {
630
+ const model = /^\/models?\s+(\S*)$/.exec(line);
631
+ if (model) {
632
+ const fragment = model[1].toLowerCase();
633
+ const matched = models.filter((id) => id.toLowerCase().includes(fragment));
634
+ return matched.length === 1 && matched[0].toLowerCase() === fragment ? [] : matched.map((id) => ({ command: id, description: "" }));
635
+ }
636
+ if (!/^\/\S*$/.test(line))
637
+ return [];
638
+ const matches = COMMANDS2.filter((command) => command.name.startsWith(line));
639
+ if (matches.length === 1 && matches[0].name === line)
640
+ return [];
641
+ return matches.map((command) => ({ command: command.name, ...command.args ? { args: command.args } : {}, description: command.description }));
642
+ }
643
+ function inlineCompletion(line, models = []) {
644
+ const none = { suffix: "", alternatives: 0 };
645
+ const model = /^(\/models?\s+)(\S+)$/.exec(line);
646
+ if (model) {
647
+ const fragment = model[2].toLowerCase();
648
+ const matched2 = models.filter((id) => id.toLowerCase().startsWith(fragment)).sort((a, b) => a.length - b.length);
649
+ if (matched2.length === 0 || matched2[0].length === fragment.length)
650
+ return none;
651
+ return { suffix: matched2[0].slice(model[2].length), alternatives: matched2.length - 1 };
652
+ }
653
+ if (!/^\/\S*$/.test(line) || line === "/")
654
+ return none;
655
+ const matched = COMMANDS2.map((command) => command.name).filter((name) => name.startsWith(line)).sort((left, right) => left.length - right.length);
656
+ if (matched.length === 0 || matched[0] === line)
657
+ return none;
658
+ return { suffix: matched[0].slice(line.length), alternatives: matched.length - 1 };
659
+ }
573
660
  function isKnownCommand(name) {
574
661
  return COMMANDS2.some((command) => command.name === name);
575
662
  }
663
+ var MODE_NAMES = ["plan", "build", "auto", "defender"];
664
+ function isModeName(value) {
665
+ return MODE_NAMES.includes(value);
666
+ }
667
+ function parseModeCommand(input) {
668
+ const bareModeCommand = input.startsWith("/") ? input.slice(1) : "";
669
+ if (isModeName(bareModeCommand))
670
+ return { type: "switch", mode: bareModeCommand };
671
+ const match = /^\/mode(?:\s+(\S+))?$/.exec(input);
672
+ if (!match)
673
+ return null;
674
+ if (!match[1])
675
+ return { type: "show" };
676
+ return isModeName(match[1]) ? { type: "switch", mode: match[1] } : { type: "invalid" };
677
+ }
576
678
  function editDistance(left, right) {
577
679
  const previous = Array.from({ length: right.length + 1 }, (_unused, index) => index);
578
680
  for (let row = 1;row <= left.length; row += 1) {
@@ -591,8 +693,28 @@ function suggestCommand(name) {
591
693
  const best = ranked[0];
592
694
  return best && best.distance <= 2 ? best.name : undefined;
593
695
  }
696
+ var KEYBOARD_SHORTCUTS = [
697
+ ["menu-move", "↑ / ↓", "Move through any menu — settings, models, the palette"],
698
+ ["menu-number", "1-9", "Jump straight to a numbered row in a menu"],
699
+ ["menu-take", "Enter / Esc", "Take the highlighted row / leave the menu"],
700
+ ["complete-command", "Tab", "Complete a slash command"],
701
+ ["complete-path", "@ + path", "Complete a project file mention"],
702
+ ["history", "Up / Down", "Search persistent prompt history"],
703
+ ["line-ends", "Home / End", "Move to the start / end of the input"],
704
+ ["delete", "Alt-Backspace / Ctrl-U", "Delete the previous word / whole input"],
705
+ ["clear", "Ctrl-L", "Clear and redraw the terminal"],
706
+ ["interrupt", "Ctrl-C", "Interrupt the current turn"],
707
+ ["voice", "/voice", "Record a prompt from the microphone"],
708
+ ["expand", "Ctrl-O", "Unfold the most recent folded block"],
709
+ ["memory", "# fact", "Remember a fact for every future session in this project"]
710
+ ];
711
+ function renderKeyboardShortcuts(language = "en") {
712
+ const width = Math.max(...KEYBOARD_SHORTCUTS.map(([, key]) => key.length));
713
+ return KEYBOARD_SHORTCUTS.map(([id, key, description]) => ` ${key.padEnd(width + 2)}${keyboardDescription(language, id, description)}`).join(`
714
+ `);
715
+ }
594
716
 
595
- // src/render/guide.ts
717
+ // packages/archymedes-cli/src/render/guide.ts
596
718
  var GUIDE_TOPICS = [
597
719
  {
598
720
  id: "start",
@@ -601,13 +723,16 @@ var GUIDE_TOPICS = [
601
723
  body: [
602
724
  "Archymedes is a coding agent that works in a scrolling terminal transcript. You type what you want in plain language; it reads files, runs commands and edits code, and shows you what it did as it goes.",
603
725
  "Everything it prints stays in your terminal's scrollback, so you can scroll up, select text and pipe a session to a file the way you would with any other command.",
604
- "Type a request and press Enter. Slash commands do everything else — press / to see them, or open the searchable list."
726
+ "Type a request and press Enter. Slash commands do everything else — press / to see them, or open the searchable list.",
727
+ "One rule everywhere: Esc takes you back. Out of a menu, the guide, the file browser, the pager or the editor, Esc returns you to the chat prompt. A lone ? on the prompt shows the short help.",
728
+ "Archymedes starts in simple mode — one line of help and nothing else to learn. Turn ARCHYMEDES_SIMPLE off in /settings for the full banner, starter ideas and the grouped /help."
605
729
  ],
606
730
  examples: [
607
731
  { input: "fix the failing test in cart.ts", effect: "an ordinary request; Archymedes reads, edits and runs the tests" },
608
- { input: "/help", effect: "every command, with one line each" },
732
+ { input: "?", effect: "the short help: the handful of commands worth knowing first" },
733
+ { input: "/help all", effect: "every command, with one line each" },
609
734
  { input: "/palette", effect: "search commands by name or by what they do" },
610
- { input: "/guide", effect: "open this guide as a screen — arrows to move, / to search, q to leave" },
735
+ { input: "/guide", effect: "open this guide as a screen — arrows to move, / to search, Esc to come back" },
611
736
  { input: "/guide tabs", effect: "print one topic into the transcript, to keep or to copy" },
612
737
  { input: "/exit", effect: "leave; the session is saved and can be resumed" }
613
738
  ],
@@ -664,7 +789,8 @@ var GUIDE_TOPICS = [
664
789
  "The list is the union of what this build knows the prices of and what each provider says it has today, so a model released last week is still offerable — it is simply shown without a price rather than hidden. The live half is cached for six hours; /models refresh asks again.",
665
790
  "A model switch applies to the tab you are in. Opening a tab with --model is how you compare two models on the same problem.",
666
791
  "Type in /model to narrow a long list; Esc clears the search before it leaves.",
667
- "archymedes --free needs only an OpenRouter key. It accepts zero-priced tool models (openrouter/free or an exact model:free ID), sends a zero price cap with every request, and never falls back to a paid provider. Free quotas and rate limits still apply.",
792
+ "archymedes --free needs no key: it runs zero-priced tool models (openrouter/free or an exact model:free ID) through the Archymedes free gateway, which holds the shared OpenRouter key server-side and enforces a daily allowance — 100,000 tokens and 300 requests per UTC day, 20 a minute; tokens are the budget that usually runs out first. Until that gateway is live, free mode uses your own free OpenRouter key (50 free-model requests a day, 1,000 once the account has bought 10 credits). Every request carries a zero price cap and never falls back to a paid provider. When the allowance runs out, it says so and when it resets.",
793
+ "Type /upgrade (or /upgrades) to use your own OpenRouter API key instead: free-mode limits stop applying and requests go straight to OpenRouter. With your own key, --free talks to OpenRouter directly.",
668
794
  "For any other OpenRouter model — paid or free — use --provider openrouter with the same key (e.g. --provider openrouter --model openrouter/auto). That path has no price cap and needs no Archymedes Cloud account.",
669
795
  "Paste a TYPESAFE_API_KEY in archymedes settings for a Jev second opinion: it annotates approvals with a tool-fit reading and prints a post-turn verdict with probabilities. Advisory only — it never approves, denies, or blocks."
670
796
  ],
@@ -676,7 +802,7 @@ var GUIDE_TOPICS = [
676
802
  { input: "/fallback openai:gpt-5.4-mini", effect: "retry transient provider failures on this model, only before any output or tool action" },
677
803
  { input: "/providers", effect: "which providers are configured" }
678
804
  ],
679
- covers: ["/models", "/model", "/fallback", "/providers", "/settings"]
805
+ covers: ["/models", "/model", "/fallback", "/providers", "/settings", "/upgrade"]
680
806
  },
681
807
  {
682
808
  id: "where",
@@ -701,7 +827,8 @@ var GUIDE_TOPICS = [
701
827
  summary: "Code, diffs, test results, and folded detail.",
702
828
  body: [
703
829
  "When Archymedes writes or edits a file it shows the code, numbered, under the tool line. When it runs tests it re-lays the output into sections with the failures pulled out. When output is long it is folded rather than truncated, and the whole of it is one command away.",
704
- "The diff shows the actual patch since the last checkpoint — the lines, not a count of them."
830
+ "The diff shows the actual patch since the last checkpoint — the lines, not a count of them.",
831
+ "/diagnostics asks the language servers installed on this machine what they make of the code: errors, warnings and hints, file by file. It runs no model and changes nothing. A language whose server is not installed is listed as unavailable rather than passed over in silence."
705
832
  ],
706
833
  examples: [
707
834
  { input: "/diff", effect: "the real patch, file by file" },
@@ -713,9 +840,11 @@ var GUIDE_TOPICS = [
713
840
  { input: "/continue", effect: "finish an interrupted task without repeating completed work" },
714
841
  { input: "/todos", effect: "the agent's current plan" },
715
842
  { input: "/task", effect: "the whole task at a glance: request, plan, changes, verification, blockers" },
716
- { input: "/route", effect: "the hosted exchange's routing decision for the last turn — chosen model, alternatives, cost vs estimate" }
843
+ { input: "/route", effect: "the hosted exchange's routing decision for the last turn — chosen model, alternatives, cost vs estimate" },
844
+ { input: "/diagnostics", effect: "errors and warnings from installed language servers, no model call" },
845
+ { input: "/diagnostics src/**", effect: "the same, limited to files matching the glob" }
717
846
  ],
718
- covers: ["/diff", "/expand", "/undo", "/retry", "/continue", "/todos", "/task", "/route"]
847
+ covers: ["/diff", "/expand", "/undo", "/retry", "/continue", "/todos", "/task", "/route", "/diagnostics"]
719
848
  },
720
849
  {
721
850
  id: "memory",
@@ -738,16 +867,21 @@ var GUIDE_TOPICS = [
738
867
  title: "History",
739
868
  summary: "Past conversations, searchable and resumable.",
740
869
  body: [
741
- "Every session is saved. You can list them, search the ones you half-remember, read one back, or pick one up and keep going with its whole context intact."
870
+ "Every session is saved, so you can always go back to a normal conversation. /history (or /resume) opens a list of your earlier chats in this folder — arrows to choose, Enter to continue it with its whole context, Esc to stay where you are.",
871
+ "When you start Archymedes in a folder with a recent chat, it offers to continue it: press Enter on the empty prompt to pick it up, or just type to start fresh. ARCHYMEDES_RESUME in /settings changes that to always or never.",
872
+ "Chats belong to the folder they were started in: the list only ever shows this project's chats. In the picker, Del (or Ctrl+D) deletes the highlighted chat after asking — the chat you are in can't be deleted while it is open."
742
873
  ],
743
874
  examples: [
744
- { input: "/history", effect: "recent sessions, newest first" },
875
+ { input: "/history", effect: "pick an earlier chat and continue it" },
876
+ { input: "/resume latest", effect: "continue the most recent one straight away" },
877
+ { input: "/history list", effect: "print recent sessions, newest first" },
745
878
  { input: "/history search failing tests", effect: "find one by what you typed" },
746
- { input: "/history resume", effect: "pick up where a past session left off" },
879
+ { input: "/history delete --all", effect: "delete every other chat in this folder, after a yes (Del in the picker deletes one)" },
880
+ { input: "archymedes --resume", effect: "start Archymedes on the picker" },
747
881
  { input: "/export support", effect: "write a compact redacted artifact safe to attach to a support request" },
748
882
  { input: "/clear", effect: "start a fresh thread in this tab" }
749
883
  ],
750
- covers: ["/history", "/sessions", "/export", "/clear"]
884
+ covers: ["/history", "/sessions", "/resume", "/export", "/clear"]
751
885
  },
752
886
  {
753
887
  id: "cost",
@@ -828,7 +962,7 @@ var GUIDE_TOPICS = [
828
962
  body: [
829
963
  "The workspace is a full-screen view of everything at once: each tab as a pane showing its live output, what model it is running and where, and each watched job alongside them.",
830
964
  "It is a view — it reads the session and changes nothing — so leaving it puts you back exactly where you were.",
831
- "Inside it: 1-9 select a pane, arrows or Tab move between them, arrows or j/k scroll, g and G jump to the ends, q leaves."
965
+ "Inside it: 1-9 select a pane, arrows or Tab move between them, arrows or j/k scroll, g and G jump to the ends, Esc comes back to the chat."
832
966
  ],
833
967
  examples: [
834
968
  { input: "/workspace", effect: "open the panel" },
@@ -841,7 +975,8 @@ var GUIDE_TOPICS = [
841
975
  title: "How it looks",
842
976
  summary: "Themes, symbols, the fixed workspace, and terminals that need help.",
843
977
  body: [
844
- "Archymedes ships a blueprint theme and three others, and reads themes you write yourself as .tss files in .archymedes/themes — the same format TermUI apps use, so a palette written once works in both.",
978
+ "Archymedes ships a blueprint theme and three others, and reads themes you write yourself as .tss files in .archymedes/themes — the same format TermUI apps use, so a palette written once works in both. /theme <name> also saves the choice, so it is still your theme tomorrow.",
979
+ "Code is coloured like VS Code — Dark+ on a dark theme, Light+ on a light one — with numbered lines in code blocks and green and red bands on changed lines in diffs. In /settings, Code colours set to theme paints code in the theme's own palette instead, and Line numbers in code blocks turns the numbers off.",
845
980
  "Sessions open in the fixed workspace: a mode rail and composer stay anchored while the transcript scrolls underneath. Read history with the mouse wheel or Page Up / Page Down, move a line at a time with Alt+Up / Alt+Down, jump to the top with Ctrl+Home, and press Escape to return to live output. Menus open inside the transcript area so the composer never jumps. Prefer the plain terminal log? Use --layout scrollback, ARCHYMEDES_LAYOUT=scrollback, or /layout. ARCHYMEDES_MOUSE=0 keeps the terminal's own text selection instead of wheel scrolling.",
846
981
  "If your terminal draws question marks instead of symbols, --ascii switches to characters every terminal has. If you would rather have a status line pinned to the bottom row, --pin does that, at the cost of your terminal's scrollback. ARCHYMEDES_NO_MOTION=1 keeps the opening geometry and menu focus still."
847
982
  ],
@@ -851,11 +986,12 @@ var GUIDE_TOPICS = [
851
986
  { input: "/theme where", effect: "where to put a theme file of your own" },
852
987
  { input: "/layout fixed", effect: "mode rail, anchored composer, menus in the transcript" },
853
988
  { input: "/find TypeError", effect: "search this tab's history; /find again for the next match, Esc to return" },
854
- { input: "/pager", effect: "open the whole transcript in less, to search, select or save" },
989
+ { input: "/pager", effect: "open the whole transcript to scroll and search — in less, or the built-in pager on Windows (Esc back)" },
990
+ { input: "/settings", effect: "theme, code colours, line numbers, simple mode, startup and free-mode savings" },
855
991
  { input: "/layout scrollback", effect: "return to the ordinary terminal log" },
856
992
  { input: "archymedes --ascii", effect: "plain characters for terminals that mangle symbols" }
857
993
  ],
858
- covers: ["/theme", "/layout", "/find", "/pager"]
994
+ covers: ["/theme", "/layout", "/find", "/pager", "/settings"]
859
995
  },
860
996
  {
861
997
  id: "extend",
@@ -963,58 +1099,21 @@ function renderWholeGuide(style) {
963
1099
  ].join(`
964
1100
  `);
965
1101
  }
966
-
967
- // src/terminal/viewport.ts
968
- function newViewport(lines, height) {
969
- return clamp({ lines, height: Math.max(1, Math.floor(height)), top: 0 });
970
- }
971
- function maxTop(state) {
972
- return Math.max(0, state.lines.length - state.height);
973
- }
974
- function clamp(state) {
975
- const height = Math.max(1, Math.floor(state.height));
976
- const bounded = { ...state, height };
977
- const top = Math.max(0, Math.min(Math.floor(bounded.top) || 0, maxTop(bounded)));
978
- return { ...bounded, top };
979
- }
980
- function applyViewport(state, action) {
981
- switch (action.kind) {
982
- case "up":
983
- return clamp({ ...state, top: state.top - Math.max(1, action.rows ?? 1) });
984
- case "down":
985
- return clamp({ ...state, top: state.top + Math.max(1, action.rows ?? 1) });
986
- case "halfUp":
987
- return clamp({ ...state, top: state.top - Math.max(1, Math.floor(state.height / 2)) });
988
- case "halfDown":
989
- return clamp({ ...state, top: state.top + Math.max(1, Math.floor(state.height / 2)) });
990
- case "pageUp":
991
- return clamp({ ...state, top: state.top - Math.max(1, state.height - 1) });
992
- case "pageDown":
993
- return clamp({ ...state, top: state.top + Math.max(1, state.height - 1) });
994
- case "top":
995
- return clamp({ ...state, top: 0 });
996
- case "bottom":
997
- return clamp({ ...state, top: maxTop(state) });
998
- case "resize":
999
- return clamp({ ...state, height: action.height });
1000
- case "content": {
1001
- const next = clamp({ ...state, lines: action.lines });
1002
- return action.follow ? clamp({ ...next, top: maxTop(next) }) : next;
1003
- }
1102
+ function parseGuideCommand(input) {
1103
+ const match = /^\/(?:guide|tutorial)(?:\s+([\s\S]*))?$/.exec(input.trim());
1104
+ if (!match)
1105
+ return null;
1106
+ const rest = (match[1] ?? "").trim().replace(/\s+/g, " ");
1107
+ if (!rest)
1108
+ return { kind: "index" };
1109
+ const [verb, ...words] = rest.split(" ");
1110
+ if (verb.toLowerCase() === "all")
1111
+ return { kind: "all" };
1112
+ if (verb.toLowerCase() === "search") {
1113
+ const query = words.join(" ").trim();
1114
+ return query ? { kind: "search", query } : { kind: "index" };
1004
1115
  }
1005
- }
1006
- function visibleLines(state) {
1007
- return state.lines.slice(state.top, state.top + state.height);
1008
- }
1009
- function atTop(state) {
1010
- return state.top <= 0;
1011
- }
1012
- function atBottom(state) {
1013
- return state.top >= maxTop(state);
1014
- }
1015
- function scrollFraction(state) {
1016
- const furthest = maxTop(state);
1017
- return furthest === 0 ? 1 : state.top / furthest;
1116
+ return findTopic(rest) ? { kind: "topic", id: findTopic(rest).id } : { kind: "unknown", id: rest };
1018
1117
  }
1019
1118
 
1020
- export { CONTROL_LANGUAGES, resolveControlLanguage, controlLabel, keyboardDescription, t, COMMANDS2 as COMMANDS, renderCommandHelp, isKnownCommand, suggestCommand, GUIDE_TOPICS, findTopic, searchTopics, wrapText, renderGuideIndex, renderGuideTopic, renderWholeGuide, newViewport, maxTop, applyViewport, visibleLines, atTop, atBottom, scrollFraction };
1119
+ export { newViewport, maxTop, applyViewport, visibleLines, atTop, atBottom, scrollFraction, CONTROL_LANGUAGES, resolveControlLanguage, controlLabel, t, COMMANDS2 as COMMANDS, renderCommandHelp, suggestionsFor, inlineCompletion, isKnownCommand, parseModeCommand, suggestCommand, renderKeyboardShortcuts, GUIDE_TOPICS, findTopic, searchTopics, wrapText, renderGuideIndex, renderGuideTopic, renderWholeGuide, parseGuideCommand };