archymedes-cli 2.3.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.
- package/dist/{archymedes-aj30app4.mjs → archymedes-06fh0z84.mjs} +370 -159
- package/dist/{archymedes-d5wqcmdr.mjs → archymedes-0jxzsh4z.mjs} +97 -97
- package/dist/archymedes-130jt0wr.mjs +1231 -0
- package/dist/{archymedes-twbks7g3.mjs → archymedes-138genyy.mjs} +214 -5
- package/dist/{archymedes-h13ap0za.mjs → archymedes-1g1y6q74.mjs} +1 -1
- package/dist/{archymedes-dz59nvw7.mjs → archymedes-2kvjaajw.mjs} +21 -11
- package/dist/{archymedes-g8y3rt6t.mjs → archymedes-5vhvv8kr.mjs} +332 -9
- package/dist/{archymedes-4kx65anz.mjs → archymedes-a3ms7jvr.mjs} +43 -12
- package/dist/{archymedes-tsne2m7p.mjs → archymedes-bmtdqqwt.mjs} +2 -2
- package/dist/{archymedes-sf9t1hz9.mjs → archymedes-d3azqkay.mjs} +176 -77
- package/dist/archymedes-main.mjs +31312 -28352
- package/dist/archymedes-w4bjfye7.mjs +280 -0
- package/dist/{editor-screen-g08zzt70.mjs → editor-screen-e01mn7dd.mjs} +49 -14
- package/dist/{factory-7vtm2fed.mjs → factory-hyv8agr0.mjs} +326 -326
- package/dist/{file-screen-ma9yxq2b.mjs → file-screen-stcxznwy.mjs} +11 -9
- package/dist/{guide-screen-gd129txd.mjs → guide-screen-w5nb9sry.mjs} +10 -8
- package/dist/{index-qvgcwsvm.mjs → index-teq1cta4.mjs} +1 -1
- package/dist/{node-mdgzx68h.mjs → node-0fh63p8z.mjs} +7 -7
- package/dist/pager-screen-aamt6xv0.mjs +155 -0
- package/dist/projection-x6x6hm9t.mjs +248 -0
- package/dist/{sandbox-templates-9jcjws23.mjs → sandbox-templates-8sd1psez.mjs} +2 -2
- package/dist/workspace-screen-62k6hnq7.mjs +70 -0
- package/package.json +2 -2
- package/dist/archymedes-a10k8yrk.mjs +0 -464
- package/dist/workspace-screen-8cwk6jz9.mjs +0 -241
|
@@ -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-
|
|
5
|
-
import"./archymedes-
|
|
4
|
+
} from "./archymedes-2kvjaajw.mjs";
|
|
5
|
+
import"./archymedes-bmtdqqwt.mjs";
|
|
6
6
|
import {
|
|
7
|
-
NO_COLOR_PALETTE
|
|
8
|
-
|
|
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
|
-
// src/ui/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
|
-
// src/ui/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: "
|
|
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
|
-
// src/ui/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+["'](.+)["']/,
|
|
@@ -531,7 +550,7 @@ function composeExplainPanel(input, width, height, colors = DEFAULT_EXPLAIN_COLO
|
|
|
531
550
|
return padded;
|
|
532
551
|
}
|
|
533
552
|
|
|
534
|
-
// src/ui/editor-screen.tsx
|
|
553
|
+
// packages/archymedes-cli/src/ui/editor-screen.tsx
|
|
535
554
|
import { jsxDEV } from "@termuijs/jsx/jsx-dev-runtime";
|
|
536
555
|
var Panel = Box;
|
|
537
556
|
var Line = Text;
|
|
@@ -543,8 +562,22 @@ function panelWidthFor(columns) {
|
|
|
543
562
|
return 0;
|
|
544
563
|
return Math.min(MAX_PANEL_COLUMNS, Math.floor(columns * 0.38), columns - MIN_CODE_COLUMNS);
|
|
545
564
|
}
|
|
546
|
-
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 }) {
|
|
547
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]);
|
|
548
581
|
const [panel, setPanel] = useState(() => initialExplainPanelState());
|
|
549
582
|
const panelWidth = panel.open ? panelWidthFor(columns) : 0;
|
|
550
583
|
const codeWidth = panelWidth > 0 ? columns - panelWidth - 1 : columns;
|
|
@@ -554,7 +587,7 @@ function EditorScreen({ columns, rows, path, content, onExit, palette = NO_COLOR
|
|
|
554
587
|
return;
|
|
555
588
|
}
|
|
556
589
|
setPanel((current) => withAiExplanation(current, { status: "loading" }));
|
|
557
|
-
explain(currentContent, path).then((
|
|
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) })));
|
|
558
591
|
};
|
|
559
592
|
useInput((input, key) => {
|
|
560
593
|
if (key?.ctrl && key?.key === "e") {
|
|
@@ -629,6 +662,7 @@ async function runEditorScreen(options) {
|
|
|
629
662
|
if (settled)
|
|
630
663
|
return;
|
|
631
664
|
settled = true;
|
|
665
|
+
exitTopTermUIApp();
|
|
632
666
|
resolve(saved);
|
|
633
667
|
};
|
|
634
668
|
renderApp(EditorScreen, {
|
|
@@ -638,6 +672,7 @@ async function runEditorScreen(options) {
|
|
|
638
672
|
content: options.content,
|
|
639
673
|
palette: options.palette,
|
|
640
674
|
explain: options.explain,
|
|
675
|
+
autosave: options.autosave,
|
|
641
676
|
onExit: finish,
|
|
642
677
|
fullscreen: true
|
|
643
678
|
}).catch(() => finish(undefined));
|