archymedes-cli 2.2.0 → 2.5.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.
@@ -0,0 +1,280 @@
1
+ // packages/core/src/cli/protocol.ts
2
+ import { createHash } from "node:crypto";
3
+ import { promises as fs2 } from "node:fs";
4
+ import path2 from "node:path";
5
+
6
+ // packages/core/src/cli/file-ownership.ts
7
+ import { randomUUID } from "node:crypto";
8
+ import { promises as fs } from "node:fs";
9
+ import os from "node:os";
10
+ import path from "node:path";
11
+
12
+ class OwnershipHeldError extends Error {
13
+ pid;
14
+ constructor(pid) {
15
+ super(`Session is already owned by process ${pid}; close or detach the other writer first.`);
16
+ this.pid = pid;
17
+ }
18
+ }
19
+ async function processStart(pid) {
20
+ if (process.platform !== "linux")
21
+ return;
22
+ try {
23
+ const stat = await fs.readFile(`/proc/${pid}/stat`, "utf8");
24
+ const boot = await fs.readFile("/proc/sys/kernel/random/boot_id", "utf8");
25
+ return `${boot.trim()}:${stat.slice(stat.lastIndexOf(")") + 2).split(" ")[19]}`;
26
+ } catch {
27
+ return;
28
+ }
29
+ }
30
+ async function readOwner(file) {
31
+ try {
32
+ const owner = JSON.parse(await fs.readFile(file, "utf8"));
33
+ if (!owner || typeof owner.token !== "string" || !Number.isSafeInteger(owner.pid) || owner.pid < 1 || typeof owner.host !== "string") {
34
+ throw new Error(`Invalid ownership record: ${file}`);
35
+ }
36
+ return owner;
37
+ } catch (error) {
38
+ if (error.code === "ENOENT")
39
+ return null;
40
+ throw error;
41
+ }
42
+ }
43
+ async function isAlive(owner) {
44
+ if (owner.host !== os.hostname())
45
+ return true;
46
+ try {
47
+ process.kill(owner.pid, 0);
48
+ } catch (error) {
49
+ return error.code !== "ESRCH";
50
+ }
51
+ const started = await processStart(owner.pid);
52
+ return !owner.started || !started || owner.started === started;
53
+ }
54
+ async function acquireFileOwnership(file, depth = 0) {
55
+ if (depth > 8)
56
+ throw new Error(`Ownership recovery requires inspection: ${file}`);
57
+ await fs.mkdir(path.dirname(file), { recursive: true });
58
+ const owner = { token: randomUUID(), pid: process.pid, host: os.hostname(), started: await processStart(process.pid) };
59
+ const candidate = `${file}.${owner.token}.tmp`;
60
+ const handle = await fs.open(candidate, "wx", 384);
61
+ try {
62
+ await handle.writeFile(JSON.stringify(owner));
63
+ await handle.sync();
64
+ } finally {
65
+ await handle.close();
66
+ }
67
+ try {
68
+ for (let attempt = 0;attempt < 8; attempt++) {
69
+ try {
70
+ await fs.link(candidate, file);
71
+ return async () => {
72
+ if ((await readOwner(file))?.token === owner.token)
73
+ await fs.unlink(file);
74
+ };
75
+ } catch (error) {
76
+ if (error.code !== "EEXIST")
77
+ throw error;
78
+ }
79
+ const current = await readOwner(file);
80
+ if (!current)
81
+ continue;
82
+ if (await isAlive(current))
83
+ throw new OwnershipHeldError(current.pid);
84
+ const release = await acquireFileOwnership(`${file}.reap`, depth + 1);
85
+ try {
86
+ if ((await readOwner(file))?.token === current.token)
87
+ await fs.unlink(file);
88
+ } finally {
89
+ await release();
90
+ }
91
+ }
92
+ throw new Error(`Could not acquire session ownership: ${file}`);
93
+ } finally {
94
+ await fs.unlink(candidate).catch(() => {
95
+ return;
96
+ });
97
+ }
98
+ }
99
+
100
+ // packages/core/src/cli/protocol.ts
101
+ var ARCHYMEDES_PROTOCOL_VERSION = 1;
102
+ var JOURNAL_GENESIS_HASH = "0".repeat(64);
103
+ var TRANSITIONS = {
104
+ queued: ["running", "cancelled", "failed"],
105
+ running: ["waiting_approval", "completed", "blocked", "needs_verification", "cancelled", "failed", "iteration_limit"],
106
+ waiting_approval: ["running", "blocked", "cancelled", "failed"],
107
+ completed: [],
108
+ blocked: [],
109
+ needs_verification: [],
110
+ cancelled: [],
111
+ failed: [],
112
+ iteration_limit: []
113
+ };
114
+ function assertTurnTransition(from, to) {
115
+ if (!TRANSITIONS[from].includes(to))
116
+ throw new Error(`Illegal Archymedes turn transition: ${from} -> ${to}`);
117
+ }
118
+ var SENSITIVE_KEY = /(authorization|cookie|credential|password|private.?key|secret|token)/i;
119
+ var SECRET_ASSIGNMENT = /((?:[A-Za-z0-9_]*(?:api[_-]?key|access[_-]?key(?:_id)?|authorization|credential|password|passwd|private[_-]?key|secret|token)[A-Za-z0-9_]*)\s*[:=]\s*)([^\s,;]+)/gi;
120
+ var PRIVATE_KEY_BLOCK = /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g;
121
+ var AUTHENTICATED_URL = /([a-z][a-z0-9+.-]*:\/\/)([^/\s:@]+):([^@\s/]+)@/gi;
122
+ function redactJournalString(value) {
123
+ return value.replace(PRIVATE_KEY_BLOCK, "[REDACTED PRIVATE KEY]").replace(AUTHENTICATED_URL, "$1[REDACTED]@").replace(SECRET_ASSIGNMENT, "$1[REDACTED]");
124
+ }
125
+ function boundedJournalValue(value, key = "") {
126
+ if (SENSITIVE_KEY.test(key))
127
+ return "[REDACTED]";
128
+ if (typeof value === "string") {
129
+ const redacted = redactJournalString(value);
130
+ if (redacted.length <= 4000)
131
+ return redacted;
132
+ const digest = createHash("sha256").update(value).digest("hex");
133
+ return `${redacted.slice(0, 4000)}
134
+ ...[${redacted.length - 4000} chars omitted; original sha256=${digest}]`;
135
+ }
136
+ if (Array.isArray(value))
137
+ return value.map((item) => boundedJournalValue(item));
138
+ if (value && typeof value === "object") {
139
+ return Object.fromEntries(Object.entries(value).map(([childKey, item]) => [childKey, boundedJournalValue(item, childKey)]));
140
+ }
141
+ return value;
142
+ }
143
+ function runtimeEventForJournal(event) {
144
+ if (event.type === "tool_call")
145
+ return { ...event, arguments: boundedJournalValue(event.arguments) };
146
+ if (event.type === "tool_result") {
147
+ return {
148
+ ...event,
149
+ content: boundedJournalValue(event.content),
150
+ ...event.data ? { data: boundedJournalValue(event.data) } : {}
151
+ };
152
+ }
153
+ return event;
154
+ }
155
+ function hashEnvelope(envelope) {
156
+ return createHash("sha256").update(JSON.stringify(envelope)).digest("hex");
157
+ }
158
+ function eventJournalPath(root, sessionId) {
159
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/.test(sessionId))
160
+ throw new Error("Session id contains unsafe characters");
161
+ return path2.join(root, ".archymedes", "events", `${sessionId}.jsonl`);
162
+ }
163
+ async function readEventJournal(root, sessionId) {
164
+ return (await readJournalFile(root, sessionId)).events;
165
+ }
166
+ async function readJournalFile(root, sessionId) {
167
+ const file = eventJournalPath(root, sessionId);
168
+ let text;
169
+ try {
170
+ text = await fs2.readFile(file, "utf8");
171
+ } catch (error) {
172
+ if (error.code === "ENOENT")
173
+ return { events: [], text: "", completeText: "" };
174
+ throw error;
175
+ }
176
+ const completeText = text.endsWith(`
177
+ `) ? text : text.slice(0, text.lastIndexOf(`
178
+ `) + 1);
179
+ const events = completeText.split(`
180
+ `).filter(Boolean).map((line) => JSON.parse(line));
181
+ let previousHash = JOURNAL_GENESIS_HASH;
182
+ for (let index = 0;index < events.length; index += 1) {
183
+ const event = events[index];
184
+ if (event.protocolVersion !== ARCHYMEDES_PROTOCOL_VERSION)
185
+ throw new Error(`Unsupported Archymedes protocol version ${event.protocolVersion}`);
186
+ if (event.sessionId !== sessionId || event.sequence !== index + 1 || event.previousHash !== previousHash) {
187
+ throw new Error(`Invalid Archymedes event chain at sequence ${event.sequence}`);
188
+ }
189
+ const { hash, ...withoutHash } = event;
190
+ if (hash !== hashEnvelope(withoutHash))
191
+ throw new Error(`Archymedes event integrity check failed at sequence ${event.sequence}`);
192
+ previousHash = hash;
193
+ }
194
+ return { events, text, completeText };
195
+ }
196
+
197
+ class EventJournal {
198
+ root;
199
+ sessionId;
200
+ handle = null;
201
+ releaseOwnership;
202
+ closed = false;
203
+ sequence = 0;
204
+ previousHash = JOURNAL_GENESIS_HASH;
205
+ initialized = null;
206
+ tail = Promise.resolve();
207
+ constructor(root, sessionId) {
208
+ this.root = root;
209
+ this.sessionId = sessionId;
210
+ }
211
+ async initialize() {
212
+ this.releaseOwnership = await acquireFileOwnership(`${eventJournalPath(this.root, this.sessionId)}.owner`);
213
+ try {
214
+ const { events, text, completeText } = await readJournalFile(this.root, this.sessionId);
215
+ this.sequence = events.length;
216
+ this.previousHash = events.at(-1)?.hash ?? JOURNAL_GENESIS_HASH;
217
+ const file = eventJournalPath(this.root, this.sessionId);
218
+ await fs2.mkdir(path2.dirname(file), { recursive: true });
219
+ if (text.length !== completeText.length)
220
+ await fs2.truncate(file, Buffer.byteLength(completeText, "utf8"));
221
+ this.handle = await fs2.open(file, "a", 384);
222
+ } catch (error) {
223
+ await this.releaseOwnership();
224
+ this.releaseOwnership = undefined;
225
+ throw error;
226
+ }
227
+ }
228
+ async open() {
229
+ if (this.closed)
230
+ throw new Error("Session journal is closed");
231
+ this.initialized ??= this.initialize();
232
+ await this.initialized;
233
+ }
234
+ append(payload, options = {}) {
235
+ if (this.closed)
236
+ return Promise.reject(new Error("Session journal is closed"));
237
+ let written;
238
+ const operation = this.tail.then(async () => {
239
+ await this.open();
240
+ const withoutHash = {
241
+ protocolVersion: ARCHYMEDES_PROTOCOL_VERSION,
242
+ sequence: this.sequence + 1,
243
+ sessionId: this.sessionId,
244
+ timestamp: new Date().toISOString(),
245
+ previousHash: this.previousHash,
246
+ payload
247
+ };
248
+ written = { ...withoutHash, hash: hashEnvelope(withoutHash) };
249
+ await this.handle.write(`${JSON.stringify(written)}
250
+ `);
251
+ if (options.durable)
252
+ await this.handle.sync();
253
+ this.sequence = written.sequence;
254
+ this.previousHash = written.hash;
255
+ });
256
+ this.tail = operation;
257
+ return operation.then(() => written);
258
+ }
259
+ async flush() {
260
+ await this.tail;
261
+ await this.handle?.sync();
262
+ }
263
+ async close() {
264
+ try {
265
+ await this.initialized;
266
+ await this.flush();
267
+ } finally {
268
+ this.closed = true;
269
+ try {
270
+ await this.handle?.close();
271
+ } finally {
272
+ this.handle = null;
273
+ await this.releaseOwnership?.();
274
+ this.releaseOwnership = undefined;
275
+ }
276
+ }
277
+ }
278
+ }
279
+
280
+ export { OwnershipHeldError, acquireFileOwnership, assertTurnTransition, runtimeEventForJournal, eventJournalPath, readEventJournal, EventJournal };
@@ -1,20 +1,21 @@
1
1
  import {
2
2
  diffLines,
3
3
  languageOf
4
- } from "./archymedes-sc3mwttb.mjs";
5
- import"./archymedes-cdgejs3p.mjs";
4
+ } from "./archymedes-2kvjaajw.mjs";
5
+ import"./archymedes-bmtdqqwt.mjs";
6
6
  import {
7
- NO_COLOR_PALETTE
8
- } from "./archymedes-5zn4zp84.mjs";
7
+ NO_COLOR_PALETTE,
8
+ exitTopTermUIApp
9
+ } from "./archymedes-130jt0wr.mjs";
9
10
  import {
10
11
  __require
11
12
  } from "./archymedes-x67v3n58.mjs";
12
13
 
13
- // packages/archymedes-cli/src/editor-screen.tsx
14
- import { useInput, useState } from "@termuijs/jsx";
14
+ // packages/archymedes-cli/src/ui/editor-screen.tsx
15
+ import { useEffect, useInput, useRef, useState } from "@termuijs/jsx";
15
16
  import { Box, Text } from "@termuijs/widgets";
16
17
 
17
- // packages/archymedes-cli/src/editor.ts
18
+ // packages/archymedes-cli/src/ui/editor.ts
18
19
  function initialEditorState(path, content, viewportRows = 20) {
19
20
  const lines = content.split(`
20
21
  `);
@@ -79,6 +80,15 @@ function keyToEditorAction(key, character, state) {
79
80
  return { kind: "save" };
80
81
  if (key.ctrl && (name === "q" || name === "c"))
81
82
  return { kind: "quit" };
83
+ if (state.confirmLeave) {
84
+ if (character === "y" || character === "Y" || isEnter)
85
+ return { kind: "save" };
86
+ if (character === "n" || character === "N")
87
+ return { kind: "quit" };
88
+ if (isEscape)
89
+ return { kind: "cancelLeave" };
90
+ return { kind: "none" };
91
+ }
82
92
  if (state.search.typing) {
83
93
  if (isEscape)
84
94
  return { kind: "searchCommit" };
@@ -116,7 +126,7 @@ function keyToEditorAction(key, character, state) {
116
126
  if (state.pending === "g")
117
127
  return name === "g" || character === "g" ? { kind: "jump", to: "fileStart" } : { kind: "normal" };
118
128
  if (isEscape)
119
- return { kind: "normal" };
129
+ return { kind: "leave" };
120
130
  if (name === "up" || character === "k")
121
131
  return { kind: "move", rows: -1 };
122
132
  if (name === "down" || character === "j")
@@ -269,7 +279,15 @@ function applyEditorAction(state, action) {
269
279
  case "save":
270
280
  return { state: clamp({ ...state, dirty: false, message: `saved ${state.path}`, pending: undefined }), effect: { kind: "save" } };
271
281
  case "quit":
272
- return { state, effect: { kind: "quit" } };
282
+ return { state: { ...state, confirmLeave: false }, effect: { kind: "quit" } };
283
+ case "leave":
284
+ if (!state.dirty)
285
+ return { state, effect: { kind: "quit" } };
286
+ return { state: { ...state, pending: undefined, confirmLeave: true, message: "Save changes? y yes · n discard · Esc keep editing" }, effect: undefined };
287
+ case "cancelLeave":
288
+ return plain({ ...state, confirmLeave: false, message: undefined });
289
+ case "autosaved":
290
+ return editorContent(state) === action.content ? plain({ ...state, dirty: false, message: "auto-saved" }) : plain(state);
273
291
  case "search":
274
292
  return plain({ ...state, search: { query: "", typing: true }, message: undefined });
275
293
  case "searchType":
@@ -311,7 +329,8 @@ function editorKeyBar(state, columns = Number.POSITIVE_INFINITY) {
311
329
  { text: "u undo" },
312
330
  { text: "/ find" },
313
331
  { text: "^S save", essential: true },
314
- { text: "^Q quit", essential: true }
332
+ { text: "^Q quit", essential: true },
333
+ { text: "esc back", essential: true }
315
334
  ];
316
335
  const optional = hints.filter((hint) => !hint.essential);
317
336
  for (let dropped = 0;dropped <= optional.length; dropped += 1) {
@@ -347,7 +366,7 @@ function caretInto(text, cursorCol) {
347
366
  return `${padded.slice(0, cursorCol)}█${padded.slice(cursorCol + 1)}`;
348
367
  }
349
368
 
350
- // packages/archymedes-cli/src/explain-view.ts
369
+ // packages/archymedes-cli/src/ui/explain-view.ts
351
370
  var PATTERNS = {
352
371
  typescript: {
353
372
  imports: /^\s*import\b.*from\s+["'](.+)["']/,
@@ -436,6 +455,7 @@ function cycleExplainTab(state, step = 1) {
436
455
  function withAiExplanation(state, ai) {
437
456
  return { ...state, ai };
438
457
  }
458
+ var DEFAULT_EXPLAIN_COLORS = { success: "green", warning: "yellow", error: "red" };
439
459
  var TAB_LABELS = { facts: "Structure", notes: "Notes", diff: "Diff", ai: "AI" };
440
460
  function tabStrip(active) {
441
461
  const text = EXPLAIN_TABS.map((tab) => tab === active ? `[${TAB_LABELS[tab]}]` : ` ${TAB_LABELS[tab]} `).join(" ");
@@ -482,47 +502,47 @@ function factsRows(facts, width) {
482
502
  return [{ text: "No imports, exports, functions or classes recognised.", dim: true }];
483
503
  return rows.slice(0, -1);
484
504
  }
485
- function notesRows(notes, width) {
505
+ function notesRows(notes, width, colors) {
486
506
  if (notes.length === 0)
487
507
  return [{ text: "Nothing flagged.", dim: true }];
488
508
  return notes.slice(0, 60).map((entry) => ({
489
509
  text: `${String(entry.line).padStart(4)} ${entry.note}`.slice(0, width),
490
- color: entry.severity === "warn" ? "yellow" : undefined
510
+ color: entry.severity === "warn" ? colors.warning : undefined
491
511
  }));
492
512
  }
493
- function diffRows(diff, width) {
513
+ function diffRows(diff, width, colors) {
494
514
  if (diff.length === 0 || diff.every((line) => line.kind === "context")) {
495
515
  return [{ text: "No changes yet.", dim: true }];
496
516
  }
497
517
  return diff.filter((line) => line.kind !== "context").slice(0, 200).map((line) => ({
498
518
  text: `${line.kind === "add" ? "+" : "-"} ${line.text}`.slice(0, width),
499
- color: line.kind === "add" ? "green" : "red"
519
+ color: line.kind === "add" ? colors.success : colors.error
500
520
  }));
501
521
  }
502
- function aiRows(ai, width) {
522
+ function aiRows(ai, width, colors) {
503
523
  switch (ai.status) {
504
524
  case "idle":
505
525
  return [{ text: "Press ? to ask the model to explain this file.", dim: true }];
506
526
  case "loading":
507
527
  return [{ text: "Thinking…", dim: true }];
508
528
  case "error":
509
- return [{ text: `Could not get an explanation: ${ai.message}`, color: "red" }];
529
+ return [{ text: `Could not get an explanation: ${ai.message}`, color: colors.error }];
510
530
  case "ready":
511
531
  return ai.text.split(`
512
532
  `).flatMap((line) => wrap(line, width)).map((text) => ({ text }));
513
533
  }
514
534
  }
515
- function composeExplainPanel(input, width, height) {
535
+ function composeExplainPanel(input, width, height, colors = DEFAULT_EXPLAIN_COLORS) {
516
536
  const body = (() => {
517
537
  switch (input.panel.tab) {
518
538
  case "facts":
519
539
  return factsRows(structuralFacts(input.after, input.path), width);
520
540
  case "notes":
521
- return notesRows(annotateLines(input.after), width);
541
+ return notesRows(annotateLines(input.after), width, colors);
522
542
  case "diff":
523
- return diffRows(diffLines(input.before, input.after), width);
543
+ return diffRows(diffLines(input.before, input.after), width, colors);
524
544
  case "ai":
525
- return aiRows(input.panel.ai, width);
545
+ return aiRows(input.panel.ai, width, colors);
526
546
  }
527
547
  })();
528
548
  const rows = [tabStrip(input.panel.tab), hintRow(input.panel.tab), { text: "" }, ...body];
@@ -530,7 +550,7 @@ function composeExplainPanel(input, width, height) {
530
550
  return padded;
531
551
  }
532
552
 
533
- // packages/archymedes-cli/src/editor-screen.tsx
553
+ // packages/archymedes-cli/src/ui/editor-screen.tsx
534
554
  import { jsxDEV } from "@termuijs/jsx/jsx-dev-runtime";
535
555
  var Panel = Box;
536
556
  var Line = Text;
@@ -542,8 +562,22 @@ function panelWidthFor(columns) {
542
562
  return 0;
543
563
  return Math.min(MAX_PANEL_COLUMNS, Math.floor(columns * 0.38), columns - MIN_CODE_COLUMNS);
544
564
  }
545
- function EditorScreen({ columns, rows, path, content, onExit, palette = NO_COLOR_PALETTE, explain }) {
565
+ function EditorScreen({ columns, rows, path, content, onExit, palette = NO_COLOR_PALETTE, explain, autosave, autosaveDelayMs = 1000 }) {
546
566
  const [state, setState] = useState(() => initialEditorState(path, content, Math.max(1, rows - CHROME_ROWS)));
567
+ const timer = useRef(undefined);
568
+ const text = editorContent(state);
569
+ useEffect(() => {
570
+ if (!autosave || !state.dirty || state.confirmLeave)
571
+ return;
572
+ const pending = text;
573
+ timer.current = setTimeout(() => {
574
+ autosave(pending).then(() => setState((current) => applyEditorAction(current, { kind: "autosaved", content: pending }).state), () => setState((current) => ({ ...current, message: "auto-save failed — Ctrl+S to retry" })));
575
+ }, autosaveDelayMs);
576
+ return () => {
577
+ if (timer.current)
578
+ clearTimeout(timer.current);
579
+ };
580
+ }, [text, state.dirty, state.confirmLeave]);
547
581
  const [panel, setPanel] = useState(() => initialExplainPanelState());
548
582
  const panelWidth = panel.open ? panelWidthFor(columns) : 0;
549
583
  const codeWidth = panelWidth > 0 ? columns - panelWidth - 1 : columns;
@@ -553,7 +587,7 @@ function EditorScreen({ columns, rows, path, content, onExit, palette = NO_COLOR
553
587
  return;
554
588
  }
555
589
  setPanel((current) => withAiExplanation(current, { status: "loading" }));
556
- explain(currentContent, path).then((text) => setPanel((current) => withAiExplanation(current, { status: "ready", text })), (error) => setPanel((current) => withAiExplanation(current, { status: "error", message: error instanceof Error ? error.message : String(error) })));
590
+ explain(currentContent, path).then((text2) => setPanel((current) => withAiExplanation(current, { status: "ready", text: text2 })), (error) => setPanel((current) => withAiExplanation(current, { status: "error", message: error instanceof Error ? error.message : String(error) })));
557
591
  };
558
592
  useInput((input, key) => {
559
593
  if (key?.ctrl && key?.key === "e") {
@@ -584,12 +618,12 @@ function EditorScreen({ columns, rows, path, content, onExit, palette = NO_COLOR
584
618
  return next;
585
619
  });
586
620
  });
587
- const frame = composeEditorFrame(state, codeWidth, palette.accent);
621
+ const frame = composeEditorFrame(state, codeWidth, palette.tokens.accent);
588
622
  const lastRow = frame.length - 1;
589
623
  if (!panel.open && lastRow >= 0 && frame[lastRow].text.length + " ^E explain".length <= codeWidth) {
590
624
  frame[lastRow] = { ...frame[lastRow], text: `${frame[lastRow].text} ^E explain` };
591
625
  }
592
- const panelRows = panelWidth > 0 ? composeExplainPanel({ path, before: content, after: editorContent(state), panel }, panelWidth - 2, rows) : [];
626
+ const panelRows = panelWidth > 0 ? composeExplainPanel({ path, before: content, after: editorContent(state), panel }, panelWidth - 2, rows, palette.tokens) : [];
593
627
  return /* @__PURE__ */ jsxDEV(Panel, {
594
628
  flexDirection: "row",
595
629
  width: columns,
@@ -628,6 +662,7 @@ async function runEditorScreen(options) {
628
662
  if (settled)
629
663
  return;
630
664
  settled = true;
665
+ exitTopTermUIApp();
631
666
  resolve(saved);
632
667
  };
633
668
  renderApp(EditorScreen, {
@@ -637,6 +672,7 @@ async function runEditorScreen(options) {
637
672
  content: options.content,
638
673
  palette: options.palette,
639
674
  explain: options.explain,
675
+ autosave: options.autosave,
640
676
  onExit: finish,
641
677
  fullscreen: true
642
678
  }).catch(() => finish(undefined));
@@ -6,7 +6,7 @@ import {
6
6
  makeParseableTextFormat,
7
7
  modelPricesFromEnvironment,
8
8
  wanderPlannerInstructions
9
- } from "./archymedes-76z6n643.mjs";
9
+ } from "./archymedes-06fh0z84.mjs";
10
10
  import {
11
11
  ALLOWED_GIT_SUBCOMMANDS,
12
12
  ALLOWED_SANDBOX_PROGRAMS,
@@ -1,20 +1,21 @@
1
1
  import {
2
2
  windowStart
3
- } from "./archymedes-wjaqydyg.mjs";
3
+ } from "./archymedes-a3ms7jvr.mjs";
4
4
  import {
5
5
  joinHorizontal,
6
6
  padToWidth,
7
7
  paginator
8
- } from "./archymedes-r54qthg5.mjs";
8
+ } from "./archymedes-5vhvv8kr.mjs";
9
9
  import {
10
10
  NO_COLOR_PALETTE,
11
- UNICODE_GLYPHS
12
- } from "./archymedes-5zn4zp84.mjs";
11
+ UNICODE_GLYPHS,
12
+ exitTopTermUIApp
13
+ } from "./archymedes-130jt0wr.mjs";
13
14
  import {
14
15
  __require
15
16
  } from "./archymedes-x67v3n58.mjs";
16
17
 
17
- // packages/archymedes-cli/src/file-screen.tsx
18
+ // packages/archymedes-cli/src/ui/file-screen.tsx
18
19
  import { useAsync, useInput, useState } from "@termuijs/jsx";
19
20
  import { Box, Text } from "@termuijs/widgets";
20
21
 
@@ -59,7 +60,7 @@ function directorySummary(node) {
59
60
  }
60
61
  return { files, dirs };
61
62
  }
62
- // packages/archymedes-cli/src/file-browser.ts
63
+ // packages/archymedes-cli/src/ui/file-browser.ts
63
64
  function flattenTree(tree, expanded, depth = 0, trunks = []) {
64
65
  const rows = [];
65
66
  for (const [index, node] of tree.entries()) {
@@ -228,7 +229,7 @@ function composeFileFrame(state, preview, glyphs = UNICODE_GLYPHS) {
228
229
  });
229
230
  }
230
231
  const overflow = rows.length > height ? ` ${paginator(selectedIndex < 0 ? 0 : selectedIndex, rows.length)}` : "";
231
- const footer = state.searching ? `search: ${state.query}▏ Enter open · Esc done` : `↑↓ move · →/l open · ←/h close · Enter pick a file · / search · q leave${state.query ? ` filter: ${state.query}` : ""}${overflow}`;
232
+ const footer = state.searching ? `search: ${state.query}▏ Enter open · Esc done` : `↑↓ move · →/l open · ←/h close · Enter pick a file · / search · Esc back${state.query ? ` filter: ${state.query}` : ""}${overflow}`;
232
233
  out.push({ text: padToWidth(` ${footer}`, columns), dim: true, color: theme.textMuted });
233
234
  return out;
234
235
  }
@@ -251,7 +252,7 @@ function previewToLines(preview, width) {
251
252
  }
252
253
  }
253
254
 
254
- // packages/archymedes-cli/src/file-screen.tsx
255
+ // packages/archymedes-cli/src/ui/file-screen.tsx
255
256
  import { jsxDEV } from "@termuijs/jsx/jsx-dev-runtime";
256
257
  var Panel = Box;
257
258
  var Line = Text;
@@ -301,6 +302,7 @@ async function runFileScreen(options) {
301
302
  if (settled)
302
303
  return;
303
304
  settled = true;
305
+ exitTopTermUIApp();
304
306
  resolve(picked);
305
307
  };
306
308
  renderApp(FileScreen, {