archymedes-cli 3.0.0 → 3.2.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.
@@ -1,684 +0,0 @@
1
- import {
2
- diffLines,
3
- languageOf
4
- } from "./archymedes-hf7kq83m.mjs";
5
- import"./archymedes-2emwtckp.mjs";
6
- import {
7
- NO_COLOR_PALETTE,
8
- exitTopTermUIApp
9
- } from "./archymedes-y64yssbc.mjs";
10
- import {
11
- __require
12
- } from "./archymedes-x67v3n58.mjs";
13
-
14
- // packages/archymedes-cli/src/ui/editor-screen.tsx
15
- import { useEffect, useInput, useRef, useState } from "@termuijs/jsx";
16
- import { Box, Text } from "@termuijs/widgets";
17
-
18
- // packages/archymedes-cli/src/ui/editor.ts
19
- function initialEditorState(path, content, viewportRows = 20) {
20
- const lines = content.split(`
21
- `);
22
- return {
23
- path,
24
- lines,
25
- cursor: { row: 0, col: 0 },
26
- mode: "normal",
27
- scroll: 0,
28
- viewportRows: Math.max(1, viewportRows),
29
- dirty: false,
30
- history: [],
31
- future: [],
32
- search: { query: "", typing: false }
33
- };
34
- }
35
- function editorContent(state) {
36
- return state.lines.join(`
37
- `);
38
- }
39
- function clamp(state) {
40
- const row = Math.max(0, Math.min(state.lines.length - 1, state.cursor.row));
41
- const limit = state.mode === "insert" ? state.lines[row].length : Math.max(0, state.lines[row].length - 1);
42
- const col = Math.max(0, Math.min(limit, state.cursor.col));
43
- let scroll = state.scroll;
44
- if (row < scroll)
45
- scroll = row;
46
- if (row >= scroll + state.viewportRows)
47
- scroll = row - state.viewportRows + 1;
48
- scroll = Math.max(0, Math.min(scroll, Math.max(0, state.lines.length - 1)));
49
- return { ...state, cursor: { row, col }, scroll };
50
- }
51
- function checkpoint(state) {
52
- return {
53
- ...state,
54
- history: [...state.history, { lines: [...state.lines], cursor: { ...state.cursor } }].slice(-200),
55
- future: []
56
- };
57
- }
58
- var WORD = /[A-Za-z0-9_]/;
59
- function wordForward(line, col) {
60
- let index = col;
61
- while (index < line.length && WORD.test(line[index]))
62
- index += 1;
63
- while (index < line.length && !WORD.test(line[index]))
64
- index += 1;
65
- return index;
66
- }
67
- function wordBack(line, col) {
68
- let index = Math.max(0, col - 1);
69
- while (index > 0 && !WORD.test(line[index]))
70
- index -= 1;
71
- while (index > 0 && WORD.test(line[index - 1]))
72
- index -= 1;
73
- return index;
74
- }
75
- function keyToEditorAction(key, character, state) {
76
- const name = key.name ?? "";
77
- const isEnter = name === "return" || name === "enter";
78
- const isEscape = name === "escape";
79
- if (key.ctrl && name === "s")
80
- return { kind: "save" };
81
- if (key.ctrl && (name === "q" || name === "c"))
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
- }
92
- if (state.search.typing) {
93
- if (isEscape)
94
- return { kind: "searchCommit" };
95
- if (isEnter)
96
- return { kind: "searchNext" };
97
- if (name === "backspace")
98
- return { kind: "searchBackspace" };
99
- if (character && character.length === 1 && character >= " ")
100
- return { kind: "searchType", character };
101
- return { kind: "none" };
102
- }
103
- if (state.mode === "insert") {
104
- if (isEscape)
105
- return { kind: "normal" };
106
- if (isEnter)
107
- return { kind: "newline" };
108
- if (name === "backspace")
109
- return { kind: "backspace" };
110
- if (name === "up")
111
- return { kind: "move", rows: -1 };
112
- if (name === "down")
113
- return { kind: "move", rows: 1 };
114
- if (name === "left")
115
- return { kind: "move", cols: -1 };
116
- if (name === "right")
117
- return { kind: "move", cols: 1 };
118
- if (name === "tab")
119
- return { kind: "type", character: " " };
120
- if (character && character.length === 1 && character >= " ")
121
- return { kind: "type", character };
122
- return { kind: "none" };
123
- }
124
- if (state.pending === "d")
125
- return name === "d" || character === "d" ? { kind: "deleteLine" } : { kind: "normal" };
126
- if (state.pending === "g")
127
- return name === "g" || character === "g" ? { kind: "jump", to: "fileStart" } : { kind: "normal" };
128
- if (isEscape)
129
- return { kind: "leave" };
130
- if (name === "up" || character === "k")
131
- return { kind: "move", rows: -1 };
132
- if (name === "down" || character === "j")
133
- return { kind: "move", rows: 1 };
134
- if (name === "left" || character === "h")
135
- return { kind: "move", cols: -1 };
136
- if (name === "right" || character === "l")
137
- return { kind: "move", cols: 1 };
138
- if (name === "pageup")
139
- return { kind: "move", rows: -state.viewportRows };
140
- if (name === "pagedown")
141
- return { kind: "move", rows: state.viewportRows };
142
- if (character === "0" || name === "home")
143
- return { kind: "jump", to: "lineStart" };
144
- if (character === "$" || name === "end")
145
- return { kind: "jump", to: "lineEnd" };
146
- if (character === "G")
147
- return { kind: "jump", to: "fileEnd" };
148
- if (character === "w")
149
- return { kind: "jump", to: "wordForward" };
150
- if (character === "b")
151
- return { kind: "jump", to: "wordBack" };
152
- if (character === "i")
153
- return { kind: "insert" };
154
- if (character === "a")
155
- return { kind: "insert", at: "after" };
156
- if (character === "I")
157
- return { kind: "insert", at: "lineStart" };
158
- if (character === "A")
159
- return { kind: "insert", at: "lineEnd" };
160
- if (character === "o")
161
- return { kind: "insert", at: "below" };
162
- if (character === "O")
163
- return { kind: "insert", at: "above" };
164
- if (character === "x")
165
- return { kind: "deleteChar" };
166
- if (character === "u")
167
- return { kind: "undo" };
168
- if (key.ctrl && name === "r")
169
- return { kind: "redo" };
170
- if (character === "/")
171
- return { kind: "search" };
172
- if (character === "n")
173
- return { kind: "searchNext" };
174
- if (character === "d" || character === "g")
175
- return { kind: "pending", key: character };
176
- return { kind: "none" };
177
- }
178
- function applyEditorAction(state, action) {
179
- const plain = (next) => ({ state: clamp({ ...next, pending: undefined }), effect: undefined });
180
- const line = state.lines[state.cursor.row] ?? "";
181
- switch (action.kind) {
182
- case "move":
183
- return plain({
184
- ...state,
185
- message: undefined,
186
- cursor: { row: state.cursor.row + (action.rows ?? 0), col: state.cursor.col + (action.cols ?? 0) }
187
- });
188
- case "jump": {
189
- const targets = {
190
- lineStart: { row: state.cursor.row, col: 0 },
191
- lineEnd: { row: state.cursor.row, col: line.length },
192
- fileStart: { row: 0, col: 0 },
193
- fileEnd: { row: state.lines.length - 1, col: 0 },
194
- wordForward: { row: state.cursor.row, col: wordForward(line, state.cursor.col) },
195
- wordBack: { row: state.cursor.row, col: wordBack(line, state.cursor.col) }
196
- };
197
- return plain({ ...state, message: undefined, cursor: targets[action.to] });
198
- }
199
- case "insert": {
200
- if (action.at === "below" || action.at === "above") {
201
- const at = action.at === "below" ? state.cursor.row + 1 : state.cursor.row;
202
- const next = checkpoint(state);
203
- const lines = [...state.lines];
204
- lines.splice(at, 0, "");
205
- return plain({ ...next, lines, dirty: true, mode: "insert", cursor: { row: at, col: 0 } });
206
- }
207
- const col = action.at === "after" ? state.cursor.col + 1 : action.at === "lineStart" ? 0 : action.at === "lineEnd" ? line.length : state.cursor.col;
208
- return plain({ ...state, mode: "insert", message: undefined, cursor: { row: state.cursor.row, col } });
209
- }
210
- case "normal":
211
- return plain({ ...state, mode: "normal", message: undefined });
212
- case "type": {
213
- const next = checkpoint(state);
214
- const lines = [...state.lines];
215
- lines[state.cursor.row] = line.slice(0, state.cursor.col) + action.character + line.slice(state.cursor.col);
216
- return plain({ ...next, lines, dirty: true, cursor: { row: state.cursor.row, col: state.cursor.col + action.character.length } });
217
- }
218
- case "newline": {
219
- const next = checkpoint(state);
220
- const lines = [...state.lines];
221
- lines.splice(state.cursor.row, 1, line.slice(0, state.cursor.col), line.slice(state.cursor.col));
222
- return plain({ ...next, lines, dirty: true, cursor: { row: state.cursor.row + 1, col: 0 } });
223
- }
224
- case "backspace": {
225
- if (state.cursor.col === 0 && state.cursor.row === 0)
226
- return plain(state);
227
- const next = checkpoint(state);
228
- const lines = [...state.lines];
229
- if (state.cursor.col === 0) {
230
- const previous = lines[state.cursor.row - 1];
231
- lines.splice(state.cursor.row - 1, 2, previous + line);
232
- return plain({ ...next, lines, dirty: true, cursor: { row: state.cursor.row - 1, col: previous.length } });
233
- }
234
- lines[state.cursor.row] = line.slice(0, state.cursor.col - 1) + line.slice(state.cursor.col);
235
- return plain({ ...next, lines, dirty: true, cursor: { row: state.cursor.row, col: state.cursor.col - 1 } });
236
- }
237
- case "deleteChar": {
238
- if (line.length === 0)
239
- return plain(state);
240
- const next = checkpoint(state);
241
- const lines = [...state.lines];
242
- lines[state.cursor.row] = line.slice(0, state.cursor.col) + line.slice(state.cursor.col + 1);
243
- return plain({ ...next, lines, dirty: true });
244
- }
245
- case "deleteLine": {
246
- const next = checkpoint(state);
247
- const lines = [...state.lines];
248
- lines.splice(state.cursor.row, 1);
249
- return plain({ ...next, lines: lines.length > 0 ? lines : [""], dirty: true, cursor: { row: state.cursor.row, col: 0 } });
250
- }
251
- case "undo": {
252
- const previous = state.history.at(-1);
253
- if (!previous)
254
- return plain({ ...state, message: "nothing to undo" });
255
- return plain({
256
- ...state,
257
- lines: previous.lines,
258
- cursor: previous.cursor,
259
- history: state.history.slice(0, -1),
260
- future: [...state.future, { lines: [...state.lines], cursor: { ...state.cursor } }],
261
- dirty: true,
262
- message: undefined
263
- });
264
- }
265
- case "redo": {
266
- const next = state.future.at(-1);
267
- if (!next)
268
- return plain({ ...state, message: "nothing to redo" });
269
- return plain({
270
- ...state,
271
- lines: next.lines,
272
- cursor: next.cursor,
273
- future: state.future.slice(0, -1),
274
- history: [...state.history, { lines: [...state.lines], cursor: { ...state.cursor } }],
275
- dirty: true,
276
- message: undefined
277
- });
278
- }
279
- case "save":
280
- return { state: clamp({ ...state, dirty: false, message: `saved ${state.path}`, pending: undefined }), effect: { kind: "save" } };
281
- case "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);
291
- case "search":
292
- return plain({ ...state, search: { query: "", typing: true }, message: undefined });
293
- case "searchType":
294
- return plain({ ...state, search: { ...state.search, query: state.search.query + action.character } });
295
- case "searchBackspace":
296
- return plain({ ...state, search: { ...state.search, query: state.search.query.slice(0, -1) } });
297
- case "searchCommit":
298
- return plain({ ...state, search: { ...state.search, typing: false } });
299
- case "searchNext": {
300
- const query = state.search.query;
301
- if (!query)
302
- return plain({ ...state, search: { ...state.search, typing: false } });
303
- const order = [...state.lines.keys()].map((offset) => (state.cursor.row + offset) % state.lines.length);
304
- for (const [index, row] of order.entries()) {
305
- const from = index === 0 ? state.cursor.col + 1 : 0;
306
- const found = state.lines[row].indexOf(query, from);
307
- if (found >= 0) {
308
- return plain({ ...state, cursor: { row, col: found }, search: { ...state.search, typing: false }, message: undefined });
309
- }
310
- }
311
- return plain({ ...state, search: { ...state.search, typing: false }, message: `no match for "${query}"` });
312
- }
313
- case "pending":
314
- return { state: { ...state, pending: action.key }, effect: undefined };
315
- default:
316
- return plain(state);
317
- }
318
- }
319
- function editorKeyBar(state, columns = Number.POSITIVE_INFINITY) {
320
- if (state.search.typing)
321
- return `/${state.search.query} enter next esc done`;
322
- if (state.mode === "insert")
323
- return "esc normal ^S save ^Q quit";
324
- const hints = [
325
- { text: "i insert" },
326
- { text: "o new line" },
327
- { text: "x del" },
328
- { text: "dd del line" },
329
- { text: "u undo" },
330
- { text: "/ find" },
331
- { text: "^S save", essential: true },
332
- { text: "^Q quit", essential: true },
333
- { text: "esc back", essential: true }
334
- ];
335
- const optional = hints.filter((hint) => !hint.essential);
336
- for (let dropped = 0;dropped <= optional.length; dropped += 1) {
337
- const keep = new Set(optional.slice(0, optional.length - dropped));
338
- const bar = hints.filter((hint) => hint.essential || keep.has(hint)).map((hint) => hint.text).join(" ");
339
- if (bar.length <= columns)
340
- return bar;
341
- }
342
- return "^S save ^Q quit";
343
- }
344
- function editorStatus(state) {
345
- const position = `${state.cursor.row + 1}:${state.cursor.col + 1}`;
346
- return `${state.path}${state.dirty ? " [+]" : ""} ${state.mode} ${position} ${state.lines.length} lines`;
347
- }
348
- function visibleEditorLines(state) {
349
- return state.lines.slice(state.scroll, state.scroll + state.viewportRows).map((text, offset) => ({ number: state.scroll + offset + 1, text }));
350
- }
351
- function composeEditorFrame(state, columns, accent) {
352
- const gutter = String(state.lines.length).length;
353
- const clip = (text) => text.length <= columns ? text : text.slice(0, Math.max(0, columns - 1));
354
- return [
355
- { text: clip(editorStatus(state)), bold: true, ...accent ? { color: accent } : {} },
356
- ...visibleEditorLines(state).map((entry) => ({
357
- text: clip(`${String(entry.number).padStart(gutter, " ")} ${caretInto(entry.text, entry.number - 1 === state.cursor.row ? state.cursor.col : undefined)}`)
358
- })),
359
- { text: clip(state.message ?? editorKeyBar(state, columns)), dim: true }
360
- ];
361
- }
362
- function caretInto(text, cursorCol) {
363
- if (cursorCol === undefined)
364
- return text;
365
- const padded = text.length > cursorCol ? text : text.padEnd(cursorCol + 1, " ");
366
- return `${padded.slice(0, cursorCol)}█${padded.slice(cursorCol + 1)}`;
367
- }
368
-
369
- // packages/archymedes-cli/src/ui/explain-view.ts
370
- var PATTERNS = {
371
- typescript: {
372
- imports: /^\s*import\b.*from\s+["'](.+)["']/,
373
- exports: /^\s*export\s+(?:default\s+)?(?:const|function|class|interface|type|enum)\s+([\w$]+)/,
374
- functions: /^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*([\w$]+)/,
375
- classes: /^\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+([\w$]+)/
376
- },
377
- javascript: {},
378
- jsx: {},
379
- tsx: {},
380
- python: {
381
- imports: /^\s*(?:import\s+([\w.]+)|from\s+([\w.]+)\s+import\b)/,
382
- functions: /^\s*(?:async\s+)?def\s+([\w]+)/,
383
- classes: /^\s*class\s+([\w]+)/
384
- },
385
- go: {
386
- imports: /^\s*import\s+["'](.+)["']/,
387
- functions: /^\s*func\s+(?:\([^)]*\)\s*)?([\w]+)/
388
- },
389
- rust: {
390
- imports: /^\s*use\s+([\w:]+)/,
391
- functions: /^\s*(?:pub\s+)?(?:async\s+)?fn\s+([\w]+)/,
392
- classes: /^\s*(?:pub\s+)?struct\s+([\w]+)/
393
- },
394
- ruby: {
395
- imports: /^\s*require\s+["'](.+)["']/,
396
- functions: /^\s*def\s+([\w?!]+)/,
397
- classes: /^\s*class\s+([\w:]+)/
398
- }
399
- };
400
- PATTERNS.javascript = PATTERNS.typescript;
401
- PATTERNS.jsx = PATTERNS.typescript;
402
- PATTERNS.tsx = PATTERNS.typescript;
403
- function structuralFacts(content, path) {
404
- const language = languageOf(path);
405
- const rules = PATTERNS[language] ?? {};
406
- const facts = { imports: [], exports: [], functions: [], classes: [] };
407
- const lines = content.split(`
408
- `);
409
- lines.forEach((raw, index) => {
410
- for (const key of Object.keys(rules)) {
411
- const pattern = rules[key];
412
- if (!pattern)
413
- continue;
414
- const match = pattern.exec(raw);
415
- if (!match)
416
- continue;
417
- const name = match.slice(1).find((group) => group !== undefined);
418
- facts[key].push({ line: index + 1, text: name ?? raw.trim() });
419
- }
420
- });
421
- return facts;
422
- }
423
- var MAX_LINE_LENGTH = 120;
424
- var MAGIC_NUMBER = /(?<![\w.])(?!0\b|1\b)\d{2,}(?![\w.])/;
425
- function annotateLines(content) {
426
- const lines = content.split(`
427
- `);
428
- const notes = [];
429
- lines.forEach((raw, index) => {
430
- const line = index + 1;
431
- if (/\b(TODO|FIXME|HACK|XXX)\b/.test(raw)) {
432
- notes.push({ line, note: "unfinished work flagged here", severity: "warn" });
433
- }
434
- if (raw.length > MAX_LINE_LENGTH) {
435
- notes.push({ line, note: `long line (${raw.length} chars)`, severity: "info" });
436
- }
437
- if (MAGIC_NUMBER.test(raw) && !/^\s*(?:\/\/|#|\*)/.test(raw)) {
438
- notes.push({ line, note: "unexplained numeric literal", severity: "info" });
439
- }
440
- });
441
- return notes;
442
- }
443
- var EXPLAIN_TABS = ["facts", "notes", "diff", "ai"];
444
- function initialExplainPanelState() {
445
- return { open: false, tab: "facts", ai: { status: "idle" } };
446
- }
447
- function toggleExplainPanel(state) {
448
- return { ...state, open: !state.open };
449
- }
450
- function cycleExplainTab(state, step = 1) {
451
- const index = EXPLAIN_TABS.indexOf(state.tab);
452
- const next = ((index + step) % EXPLAIN_TABS.length + EXPLAIN_TABS.length) % EXPLAIN_TABS.length;
453
- return { ...state, tab: EXPLAIN_TABS[next] };
454
- }
455
- function withAiExplanation(state, ai) {
456
- return { ...state, ai };
457
- }
458
- var DEFAULT_EXPLAIN_COLORS = { success: "green", warning: "yellow", error: "red" };
459
- var TAB_LABELS = { facts: "Structure", notes: "Notes", diff: "Diff", ai: "AI" };
460
- function tabStrip(active) {
461
- const text = EXPLAIN_TABS.map((tab) => tab === active ? `[${TAB_LABELS[tab]}]` : ` ${TAB_LABELS[tab]} `).join(" ");
462
- return { text, bold: true };
463
- }
464
- function hintRow(active) {
465
- const ask = active === "ai" ? " ? ask" : "";
466
- return { text: `[ ] tabs ^E close${ask}`, dim: true };
467
- }
468
- function wrap(text, width) {
469
- const words = text.split(/\s+/).filter(Boolean);
470
- const rows = [];
471
- let line = "";
472
- for (const word of words) {
473
- const candidate = line ? `${line} ${word}` : word;
474
- if (candidate.length > width && line) {
475
- rows.push(line);
476
- line = word;
477
- } else {
478
- line = candidate;
479
- }
480
- }
481
- if (line)
482
- rows.push(line);
483
- return rows.length > 0 ? rows : [""];
484
- }
485
- function factsRows(facts, width) {
486
- const section = (title, entries) => {
487
- if (entries.length === 0)
488
- return [];
489
- return [
490
- { text: title, bold: true },
491
- ...entries.slice(0, 40).map((entry) => ({ text: ` ${String(entry.line).padStart(4)} ${entry.text}`.slice(0, width) })),
492
- { text: "" }
493
- ];
494
- };
495
- const rows = [
496
- ...section("Imports", facts.imports),
497
- ...section("Exports", facts.exports),
498
- ...section("Classes", facts.classes),
499
- ...section("Functions", facts.functions)
500
- ];
501
- if (rows.length === 0)
502
- return [{ text: "No imports, exports, functions or classes recognised.", dim: true }];
503
- return rows.slice(0, -1);
504
- }
505
- function notesRows(notes, width, colors) {
506
- if (notes.length === 0)
507
- return [{ text: "Nothing flagged.", dim: true }];
508
- return notes.slice(0, 60).map((entry) => ({
509
- text: `${String(entry.line).padStart(4)} ${entry.note}`.slice(0, width),
510
- color: entry.severity === "warn" ? colors.warning : undefined
511
- }));
512
- }
513
- function diffRows(diff, width, colors) {
514
- if (diff.length === 0 || diff.every((line) => line.kind === "context")) {
515
- return [{ text: "No changes yet.", dim: true }];
516
- }
517
- return diff.filter((line) => line.kind !== "context").slice(0, 200).map((line) => ({
518
- text: `${line.kind === "add" ? "+" : "-"} ${line.text}`.slice(0, width),
519
- color: line.kind === "add" ? colors.success : colors.error
520
- }));
521
- }
522
- function aiRows(ai, width, colors) {
523
- switch (ai.status) {
524
- case "idle":
525
- return [{ text: "Press ? to ask the model to explain this file.", dim: true }];
526
- case "loading":
527
- return [{ text: "Thinking…", dim: true }];
528
- case "error":
529
- return [{ text: `Could not get an explanation: ${ai.message}`, color: colors.error }];
530
- case "ready":
531
- return ai.text.split(`
532
- `).flatMap((line) => wrap(line, width)).map((text) => ({ text }));
533
- }
534
- }
535
- function composeExplainPanel(input, width, height, colors = DEFAULT_EXPLAIN_COLORS) {
536
- const body = (() => {
537
- switch (input.panel.tab) {
538
- case "facts":
539
- return factsRows(structuralFacts(input.after, input.path), width);
540
- case "notes":
541
- return notesRows(annotateLines(input.after), width, colors);
542
- case "diff":
543
- return diffRows(diffLines(input.before, input.after), width, colors);
544
- case "ai":
545
- return aiRows(input.panel.ai, width, colors);
546
- }
547
- })();
548
- const rows = [tabStrip(input.panel.tab), hintRow(input.panel.tab), { text: "" }, ...body];
549
- const padded = rows.length >= height ? rows.slice(0, height) : [...rows, ...Array.from({ length: height - rows.length }, () => ({ text: "" }))];
550
- return padded;
551
- }
552
-
553
- // packages/archymedes-cli/src/ui/editor-screen.tsx
554
- import { jsxDEV } from "@termuijs/jsx/jsx-dev-runtime";
555
- var Panel = Box;
556
- var Line = Text;
557
- var CHROME_ROWS = 2;
558
- var MIN_CODE_COLUMNS = 30;
559
- var MAX_PANEL_COLUMNS = 52;
560
- function panelWidthFor(columns) {
561
- if (columns - MIN_CODE_COLUMNS < 20)
562
- return 0;
563
- return Math.min(MAX_PANEL_COLUMNS, Math.floor(columns * 0.38), columns - MIN_CODE_COLUMNS);
564
- }
565
- function EditorScreen({ columns, rows, path, content, onExit, palette = NO_COLOR_PALETTE, explain, autosave, autosaveDelayMs = 1000 }) {
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]);
581
- const [panel, setPanel] = useState(() => initialExplainPanelState());
582
- const panelWidth = panel.open ? panelWidthFor(columns) : 0;
583
- const codeWidth = panelWidth > 0 ? columns - panelWidth - 1 : columns;
584
- const askAi = (currentContent) => {
585
- if (!explain) {
586
- setPanel((current) => withAiExplanation(current, { status: "error", message: "no model available in this session" }));
587
- return;
588
- }
589
- setPanel((current) => withAiExplanation(current, { status: "loading" }));
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) })));
591
- };
592
- useInput((input, key) => {
593
- if (key?.ctrl && key?.key === "e") {
594
- setPanel((current) => toggleExplainPanel(current));
595
- return;
596
- }
597
- if (panel.open && state.mode === "normal" && !state.search.typing) {
598
- if (input === "]") {
599
- setPanel((current) => cycleExplainTab(current, 1));
600
- return;
601
- }
602
- if (input === "[") {
603
- setPanel((current) => cycleExplainTab(current, -1));
604
- return;
605
- }
606
- if (input === "?" && panel.tab === "ai" && panel.ai.status !== "loading") {
607
- askAi(editorContent(state));
608
- return;
609
- }
610
- }
611
- setState((current) => {
612
- const action = keyToEditorAction({ name: key?.key, ctrl: key?.ctrl, shift: key?.shift }, input, current);
613
- const { state: next, effect } = applyEditorAction(current, action);
614
- if (effect?.kind === "save")
615
- onExit(editorContent(next));
616
- else if (effect?.kind === "quit")
617
- onExit(undefined);
618
- return next;
619
- });
620
- });
621
- const frame = composeEditorFrame(state, codeWidth, palette.tokens.accent);
622
- const lastRow = frame.length - 1;
623
- if (!panel.open && lastRow >= 0 && frame[lastRow].text.length + " ^E explain".length <= codeWidth) {
624
- frame[lastRow] = { ...frame[lastRow], text: `${frame[lastRow].text} ^E explain` };
625
- }
626
- const panelRows = panelWidth > 0 ? composeExplainPanel({ path, before: content, after: editorContent(state), panel }, panelWidth - 2, rows, palette.tokens) : [];
627
- return /* @__PURE__ */ jsxDEV(Panel, {
628
- flexDirection: "row",
629
- width: columns,
630
- height: rows,
631
- children: [
632
- /* @__PURE__ */ jsxDEV(Panel, {
633
- flexDirection: "column",
634
- width: codeWidth,
635
- height: rows,
636
- children: frame.map((line, index) => /* @__PURE__ */ jsxDEV(Line, {
637
- bold: line.bold,
638
- dimColor: line.dim,
639
- color: line.color,
640
- children: line.text
641
- }, `row-${index}`, false, undefined, this))
642
- }, undefined, false, undefined, this),
643
- panelWidth > 0 && /* @__PURE__ */ jsxDEV(Panel, {
644
- flexDirection: "column",
645
- width: panelWidth,
646
- height: rows,
647
- children: panelRows.map((line, index) => /* @__PURE__ */ jsxDEV(Line, {
648
- bold: line.bold,
649
- dimColor: line.dim,
650
- color: line.color,
651
- children: line.text
652
- }, `explain-${index}`, false, undefined, this))
653
- }, undefined, false, undefined, this)
654
- ]
655
- }, undefined, true, undefined, this);
656
- }
657
- async function runEditorScreen(options) {
658
- const { renderApp } = await import("@termuijs/jsx");
659
- return new Promise((resolve) => {
660
- let settled = false;
661
- const finish = (saved) => {
662
- if (settled)
663
- return;
664
- settled = true;
665
- exitTopTermUIApp();
666
- resolve(saved);
667
- };
668
- renderApp(EditorScreen, {
669
- columns: options.columns,
670
- rows: options.rows,
671
- path: options.path,
672
- content: options.content,
673
- palette: options.palette,
674
- explain: options.explain,
675
- autosave: options.autosave,
676
- onExit: finish,
677
- fullscreen: true
678
- }).catch(() => finish(undefined));
679
- });
680
- }
681
- export {
682
- runEditorScreen,
683
- EditorScreen
684
- };