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.
- package/README.md +17 -7
- package/dist/{archymedes-76z6n643.mjs → archymedes-06fh0z84.mjs} +233 -12
- package/dist/archymedes-130jt0wr.mjs +1231 -0
- package/dist/{archymedes-pg3pvh9r.mjs → archymedes-138genyy.mjs} +214 -5
- package/dist/{archymedes-sc3mwttb.mjs → archymedes-2kvjaajw.mjs} +66 -25
- package/dist/archymedes-5vhvv8kr.mjs +1169 -0
- package/dist/{archymedes-wjaqydyg.mjs → archymedes-a3ms7jvr.mjs} +45 -29
- package/dist/{archymedes-cdgejs3p.mjs → archymedes-bmtdqqwt.mjs} +17 -44
- package/dist/{archymedes-npcrm9a0.mjs → archymedes-d3azqkay.mjs} +537 -9
- package/dist/archymedes-main.mjs +31452 -25912
- package/dist/archymedes-w4bjfye7.mjs +280 -0
- package/dist/{editor-screen-1b3z96tn.mjs → editor-screen-e01mn7dd.mjs} +62 -26
- package/dist/{factory-96yy6mxr.mjs → factory-hyv8agr0.mjs} +1 -1
- package/dist/{file-screen-m7q1xcer.mjs → file-screen-stcxznwy.mjs} +10 -8
- package/dist/guide-screen-w5nb9sry.mjs +273 -0
- package/dist/pager-screen-aamt6xv0.mjs +155 -0
- package/dist/projection-x6x6hm9t.mjs +248 -0
- package/dist/workspace-screen-62k6hnq7.mjs +70 -0
- package/package.json +1 -1
- package/dist/archymedes-5zn4zp84.mjs +0 -646
- package/dist/archymedes-r54qthg5.mjs +0 -495
- package/dist/guide-screen-aj9n9v80.mjs +0 -569
- package/dist/workspace-screen-w88mcw4w.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
|
-
// 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: "
|
|
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" ?
|
|
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" ?
|
|
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:
|
|
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((
|
|
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));
|
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import {
|
|
2
2
|
windowStart
|
|
3
|
-
} from "./archymedes-
|
|
3
|
+
} from "./archymedes-a3ms7jvr.mjs";
|
|
4
4
|
import {
|
|
5
5
|
joinHorizontal,
|
|
6
6
|
padToWidth,
|
|
7
7
|
paginator
|
|
8
|
-
} from "./archymedes-
|
|
8
|
+
} from "./archymedes-5vhvv8kr.mjs";
|
|
9
9
|
import {
|
|
10
10
|
NO_COLOR_PALETTE,
|
|
11
|
-
UNICODE_GLYPHS
|
|
12
|
-
|
|
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 ·
|
|
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, {
|