@turri-dev/cli 0.1.2-next.48dacfc → 0.1.3-next.04c4f71

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 CHANGED
@@ -212,7 +212,7 @@ It runs the interview skill, asks you about the problem, the stack and the produ
212
212
  model and cost - and the queue behind it;
213
213
  - ask to try it at any time - "let me try it" - and **`release`** puts what is built so far in
214
214
  `dist/<module>` and **`activate`** starts it, installing what it needs - nothing to approve;
215
- - the **side panel** (`ctrl+h`) takes the screen and opens on **regen** (`/regen`) - \*\*\*plain, every item marked planned, working, regenerated, validating, conform or needs you - and `p` turns it to the **pulses** (`/pulses`) - every pulse, running, queued and finished, and what each regenerates; what its agents did is in each pulse's log in `.turri/jobs/`;
215
+ - the **side panel** (`ctrl+h`) takes the screen and opens on **regen** - \*\*\*plain, every item marked planned, working, regenerated, validating, conform or needs you - and `p` turns it to the **pulses** (`/pulses`) - every pulse, running, queued and finished, and what each regenerates; what its agents did is in each pulse's log in `.turri/jobs/`;
216
216
  - **`/halt`** halts a pulse, **`/lineage`** shows every generation of a
217
217
  module, and the **`drift`** tool says what is left to regenerate;
218
218
  - **`/btw <question>`** asks turri something on the side - answered at once, even in the middle of a skill's interview
@@ -11,7 +11,7 @@
11
11
  * message, a deploy - is offered as pi offers it.
12
12
  */
13
13
  /** What a skill says it is about, that turri covers: ***plain specs and their testing scripts. */
14
- const ABOUT_PLAIN = /\*\*\*plain|\\\*\\\*\\\*plain|\.plain\b|\bcodeplain\b|functional specs?\b|testing scripts?\b|run_unittests|run_conformance_tests/i;
14
+ const ABOUT_PLAIN = /\*\*\*plain|\\\*\\\*\\\*plain|\.plain\b|functional specs?\b|testing scripts?\b|run_unittests|run_conformance_tests/i;
15
15
  /** Whether turri has what a pi skill offers: it runs a skill of that name itself, or the skill is about ***plain. */
16
16
  export function supersededByTurri(skill, turriRuns) {
17
17
  return turriRuns.has(skill.name) || ABOUT_PLAIN.test(`${skill.name} ${skill.description}`);
@@ -0,0 +1,82 @@
1
+ import { conformanceOption, overrideConfig, reviewOption, splitModelSpec } from "./config.js";
2
+ /**
3
+ * The command line of turri's commands: their words, and the options that stand for the config's own keys - kept apart
4
+ * from the commands that use them, so a light one (doctor) reads them as a regenerate does without loading it.
5
+ */
6
+ /**
7
+ * A command's arguments: the positional ones, `--flag`s and `--name value` options (`--name=value` too) -
8
+ * `--coding-model` is `--model`. An option it does not take is refused, rather than taking the word after it, and so is an
9
+ * option with no value - the end of the words, or another option where its value should be.
10
+ */
11
+ export function parseArgs(argv, flags, usage, valued = []) {
12
+ const positional = [];
13
+ const options = {};
14
+ const takes = new Set(["--model", ...valued]);
15
+ for (let i = 0; i < argv.length; i += 1) {
16
+ const arg = argv[i];
17
+ const [name, inline] = arg.startsWith("--") && arg.includes("=") ? [arg.slice(0, arg.indexOf("=")), arg.slice(arg.indexOf("=") + 1)] : [arg, undefined];
18
+ const option = name === "--coding-model" ? "--model" : name;
19
+ if (arg === "-h" || arg === "--help") {
20
+ console.log(usage);
21
+ process.exit(0);
22
+ }
23
+ else if (flags.includes(option) && inline === undefined)
24
+ options[option.slice(2)] = "yes";
25
+ else if (takes.has(option)) {
26
+ // Its value is the next word - never another option, which would be taken as the value and lost.
27
+ const value = inline ?? argv[i + 1];
28
+ if (value === undefined || (inline === undefined && value.startsWith("--"))) {
29
+ console.error(`${usage}\n\n${name} needs a value${value !== undefined ? `, not the option ${value}` : ""} - ${name}=<value> gives one that starts with --.`);
30
+ process.exit(1);
31
+ }
32
+ if (inline === undefined)
33
+ i += 1;
34
+ options[option.slice(2)] = value;
35
+ }
36
+ else if (arg.startsWith("-")) {
37
+ console.error(`${usage}\n\nNo such option: ${name}.`);
38
+ process.exit(1);
39
+ }
40
+ else
41
+ positional.push(arg);
42
+ }
43
+ return { positional, options };
44
+ }
45
+ /** An option's value, as parseArgs left it: refused when it was given with none. */
46
+ export function optionValue(options, name) {
47
+ const value = options[name];
48
+ if (name in options && (value === undefined || value.trim() === ""))
49
+ failOption(`--${name} needs a value`);
50
+ return value;
51
+ }
52
+ /**
53
+ * The options that stand for the config's own keys - `--harness-model`, `--coding-model` (`--model`), `--judge-model`
54
+ * and, as asked, `--review` and `--conformance-tests` - read as if config.turri.yaml had them, in their place in the
55
+ * order the config resolves them in (overrideConfig): a run, or doctor, with them is the same as one with them in the
56
+ * file. Exits on a value it cannot read.
57
+ */
58
+ export function configKeyOptions(options, take = {}) {
59
+ const tests = take.conformance === true ? optionValue(options, "conformance-tests") : undefined;
60
+ const setting = tests !== undefined ? conformanceOption(tests) : undefined;
61
+ if (tests !== undefined && setting === undefined)
62
+ failOption(`--conformance-tests takes all, chosen, module or false, not ${tests}`);
63
+ const reviewed = take.review === true ? optionValue(options, "review") : undefined;
64
+ const review = reviewed !== undefined ? reviewOption(reviewed) : undefined;
65
+ if (reviewed !== undefined && review === undefined)
66
+ failOption(`--review takes true or false, not ${reviewed}`);
67
+ const model = (name, key) => {
68
+ const spec = optionValue(options, name);
69
+ return spec !== undefined ? { [key]: splitModelSpec(spec) } : {};
70
+ };
71
+ overrideConfig({
72
+ ...model("harness-model", "harnessModel"),
73
+ ...model("model", "codingModel"),
74
+ ...model("judge-model", "judgeModel"),
75
+ ...(setting !== undefined ? { conformanceTests: setting } : {}),
76
+ ...(review !== undefined ? { review } : {}),
77
+ });
78
+ }
79
+ function failOption(message) {
80
+ console.error(message);
81
+ process.exit(1);
82
+ }
package/dist/config.js CHANGED
@@ -49,7 +49,7 @@ import { projectDirOf } from "./regenerate/paths.js";
49
49
  * as `coding-model`, which it was before there were two.
50
50
  *
51
51
  * Keys are written with dashes; underscores are read the same (`conformance_tests`). Keys it
52
- * does not know - a codeplain `config.yaml`, read while it is the old file, carries several - are ignored, and so is a value of
52
+ * does not know - another tool's `config.yaml`, read while it is the old file, may carry several - are ignored, and so is a value of
53
53
  * the wrong type. Everything is optional, and what is given on the command line wins.
54
54
  *
55
55
  * A project whose parts do not share a stack - a Python backend and a React frontend beside each other - keeps a
@@ -385,6 +385,9 @@ function text(value) {
385
385
  * not parse.
386
386
  */
387
387
  export function readProjectConfig(root, stack) {
388
+ return withRunOverrides(readStackedConfig(root, stack));
389
+ }
390
+ function readStackedConfig(root, stack) {
388
391
  const project = readConfigFile(join(root, projectConfigFile(root)));
389
392
  if (stack === undefined)
390
393
  return project;
@@ -403,6 +406,25 @@ export function readProjectConfig(root, stack) {
403
406
  merged.models = joinModels(project.models, own.models);
404
407
  return withoutUndefined(merged);
405
408
  }
409
+ let runOverrides = {};
410
+ /** Read a command's options as the config's keys for the rest of this process. */
411
+ export function overrideConfig(overrides) {
412
+ runOverrides = { ...overrides };
413
+ }
414
+ /** `--conformance-tests <value>`: the config's own values - all (or true), chosen, module, false. Undefined for anything else. */
415
+ export function conformanceOption(value) {
416
+ return conformanceSetting(value);
417
+ }
418
+ /** `--review <value>`: the config's own, true or false. Undefined for anything else. */
419
+ export function reviewOption(value) {
420
+ const word = value.trim().toLowerCase();
421
+ return word === "true" ? true : word === "false" ? false : undefined;
422
+ }
423
+ /** The config with the command's options in place of the keys they name. */
424
+ function withRunOverrides(config) {
425
+ const given = Object.entries(runOverrides).filter(([, value]) => value !== undefined);
426
+ return given.length === 0 ? config : { ...config, ...Object.fromEntries(given) };
427
+ }
406
428
  /** A module's `models:` over the project's, role by role - each entry taking what it leaves out from the project's; the chat's the project's alone. */
407
429
  function joinModels(project, own) {
408
430
  if (own === undefined)
@@ -1,4 +1,5 @@
1
- import { parseArgs, runLoop } from "./loop-cli.js";
1
+ import { parseArgs } from "./cli-options.js";
2
+ import { headlessOptions, runLoop } from "./loop-cli.js";
2
3
  const USAGE = `usage: turri conform <file.plain> [frid ...] [options]
3
4
 
4
5
  Writes and runs the black-box conformance tests of a module's functional specs - those named, or
@@ -8,8 +9,14 @@ module inherits, a failure fixed in the test or the code, whichever is wrong. Ev
8
9
  tested goes in one pulse, as far as the size guardrail lets.
9
10
 
10
11
  --one-by-one a pulse a functional spec instead
11
- --model <provider/id> model for the agents (default: config.turri.yaml's coding-model)`;
12
- const { positional, options } = parseArgs(process.argv.slice(2), ["--one-by-one"], USAGE);
12
+ --harness-model <p/id> config.turri.yaml's harness-model for this run: the planner, the conformance chooser, a diagnosis
13
+ --coding-model <p/id> its coding-model, --model too: the agents that build
14
+ --judge-model <p/id> its judge-model: the judge and the test review
15
+ --review <true|false> its review: whether the code and its tests are reviewed once they pass
16
+ --build-dir <dir> as the run ends, export the code there (see turri regenerate --help)
17
+ --report <file.json> write a report of the run there (see turri regenerate --help)`;
18
+ const { positional, options } = parseArgs(process.argv.slice(2), ["--one-by-one"], USAGE, ["--build-dir", "--report", "--harness-model", "--judge-model", "--review"]);
19
+ const { report, buildDir } = headlessOptions(options, false);
13
20
  const [plainFile, ...frids] = positional;
14
21
  if (plainFile === undefined) {
15
22
  console.error(USAGE);
@@ -17,10 +24,11 @@ if (plainFile === undefined) {
17
24
  }
18
25
  runLoop({
19
26
  plainFile,
20
- ...(options.model !== undefined ? { model: options.model } : {}),
27
+ ...(buildDir !== undefined ? { buildDir } : {}),
28
+ ...(report !== undefined ? { report } : {}),
29
+ command: ["conform", ...process.argv.slice(2)],
21
30
  ask: (loop) => loop.conformance(plainFile, {
22
31
  ...(frids.length > 0 ? { frids } : {}),
23
- ...(options.model !== undefined ? { model: options.model } : {}),
24
32
  ...(options["one-by-one"] !== undefined ? { separately: true } : {}),
25
33
  }),
26
34
  }).catch((error) => {
@@ -68,3 +68,22 @@ export function turriOnPath() {
68
68
  const found = which.status === 0 ? which.stdout.trim().split("\n")[0] : undefined;
69
69
  return found === "" ? undefined : found;
70
70
  }
71
+ /** turri's version, and the commit when it runs from a clone. */
72
+ export function turriIdentity() {
73
+ let version = null;
74
+ try {
75
+ version = JSON.parse(readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8")).version ?? null;
76
+ }
77
+ catch {
78
+ /* no package.json: left out */
79
+ }
80
+ if (!existsSync(join(PACKAGE_ROOT, ".git")))
81
+ return { version, git_commit: null, git_dirty: null };
82
+ const commit = spawnSync("git", ["-C", PACKAGE_ROOT, "rev-parse", "HEAD"], { encoding: "utf8" });
83
+ const status = spawnSync("git", ["-C", PACKAGE_ROOT, "status", "--porcelain", "--untracked-files=no"], { encoding: "utf8" });
84
+ return {
85
+ version,
86
+ git_commit: commit.status === 0 ? commit.stdout.trim() : null,
87
+ git_dirty: status.status === 0 ? status.stdout.trim() !== "" : null,
88
+ };
89
+ }
package/dist/extension.js CHANGED
@@ -5,7 +5,7 @@ import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
5
5
  import { basename, dirname, join, relative, resolve } from "node:path";
6
6
  import { conversationCommand, conversationSettled, conversationWriting } from "./regenerate/conversation-writes.js";
7
7
  import { fileURLToPath } from "node:url";
8
- import { allocateImageId, Box, compositeTuiLine, getCapabilities, getPngDimensions, isKeyRelease, isKeyRepeat, matchesKey, renderImage, Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
8
+ import { allocateImageId, Box, getCapabilities, getPngDimensions, isKeyRelease, isKeyRepeat, matchesKey, renderImage, Text, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
9
9
  import { defineTool } from "@earendil-works/pi-coding-agent";
10
10
  import { Type } from "typebox";
11
11
  import { CODING_MODEL_ENV } from "./chat-models.js";
@@ -31,6 +31,8 @@ import { whyFailed } from "./regenerate/diagnose-job.js";
31
31
  import { createWorkLoop, describeModules, isUpToDate, previewJobs } from "./regenerate/loop.js";
32
32
  import { brandBlue, plainPaint } from "./regenerate/plain-palette.js";
33
33
  import { createJobPanel, didNotPass, keyPressed, PANEL_KEY, spinner } from "./regenerate/job-panel.js";
34
+ import { clickToFocus, dockedWidth, narrowChat, shade, shadeChat } from "./regenerate/panel-dock.js";
35
+ import { overPictureRow, PICTURE_AT } from "./picture-row.js";
34
36
  import { skillAboutLine, skillProgressLine } from "./skill-progress.js";
35
37
  import { createSidePanel } from "./regenerate/side-panel.js";
36
38
  import { createSpecMapSource, defaultMapFiles } from "./regenerate/spec-map.js";
@@ -52,6 +54,7 @@ import { registerStacks } from "./stacks.js";
52
54
  import { catalogueLine, discoverSkills, leftOutNote } from "./skills.js";
53
55
  import { supersededByTurri } from "./chat-skills.js";
54
56
  import { TURRI_IDENTITY } from "./identity.js";
57
+ import { turriIdentity } from "./environment.js";
55
58
  import { conformanceChooser } from "./regenerate/choose-conformance.js";
56
59
  import { answerSideQuestion, askWithAsides, BTW_HOW, runningSkill, skillRunStarted, skillSessionText, transcriptOf } from "./btw.js";
57
60
  import { createJobRegistry, defaultParallelJobs, describeJob, formatDuration, pulseName, shouldWait, STOPPED_ON_REQUEST, } from "./regenerate/jobs.js";
@@ -1467,9 +1470,17 @@ function registerJobTools(pi, jobs, cwd) {
1467
1470
  return { content: [{ type: "text", text }], details: {} };
1468
1471
  },
1469
1472
  });
1470
- // The side panel: the regeneration - and the pulses behind p - over the whole screen. Its key (PANEL_KEY) opens it; the
1471
- // key again, or esc, closes it and goes back to the chat. While it shows, every key is its own.
1473
+ // The side panel: the regeneration - and the pulses behind p - over the whole screen, or beside the chat (s, from one to
1474
+ // the other). Its key (PANEL_KEY) opens it; the key again closes it and goes back to the chat, as esc does over the
1475
+ // screen - beside the chat, esc gives the chat its keys back and the panel stays, and the key gives them to the panel.
1476
+ // While the panel has the keys, every key is its own.
1472
1477
  let panel;
1478
+ // Beside the chat or over it: as the person last left it, for the conversation.
1479
+ let panelDocked = false;
1480
+ // The terminal the panel is drawn on, for its width: set as it opens.
1481
+ let panelTerminal;
1482
+ /** The columns the panel takes beside the chat - 0 over the whole screen, or when the terminal is too narrow to share. */
1483
+ const panelBeside = () => (panelDocked && panelTerminal !== undefined ? dockedWidth(panelTerminal.columns) : 0);
1473
1484
  // What is open in the pulse list and the map, and where their cursors are: the conversation's, as left each time it opens.
1474
1485
  const jobsTree = createJobsTreeState();
1475
1486
  const mapState = createSpecMapState();
@@ -1497,13 +1508,54 @@ function registerJobTools(pi, jobs, cwd) {
1497
1508
  needsLooking.delete(jobs);
1498
1509
  if (panel !== undefined) {
1499
1510
  panel.handle?.setHidden(false);
1511
+ panel.keys = true;
1500
1512
  panel.handle?.focus();
1501
1513
  return;
1502
1514
  }
1503
1515
  // Not awaited: the panel lives on while the jobs it shows run.
1504
1516
  void ctx.ui.custom((tui, theme, _keybindings, done) => {
1505
1517
  const close = () => done(undefined);
1518
+ panelTerminal = tui.terminal;
1506
1519
  let component;
1520
+ // Beside the chat: the chat drawn narrower, on the left, and a click on either side giving it the keys - undone
1521
+ // when the panel goes over the screen, or closes.
1522
+ let undock;
1523
+ const layOut = () => {
1524
+ if (panelDocked && undock === undefined) {
1525
+ const widen = narrowChat(tui, panelBeside);
1526
+ const unwatch = clickToFocus(tui, () => tui.terminal.columns - panelBeside(), (side) => {
1527
+ if (panelBeside() === 0)
1528
+ return;
1529
+ if (side === "chat")
1530
+ toChat();
1531
+ else
1532
+ toPanel();
1533
+ });
1534
+ // The side without the keys in the shade: the chat while the panel has them.
1535
+ const unshade = shadeChat(tui, () => panelBeside() > 0 && entry.keys);
1536
+ undock = () => {
1537
+ unshade();
1538
+ unwatch();
1539
+ widen();
1540
+ };
1541
+ }
1542
+ else if (!panelDocked && undock !== undefined) {
1543
+ undock();
1544
+ undock = undefined;
1545
+ }
1546
+ };
1547
+ // Beside the chat, back to it: the chat has the keys, the panel stays.
1548
+ const toChat = () => {
1549
+ entry.keys = false;
1550
+ entry.handle?.unfocus();
1551
+ tui.requestRender();
1552
+ };
1553
+ const toPanel = () => {
1554
+ entry.keys = true;
1555
+ entry.handle?.focus();
1556
+ tui.requestRender();
1557
+ };
1558
+ const back = () => (panelBeside() > 0 ? toChat() : close());
1507
1559
  // Opened, it reads the files again at once: they may have changed while it was closed.
1508
1560
  specMap.refresh();
1509
1561
  const view = createSidePanel({
@@ -1513,6 +1565,14 @@ function registerJobTools(pi, jobs, cwd) {
1513
1565
  mapState,
1514
1566
  rows: () => tui.terminal.rows,
1515
1567
  key: PANEL_KEY,
1568
+ dock: {
1569
+ docked: () => panelBeside() > 0,
1570
+ toggle: () => {
1571
+ panelDocked = panelBeside() === 0;
1572
+ layOut();
1573
+ tui.requestRender();
1574
+ },
1575
+ },
1516
1576
  style: {
1517
1577
  heading: (text) => theme.bold(theme.fg("accent", text)),
1518
1578
  muted: (text) => theme.fg("muted", text),
@@ -1527,22 +1587,25 @@ function registerJobTools(pi, jobs, cwd) {
1527
1587
  // What conformance tests show does what its spec says: brand blue.
1528
1588
  conforms: brandBlue(theme),
1529
1589
  },
1530
- back: close,
1590
+ back,
1531
1591
  close,
1532
1592
  // A module that needs the person: the chat takes it up with them, in their words - what went wrong, and what now.
1533
1593
  discuss: (page) => {
1534
- close();
1594
+ back();
1535
1595
  const why = page.codebase?.stuck?.why;
1536
1596
  pi.sendUserMessage(`regen says ${page.module} needs me: ${page.status}. What went wrong, and what do we do about it?${why !== undefined ? `\n\n(turri's reason: ${why})` : ""}`, { deliverAs: "followUp" });
1537
1597
  },
1538
1598
  // Something took the keys' focus while the panel showed - a question in the chat - and waits there.
1539
1599
  waiting: () => {
1600
+ if (!entry.keys)
1601
+ return false;
1540
1602
  const focused = tui.getFocusedComponent?.() ?? null;
1541
1603
  return focused !== null && focused !== component;
1542
1604
  },
1543
1605
  });
1544
- const entry = { view, close };
1606
+ const entry = { view, close, keys: true };
1545
1607
  panel = entry;
1608
+ layOut();
1546
1609
  // Every key goes to the panel while it shows, whatever has the focus: a question the chat model asks in the
1547
1610
  // background takes the focus, but no key reaches it - an enter never answers it unseen. It waits in the chat
1548
1611
  // until the panel closes, and has the keys then. Mouse events are the screen's, before this sees them.
@@ -1564,6 +1627,22 @@ function registerJobTools(pi, jobs, cwd) {
1564
1627
  }
1565
1628
  if (!state)
1566
1629
  return undefined;
1630
+ // Beside the chat, ⌥⌘← and ⌥⌘→ go to the side they point at, whichever has the keys - on a terminal that tells
1631
+ // turri of ⌘ (the Kitty keyboard protocol). Its release goes nowhere.
1632
+ if (panelBeside() > 0 && (matchesKey(data, SIDE_KEYS.chat) || matchesKey(data, SIDE_KEYS.panel))) {
1633
+ if (!isKeyRelease(data)) {
1634
+ if (matchesKey(data, SIDE_KEYS.chat))
1635
+ toChat();
1636
+ else
1637
+ toPanel();
1638
+ }
1639
+ return { consume: true };
1640
+ }
1641
+ // Beside the chat, with the chat's keys: they are the chat's - PANEL_KEY among them, for the shortcut that gives
1642
+ // them to the panel. A click on the panel gives it the focus, and the keys with it.
1643
+ if (!entry.keys && entry.handle?.isFocused() !== true)
1644
+ return undefined;
1645
+ entry.keys = true;
1567
1646
  // A terminal on the Kitty keyboard protocol reports a key's release, and listeners are given it: the release of
1568
1647
  // the ctrl+h that opened the panel would close it again at once, and every arrow would move twice. Only
1569
1648
  // presses count - and ctrl+h held down, repeating, does not open and close it over and over.
@@ -1590,7 +1669,13 @@ function registerJobTools(pi, jobs, cwd) {
1590
1669
  }, WATCH_REDRAW_MS);
1591
1670
  timer.unref?.();
1592
1671
  component = {
1593
- render: (width) => view.render(width),
1672
+ // Beside the chat, a rule down its left edge between them - and the panel in the shade while the chat has the keys.
1673
+ render: (width) => {
1674
+ if (panelBeside() === 0)
1675
+ return view.render(width);
1676
+ const lines = view.render(width - 2).map((line) => `${theme.fg("borderMuted", "│")} ${line}`);
1677
+ return entry.keys ? lines : lines.map(shade);
1678
+ },
1594
1679
  // Reached only if the listener above is gone: the same, so the panel never passes a key on.
1595
1680
  handleInput: (data) => {
1596
1681
  if (isKeyRepeat(data) && matchesKey(data, PANEL_KEY))
@@ -1606,6 +1691,7 @@ function registerJobTools(pi, jobs, cwd) {
1606
1691
  dispose: () => {
1607
1692
  unlisten();
1608
1693
  clearInterval(timer);
1694
+ undock?.();
1609
1695
  if (panel === entry)
1610
1696
  panel = undefined;
1611
1697
  },
@@ -1613,34 +1699,36 @@ function registerJobTools(pi, jobs, cwd) {
1613
1699
  return component;
1614
1700
  }, {
1615
1701
  overlay: true,
1616
- // The whole screen: the panel or the chat, one at a time.
1617
- overlayOptions: { width: "100%", maxHeight: "100%", anchor: "top-left", margin: 0 },
1702
+ // The whole screen, or the columns on the right beside the chat - read at each frame, so it follows the terminal.
1703
+ overlayOptions: {
1704
+ get width() {
1705
+ return panelBeside() > 0 ? panelBeside() : "100%";
1706
+ },
1707
+ maxHeight: "100%",
1708
+ anchor: "top-right",
1709
+ margin: 0,
1710
+ },
1618
1711
  onHandle: (handle) => {
1619
1712
  if (panel !== undefined)
1620
1713
  panel.handle = handle;
1621
1714
  },
1622
1715
  });
1623
1716
  };
1624
- // The side panel's two views: /regen opens it on regen - the spec, with how far the code has been regenerated into it
1625
- // - and /pulses on every pulse, regenerating, planned and finished, and what each regenerates. What the agents did is
1626
- // in each pulse's log.
1627
- for (const [name, description, view] of [
1628
- ["regen", `regen, in the side panel: every item of the spec, built, regenerating, planned or waiting. ${PANEL_KEY} opens the panel too.`, "map"],
1629
- ["pulses", `The pulses, in the side panel: every pulse, regenerating, planned and finished, and what each regenerates. ${PANEL_KEY} opens the panel too.`, "pulses"],
1630
- ]) {
1631
- pi.registerCommand(name, {
1632
- description,
1633
- handler: async (_args, ctx) => {
1634
- if (!ctx.hasUI)
1635
- return;
1636
- openPanel(ctx, view);
1637
- },
1638
- });
1639
- }
1717
+ // /pulses opens the side panel on every pulse, regenerating, planned and finished, and what each regenerates; regen -
1718
+ // the spec, with how far the code has been regenerated into it - is PANEL_KEY's, no command of its own beside
1719
+ // /regenerate. What the agents did is in each pulse's log.
1720
+ pi.registerCommand("pulses", {
1721
+ description: `The pulses, in the side panel: every pulse, regenerating, planned and finished, and what each regenerates. ${PANEL_KEY} opens the panel too.`,
1722
+ handler: async (_args, ctx) => {
1723
+ if (!ctx.hasUI)
1724
+ return;
1725
+ openPanel(ctx, "pulses");
1726
+ },
1727
+ });
1640
1728
  panelOpeners.set(jobs, (ctx) => openPanel(ctx));
1641
1729
  questionPeek = {
1642
1730
  open: (ctx) => openPanel(ctx),
1643
- shown: () => panel !== undefined,
1731
+ shown: () => panel !== undefined && panel.keys,
1644
1732
  };
1645
1733
  pi.registerShortcut(PANEL_KEY, {
1646
1734
  description: "Peek at regen and the pulses - a question waiting in the chat waits for you - or go back to it",
@@ -1711,8 +1799,12 @@ export const JOB_FINISHED_REACTION = [
1711
1799
  "A pulse that stopped on its environment - \"stopped: turri's sandbox kept its agents from…\" or \"stopped: the",
1712
1800
  "environment lacks…\" - stops the same way again until that is fixed: never regenerate it before. Heal it, then regenerate the",
1713
1801
  "module to resume:",
1714
- "- turri's sandbox kept the agents from a folder on this machine: nothing is missing. Add the folder its report names to",
1715
- " `toolchains:` in config.turri.yaml - a list, keeping what is there - say so in a line, and resume.",
1802
+ "- turri's sandbox kept the agents from reading a folder on this machine: nothing is missing. Add the folder its report",
1803
+ " names to `toolchains:` in config.turri.yaml - a list, keeping what is there - say so in a line, and resume.",
1804
+ "- turri's sandbox kept the agents from writing a file - a lock a build tool takes, say: no setting lets them, and",
1805
+ " `toolchains:` does not - never add it there, and never resume before the fix. Do what the report says if it is the",
1806
+ " project's to change - a tool told to use `$HOME` in the build's own files - say so, and resume; otherwise tell the",
1807
+ " person, in a line or two, what the report says and that \"resume\" picks it up once it is done.",
1716
1808
  "- the machine lacks a toolchain or a tool - a JDK, Maven, Gradle, Go, uv, Node, a system library: first find out, in your",
1717
1809
  " shell, whether it is installed somewhere off the PATH (`command -v`, Homebrew's opt folders, /Library/Java/",
1718
1810
  " JavaVirtualMachines, a version manager the person uses - mise, asdf, sdkman, nvm). Then ask the person with ask_user",
@@ -1724,6 +1816,8 @@ export const JOB_FINISHED_REACTION = [
1724
1816
  " to do, and that \"resume\" picks it up once it is in.",
1725
1817
  "- a credential `agent-env:` names that is not there: never make one up - tell the person where it is read from.",
1726
1818
  ].join("\n");
1819
+ /** The keys that go to the chat and to the panel, side by side: ⌥⌘← and ⌥⌘→. */
1820
+ const SIDE_KEYS = { chat: "alt+super+left", panel: "alt+super+right" };
1727
1821
  /** Opens the side panel, and goes back to it from the conversation. */
1728
1822
  /** The modules in this project that have a history, by name. */
1729
1823
  function modulesWithHistory(cwd) {
@@ -2158,7 +2252,8 @@ function registerBranding(pi, ruleCount, skills) {
2158
2252
  if (data === undefined)
2159
2253
  return undefined;
2160
2254
  const lines = [
2161
- `${theme.fg("accent", "turri")} ${theme.fg("muted", `v${data.version}`)}`,
2255
+ `${theme.fg("accent", "turri")} ${theme.fg("muted", `v${data.version}`)}` +
2256
+ (data.commit !== undefined ? theme.fg("dim", ` from a clone, ${data.commit}${data.changed ? " with changes" : ""}`) : ""),
2162
2257
  theme.fg("text", "regenerative software - the code is regenerated from the spec"),
2163
2258
  theme.fg("muted", `${data.rules} rules · ${data.skills} skill${data.skills === 1 ? "" : "s"}` +
2164
2259
  (data.projectSkills !== undefined && data.projectSkills > 0 ? ` (${data.projectSkills} the project's)` : "") +
@@ -2180,8 +2275,11 @@ function registerBranding(pi, ruleCount, skills) {
2180
2275
  if (ctx.hasUI)
2181
2276
  animateJellyfish(ctx);
2182
2277
  try {
2278
+ const identity = turriIdentity();
2183
2279
  pi.appendEntry(BANNER_ENTRY, {
2184
- version: packageVersion(),
2280
+ version: identity.version ?? "0.0.0",
2281
+ commit: identity.git_commit?.slice(0, 7),
2282
+ changed: identity.git_dirty === true,
2185
2283
  rules: ruleCount,
2186
2284
  skills: skills.skills.length,
2187
2285
  projectSkills: skills.skills.filter((skill) => skill.origin !== "turri").length,
@@ -2234,8 +2332,6 @@ function animateJellyfish(ctx) {
2234
2332
  }, { placement: "belowEditor" });
2235
2333
  }
2236
2334
  const JELLYFISH_CLOCK_KEY = "turri-jellyfish";
2237
- /** What the banner puts between a row's text and the picture after it: back to the jellyfish's column. */
2238
- const PICTURE_AT = "\x1b[2G";
2239
2335
  /** The columns the banner's picture takes from the left edge, while it shows one; 0 when it does not. */
2240
2336
  let pictureRight = 0;
2241
2337
  /**
@@ -2250,14 +2346,7 @@ function drawOverlaysBesidePicture(tui) {
2250
2346
  return;
2251
2347
  target.turriPicture = true;
2252
2348
  const original = target.compositeLineAt.bind(tui);
2253
- target.compositeLineAt = (base, overlay, col, width, total) => {
2254
- const at = base.lastIndexOf(PICTURE_AT);
2255
- const picture = at === -1 ? "" : base.slice(at + PICTURE_AT.length);
2256
- if (!/^(\x1b\[\d+A)?(\x1b_G|\x1b\]1337;File=)/.test(picture))
2257
- return original(base, overlay, col, width, total);
2258
- const drawn = compositeTuiLine(base.slice(0, at), overlay, col, width, total);
2259
- return col >= pictureRight ? `${drawn}${PICTURE_AT}${picture}` : drawn;
2260
- };
2349
+ target.compositeLineAt = (base, overlay, col, width, total) => overPictureRow(base, overlay, col, width, total, pictureRight) ?? original(base, overlay, col, width, total);
2261
2350
  }
2262
2351
  /** Columns between the jellyfish and the lines beside it. */
2263
2352
  const JELLYFISH_GAP = 3;
@@ -2289,7 +2378,8 @@ class Banner {
2289
2378
  const art = this.art;
2290
2379
  const placed = art?.kind === "image" ? this.placeImage(art, Math.max(JELLYFISH_ROWS, this.lines.length), width - 2 - JELLYFISH_GAP - BANNER_TEXT_MIN) : art?.kind === "text" && art.columns <= width - 2 - JELLYFISH_GAP - BANNER_TEXT_MIN ? art : undefined;
2291
2380
  const left = placed !== undefined ? 1 + placed.columns + JELLYFISH_GAP : 1;
2292
- const text = this.lines.map((line) => truncateToWidth(line, Math.max(1, width - 1 - left)));
2381
+ // Wrapped in the columns beside the picture, never cut short - the chat beside the docked panel is narrow.
2382
+ const text = this.lines.flatMap((line) => wrapTextWithAnsi(line, Math.max(1, width - 1 - left)));
2293
2383
  const height = Math.max(text.length, placed?.rows ?? 0);
2294
2384
  const above = Math.floor((height - text.length) / 2);
2295
2385
  const drawn = placed?.kind === "text" ? placed.frames[jellyfishFrame % placed.frames.length] : undefined;
@@ -2361,12 +2451,3 @@ function jellyfishArt(appearance, ink) {
2361
2451
  return undefined;
2362
2452
  }
2363
2453
  }
2364
- function packageVersion() {
2365
- try {
2366
- const raw = readFileSync(join(PACKAGE_ROOT, "package.json"), "utf8");
2367
- return JSON.parse(raw).version ?? "0.0.0";
2368
- }
2369
- catch {
2370
- return "0.0.0";
2371
- }
2372
- }
package/dist/identity.js CHANGED
@@ -25,7 +25,7 @@ export const TURRI_IDENTITY = [
25
25
  "- A pulse is one unit of work regenerating the code - a spec diff regenerated in a codebase, or its conformance",
26
26
  " tests. It was called a job, and still is in places: the job_id a tool takes is a pulse's id, and parallel-jobs and",
27
27
  " allow-to-stop-jobs in config.turri.yaml are about pulses. \"Job\" in what you read is a pulse; say pulse.",
28
- "- regen is the side panel (/regen, or ctrl+h): the spec with each functionality and item marked built, regenerating,",
28
+ "- regen is the side panel (ctrl+h): the spec with each functionality and item marked built, regenerating,",
29
29
  " planned or waiting - and behind it the pulses (/pulses), every pulse regenerating, planned and finished. Say regen,",
30
30
  " lowercase. It was the regeneration, the job queue, or the job panel.",
31
31
  "- mutate, in a skill's activity, is write_spec_diff: a spec change written and regenerated in one step.",
@@ -292,7 +292,7 @@ function whose(vendor, bedrock) {
292
292
  return `${VENDORS[choice.maker].label}'s on Amazon Bedrock (${choice.region})`;
293
293
  }
294
294
  /**
295
- * What a config.yaml in `folder` says that turri reads too - codeplain's testing scripts, say - by turri's names
295
+ * What a config.yaml in `folder` says that turri reads too - another tool's testing scripts, say - by turri's names
296
296
  * (`template_dir` read as `template-dir`), in the file's order; and the keys it leaves, turri reading none of them. The
297
297
  * two models are the vendor's, so they are not taken.
298
298
  */
@@ -338,7 +338,7 @@ export function initConfig(dir, options = {}) {
338
338
  const existing = existsSync(path) && options.force !== true;
339
339
  const { harness, coding } = modelsOf(vendor, options.bedrock);
340
340
  const lines = modelLines(harness, coding);
341
- // A new file takes what a config.yaml beside it - codeplain's - says that turri reads too: its testing scripts, say.
341
+ // A new file takes what a config.yaml beside it - another tool's, say - says that turri reads too: its testing scripts, say.
342
342
  const shared = existing ? { carried: [], left: [] } : sharedWithLegacy(folder);
343
343
  const config = existing
344
344
  ? withModels(readFileSync(path, "utf8"), lines)
@@ -384,7 +384,7 @@ const USAGE = [
384
384
  "a mix of Anthropic and Google, the starting point for any provider pi supports. On amazon-bedrock, --maker and",
385
385
  "--region say whose models and where (anthropic: global, us, eu, jp, au; openai: global, us) - asked when not given.",
386
386
  "A config.turri.yaml already there keeps everything but its two models; --force writes the whole of it again. A new",
387
- "one takes from a config.yaml beside it - codeplain's - the keys turri reads too, its testing scripts say, and leaves",
387
+ "one takes from a config.yaml beside it - another tool's, say - the keys turri reads too, its testing scripts say, and leaves",
388
388
  "config.yaml as it is.",
389
389
  ].join("\n");
390
390
  /** Ask until the answer is one `read` takes. */