archymedes-cli 2.0.0 → 2.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.
@@ -0,0 +1,284 @@
1
+ import {
2
+ CYAN,
3
+ DIM,
4
+ GREEN,
5
+ GUTTER,
6
+ MAGENTA,
7
+ RED,
8
+ clip,
9
+ paint
10
+ } from "./archymedes-cdgejs3p.mjs";
11
+ import {
12
+ UNICODE_GLYPHS
13
+ } from "./archymedes-5zn4zp84.mjs";
14
+
15
+ // packages/archymedes-cli/src/code-view.ts
16
+ var MAX_DIFF_CELLS = 250000;
17
+ function diffLines(before, after) {
18
+ const left = before === "" ? [] : before.split(`
19
+ `);
20
+ const right = after === "" ? [] : after.split(`
21
+ `);
22
+ if (left.length * right.length > MAX_DIFF_CELLS) {
23
+ return [
24
+ ...left.map((text) => ({ kind: "remove", text })),
25
+ ...right.map((text) => ({ kind: "add", text }))
26
+ ];
27
+ }
28
+ const lengths = Array.from({ length: left.length + 1 }, () => new Array(right.length + 1).fill(0));
29
+ for (let i2 = left.length - 1;i2 >= 0; i2 -= 1) {
30
+ for (let j2 = right.length - 1;j2 >= 0; j2 -= 1) {
31
+ lengths[i2][j2] = left[i2] === right[j2] ? lengths[i2 + 1][j2 + 1] + 1 : Math.max(lengths[i2 + 1][j2], lengths[i2][j2 + 1]);
32
+ }
33
+ }
34
+ const result = [];
35
+ let i = 0;
36
+ let j = 0;
37
+ while (i < left.length && j < right.length) {
38
+ if (left[i] === right[j]) {
39
+ result.push({ kind: "context", text: left[i] });
40
+ i += 1;
41
+ j += 1;
42
+ } else if (lengths[i + 1][j] >= lengths[i][j + 1]) {
43
+ result.push({ kind: "remove", text: left[i] });
44
+ i += 1;
45
+ } else {
46
+ result.push({ kind: "add", text: right[j] });
47
+ j += 1;
48
+ }
49
+ }
50
+ while (i < left.length) {
51
+ result.push({ kind: "remove", text: left[i] });
52
+ i += 1;
53
+ }
54
+ while (j < right.length) {
55
+ result.push({ kind: "add", text: right[j] });
56
+ j += 1;
57
+ }
58
+ return result;
59
+ }
60
+ function condenseDiff(diff, context = 2) {
61
+ const keep = new Array(diff.length).fill(false);
62
+ diff.forEach((line, index) => {
63
+ if (line.kind === "context")
64
+ return;
65
+ for (let offset = -context;offset <= context; offset += 1) {
66
+ const target = index + offset;
67
+ if (target >= 0 && target < diff.length)
68
+ keep[target] = true;
69
+ }
70
+ });
71
+ const result = [];
72
+ let skipped = 0;
73
+ diff.forEach((line, index) => {
74
+ if (keep[index]) {
75
+ if (skipped > 0) {
76
+ result.push({ kind: "gap", text: `${skipped} unchanged line${skipped === 1 ? "" : "s"}` });
77
+ skipped = 0;
78
+ }
79
+ result.push(line);
80
+ return;
81
+ }
82
+ skipped += 1;
83
+ });
84
+ if (skipped > 0)
85
+ result.push({ kind: "gap", text: `${skipped} unchanged line${skipped === 1 ? "" : "s"}` });
86
+ return result;
87
+ }
88
+ function languageOf(filePath) {
89
+ const name = filePath.split(/[\\/]/).pop() ?? "";
90
+ const extension = name.includes(".") ? name.split(".").pop().toLowerCase() : "";
91
+ const byExtension = {
92
+ ts: "typescript",
93
+ tsx: "tsx",
94
+ js: "javascript",
95
+ jsx: "jsx",
96
+ mjs: "javascript",
97
+ cjs: "javascript",
98
+ py: "python",
99
+ rb: "ruby",
100
+ go: "go",
101
+ rs: "rust",
102
+ java: "java",
103
+ kt: "kotlin",
104
+ swift: "swift",
105
+ c: "c",
106
+ h: "c",
107
+ cc: "cpp",
108
+ cpp: "cpp",
109
+ hpp: "cpp",
110
+ cs: "csharp",
111
+ php: "php",
112
+ sh: "shell",
113
+ bash: "shell",
114
+ zsh: "shell",
115
+ fish: "shell",
116
+ json: "json",
117
+ yml: "yaml",
118
+ yaml: "yaml",
119
+ toml: "toml",
120
+ ini: "ini",
121
+ xml: "xml",
122
+ html: "html",
123
+ css: "css",
124
+ scss: "scss",
125
+ sql: "sql",
126
+ md: "markdown",
127
+ mdx: "markdown",
128
+ txt: "text"
129
+ };
130
+ if (byExtension[extension])
131
+ return byExtension[extension];
132
+ if (/^(?:dockerfile|makefile|justfile)$/i.test(name))
133
+ return name.toLowerCase();
134
+ return extension || "text";
135
+ }
136
+ var KEYWORDS = new Set([
137
+ "async",
138
+ "await",
139
+ "break",
140
+ "case",
141
+ "catch",
142
+ "class",
143
+ "const",
144
+ "continue",
145
+ "def",
146
+ "default",
147
+ "elif",
148
+ "else",
149
+ "end",
150
+ "enum",
151
+ "export",
152
+ "extends",
153
+ "finally",
154
+ "fn",
155
+ "for",
156
+ "from",
157
+ "func",
158
+ "function",
159
+ "if",
160
+ "impl",
161
+ "import",
162
+ "in",
163
+ "interface",
164
+ "let",
165
+ "match",
166
+ "mod",
167
+ "new",
168
+ "package",
169
+ "private",
170
+ "public",
171
+ "pub",
172
+ "raise",
173
+ "return",
174
+ "static",
175
+ "struct",
176
+ "switch",
177
+ "throw",
178
+ "trait",
179
+ "try",
180
+ "type",
181
+ "use",
182
+ "var",
183
+ "void",
184
+ "while",
185
+ "with",
186
+ "yield"
187
+ ]);
188
+ var LITERALS = new Set(["true", "false", "null", "nil", "None", "True", "False", "undefined", "self", "this"]);
189
+ function highlightCode(line, depth) {
190
+ if (depth === "none")
191
+ return line;
192
+ const commentAt = findCommentStart(line);
193
+ const code = commentAt === -1 ? line : line.slice(0, commentAt);
194
+ const comment = commentAt === -1 ? "" : paint(line.slice(commentAt), DIM, depth);
195
+ const painted = code.replace(/("(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`)|\b(\d+(?:\.\d+)?)\b|\b([A-Za-z_$][\w$]*)\b/g, (whole, text, numeric, word) => {
196
+ if (text !== undefined)
197
+ return paint(text, GREEN, depth);
198
+ if (numeric !== undefined)
199
+ return paint(numeric, MAGENTA, depth);
200
+ if (word !== undefined && KEYWORDS.has(word))
201
+ return paint(word, CYAN, depth);
202
+ if (word !== undefined && LITERALS.has(word))
203
+ return paint(word, MAGENTA, depth);
204
+ return whole;
205
+ });
206
+ return `${painted}${comment}`;
207
+ }
208
+ function findCommentStart(line) {
209
+ let quote;
210
+ for (let index = 0;index < line.length; index += 1) {
211
+ const character = line[index];
212
+ if (quote) {
213
+ if (character === "\\") {
214
+ index += 1;
215
+ continue;
216
+ }
217
+ if (character === quote)
218
+ quote = undefined;
219
+ continue;
220
+ }
221
+ if (character === '"' || character === "'" || character === "`") {
222
+ quote = character;
223
+ continue;
224
+ }
225
+ if (character === "#")
226
+ return index;
227
+ if (character === "/" && line[index + 1] === "/")
228
+ return index;
229
+ if (character === "-" && line[index + 1] === "-")
230
+ return index;
231
+ }
232
+ return -1;
233
+ }
234
+ function renderCode(content, style, options = {}) {
235
+ const glyphs = style.glyphs ?? UNICODE_GLYPHS;
236
+ const lines = content.replace(/\n$/, "").split(`
237
+ `);
238
+ const maxLines = options.maxLines ?? 16;
239
+ const start = options.startLine ?? 1;
240
+ const numberWidth = String(start + lines.length - 1).length;
241
+ const bodyWidth = Math.max(8, style.width - GUTTER.length - numberWidth - 4);
242
+ const render = (from, to) => lines.slice(from, to).map((line, index) => {
243
+ const number = paint(String(start + from + index).padStart(numberWidth), DIM, style.depth);
244
+ const painted = options.highlight === false ? line : highlightCode(line, style.depth);
245
+ return `${number} ${clip(painted, bodyWidth, glyphs)}`;
246
+ });
247
+ const hidden = Math.max(0, lines.length - maxLines);
248
+ const shown = render(0, hidden > 0 ? maxLines : lines.length);
249
+ return {
250
+ text: shown.join(`
251
+ `),
252
+ hidden,
253
+ full: render(0, lines.length).join(`
254
+ `)
255
+ };
256
+ }
257
+ function renderDiff(diff, style, options = {}) {
258
+ const glyphs = style.glyphs ?? UNICODE_GLYPHS;
259
+ const maxLines = options.maxLines ?? 16;
260
+ const condensed = condenseDiff(diff);
261
+ const bodyWidth = Math.max(8, style.width - GUTTER.length - 4);
262
+ const row = (line) => {
263
+ if (line.kind === "gap")
264
+ return paint(`${glyphs.middot.repeat(3)} ${line.text}`, DIM, style.depth);
265
+ const mark = line.kind === "add" ? glyphs.plus : line.kind === "remove" ? glyphs.minus : " ";
266
+ const code = line.kind === "add" ? GREEN : line.kind === "remove" ? RED : DIM;
267
+ const body = line.kind === "context" ? paint(line.text, DIM, style.depth) : highlightCode(line.text, style.depth);
268
+ return `${paint(mark, code, style.depth)} ${clip(body, bodyWidth, glyphs)}`;
269
+ };
270
+ const rows = condensed.map(row);
271
+ const hidden = Math.max(0, rows.length - maxLines);
272
+ return {
273
+ text: (hidden > 0 ? rows.slice(0, maxLines) : rows).join(`
274
+ `),
275
+ hidden,
276
+ full: diff.map(row).join(`
277
+ `)
278
+ };
279
+ }
280
+ function describeChange(stat, depth) {
281
+ return `${paint(`+${stat.added}`, GREEN, depth)} ${paint(`-${stat.removed}`, RED, depth)}`;
282
+ }
283
+
284
+ export { diffLines, languageOf, highlightCode, renderCode, renderDiff, describeChange };
@@ -1,9 +1,11 @@
1
1
  import {
2
2
  SpringAnimator,
3
+ paginator
4
+ } from "./archymedes-r54qthg5.mjs";
5
+ import {
3
6
  UNICODE_GLYPHS,
4
- paginator,
5
7
  visibleWidth
6
- } from "./archymedes-dwvn4c51.mjs";
8
+ } from "./archymedes-5zn4zp84.mjs";
7
9
 
8
10
  // packages/archymedes-cli/src/chooser.ts
9
11
  var ALL_CHROME = { title: true, status: true, pagination: true, help: true };
@@ -129,7 +131,11 @@ function renderChooser(state, items, options) {
129
131
  const shownLabel = clip(item.label, labelBudget);
130
132
  const padded = shownLabel + " ".repeat(Math.max(0, labelBudget - visibleWidth(shownLabel)));
131
133
  const marker = active ? paint.green(glyphs.prompt) : fadingOut ? paint.dim(glyphs.prompt) : " ";
132
- const head = `${marker} ${paint.dim(number)} ${padded} `;
134
+ const focus = Math.max(0, Math.min(1, options.focusProgress ?? 1));
135
+ const graphemes = [...new Intl.Segmenter(undefined, { granularity: "grapheme" }).segment(padded)].map((part) => part.segment);
136
+ const illuminated = Math.round(graphemes.length * focus);
137
+ const label = active ? paint.cyan(graphemes.slice(0, illuminated).join("")) + graphemes.slice(illuminated).join("") : padded;
138
+ const head = `${marker} ${paint.dim(number)} ${label} `;
133
139
  const tail = `${item.hint ? ` ${item.hint}` : ""}${item.description ? ` ${item.description}` : ""}`;
134
140
  lines.push(` ${head}${paint.dim(clip(tail, Math.max(0, room - visibleWidth(padded) - 2)))}`);
135
141
  }
@@ -173,39 +179,45 @@ async function runChooser(keys, items, paint, options) {
173
179
  glide?.stop();
174
180
  glide = undefined;
175
181
  };
176
- for await (const input of keys) {
177
- const current = liveOptions();
178
- const previousSelected = state.selected;
179
- const step = advanceChooser(state, items, input, {
180
- filter: options.filter ?? false,
181
- page: options.page ?? current.height ?? 8,
182
- ...current.height === undefined ? {} : { height: current.height }
183
- });
184
- state = step.state;
185
- if (step.done) {
182
+ try {
183
+ for await (const input of keys) {
184
+ const current = liveOptions();
185
+ const previousSelected = state.selected;
186
+ const step = advanceChooser(state, items, input, {
187
+ filter: options.filter ?? false,
188
+ page: options.page ?? current.height ?? 8,
189
+ ...current.height === undefined ? {} : { height: current.height }
190
+ });
191
+ state = step.state;
192
+ if (step.done) {
193
+ settleGlide();
194
+ if (step.done.index === undefined)
195
+ return;
196
+ return filterItems(items, state.query)[step.done.index]?.value;
197
+ }
186
198
  settleGlide();
187
- if (step.done.index === undefined)
188
- return;
189
- return filterItems(items, state.query)[step.done.index]?.value;
199
+ const isSingleStep = (input.key.name === "up" || input.key.name === "down") && Math.abs(state.selected - previousSelected) === 1;
200
+ const motion = options.motion ?? (process.env.ARCHYMEDES_NO_MOTION !== "1" && process.env.NO_COLOR === undefined && process.env.TERM !== "dumb");
201
+ if (!motion || !isSingleStep || previousSelected === state.selected) {
202
+ paint(renderChooser(state, items, liveOptions()));
203
+ continue;
204
+ }
205
+ paint(renderChooser(state, items, { ...liveOptions(), transitionFrom: previousSelected, focusProgress: 0 }));
206
+ const animator = new SpringAnimator(0, (value) => {
207
+ if (value < 0.95) {
208
+ paint(renderChooser(state, items, { ...liveOptions(), transitionFrom: value < 0.5 ? previousSelected : undefined, focusProgress: value }));
209
+ return;
210
+ }
211
+ animator.stop();
212
+ glide = undefined;
213
+ paint(renderChooser(state, items, liveOptions()));
214
+ }, { intervalMs: 30 });
215
+ glide = animator;
216
+ animator.retarget(1);
190
217
  }
218
+ } finally {
191
219
  settleGlide();
192
- const isSingleStep = (input.key.name === "up" || input.key.name === "down") && Math.abs(state.selected - previousSelected) === 1;
193
- if (!isSingleStep || previousSelected === state.selected) {
194
- paint(renderChooser(state, items, liveOptions()));
195
- continue;
196
- }
197
- paint(renderChooser(state, items, { ...liveOptions(), transitionFrom: previousSelected }));
198
- const animator = new SpringAnimator(0, (value) => {
199
- if (value < 0.6)
200
- return;
201
- animator.stop();
202
- glide = undefined;
203
- paint(renderChooser(state, items, liveOptions()));
204
- }, { intervalMs: 30 });
205
- glide = animator;
206
- animator.retarget(1);
207
220
  }
208
- settleGlide();
209
221
  return;
210
222
  }
211
223
 
@@ -1,6 +1,11 @@
1
+ import {
2
+ diffLines,
3
+ languageOf
4
+ } from "./archymedes-sc3mwttb.mjs";
5
+ import"./archymedes-cdgejs3p.mjs";
1
6
  import {
2
7
  NO_COLOR_PALETTE
3
- } from "./archymedes-jecyxv5c.mjs";
8
+ } from "./archymedes-5zn4zp84.mjs";
4
9
  import {
5
10
  __require
6
11
  } from "./archymedes-x67v3n58.mjs";
@@ -342,14 +347,233 @@ function caretInto(text, cursorCol) {
342
347
  return `${padded.slice(0, cursorCol)}█${padded.slice(cursorCol + 1)}`;
343
348
  }
344
349
 
350
+ // packages/archymedes-cli/src/explain-view.ts
351
+ var PATTERNS = {
352
+ typescript: {
353
+ imports: /^\s*import\b.*from\s+["'](.+)["']/,
354
+ exports: /^\s*export\s+(?:default\s+)?(?:const|function|class|interface|type|enum)\s+([\w$]+)/,
355
+ functions: /^\s*(?:export\s+)?(?:default\s+)?(?:async\s+)?function\s*\*?\s*([\w$]+)/,
356
+ classes: /^\s*(?:export\s+)?(?:default\s+)?(?:abstract\s+)?class\s+([\w$]+)/
357
+ },
358
+ javascript: {},
359
+ jsx: {},
360
+ tsx: {},
361
+ python: {
362
+ imports: /^\s*(?:import\s+([\w.]+)|from\s+([\w.]+)\s+import\b)/,
363
+ functions: /^\s*(?:async\s+)?def\s+([\w]+)/,
364
+ classes: /^\s*class\s+([\w]+)/
365
+ },
366
+ go: {
367
+ imports: /^\s*import\s+["'](.+)["']/,
368
+ functions: /^\s*func\s+(?:\([^)]*\)\s*)?([\w]+)/
369
+ },
370
+ rust: {
371
+ imports: /^\s*use\s+([\w:]+)/,
372
+ functions: /^\s*(?:pub\s+)?(?:async\s+)?fn\s+([\w]+)/,
373
+ classes: /^\s*(?:pub\s+)?struct\s+([\w]+)/
374
+ },
375
+ ruby: {
376
+ imports: /^\s*require\s+["'](.+)["']/,
377
+ functions: /^\s*def\s+([\w?!]+)/,
378
+ classes: /^\s*class\s+([\w:]+)/
379
+ }
380
+ };
381
+ PATTERNS.javascript = PATTERNS.typescript;
382
+ PATTERNS.jsx = PATTERNS.typescript;
383
+ PATTERNS.tsx = PATTERNS.typescript;
384
+ function structuralFacts(content, path) {
385
+ const language = languageOf(path);
386
+ const rules = PATTERNS[language] ?? {};
387
+ const facts = { imports: [], exports: [], functions: [], classes: [] };
388
+ const lines = content.split(`
389
+ `);
390
+ lines.forEach((raw, index) => {
391
+ for (const key of Object.keys(rules)) {
392
+ const pattern = rules[key];
393
+ if (!pattern)
394
+ continue;
395
+ const match = pattern.exec(raw);
396
+ if (!match)
397
+ continue;
398
+ const name = match.slice(1).find((group) => group !== undefined);
399
+ facts[key].push({ line: index + 1, text: name ?? raw.trim() });
400
+ }
401
+ });
402
+ return facts;
403
+ }
404
+ var MAX_LINE_LENGTH = 120;
405
+ var MAGIC_NUMBER = /(?<![\w.])(?!0\b|1\b)\d{2,}(?![\w.])/;
406
+ function annotateLines(content) {
407
+ const lines = content.split(`
408
+ `);
409
+ const notes = [];
410
+ lines.forEach((raw, index) => {
411
+ const line = index + 1;
412
+ if (/\b(TODO|FIXME|HACK|XXX)\b/.test(raw)) {
413
+ notes.push({ line, note: "unfinished work flagged here", severity: "warn" });
414
+ }
415
+ if (raw.length > MAX_LINE_LENGTH) {
416
+ notes.push({ line, note: `long line (${raw.length} chars)`, severity: "info" });
417
+ }
418
+ if (MAGIC_NUMBER.test(raw) && !/^\s*(?:\/\/|#|\*)/.test(raw)) {
419
+ notes.push({ line, note: "unexplained numeric literal", severity: "info" });
420
+ }
421
+ });
422
+ return notes;
423
+ }
424
+ var EXPLAIN_TABS = ["facts", "notes", "diff", "ai"];
425
+ function initialExplainPanelState() {
426
+ return { open: false, tab: "facts", ai: { status: "idle" } };
427
+ }
428
+ function toggleExplainPanel(state) {
429
+ return { ...state, open: !state.open };
430
+ }
431
+ function cycleExplainTab(state, step = 1) {
432
+ const index = EXPLAIN_TABS.indexOf(state.tab);
433
+ const next = ((index + step) % EXPLAIN_TABS.length + EXPLAIN_TABS.length) % EXPLAIN_TABS.length;
434
+ return { ...state, tab: EXPLAIN_TABS[next] };
435
+ }
436
+ function withAiExplanation(state, ai) {
437
+ return { ...state, ai };
438
+ }
439
+ var TAB_LABELS = { facts: "Structure", notes: "Notes", diff: "Diff", ai: "AI" };
440
+ function tabStrip(active) {
441
+ const text = EXPLAIN_TABS.map((tab) => tab === active ? `[${TAB_LABELS[tab]}]` : ` ${TAB_LABELS[tab]} `).join(" ");
442
+ return { text, bold: true };
443
+ }
444
+ function hintRow(active) {
445
+ const ask = active === "ai" ? " ? ask" : "";
446
+ return { text: `[ ] tabs ^E close${ask}`, dim: true };
447
+ }
448
+ function wrap(text, width) {
449
+ const words = text.split(/\s+/).filter(Boolean);
450
+ const rows = [];
451
+ let line = "";
452
+ for (const word of words) {
453
+ const candidate = line ? `${line} ${word}` : word;
454
+ if (candidate.length > width && line) {
455
+ rows.push(line);
456
+ line = word;
457
+ } else {
458
+ line = candidate;
459
+ }
460
+ }
461
+ if (line)
462
+ rows.push(line);
463
+ return rows.length > 0 ? rows : [""];
464
+ }
465
+ function factsRows(facts, width) {
466
+ const section = (title, entries) => {
467
+ if (entries.length === 0)
468
+ return [];
469
+ return [
470
+ { text: title, bold: true },
471
+ ...entries.slice(0, 40).map((entry) => ({ text: ` ${String(entry.line).padStart(4)} ${entry.text}`.slice(0, width) })),
472
+ { text: "" }
473
+ ];
474
+ };
475
+ const rows = [
476
+ ...section("Imports", facts.imports),
477
+ ...section("Exports", facts.exports),
478
+ ...section("Classes", facts.classes),
479
+ ...section("Functions", facts.functions)
480
+ ];
481
+ if (rows.length === 0)
482
+ return [{ text: "No imports, exports, functions or classes recognised.", dim: true }];
483
+ return rows.slice(0, -1);
484
+ }
485
+ function notesRows(notes, width) {
486
+ if (notes.length === 0)
487
+ return [{ text: "Nothing flagged.", dim: true }];
488
+ return notes.slice(0, 60).map((entry) => ({
489
+ text: `${String(entry.line).padStart(4)} ${entry.note}`.slice(0, width),
490
+ color: entry.severity === "warn" ? "yellow" : undefined
491
+ }));
492
+ }
493
+ function diffRows(diff, width) {
494
+ if (diff.length === 0 || diff.every((line) => line.kind === "context")) {
495
+ return [{ text: "No changes yet.", dim: true }];
496
+ }
497
+ return diff.filter((line) => line.kind !== "context").slice(0, 200).map((line) => ({
498
+ text: `${line.kind === "add" ? "+" : "-"} ${line.text}`.slice(0, width),
499
+ color: line.kind === "add" ? "green" : "red"
500
+ }));
501
+ }
502
+ function aiRows(ai, width) {
503
+ switch (ai.status) {
504
+ case "idle":
505
+ return [{ text: "Press ? to ask the model to explain this file.", dim: true }];
506
+ case "loading":
507
+ return [{ text: "Thinking…", dim: true }];
508
+ case "error":
509
+ return [{ text: `Could not get an explanation: ${ai.message}`, color: "red" }];
510
+ case "ready":
511
+ return ai.text.split(`
512
+ `).flatMap((line) => wrap(line, width)).map((text) => ({ text }));
513
+ }
514
+ }
515
+ function composeExplainPanel(input, width, height) {
516
+ const body = (() => {
517
+ switch (input.panel.tab) {
518
+ case "facts":
519
+ return factsRows(structuralFacts(input.after, input.path), width);
520
+ case "notes":
521
+ return notesRows(annotateLines(input.after), width);
522
+ case "diff":
523
+ return diffRows(diffLines(input.before, input.after), width);
524
+ case "ai":
525
+ return aiRows(input.panel.ai, width);
526
+ }
527
+ })();
528
+ const rows = [tabStrip(input.panel.tab), hintRow(input.panel.tab), { text: "" }, ...body];
529
+ const padded = rows.length >= height ? rows.slice(0, height) : [...rows, ...Array.from({ length: height - rows.length }, () => ({ text: "" }))];
530
+ return padded;
531
+ }
532
+
345
533
  // packages/archymedes-cli/src/editor-screen.tsx
346
534
  import { jsxDEV } from "@termuijs/jsx/jsx-dev-runtime";
347
535
  var Panel = Box;
348
536
  var Line = Text;
349
537
  var CHROME_ROWS = 2;
350
- function EditorScreen({ columns, rows, path, content, onExit, palette = NO_COLOR_PALETTE }) {
538
+ var MIN_CODE_COLUMNS = 30;
539
+ var MAX_PANEL_COLUMNS = 52;
540
+ function panelWidthFor(columns) {
541
+ if (columns - MIN_CODE_COLUMNS < 20)
542
+ return 0;
543
+ return Math.min(MAX_PANEL_COLUMNS, Math.floor(columns * 0.38), columns - MIN_CODE_COLUMNS);
544
+ }
545
+ function EditorScreen({ columns, rows, path, content, onExit, palette = NO_COLOR_PALETTE, explain }) {
351
546
  const [state, setState] = useState(() => initialEditorState(path, content, Math.max(1, rows - CHROME_ROWS)));
547
+ const [panel, setPanel] = useState(() => initialExplainPanelState());
548
+ const panelWidth = panel.open ? panelWidthFor(columns) : 0;
549
+ const codeWidth = panelWidth > 0 ? columns - panelWidth - 1 : columns;
550
+ const askAi = (currentContent) => {
551
+ if (!explain) {
552
+ setPanel((current) => withAiExplanation(current, { status: "error", message: "no model available in this session" }));
553
+ return;
554
+ }
555
+ 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) })));
557
+ };
352
558
  useInput((input, key) => {
559
+ if (key?.ctrl && key?.key === "e") {
560
+ setPanel((current) => toggleExplainPanel(current));
561
+ return;
562
+ }
563
+ if (panel.open && state.mode === "normal" && !state.search.typing) {
564
+ if (input === "]") {
565
+ setPanel((current) => cycleExplainTab(current, 1));
566
+ return;
567
+ }
568
+ if (input === "[") {
569
+ setPanel((current) => cycleExplainTab(current, -1));
570
+ return;
571
+ }
572
+ if (input === "?" && panel.tab === "ai" && panel.ai.status !== "loading") {
573
+ askAi(editorContent(state));
574
+ return;
575
+ }
576
+ }
353
577
  setState((current) => {
354
578
  const action = keyToEditorAction({ name: key?.key, ctrl: key?.ctrl, shift: key?.shift }, input, current);
355
579
  const { state: next, effect } = applyEditorAction(current, action);
@@ -360,18 +584,41 @@ function EditorScreen({ columns, rows, path, content, onExit, palette = NO_COLOR
360
584
  return next;
361
585
  });
362
586
  });
363
- const frame = composeEditorFrame(state, columns, palette.accent);
587
+ const frame = composeEditorFrame(state, codeWidth, palette.accent);
588
+ const lastRow = frame.length - 1;
589
+ if (!panel.open && lastRow >= 0 && frame[lastRow].text.length + " ^E explain".length <= codeWidth) {
590
+ frame[lastRow] = { ...frame[lastRow], text: `${frame[lastRow].text} ^E explain` };
591
+ }
592
+ const panelRows = panelWidth > 0 ? composeExplainPanel({ path, before: content, after: editorContent(state), panel }, panelWidth - 2, rows) : [];
364
593
  return /* @__PURE__ */ jsxDEV(Panel, {
365
- flexDirection: "column",
594
+ flexDirection: "row",
366
595
  width: columns,
367
596
  height: rows,
368
- children: frame.map((line, index) => /* @__PURE__ */ jsxDEV(Line, {
369
- bold: line.bold,
370
- dimColor: line.dim,
371
- color: line.color,
372
- children: line.text
373
- }, `row-${index}`, false, undefined, this))
374
- }, undefined, false, undefined, this);
597
+ children: [
598
+ /* @__PURE__ */ jsxDEV(Panel, {
599
+ flexDirection: "column",
600
+ width: codeWidth,
601
+ height: rows,
602
+ children: frame.map((line, index) => /* @__PURE__ */ jsxDEV(Line, {
603
+ bold: line.bold,
604
+ dimColor: line.dim,
605
+ color: line.color,
606
+ children: line.text
607
+ }, `row-${index}`, false, undefined, this))
608
+ }, undefined, false, undefined, this),
609
+ panelWidth > 0 && /* @__PURE__ */ jsxDEV(Panel, {
610
+ flexDirection: "column",
611
+ width: panelWidth,
612
+ height: rows,
613
+ children: panelRows.map((line, index) => /* @__PURE__ */ jsxDEV(Line, {
614
+ bold: line.bold,
615
+ dimColor: line.dim,
616
+ color: line.color,
617
+ children: line.text
618
+ }, `explain-${index}`, false, undefined, this))
619
+ }, undefined, false, undefined, this)
620
+ ]
621
+ }, undefined, true, undefined, this);
375
622
  }
376
623
  async function runEditorScreen(options) {
377
624
  const { renderApp } = await import("@termuijs/jsx");
@@ -389,6 +636,7 @@ async function runEditorScreen(options) {
389
636
  path: options.path,
390
637
  content: options.content,
391
638
  palette: options.palette,
639
+ explain: options.explain,
392
640
  onExit: finish,
393
641
  fullscreen: true
394
642
  }).catch(() => finish(undefined));