archymedes-cli 2.1.0 → 2.3.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,13 +1,15 @@
1
1
  import {
2
- ASCII_GLYPHS,
3
- UNICODE_GLYPHS,
4
2
  padToWidth,
5
3
  progressBar,
6
- sliceToWidth,
4
+ sliceToWidth
5
+ } from "./archymedes-g8y3rt6t.mjs";
6
+ import {
7
+ ASCII_GLYPHS,
8
+ UNICODE_GLYPHS,
7
9
  visibleWidth
8
- } from "./archymedes-dwvn4c51.mjs";
10
+ } from "./archymedes-a10k8yrk.mjs";
9
11
 
10
- // packages/archymedes-cli/src/canvas.ts
12
+ // src/render/canvas.ts
11
13
  var BRAILLE_DOTS = [
12
14
  [1, 2, 4, 64],
13
15
  [8, 16, 32, 128]
@@ -93,7 +95,7 @@ function seriesBounds(values) {
93
95
  return { low, high, span: high - low === 0 ? 1 : high - low };
94
96
  }
95
97
 
96
- // packages/archymedes-cli/src/charts.ts
98
+ // src/render/charts.ts
97
99
  function barChart(data, options) {
98
100
  if (data.length === 0)
99
101
  return [];
@@ -251,4 +253,4 @@ function heatStrip(data, options) {
251
253
  });
252
254
  }
253
255
 
254
- export { BrailleCanvas, dotRowFor, seriesBounds, barChart, lineChart, plotSeries, scatterChart, waveLine, StreamSeries, heatStrip };
256
+ export { barChart, lineChart, plotSeries, scatterChart, waveLine, StreamSeries, heatStrip };
@@ -1,15 +1,20 @@
1
+ import {
2
+ diffLines,
3
+ languageOf
4
+ } from "./archymedes-dz59nvw7.mjs";
5
+ import"./archymedes-tsne2m7p.mjs";
1
6
  import {
2
7
  NO_COLOR_PALETTE
3
- } from "./archymedes-jecyxv5c.mjs";
8
+ } from "./archymedes-a10k8yrk.mjs";
4
9
  import {
5
10
  __require
6
11
  } from "./archymedes-x67v3n58.mjs";
7
12
 
8
- // packages/archymedes-cli/src/editor-screen.tsx
13
+ // src/ui/editor-screen.tsx
9
14
  import { useInput, useState } from "@termuijs/jsx";
10
15
  import { Box, Text } from "@termuijs/widgets";
11
16
 
12
- // packages/archymedes-cli/src/editor.ts
17
+ // src/ui/editor.ts
13
18
  function initialEditorState(path, content, viewportRows = 20) {
14
19
  const lines = content.split(`
15
20
  `);
@@ -342,14 +347,234 @@ function caretInto(text, cursorCol) {
342
347
  return `${padded.slice(0, cursorCol)}█${padded.slice(cursorCol + 1)}`;
343
348
  }
344
349
 
345
- // packages/archymedes-cli/src/editor-screen.tsx
350
+ // src/ui/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 DEFAULT_EXPLAIN_COLORS = { success: "green", warning: "yellow", error: "red" };
440
+ var TAB_LABELS = { facts: "Structure", notes: "Notes", diff: "Diff", ai: "AI" };
441
+ function tabStrip(active) {
442
+ const text = EXPLAIN_TABS.map((tab) => tab === active ? `[${TAB_LABELS[tab]}]` : ` ${TAB_LABELS[tab]} `).join(" ");
443
+ return { text, bold: true };
444
+ }
445
+ function hintRow(active) {
446
+ const ask = active === "ai" ? " ? ask" : "";
447
+ return { text: `[ ] tabs ^E close${ask}`, dim: true };
448
+ }
449
+ function wrap(text, width) {
450
+ const words = text.split(/\s+/).filter(Boolean);
451
+ const rows = [];
452
+ let line = "";
453
+ for (const word of words) {
454
+ const candidate = line ? `${line} ${word}` : word;
455
+ if (candidate.length > width && line) {
456
+ rows.push(line);
457
+ line = word;
458
+ } else {
459
+ line = candidate;
460
+ }
461
+ }
462
+ if (line)
463
+ rows.push(line);
464
+ return rows.length > 0 ? rows : [""];
465
+ }
466
+ function factsRows(facts, width) {
467
+ const section = (title, entries) => {
468
+ if (entries.length === 0)
469
+ return [];
470
+ return [
471
+ { text: title, bold: true },
472
+ ...entries.slice(0, 40).map((entry) => ({ text: ` ${String(entry.line).padStart(4)} ${entry.text}`.slice(0, width) })),
473
+ { text: "" }
474
+ ];
475
+ };
476
+ const rows = [
477
+ ...section("Imports", facts.imports),
478
+ ...section("Exports", facts.exports),
479
+ ...section("Classes", facts.classes),
480
+ ...section("Functions", facts.functions)
481
+ ];
482
+ if (rows.length === 0)
483
+ return [{ text: "No imports, exports, functions or classes recognised.", dim: true }];
484
+ return rows.slice(0, -1);
485
+ }
486
+ function notesRows(notes, width, colors) {
487
+ if (notes.length === 0)
488
+ return [{ text: "Nothing flagged.", dim: true }];
489
+ return notes.slice(0, 60).map((entry) => ({
490
+ text: `${String(entry.line).padStart(4)} ${entry.note}`.slice(0, width),
491
+ color: entry.severity === "warn" ? colors.warning : undefined
492
+ }));
493
+ }
494
+ function diffRows(diff, width, colors) {
495
+ if (diff.length === 0 || diff.every((line) => line.kind === "context")) {
496
+ return [{ text: "No changes yet.", dim: true }];
497
+ }
498
+ return diff.filter((line) => line.kind !== "context").slice(0, 200).map((line) => ({
499
+ text: `${line.kind === "add" ? "+" : "-"} ${line.text}`.slice(0, width),
500
+ color: line.kind === "add" ? colors.success : colors.error
501
+ }));
502
+ }
503
+ function aiRows(ai, width, colors) {
504
+ switch (ai.status) {
505
+ case "idle":
506
+ return [{ text: "Press ? to ask the model to explain this file.", dim: true }];
507
+ case "loading":
508
+ return [{ text: "Thinking…", dim: true }];
509
+ case "error":
510
+ return [{ text: `Could not get an explanation: ${ai.message}`, color: colors.error }];
511
+ case "ready":
512
+ return ai.text.split(`
513
+ `).flatMap((line) => wrap(line, width)).map((text) => ({ text }));
514
+ }
515
+ }
516
+ function composeExplainPanel(input, width, height, colors = DEFAULT_EXPLAIN_COLORS) {
517
+ const body = (() => {
518
+ switch (input.panel.tab) {
519
+ case "facts":
520
+ return factsRows(structuralFacts(input.after, input.path), width);
521
+ case "notes":
522
+ return notesRows(annotateLines(input.after), width, colors);
523
+ case "diff":
524
+ return diffRows(diffLines(input.before, input.after), width, colors);
525
+ case "ai":
526
+ return aiRows(input.panel.ai, width, colors);
527
+ }
528
+ })();
529
+ const rows = [tabStrip(input.panel.tab), hintRow(input.panel.tab), { text: "" }, ...body];
530
+ const padded = rows.length >= height ? rows.slice(0, height) : [...rows, ...Array.from({ length: height - rows.length }, () => ({ text: "" }))];
531
+ return padded;
532
+ }
533
+
534
+ // src/ui/editor-screen.tsx
346
535
  import { jsxDEV } from "@termuijs/jsx/jsx-dev-runtime";
347
536
  var Panel = Box;
348
537
  var Line = Text;
349
538
  var CHROME_ROWS = 2;
350
- function EditorScreen({ columns, rows, path, content, onExit, palette = NO_COLOR_PALETTE }) {
539
+ var MIN_CODE_COLUMNS = 30;
540
+ var MAX_PANEL_COLUMNS = 52;
541
+ function panelWidthFor(columns) {
542
+ if (columns - MIN_CODE_COLUMNS < 20)
543
+ return 0;
544
+ return Math.min(MAX_PANEL_COLUMNS, Math.floor(columns * 0.38), columns - MIN_CODE_COLUMNS);
545
+ }
546
+ function EditorScreen({ columns, rows, path, content, onExit, palette = NO_COLOR_PALETTE, explain }) {
351
547
  const [state, setState] = useState(() => initialEditorState(path, content, Math.max(1, rows - CHROME_ROWS)));
548
+ const [panel, setPanel] = useState(() => initialExplainPanelState());
549
+ const panelWidth = panel.open ? panelWidthFor(columns) : 0;
550
+ const codeWidth = panelWidth > 0 ? columns - panelWidth - 1 : columns;
551
+ const askAi = (currentContent) => {
552
+ if (!explain) {
553
+ setPanel((current) => withAiExplanation(current, { status: "error", message: "no model available in this session" }));
554
+ return;
555
+ }
556
+ setPanel((current) => withAiExplanation(current, { status: "loading" }));
557
+ 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) })));
558
+ };
352
559
  useInput((input, key) => {
560
+ if (key?.ctrl && key?.key === "e") {
561
+ setPanel((current) => toggleExplainPanel(current));
562
+ return;
563
+ }
564
+ if (panel.open && state.mode === "normal" && !state.search.typing) {
565
+ if (input === "]") {
566
+ setPanel((current) => cycleExplainTab(current, 1));
567
+ return;
568
+ }
569
+ if (input === "[") {
570
+ setPanel((current) => cycleExplainTab(current, -1));
571
+ return;
572
+ }
573
+ if (input === "?" && panel.tab === "ai" && panel.ai.status !== "loading") {
574
+ askAi(editorContent(state));
575
+ return;
576
+ }
577
+ }
353
578
  setState((current) => {
354
579
  const action = keyToEditorAction({ name: key?.key, ctrl: key?.ctrl, shift: key?.shift }, input, current);
355
580
  const { state: next, effect } = applyEditorAction(current, action);
@@ -360,18 +585,41 @@ function EditorScreen({ columns, rows, path, content, onExit, palette = NO_COLOR
360
585
  return next;
361
586
  });
362
587
  });
363
- const frame = composeEditorFrame(state, columns, palette.accent);
588
+ const frame = composeEditorFrame(state, codeWidth, palette.tokens.accent);
589
+ const lastRow = frame.length - 1;
590
+ if (!panel.open && lastRow >= 0 && frame[lastRow].text.length + " ^E explain".length <= codeWidth) {
591
+ frame[lastRow] = { ...frame[lastRow], text: `${frame[lastRow].text} ^E explain` };
592
+ }
593
+ const panelRows = panelWidth > 0 ? composeExplainPanel({ path, before: content, after: editorContent(state), panel }, panelWidth - 2, rows, palette.tokens) : [];
364
594
  return /* @__PURE__ */ jsxDEV(Panel, {
365
- flexDirection: "column",
595
+ flexDirection: "row",
366
596
  width: columns,
367
597
  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);
598
+ children: [
599
+ /* @__PURE__ */ jsxDEV(Panel, {
600
+ flexDirection: "column",
601
+ width: codeWidth,
602
+ height: rows,
603
+ children: frame.map((line, index) => /* @__PURE__ */ jsxDEV(Line, {
604
+ bold: line.bold,
605
+ dimColor: line.dim,
606
+ color: line.color,
607
+ children: line.text
608
+ }, `row-${index}`, false, undefined, this))
609
+ }, undefined, false, undefined, this),
610
+ panelWidth > 0 && /* @__PURE__ */ jsxDEV(Panel, {
611
+ flexDirection: "column",
612
+ width: panelWidth,
613
+ height: rows,
614
+ children: panelRows.map((line, index) => /* @__PURE__ */ jsxDEV(Line, {
615
+ bold: line.bold,
616
+ dimColor: line.dim,
617
+ color: line.color,
618
+ children: line.text
619
+ }, `explain-${index}`, false, undefined, this))
620
+ }, undefined, false, undefined, this)
621
+ ]
622
+ }, undefined, true, undefined, this);
375
623
  }
376
624
  async function runEditorScreen(options) {
377
625
  const { renderApp } = await import("@termuijs/jsx");
@@ -389,6 +637,7 @@ async function runEditorScreen(options) {
389
637
  path: options.path,
390
638
  content: options.content,
391
639
  palette: options.palette,
640
+ explain: options.explain,
392
641
  onExit: finish,
393
642
  fullscreen: true
394
643
  }).catch(() => finish(undefined));