@turri-dev/cli 0.1.3-next.26f08c5 → 0.1.3-next.2babf06
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/dist/extension.js +86 -21
- package/dist/picture-row.js +21 -0
- package/dist/regenerate/panel-dock.js +57 -0
- package/dist/regenerate/side-panel.js +14 -4
- package/dist/regenerate/test-scripts.js +23 -8
- package/docs/development.md +1 -0
- package/docs/regenerating.md +6 -4
- package/docs/using.md +9 -2
- package/package.json +1 -1
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,
|
|
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 } 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";
|
|
@@ -1468,9 +1470,17 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1468
1470
|
return { content: [{ type: "text", text }], details: {} };
|
|
1469
1471
|
},
|
|
1470
1472
|
});
|
|
1471
|
-
// The side panel: the regeneration - and the pulses behind p - over the whole screen
|
|
1472
|
-
// key
|
|
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.
|
|
1473
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);
|
|
1474
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.
|
|
1475
1485
|
const jobsTree = createJobsTreeState();
|
|
1476
1486
|
const mapState = createSpecMapState();
|
|
@@ -1498,13 +1508,51 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1498
1508
|
needsLooking.delete(jobs);
|
|
1499
1509
|
if (panel !== undefined) {
|
|
1500
1510
|
panel.handle?.setHidden(false);
|
|
1511
|
+
panel.keys = true;
|
|
1501
1512
|
panel.handle?.focus();
|
|
1502
1513
|
return;
|
|
1503
1514
|
}
|
|
1504
1515
|
// Not awaited: the panel lives on while the jobs it shows run.
|
|
1505
1516
|
void ctx.ui.custom((tui, theme, _keybindings, done) => {
|
|
1506
1517
|
const close = () => done(undefined);
|
|
1518
|
+
panelTerminal = tui.terminal;
|
|
1507
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
|
+
undock = () => {
|
|
1535
|
+
unwatch();
|
|
1536
|
+
widen();
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
else if (!panelDocked && undock !== undefined) {
|
|
1540
|
+
undock();
|
|
1541
|
+
undock = undefined;
|
|
1542
|
+
}
|
|
1543
|
+
};
|
|
1544
|
+
// Beside the chat, back to it: the chat has the keys, the panel stays.
|
|
1545
|
+
const toChat = () => {
|
|
1546
|
+
entry.keys = false;
|
|
1547
|
+
entry.handle?.unfocus();
|
|
1548
|
+
tui.requestRender();
|
|
1549
|
+
};
|
|
1550
|
+
const toPanel = () => {
|
|
1551
|
+
entry.keys = true;
|
|
1552
|
+
entry.handle?.focus();
|
|
1553
|
+
tui.requestRender();
|
|
1554
|
+
};
|
|
1555
|
+
const back = () => (panelBeside() > 0 ? toChat() : close());
|
|
1508
1556
|
// Opened, it reads the files again at once: they may have changed while it was closed.
|
|
1509
1557
|
specMap.refresh();
|
|
1510
1558
|
const view = createSidePanel({
|
|
@@ -1514,6 +1562,14 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1514
1562
|
mapState,
|
|
1515
1563
|
rows: () => tui.terminal.rows,
|
|
1516
1564
|
key: PANEL_KEY,
|
|
1565
|
+
dock: {
|
|
1566
|
+
docked: () => panelBeside() > 0,
|
|
1567
|
+
toggle: () => {
|
|
1568
|
+
panelDocked = panelBeside() === 0;
|
|
1569
|
+
layOut();
|
|
1570
|
+
tui.requestRender();
|
|
1571
|
+
},
|
|
1572
|
+
},
|
|
1517
1573
|
style: {
|
|
1518
1574
|
heading: (text) => theme.bold(theme.fg("accent", text)),
|
|
1519
1575
|
muted: (text) => theme.fg("muted", text),
|
|
@@ -1528,22 +1584,25 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1528
1584
|
// What conformance tests show does what its spec says: brand blue.
|
|
1529
1585
|
conforms: brandBlue(theme),
|
|
1530
1586
|
},
|
|
1531
|
-
back
|
|
1587
|
+
back,
|
|
1532
1588
|
close,
|
|
1533
1589
|
// A module that needs the person: the chat takes it up with them, in their words - what went wrong, and what now.
|
|
1534
1590
|
discuss: (page) => {
|
|
1535
|
-
|
|
1591
|
+
back();
|
|
1536
1592
|
const why = page.codebase?.stuck?.why;
|
|
1537
1593
|
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" });
|
|
1538
1594
|
},
|
|
1539
1595
|
// Something took the keys' focus while the panel showed - a question in the chat - and waits there.
|
|
1540
1596
|
waiting: () => {
|
|
1597
|
+
if (!entry.keys)
|
|
1598
|
+
return false;
|
|
1541
1599
|
const focused = tui.getFocusedComponent?.() ?? null;
|
|
1542
1600
|
return focused !== null && focused !== component;
|
|
1543
1601
|
},
|
|
1544
1602
|
});
|
|
1545
|
-
const entry = { view, close };
|
|
1603
|
+
const entry = { view, close, keys: true };
|
|
1546
1604
|
panel = entry;
|
|
1605
|
+
layOut();
|
|
1547
1606
|
// Every key goes to the panel while it shows, whatever has the focus: a question the chat model asks in the
|
|
1548
1607
|
// background takes the focus, but no key reaches it - an enter never answers it unseen. It waits in the chat
|
|
1549
1608
|
// until the panel closes, and has the keys then. Mouse events are the screen's, before this sees them.
|
|
@@ -1565,6 +1624,11 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1565
1624
|
}
|
|
1566
1625
|
if (!state)
|
|
1567
1626
|
return undefined;
|
|
1627
|
+
// Beside the chat, with the chat's keys: they are the chat's - PANEL_KEY among them, for the shortcut that gives
|
|
1628
|
+
// them to the panel. A click on the panel gives it the focus, and the keys with it.
|
|
1629
|
+
if (!entry.keys && entry.handle?.isFocused() !== true)
|
|
1630
|
+
return undefined;
|
|
1631
|
+
entry.keys = true;
|
|
1568
1632
|
// A terminal on the Kitty keyboard protocol reports a key's release, and listeners are given it: the release of
|
|
1569
1633
|
// the ctrl+h that opened the panel would close it again at once, and every arrow would move twice. Only
|
|
1570
1634
|
// presses count - and ctrl+h held down, repeating, does not open and close it over and over.
|
|
@@ -1591,7 +1655,8 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1591
1655
|
}, WATCH_REDRAW_MS);
|
|
1592
1656
|
timer.unref?.();
|
|
1593
1657
|
component = {
|
|
1594
|
-
|
|
1658
|
+
// Beside the chat, a rule down its left edge between them.
|
|
1659
|
+
render: (width) => (panelBeside() > 0 ? view.render(width - 2).map((line) => `${theme.fg("borderMuted", "│")} ${line}`) : view.render(width)),
|
|
1595
1660
|
// Reached only if the listener above is gone: the same, so the panel never passes a key on.
|
|
1596
1661
|
handleInput: (data) => {
|
|
1597
1662
|
if (isKeyRepeat(data) && matchesKey(data, PANEL_KEY))
|
|
@@ -1607,6 +1672,7 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1607
1672
|
dispose: () => {
|
|
1608
1673
|
unlisten();
|
|
1609
1674
|
clearInterval(timer);
|
|
1675
|
+
undock?.();
|
|
1610
1676
|
if (panel === entry)
|
|
1611
1677
|
panel = undefined;
|
|
1612
1678
|
},
|
|
@@ -1614,8 +1680,15 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1614
1680
|
return component;
|
|
1615
1681
|
}, {
|
|
1616
1682
|
overlay: true,
|
|
1617
|
-
// The whole screen
|
|
1618
|
-
overlayOptions: {
|
|
1683
|
+
// The whole screen, or the columns on the right beside the chat - read at each frame, so it follows the terminal.
|
|
1684
|
+
overlayOptions: {
|
|
1685
|
+
get width() {
|
|
1686
|
+
return panelBeside() > 0 ? panelBeside() : "100%";
|
|
1687
|
+
},
|
|
1688
|
+
maxHeight: "100%",
|
|
1689
|
+
anchor: "top-right",
|
|
1690
|
+
margin: 0,
|
|
1691
|
+
},
|
|
1619
1692
|
onHandle: (handle) => {
|
|
1620
1693
|
if (panel !== undefined)
|
|
1621
1694
|
panel.handle = handle;
|
|
@@ -1641,7 +1714,7 @@ function registerJobTools(pi, jobs, cwd) {
|
|
|
1641
1714
|
panelOpeners.set(jobs, (ctx) => openPanel(ctx));
|
|
1642
1715
|
questionPeek = {
|
|
1643
1716
|
open: (ctx) => openPanel(ctx),
|
|
1644
|
-
shown: () => panel !== undefined,
|
|
1717
|
+
shown: () => panel !== undefined && panel.keys,
|
|
1645
1718
|
};
|
|
1646
1719
|
pi.registerShortcut(PANEL_KEY, {
|
|
1647
1720
|
description: "Peek at regen and the pulses - a question waiting in the chat waits for you - or go back to it",
|
|
@@ -2239,8 +2312,6 @@ function animateJellyfish(ctx) {
|
|
|
2239
2312
|
}, { placement: "belowEditor" });
|
|
2240
2313
|
}
|
|
2241
2314
|
const JELLYFISH_CLOCK_KEY = "turri-jellyfish";
|
|
2242
|
-
/** What the banner puts between a row's text and the picture after it: back to the jellyfish's column. */
|
|
2243
|
-
const PICTURE_AT = "\x1b[2G";
|
|
2244
2315
|
/** The columns the banner's picture takes from the left edge, while it shows one; 0 when it does not. */
|
|
2245
2316
|
let pictureRight = 0;
|
|
2246
2317
|
/**
|
|
@@ -2255,14 +2326,7 @@ function drawOverlaysBesidePicture(tui) {
|
|
|
2255
2326
|
return;
|
|
2256
2327
|
target.turriPicture = true;
|
|
2257
2328
|
const original = target.compositeLineAt.bind(tui);
|
|
2258
|
-
target.compositeLineAt = (base, overlay, col, width, total) =>
|
|
2259
|
-
const at = base.lastIndexOf(PICTURE_AT);
|
|
2260
|
-
const picture = at === -1 ? "" : base.slice(at + PICTURE_AT.length);
|
|
2261
|
-
if (!/^(\x1b\[\d+A)?(\x1b_G|\x1b\]1337;File=)/.test(picture))
|
|
2262
|
-
return original(base, overlay, col, width, total);
|
|
2263
|
-
const drawn = compositeTuiLine(base.slice(0, at), overlay, col, width, total);
|
|
2264
|
-
return col >= pictureRight ? `${drawn}${PICTURE_AT}${picture}` : drawn;
|
|
2265
|
-
};
|
|
2329
|
+
target.compositeLineAt = (base, overlay, col, width, total) => overPictureRow(base, overlay, col, width, total, pictureRight) ?? original(base, overlay, col, width, total);
|
|
2266
2330
|
}
|
|
2267
2331
|
/** Columns between the jellyfish and the lines beside it. */
|
|
2268
2332
|
const JELLYFISH_GAP = 3;
|
|
@@ -2294,7 +2358,8 @@ class Banner {
|
|
|
2294
2358
|
const art = this.art;
|
|
2295
2359
|
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;
|
|
2296
2360
|
const left = placed !== undefined ? 1 + placed.columns + JELLYFISH_GAP : 1;
|
|
2297
|
-
|
|
2361
|
+
// Wrapped in the columns beside the picture, never cut short - the chat beside the docked panel is narrow.
|
|
2362
|
+
const text = this.lines.flatMap((line) => wrapTextWithAnsi(line, Math.max(1, width - 1 - left)));
|
|
2298
2363
|
const height = Math.max(text.length, placed?.rows ?? 0);
|
|
2299
2364
|
const above = Math.floor((height - text.length) / 2);
|
|
2300
2365
|
const drawn = placed?.kind === "text" ? placed.frames[jellyfishFrame % placed.frames.length] : undefined;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { compositeTuiLine } from "@earendil-works/pi-tui";
|
|
2
|
+
/** What the banner puts between a row's text and the picture after it: back to the jellyfish's column. */
|
|
3
|
+
export const PICTURE_AT = "\x1b[2G";
|
|
4
|
+
/** The picture's escape sequence, as the banner writes it after PICTURE_AT: Kitty's, in chunks, or iTerm2's - climbing first on iTerm2. */
|
|
5
|
+
const PICTURE = /^(?:\x1b\[\d+A)?(?:(?:\x1b_G[^\x1b]*\x1b\\)+|\x1b\]1337;File=[^\x07]*\x07)/;
|
|
6
|
+
/**
|
|
7
|
+
* The banner's picture row with an overlay drawn over its text, the picture after it - left out while the overlay
|
|
8
|
+
* reaches over the picture, `pictureRight` columns from the left edge. Undefined for a row that holds no picture.
|
|
9
|
+
*
|
|
10
|
+
* What follows the picture - the padding a row narrower than the screen is given, the chat's beside the docked panel -
|
|
11
|
+
* is the row's text: written after the picture, from the picture's column, it would blank the row.
|
|
12
|
+
*/
|
|
13
|
+
export function overPictureRow(base, overlay, col, width, total, pictureRight) {
|
|
14
|
+
const at = base.lastIndexOf(PICTURE_AT);
|
|
15
|
+
const picture = at === -1 ? null : PICTURE.exec(base.slice(at + PICTURE_AT.length));
|
|
16
|
+
if (picture === null)
|
|
17
|
+
return undefined;
|
|
18
|
+
const after = base.slice(at + PICTURE_AT.length + picture[0].length);
|
|
19
|
+
const drawn = compositeTuiLine(`${base.slice(0, at)}${after}`, overlay, col, width, total);
|
|
20
|
+
return col >= pictureRight ? `${drawn}${PICTURE_AT}${picture[0]}` : drawn;
|
|
21
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { HStack } from "@earendil-works/pi-tui";
|
|
2
|
+
/**
|
|
3
|
+
* The side panel beside the chat rather than over it: the chat drawn on the left in what the panel leaves it, the panel
|
|
4
|
+
* laid over the columns on the right.
|
|
5
|
+
*
|
|
6
|
+
* pi draws its screen one of two ways, and the chat is narrowed in each as it is drawn: full screen (pi's default), a
|
|
7
|
+
* layout - its root put in a row beside a filler as wide as the panel; the regular screen, the lines its render gives -
|
|
8
|
+
* asked for in fewer columns.
|
|
9
|
+
*/
|
|
10
|
+
/** Narrower than this, the chat and the panel have no room side by side: the panel takes the whole screen. */
|
|
11
|
+
const SHARED_FROM = 100;
|
|
12
|
+
/** The columns the panel takes beside the chat - 0 when the terminal is too narrow to share. */
|
|
13
|
+
export function dockedWidth(columns) {
|
|
14
|
+
return columns < SHARED_FROM ? 0 : Math.max(48, Math.floor(columns * 0.45));
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The chat narrowed by `panelWidth()` columns, read at each frame - 0 gives it the whole width. Returns what gives it the
|
|
18
|
+
* whole width back for good.
|
|
19
|
+
*/
|
|
20
|
+
export function narrowChat(tui, panelWidth) {
|
|
21
|
+
const screen = tui;
|
|
22
|
+
if (typeof screen.setLayoutRoot === "function" && screen.layoutRoot !== undefined) {
|
|
23
|
+
const root = screen.layoutRoot;
|
|
24
|
+
// A line as wide as the panel: the row gives the filler what it measures, the chat the rest.
|
|
25
|
+
const filler = { render: () => [" ".repeat(panelWidth())], invalidate() { } };
|
|
26
|
+
screen.setLayoutRoot(new HStack([{ component: root, basis: 0, grow: 1 }, { component: filler }]));
|
|
27
|
+
return () => screen.setLayoutRoot(root);
|
|
28
|
+
}
|
|
29
|
+
// Read from the prototype: the screen's own render, not one laid over it.
|
|
30
|
+
const render = Object.getPrototypeOf(tui).render;
|
|
31
|
+
screen.render = function (width) {
|
|
32
|
+
return render.call(this, Math.max(1, width - panelWidth()));
|
|
33
|
+
};
|
|
34
|
+
return () => {
|
|
35
|
+
screen.render = render;
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* A press of a mouse button, as pi's full screen takes it - `side` told whether it was in the chat, left of `chatWidth()`
|
|
40
|
+
* columns, or in the panel - before the press goes on as it would have. Motion and the wheel are not presses. pi's
|
|
41
|
+
* regular screen reads no mouse: nothing is done there. Returns what takes the watch off.
|
|
42
|
+
*/
|
|
43
|
+
export function clickToFocus(tui, chatWidth, side) {
|
|
44
|
+
const screen = tui;
|
|
45
|
+
const handle = screen.handleMouseEvent;
|
|
46
|
+
if (typeof handle !== "function")
|
|
47
|
+
return () => { };
|
|
48
|
+
screen.handleMouseEvent = function (raw) {
|
|
49
|
+
// 32: motion; 64: the wheel.
|
|
50
|
+
if (raw.release !== true && (raw.button & (32 | 64)) === 0)
|
|
51
|
+
side(raw.x < chatWidth() ? "chat" : "panel");
|
|
52
|
+
return handle.call(this, raw);
|
|
53
|
+
};
|
|
54
|
+
return () => {
|
|
55
|
+
screen.handleMouseEvent = handle;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -12,6 +12,12 @@ export function createSidePanel(options) {
|
|
|
12
12
|
let drawnTree = { rows: [], rowStarts: [], lineCount: 0 };
|
|
13
13
|
let drawnMap = { rows: [], rowStarts: [], lineCount: 0 };
|
|
14
14
|
let pages = [];
|
|
15
|
+
/** The keys back to the chat - and `s`, from over the screen to beside the chat and back. */
|
|
16
|
+
const backKeys = () => {
|
|
17
|
+
if (options.dock === undefined)
|
|
18
|
+
return [`esc or ${options.key} back to the chat`];
|
|
19
|
+
return options.dock.docked() ? ["s full screen", "esc to the chat", `${options.key} close`] : ["s beside the chat", `esc or ${options.key} back to the chat`];
|
|
20
|
+
};
|
|
15
21
|
const waitingNote = () =>
|
|
16
22
|
// A question asked while it showed waits in the chat, unanswered: the person is told it is there.
|
|
17
23
|
options.waiting?.() === true ? ` ${style.warning?.("your question is waiting") ?? style.accent("your question is waiting")} - ${options.key} or esc to answer it, as you left it` : "";
|
|
@@ -59,7 +65,7 @@ export function createSidePanel(options) {
|
|
|
59
65
|
/** The rule under the top. */
|
|
60
66
|
const rule = (width) => style.muted("─".repeat(Math.max(1, width)));
|
|
61
67
|
const renderPulses = (width, rows) => {
|
|
62
|
-
const keys = ["↑↓ move", "enter open/close", "←→", "o/c all", "pgup pgdn scroll", ...(options.map !== undefined ? ["p regen"] : []),
|
|
68
|
+
const keys = ["↑↓ move", "enter open/close", "←→", "o/c all", "pgup pgdn scroll", ...(options.map !== undefined ? ["p regen"] : []), ...backKeys()];
|
|
63
69
|
const top = topBlock([`${style.heading("pulses")}${waitingNote()}`], keys, width);
|
|
64
70
|
// Under the top: the rule, and a line of space - as the map has.
|
|
65
71
|
const bodyHeight = Math.max(3, rows - top.length - 2);
|
|
@@ -77,7 +83,7 @@ export function createSidePanel(options) {
|
|
|
77
83
|
pages = options.map();
|
|
78
84
|
const now = Date.now();
|
|
79
85
|
if (pages.length === 0) {
|
|
80
|
-
const top = topBlock([`${style.heading("regen")}${waitingNote()}`], ["p pulses",
|
|
86
|
+
const top = topBlock([`${style.heading("regen")}${waitingNote()}`], ["p pulses", ...backKeys()], width);
|
|
81
87
|
lastBodyHeight = Math.max(3, rows - top.length - 1);
|
|
82
88
|
drawnMap = { rows: [], rowStarts: [], lineCount: 0 };
|
|
83
89
|
const body = [style.muted(" No .plain file to map: name one to regenerate, or set module: in config.turri.yaml."), ...new Array(Math.max(0, lastBodyHeight - 1)).fill("")];
|
|
@@ -87,7 +93,7 @@ export function createSidePanel(options) {
|
|
|
87
93
|
const tabbed = pages.length > 1;
|
|
88
94
|
const page = shownPage(pages, map);
|
|
89
95
|
const discuss = page.stuck && options.discuss !== undefined ? [`${DISCUSS_KEY} talk it through in the chat`] : [];
|
|
90
|
-
const keys = [...discuss, "↑↓ move", "enter open/close", ...(tabbed ? ["←→ module"] : []), "o/c all", "pgup pgdn", "p pulses",
|
|
96
|
+
const keys = [...discuss, "↑↓ move", "enter open/close", ...(tabbed ? ["←→ module"] : []), "o/c all", "pgup pgdn", "p pulses", ...backKeys()];
|
|
91
97
|
// The legend's box in the upper right corner, over what is under it; too narrow a panel has none.
|
|
92
98
|
const box = width >= 60 ? legendBox(legendLines(page, style)) : [];
|
|
93
99
|
const room = box.length > 0 ? width - visibleWidth(box[0]) - 1 : width;
|
|
@@ -210,7 +216,7 @@ export function createSidePanel(options) {
|
|
|
210
216
|
}
|
|
211
217
|
catch (error) {
|
|
212
218
|
// A frame that throws is never drawn - nor is anything after it: the screen would freeze. What failed is shown instead.
|
|
213
|
-
const top = topBlock([`${style.heading(map.view === "map" ? "regen" : "pulses")}${waitingNote()}`], [...(options.map !== undefined ? ["p the other view"] : []),
|
|
219
|
+
const top = topBlock([`${style.heading(map.view === "map" ? "regen" : "pulses")}${waitingNote()}`], [...(options.map !== undefined ? ["p the other view"] : []), ...backKeys()], width);
|
|
214
220
|
const lines = (error instanceof Error ? (error.stack ?? error.message) : String(error)).split("\n").slice(0, Math.max(1, rows - top.length - 1));
|
|
215
221
|
return [
|
|
216
222
|
...top,
|
|
@@ -225,6 +231,10 @@ export function createSidePanel(options) {
|
|
|
225
231
|
options.back();
|
|
226
232
|
return;
|
|
227
233
|
}
|
|
234
|
+
if (options.dock !== undefined && data === "s") {
|
|
235
|
+
options.dock.toggle();
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
228
238
|
if (options.map !== undefined && data === "p") {
|
|
229
239
|
if (map.view === "map")
|
|
230
240
|
toPulses();
|
|
@@ -609,19 +609,34 @@ function timedOutFailure(run, what) {
|
|
|
609
609
|
`the same ${limit} seconds, and a skipped or deleted test does not count as a fix.`,
|
|
610
610
|
].join("\n");
|
|
611
611
|
}
|
|
612
|
+
/** A command `timeout` could not start, or a shell could not find - on a line of its own. */
|
|
613
|
+
const COULD_NOT_RUN = /^(?:[^:]*: )?failed to run command [‘'`"]?([\w.+-]+)[’'`"]?: No such file or directory$/;
|
|
614
|
+
const NOT_FOUND = /^(?:[^:]*: )?(?:line \d+: )?([\w.+-]+): command not found$/;
|
|
612
615
|
/**
|
|
613
616
|
* What a failed run says is not installed where it ran - the command, when its output names it - or undefined when
|
|
614
|
-
* the run failed for another reason.
|
|
615
|
-
* or a shell or `timeout`
|
|
617
|
+
* the run failed for another reason. A missing command is the cause only on evidence that it ended the run: exit 69,
|
|
618
|
+
* the testing scripts' own way to say a toolchain is missing, or 127, what a shell or `timeout` gives for a command it
|
|
619
|
+
* could not find - else a command not found on the last line the run printed, nothing having run after it. Earlier in
|
|
620
|
+
* the output it is a warning, not the failure: a shell profile's, an optional tool probed for, a fallback taken - and a
|
|
621
|
+
* run that failed for its own reasons is the agent's to fix, not a stop for the person.
|
|
616
622
|
*/
|
|
617
623
|
export function missingToolchain(run) {
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
if (run.exitCode === 69 || run.exitCode === 127)
|
|
624
|
+
if (run.exitCode === 0)
|
|
625
|
+
return undefined;
|
|
626
|
+
const lines = run.output.split("\n").map((line) => line.trim()).filter((line) => line !== "");
|
|
627
|
+
const named = (line) => COULD_NOT_RUN.exec(line)?.[1] ?? NOT_FOUND.exec(line)?.[1];
|
|
628
|
+
if (run.exitCode === 69 || run.exitCode === 127) {
|
|
629
|
+
// The command is the last one named: the one the run stopped on.
|
|
630
|
+
for (let at = lines.length - 1; at >= 0; at -= 1) {
|
|
631
|
+
const command = named(lines[at]);
|
|
632
|
+
if (command !== undefined)
|
|
633
|
+
return { command };
|
|
634
|
+
}
|
|
623
635
|
return {};
|
|
624
|
-
|
|
636
|
+
}
|
|
637
|
+
const last = lines.at(-1);
|
|
638
|
+
const command = last !== undefined ? named(last) : undefined;
|
|
639
|
+
return command !== undefined ? { command } : undefined;
|
|
625
640
|
}
|
|
626
641
|
/** Where `command` is on this machine's PATH, or undefined when it is not there. */
|
|
627
642
|
export function onThisMachine(command) {
|
package/docs/development.md
CHANGED
|
@@ -75,6 +75,7 @@ trust `turri-oss/turri`, the workflow `publish.yml` and the environment `npm`. R
|
|
|
75
75
|
| `src/regenerate/confine.ts`, `src/regenerate/agent-shell.ts`, `src/regenerate/command-check.ts`, `src/regenerate/os-sandbox.ts`, `src/regenerate/audit.ts`, `src/regenerate/agent-env.ts`, `src/regenerate/shell.ts` | the file-tool fence and the walls around every agent command: the check, the OS sandbox, the audit, the module's environment, the host runner - see [sandbox](sandbox.md) |
|
|
76
76
|
| `src/regenerate/engine.ts` | choosing and resolving render models |
|
|
77
77
|
| `src/regenerate/job-panel.ts`, `src/regenerate/transcript-view.ts`, `src/regenerate/diff-view.ts` | the job panel, the side panel's colours, and a spec diff as they show it - highlighted and banded |
|
|
78
|
+
| `src/regenerate/side-panel.ts`, `src/regenerate/panel-dock.ts` | the side panel, and the chat narrowed beside it when it is docked |
|
|
78
79
|
| `src/regenerate/spec-map.ts`, `src/regenerate/spec-map-view.ts` | regen: each item's mark, from the records, the `.plain` files and the work loop's plan (`mapPlan`), and the map drawn |
|
|
79
80
|
| `src/regenerate/debug-log.ts` | each render agent's whole conversation, always streamed to `.turri/debug/` |
|
|
80
81
|
| `genome/` | what turri knows and can do: `genome/skills/`, the skill library, and `genome/rules/`, the \*\*\*plain authoring rules |
|
package/docs/regenerating.md
CHANGED
|
@@ -218,7 +218,7 @@ and the `implement-unit-testing-script` and `implement-conformance-testing-scrip
|
|
|
218
218
|
- **The formatting is the agents'.** The harness formats nothing: the scripts check, and leave their inputs as they
|
|
219
219
|
found them. Every agent that writes code or tests is told to run, in its shell, the formatter its script checks with,
|
|
220
220
|
on the files it changes, before the final run.
|
|
221
|
-
- **A toolchain missing** where the checks run - exit 69, 127, `command not found` - stops the pulse as it does for the
|
|
221
|
+
- **A toolchain missing** where the checks run - exit 69, 127, a `command not found` the run ended on - stops the pulse as it does for the
|
|
222
222
|
tests: the person installs it, the agents install nothing.
|
|
223
223
|
|
|
224
224
|
### Checking its own work
|
|
@@ -507,8 +507,10 @@ your own environment with this machine's toolchains, turri's model providers' AP
|
|
|
507
507
|
and whatever they do is your doing. That is the only way the two scripts are ever run: the harness's own runs, and the
|
|
508
508
|
agents' `run_unit_tests` and `run_conformance_tests`, which run them exactly as the harness does - through the host
|
|
509
509
|
runner, in a process group of their own, so the time limit and a stopped job end the script and all it started. A
|
|
510
|
-
toolchain the script needs and this machine lacks - exit 69, exit 127, or a `command not found` /
|
|
511
|
-
|
|
510
|
+
toolchain the script needs and this machine lacks - exit 69, exit 127, or a run that ended on a `command not found` /
|
|
511
|
+
`failed to run command`, the last line it printed (`missingToolchain`) - is yours to install. Only on that evidence: a
|
|
512
|
+
`command not found` earlier in the output, the run going on after it and failing for its own reasons, is a warning -
|
|
513
|
+
a shell profile's, an optional tool probed for - and the failure is the agent's to fix. A missing toolchain is yours to install, never the agent's: the agent is told to report it
|
|
512
514
|
with `report_environment_error` rather than work around it, and the pulse stops at the harness's first run of either
|
|
513
515
|
script - unit or conformance - that cannot find it, rolled back, saying what to install (see
|
|
514
516
|
[when the environment lacks something](#when-the-environment-lacks-something)). The code and the tests an agent wrote run unfenced inside the script too: what an agent runs with its own
|
|
@@ -562,7 +564,7 @@ It is raised two ways:
|
|
|
562
564
|
is shown its output once, and a report made again after that stands. A report that stands ends its turn, the pulse
|
|
563
565
|
stops, and what it said goes to you as it is;
|
|
564
566
|
- **by the harness itself**: a testing script - the unit or the conformance one, in a code pulse, a conformance pulse or
|
|
565
|
-
the whole-suite run - that cannot find its toolchain (exit 69 or 127, `command not found`), or
|
|
567
|
+
the whole-suite run - that cannot find its toolchain (exit 69 or 127, a `command not found` the run ended on), or
|
|
566
568
|
a credential `agent-env:` names that is not there when the pulse starts - before any agent does. What the harness
|
|
567
569
|
found it works out at once - the diagnosis, not after a second failure - to say how to fix it on this machine.
|
|
568
570
|
|
package/docs/using.md
CHANGED
|
@@ -130,7 +130,10 @@ come waits for a free slot, and says so.
|
|
|
130
130
|
says `ctrl+h shows this pulse`; **`/regen`** on regen, **`/pulses`** on the pulses; not `ctrl+j`, which a terminal without the Kitty
|
|
131
131
|
keyboard protocol sends as a line feed, the same as `shift+enter`, and which a dialog takes for
|
|
132
132
|
`enter`; both stay pi's new-line keys. `ctrl+h` is the byte old terminals sent for backspace; a
|
|
133
|
-
terminal set to send `^H` for its Backspace key would open the panel on it) takes the whole screen - the panel or the chat, one at a time
|
|
133
|
+
terminal set to send `^H` for its Backspace key would open the panel on it) takes the whole screen - the panel or the chat, one at a time -
|
|
134
|
+
or, `s`, sits beside the chat: the chat on the left, the panel in the right 45% of the screen, and `s` again
|
|
135
|
+
over the whole screen. It opens as it was last left, for the conversation; a terminal under 100 columns has
|
|
136
|
+
no room for both and shows the panel over the whole screen.
|
|
134
137
|
It opens on **regen**: the spec itself, in full, with how much of it the code has been regenerated from drawn
|
|
135
138
|
onto it.
|
|
136
139
|
|
|
@@ -214,7 +217,11 @@ come waits for a free slot, and says so.
|
|
|
214
217
|
closes when it moves to another section. What is open is kept while the panel is closed. What the agents
|
|
215
218
|
said and did is not shown - it is in each pulse's log (see [Agent logs](#agent-logs)).
|
|
216
219
|
|
|
217
|
-
`ctrl+h` again, or `esc`, closes it and goes back to the chat.
|
|
220
|
+
`ctrl+h` again, or `esc`, closes it and goes back to the chat. Beside the chat, a click decides which side has
|
|
221
|
+
the keys - the chat or the panel, the one clicked - and `esc` gives the chat the keys too, the panel staying and
|
|
222
|
+
following the pulses; `ctrl+h` gives the panel the keys again, and from the panel closes it. (pi's regular screen,
|
|
223
|
+
`--tui-mode regular`, reads no mouse: there it is the keys alone.)
|
|
224
|
+
While the panel has the keys, every key is the panel's,
|
|
218
225
|
whatever else happens: a question the chat model asks in the background (`ask_user`) waits in the chat,
|
|
219
226
|
unanswered - an `enter` in the panel never answers it - and the panel's top line says "the chat is waiting
|
|
220
227
|
for your answer". Back in the chat, the question is there, with the keys.
|