pi-minimal 0.1.0 → 0.2.2
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/.github/dependabot.yml +11 -0
- package/.github/workflows/publish.yml +178 -14
- package/.github/workflows/release.yml +42 -0
- package/CHANGELOG.md +81 -0
- package/README.md +28 -1
- package/SECURITY.md +21 -0
- package/dist/activity.js +0 -3
- package/dist/chrome.js +15 -0
- package/dist/code-box.js +69 -0
- package/dist/cwd.js +95 -53
- package/dist/footer.js +30 -29
- package/dist/git-state.js +63 -0
- package/dist/index.js +59 -6
- package/dist/tool-rows.js +143 -12
- package/dist/tools.js +146 -77
- package/dist/transcript.js +50 -0
- package/dist/turn-rule.js +121 -0
- package/package.json +14 -3
- package/release-notes.md +32 -0
- package/src/activity.ts +0 -4
- package/src/chrome.ts +18 -0
- package/src/code-box.ts +86 -0
- package/src/cwd.ts +104 -52
- package/src/footer.ts +38 -34
- package/src/git-state.ts +71 -0
- package/src/index.ts +60 -6
- package/src/tool-rows.ts +170 -19
- package/src/tools.ts +160 -105
- package/src/transcript.ts +51 -0
- package/src/turn-rule.ts +139 -0
- package/test/activity.test.ts +4 -15
- package/test/code-box.test.ts +55 -0
- package/test/footer.test.ts +38 -0
- package/test/git-state.test.ts +52 -0
- package/test/index.test.ts +40 -4
- package/test/package.test.ts +3 -2
- package/test/theme.test.ts +45 -0
- package/test/tool-rows.test.ts +109 -0
- package/test/tools.test.ts +49 -42
- package/test/transcript.test.ts +22 -0
- package/test/turn-rule.test.ts +33 -0
- package/themes/minimal-dark.json +84 -0
package/dist/tools.js
CHANGED
|
@@ -1,103 +1,172 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
1
|
+
import { Container, truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import { rowIsRunning } from "./activity.js";
|
|
3
|
+
import * as chrome from "./chrome.js";
|
|
4
4
|
import { createLiveWatch } from "./live-watch.js";
|
|
5
|
-
import {
|
|
5
|
+
import { bashBody, formatDuration, formatOutcome, layoutRow, resultText, rowGlyph, toolItem, } from "./tool-rows.js";
|
|
6
|
+
/** Tolerates a stale chrome module after /reload, where only CHROME_MARGIN may exist. */
|
|
7
|
+
const margin = () => (typeof chrome.chromeMargin === "function" ? chrome.chromeMargin() : (chrome.CHROME_MARGIN ?? 1));
|
|
6
8
|
const live = createLiveWatch(400);
|
|
9
|
+
/** Left indent of every tool row: chrome margin plus one cell so rows sit inside message text. */
|
|
10
|
+
const rowIndent = () => margin() + 1;
|
|
11
|
+
/** Max lines of output shown when a row is expanded. */
|
|
12
|
+
const EXPANDED_MAX_LINES = 400;
|
|
7
13
|
export function stopLiveWatch() {
|
|
8
14
|
live.stop();
|
|
9
15
|
}
|
|
10
16
|
function liveId(context, item) {
|
|
11
17
|
return context.toolCallId || item;
|
|
12
18
|
}
|
|
13
|
-
function
|
|
14
|
-
if (
|
|
15
|
-
return "";
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
.join("\n");
|
|
19
|
+
function rowStatus(context) {
|
|
20
|
+
if (context.isError)
|
|
21
|
+
return "error";
|
|
22
|
+
if (context.state.outcome?.errorLine)
|
|
23
|
+
return "error";
|
|
24
|
+
return rowIsRunning(context) ? "running" : "ok";
|
|
20
25
|
}
|
|
21
|
-
function
|
|
22
|
-
|
|
26
|
+
function statusColor(status) {
|
|
27
|
+
if (status === "error")
|
|
28
|
+
return "error";
|
|
29
|
+
if (status === "running")
|
|
30
|
+
return "warning";
|
|
31
|
+
return "success";
|
|
23
32
|
}
|
|
24
|
-
function
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
function expandedLines(kind, result) {
|
|
34
|
+
let text = resultText(result);
|
|
35
|
+
if (kind === "bash")
|
|
36
|
+
text = bashBody(text).body;
|
|
37
|
+
if (!text)
|
|
38
|
+
return [];
|
|
39
|
+
const lines = text.split("\n");
|
|
40
|
+
if (lines.length > EXPANDED_MAX_LINES) {
|
|
41
|
+
const hidden = lines.length - EXPANDED_MAX_LINES;
|
|
42
|
+
return [...lines.slice(0, EXPANDED_MAX_LINES), `… ${hidden} more lines`];
|
|
43
|
+
}
|
|
44
|
+
return lines;
|
|
45
|
+
}
|
|
46
|
+
function outputBlock(kind, result, theme) {
|
|
47
|
+
const lines = expandedLines(kind, result);
|
|
48
|
+
if (lines.length === 0)
|
|
49
|
+
return new Container();
|
|
50
|
+
const indent = " ".repeat(rowIndent() + 2);
|
|
51
|
+
const rule = theme.fg("borderMuted", "│");
|
|
52
|
+
return {
|
|
53
|
+
render(width) {
|
|
54
|
+
const inner = Math.max(0, width - indent.length - 2);
|
|
55
|
+
return lines.map((line) => `${indent}${rule} ${theme.fg("dim", truncateToWidth(line, inner))}`);
|
|
56
|
+
},
|
|
57
|
+
invalidate() { },
|
|
58
|
+
};
|
|
29
59
|
}
|
|
30
|
-
function
|
|
60
|
+
function errorLineBlock(line, theme) {
|
|
61
|
+
if (!line)
|
|
62
|
+
return undefined;
|
|
63
|
+
const indent = " ".repeat(rowIndent() + 2);
|
|
64
|
+
const rule = theme.fg("error", "│");
|
|
65
|
+
return {
|
|
66
|
+
render(width) {
|
|
67
|
+
const inner = Math.max(0, width - indent.length - 2);
|
|
68
|
+
return [`${indent}${rule} ${theme.fg("error", truncateToWidth(line, inner))}`];
|
|
69
|
+
},
|
|
70
|
+
invalidate() { },
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function activityRow(kind, args, theme, context) {
|
|
31
74
|
const item = toolItem(kind, args);
|
|
32
75
|
const id = liveId(context, item);
|
|
33
|
-
const row = nextGroupedRow(kind, item, id);
|
|
34
76
|
const running = rowIsRunning(context);
|
|
35
77
|
context.state.running = running;
|
|
36
|
-
if (running)
|
|
78
|
+
if (running) {
|
|
79
|
+
context.state.startedAt ??= Date.now();
|
|
37
80
|
live.watch(id, context.invalidate);
|
|
38
|
-
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
39
83
|
live.unwatch(id);
|
|
40
|
-
|
|
84
|
+
}
|
|
41
85
|
return {
|
|
42
86
|
render(width) {
|
|
43
|
-
const
|
|
44
|
-
const
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
87
|
+
const status = rowStatus(context);
|
|
88
|
+
const blink = status === "running" && live.has(id);
|
|
89
|
+
const glyph = rowGlyph(status, blink ? live.frame : 0);
|
|
90
|
+
const outcome = context.state.outcome;
|
|
91
|
+
const diff = status !== "running" ? outcome?.diff : undefined;
|
|
92
|
+
let right = "";
|
|
93
|
+
if (status === "running") {
|
|
94
|
+
const started = context.state.startedAt;
|
|
95
|
+
right = started ? formatDuration(Date.now() - started) : "";
|
|
50
96
|
}
|
|
51
|
-
|
|
52
|
-
|
|
97
|
+
else if (outcome) {
|
|
98
|
+
right = outcome.text;
|
|
99
|
+
if (diff) {
|
|
100
|
+
const stats = `+${diff.added} −${diff.removed}`;
|
|
101
|
+
right = right ? `${stats} · ${right}` : stats;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const inner = Math.max(8, width - rowIndent());
|
|
105
|
+
const row = layoutRow(inner, kind, item, right);
|
|
106
|
+
let coloredRight;
|
|
107
|
+
if (status === "running")
|
|
108
|
+
coloredRight = theme.fg("warning", row.outcome);
|
|
109
|
+
else if (status === "error")
|
|
110
|
+
coloredRight = theme.fg("error", row.outcome);
|
|
111
|
+
else if (diff && row.outcome) {
|
|
112
|
+
const rest = row.outcome.slice(`+${diff.added} −${diff.removed}`.length);
|
|
113
|
+
coloredRight =
|
|
114
|
+
theme.fg("success", `+${diff.added}`) + " " + theme.fg("error", `−${diff.removed}`) + theme.fg("dim", rest);
|
|
115
|
+
}
|
|
116
|
+
else
|
|
117
|
+
coloredRight = theme.fg("dim", row.outcome);
|
|
118
|
+
const line = " ".repeat(rowIndent()) +
|
|
119
|
+
theme.fg(statusColor(status), glyph) +
|
|
120
|
+
" " +
|
|
121
|
+
theme.fg("dim", row.verb) +
|
|
122
|
+
" " +
|
|
123
|
+
theme.fg(status === "error" ? "text" : "muted", row.item) +
|
|
124
|
+
" ".repeat(row.gap) +
|
|
125
|
+
coloredRight;
|
|
126
|
+
return [line];
|
|
53
127
|
},
|
|
54
128
|
invalidate() { },
|
|
55
129
|
};
|
|
56
130
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
pi.registerTool({
|
|
61
|
-
...tool,
|
|
62
|
-
renderShell: "self",
|
|
63
|
-
renderCall(args, theme, context) {
|
|
64
|
-
return activityBlock(kind, args, theme, context);
|
|
65
|
-
},
|
|
66
|
-
renderResult(result, options, _theme, context) {
|
|
67
|
-
const renderContext = context;
|
|
68
|
-
if (!renderContext.isPartial) {
|
|
69
|
-
renderContext.state.done = true;
|
|
70
|
-
renderContext.state.running = false;
|
|
71
|
-
live.unwatch(liveId(renderContext, toolItem(kind, renderContext.args)));
|
|
72
|
-
}
|
|
73
|
-
return expandedContent(result, options);
|
|
74
|
-
},
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
catch {
|
|
78
|
-
// A missing built-in tool leaves the rest of the compact renderers available.
|
|
79
|
-
}
|
|
131
|
+
const TOOL_KINDS = new Set(["bash", "read", "edit", "write", "grep", "find", "ls"]);
|
|
132
|
+
export function isToolKind(name) {
|
|
133
|
+
return TOOL_KINDS.has(name);
|
|
80
134
|
}
|
|
81
|
-
export function
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
135
|
+
export function compactRenderers(kind) {
|
|
136
|
+
return {
|
|
137
|
+
renderShell: "self",
|
|
138
|
+
renderCall(args, theme, context) {
|
|
139
|
+
return activityRow(kind, args, theme, context);
|
|
140
|
+
},
|
|
141
|
+
renderResult(result, options, theme, context) {
|
|
142
|
+
const renderContext = context;
|
|
143
|
+
const typedResult = result;
|
|
144
|
+
if (!renderContext.isPartial) {
|
|
145
|
+
renderContext.state.done = true;
|
|
146
|
+
renderContext.state.running = false;
|
|
147
|
+
renderContext.state.outcome = formatOutcome(kind, renderContext.args, typedResult, Boolean(renderContext.isError), renderContext.durationMs);
|
|
148
|
+
live.unwatch(liveId(renderContext, toolItem(kind, renderContext.args)));
|
|
149
|
+
}
|
|
150
|
+
const container = new Container();
|
|
151
|
+
const errorLine = errorLineBlock(renderContext.state.outcome?.errorLine, theme);
|
|
152
|
+
if (errorLine && !options.expanded)
|
|
153
|
+
container.addChild(errorLine);
|
|
154
|
+
if (options.expanded)
|
|
155
|
+
container.addChild(outputBlock(kind, typedResult, theme));
|
|
156
|
+
return container;
|
|
157
|
+
},
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Attach the compact renderers to the built-in tools without re-registering them. Registered at
|
|
162
|
+
* extension load so a resumed transcript draws its earlier tool calls the same way as new ones.
|
|
163
|
+
*/
|
|
164
|
+
export function installToolRenderers(pi) {
|
|
165
|
+
if (typeof pi.registerToolRenderer !== "function")
|
|
166
|
+
return;
|
|
167
|
+
pi.registerToolRenderer((toolName, next) => {
|
|
168
|
+
if (!isToolKind(toolName))
|
|
169
|
+
return next();
|
|
170
|
+
return compactRenderers(toolName);
|
|
171
|
+
});
|
|
103
172
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export const THINKING_GUTTER = "┊ ";
|
|
2
|
+
const FENCE = /^\s*(```|~~~)/;
|
|
3
|
+
const BLOCK_START = /^\s*(#{1,6}\s|[-*+]\s|\d+[.)]\s|>|\||```|~~~|---\s*$|\*\*\*\s*$|___\s*$)/;
|
|
4
|
+
/**
|
|
5
|
+
* Prefix the first line of every plain paragraph in thinking Markdown with a gutter glyph.
|
|
6
|
+
* Lines that start a Markdown block (heading, list, quote, table, fence, rule) are left alone so
|
|
7
|
+
* they keep parsing, and nothing inside a fenced code block is touched.
|
|
8
|
+
*/
|
|
9
|
+
export function gutterThinking(markdown) {
|
|
10
|
+
if (!markdown)
|
|
11
|
+
return markdown;
|
|
12
|
+
const lines = markdown.split("\n");
|
|
13
|
+
const out = [];
|
|
14
|
+
let inFence = false;
|
|
15
|
+
let atParagraphStart = true;
|
|
16
|
+
for (const line of lines) {
|
|
17
|
+
if (FENCE.test(line)) {
|
|
18
|
+
inFence = !inFence;
|
|
19
|
+
out.push(line);
|
|
20
|
+
atParagraphStart = !inFence;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (inFence) {
|
|
24
|
+
out.push(line);
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (line.trim() === "") {
|
|
28
|
+
out.push(line);
|
|
29
|
+
atParagraphStart = true;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (atParagraphStart && !BLOCK_START.test(line) && !line.startsWith(THINKING_GUTTER)) {
|
|
33
|
+
out.push(`${THINKING_GUTTER}${line}`);
|
|
34
|
+
}
|
|
35
|
+
else {
|
|
36
|
+
out.push(line);
|
|
37
|
+
}
|
|
38
|
+
atParagraphStart = false;
|
|
39
|
+
}
|
|
40
|
+
return out.join("\n");
|
|
41
|
+
}
|
|
42
|
+
export function installTranscript(pi) {
|
|
43
|
+
if (typeof pi.registerMarkdownTransformer !== "function")
|
|
44
|
+
return;
|
|
45
|
+
pi.registerMarkdownTransformer((markdown, context) => {
|
|
46
|
+
if (context.messageType !== "assistant-thinking")
|
|
47
|
+
return markdown;
|
|
48
|
+
return gutterThinking(markdown);
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import * as chrome from "./chrome.js";
|
|
3
|
+
import { formatElapsed } from "./working-status.js";
|
|
4
|
+
/** Tolerates a stale chrome module after /reload, where only CHROME_MARGIN may exist. */
|
|
5
|
+
const margin = () => (typeof chrome.chromeMargin === "function" ? chrome.chromeMargin() : (chrome.CHROME_MARGIN ?? 1));
|
|
6
|
+
export const TURN_ENTRY_TYPE = "pi-minimal.turn";
|
|
7
|
+
export function formatClock(epochMs) {
|
|
8
|
+
const d = new Date(epochMs);
|
|
9
|
+
const hh = String(d.getHours()).padStart(2, "0");
|
|
10
|
+
const mm = String(d.getMinutes()).padStart(2, "0");
|
|
11
|
+
return `${hh}:${mm}`;
|
|
12
|
+
}
|
|
13
|
+
export function turnLabel(summary) {
|
|
14
|
+
const parts = [formatClock(summary.endedAt), formatElapsed(summary.elapsedMs)];
|
|
15
|
+
if (summary.tools > 0)
|
|
16
|
+
parts.push(`${summary.tools} tool${summary.tools === 1 ? "" : "s"}`);
|
|
17
|
+
if (summary.outcome === "aborted")
|
|
18
|
+
parts.push("aborted");
|
|
19
|
+
if (summary.outcome === "error")
|
|
20
|
+
parts.push("error");
|
|
21
|
+
return parts.join(" · ");
|
|
22
|
+
}
|
|
23
|
+
/** `── label ─────────` filling `width`; label is dropped when the row is too narrow. */
|
|
24
|
+
export function ruleLine(width, label) {
|
|
25
|
+
const lead = "── ";
|
|
26
|
+
const tail = " ";
|
|
27
|
+
const minRule = 4;
|
|
28
|
+
if (width <= 0)
|
|
29
|
+
return "";
|
|
30
|
+
if (visibleWidth(lead) + visibleWidth(label) + visibleWidth(tail) + minRule > width) {
|
|
31
|
+
return "─".repeat(width);
|
|
32
|
+
}
|
|
33
|
+
const shown = truncateToWidth(label, width - lead.length - tail.length - minRule, "…");
|
|
34
|
+
const rest = Math.max(minRule, width - visibleWidth(lead) - visibleWidth(shown) - visibleWidth(tail));
|
|
35
|
+
return `${lead}${shown}${tail}${"─".repeat(rest)}`;
|
|
36
|
+
}
|
|
37
|
+
export function runOutcome(messages) {
|
|
38
|
+
if (!Array.isArray(messages))
|
|
39
|
+
return "completed";
|
|
40
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
41
|
+
const message = messages[i];
|
|
42
|
+
if (message?.role !== "assistant")
|
|
43
|
+
continue;
|
|
44
|
+
if (message.stopReason === "aborted")
|
|
45
|
+
return "aborted";
|
|
46
|
+
if (message.stopReason === "error")
|
|
47
|
+
return "error";
|
|
48
|
+
return "completed";
|
|
49
|
+
}
|
|
50
|
+
return "completed";
|
|
51
|
+
}
|
|
52
|
+
export function isTurnSummary(data) {
|
|
53
|
+
if (!data || typeof data !== "object")
|
|
54
|
+
return false;
|
|
55
|
+
const d = data;
|
|
56
|
+
return (typeof d.endedAt === "number" &&
|
|
57
|
+
typeof d.elapsedMs === "number" &&
|
|
58
|
+
typeof d.tools === "number" &&
|
|
59
|
+
(d.outcome === "completed" || d.outcome === "aborted" || d.outcome === "error"));
|
|
60
|
+
}
|
|
61
|
+
export function installTurnRule(pi) {
|
|
62
|
+
let ctx;
|
|
63
|
+
let startedAt = 0;
|
|
64
|
+
let tools = 0;
|
|
65
|
+
if (typeof pi.registerEntryRenderer !== "function" || typeof pi.appendEntry !== "function") {
|
|
66
|
+
return () => { };
|
|
67
|
+
}
|
|
68
|
+
pi.registerEntryRenderer(TURN_ENTRY_TYPE, (entry, _options, theme) => {
|
|
69
|
+
if (!isTurnSummary(entry.data))
|
|
70
|
+
return undefined;
|
|
71
|
+
const label = turnLabel(entry.data);
|
|
72
|
+
const color = entry.data.outcome === "completed" ? "borderMuted" : entry.data.outcome === "aborted" ? "warning" : "error";
|
|
73
|
+
return {
|
|
74
|
+
render(width) {
|
|
75
|
+
const inner = Math.max(0, width - margin() * 2);
|
|
76
|
+
const rule = ruleLine(inner, label);
|
|
77
|
+
const colored = rule.replace(label, theme.fg("dim", label));
|
|
78
|
+
return ["", `${" ".repeat(margin())}${theme.fg(color, colored)}`];
|
|
79
|
+
},
|
|
80
|
+
invalidate() { },
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
pi.on("session_start", (_event, next) => {
|
|
84
|
+
if (next.mode !== "tui")
|
|
85
|
+
return;
|
|
86
|
+
ctx = next;
|
|
87
|
+
});
|
|
88
|
+
pi.on("agent_start", () => {
|
|
89
|
+
startedAt = Date.now();
|
|
90
|
+
tools = 0;
|
|
91
|
+
});
|
|
92
|
+
pi.on("tool_execution_end", () => {
|
|
93
|
+
tools += 1;
|
|
94
|
+
});
|
|
95
|
+
pi.on("agent_end", (event) => {
|
|
96
|
+
if (!ctx || ctx.mode !== "tui" || !startedAt)
|
|
97
|
+
return;
|
|
98
|
+
const ev = event;
|
|
99
|
+
const summary = {
|
|
100
|
+
endedAt: Date.now(),
|
|
101
|
+
elapsedMs: Date.now() - startedAt,
|
|
102
|
+
tools,
|
|
103
|
+
outcome: runOutcome(ev.messages),
|
|
104
|
+
};
|
|
105
|
+
startedAt = 0;
|
|
106
|
+
try {
|
|
107
|
+
pi.appendEntry(TURN_ENTRY_TYPE, summary);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
// A session that cannot accept entries just goes without the separator.
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
pi.on("session_shutdown", () => {
|
|
114
|
+
ctx = undefined;
|
|
115
|
+
startedAt = 0;
|
|
116
|
+
});
|
|
117
|
+
return () => {
|
|
118
|
+
ctx = undefined;
|
|
119
|
+
startedAt = 0;
|
|
120
|
+
};
|
|
121
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-minimal",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"private": false,
|
|
@@ -10,9 +10,20 @@
|
|
|
10
10
|
"keywords": [
|
|
11
11
|
"pi-package"
|
|
12
12
|
],
|
|
13
|
+
"repository": {
|
|
14
|
+
"type": "git",
|
|
15
|
+
"url": "git+https://github.com/raviy06/pi-minimal.git"
|
|
16
|
+
},
|
|
17
|
+
"homepage": "https://github.com/raviy06/pi-minimal#readme",
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/raviy06/pi-minimal/issues"
|
|
20
|
+
},
|
|
13
21
|
"pi": {
|
|
14
22
|
"extensions": [
|
|
15
23
|
"./dist/index.js"
|
|
24
|
+
],
|
|
25
|
+
"themes": [
|
|
26
|
+
"./themes/minimal-dark.json"
|
|
16
27
|
]
|
|
17
28
|
},
|
|
18
29
|
"peerDependencies": {
|
|
@@ -20,8 +31,8 @@
|
|
|
20
31
|
"@earendil-works/pi-tui": "*"
|
|
21
32
|
},
|
|
22
33
|
"devDependencies": {
|
|
23
|
-
"@earendil-works/pi-coding-agent": "
|
|
24
|
-
"@earendil-works/pi-tui": "
|
|
34
|
+
"@earendil-works/pi-coding-agent": "^1.4.0",
|
|
35
|
+
"@earendil-works/pi-tui": "^1.4.0",
|
|
25
36
|
"@eslint/js": "^10.0.1",
|
|
26
37
|
"@types/node": "*",
|
|
27
38
|
"eslint": "^10.11.0",
|
package/release-notes.md
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
### Added
|
|
2
|
+
- Fenced code renders as a closed box (`╭─ lang ─╮` / `│ code │` / `╰───╯`)
|
|
3
|
+
instead of literal ``` lines, keeping syntax highlighting; long lines wrap
|
|
4
|
+
inside the box. pi-tui exposes no hook for this, so pi-minimal post-processes
|
|
5
|
+
the code token's rendered lines.
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- Chrome margin (status line, footer, turn rule, tool rows) follows pi's
|
|
9
|
+
`outputPad` setting so it lines up with transcript text. Set
|
|
10
|
+
`editorPaddingX` to the same value for a flush left edge.
|
|
11
|
+
- Chrome modules tolerate a stale `chrome` module after `/reload`, which
|
|
12
|
+
previously could crash the renderer when the package was updated in place.
|
|
13
|
+
- `mdCodeBlockBorder` in `minimal-dark` is a faint grey again (it frames the
|
|
14
|
+
box now) instead of background-coloured.
|
|
15
|
+
|
|
16
|
+
## What's Changed
|
|
17
|
+
* Fix trusted publishing: upgrade npm, make Publish re-runnable by @raviy06 in https://github.com/raviy06/pi-minimal/pull/7
|
|
18
|
+
* Add Dependabot configuration file by @raviy06 in https://github.com/raviy06/pi-minimal/pull/9
|
|
19
|
+
* Create SECURITY.md for security policy by @raviy06 in https://github.com/raviy06/pi-minimal/pull/10
|
|
20
|
+
* Pin npm 11 for trusted publishing by @raviy06 in https://github.com/raviy06/pi-minimal/pull/8
|
|
21
|
+
* Drop registry-url so npm uses OIDC instead of a placeholder token by @raviy06 in https://github.com/raviy06/pi-minimal/pull/11
|
|
22
|
+
* Add repository metadata required for npm provenance by @raviy06 in https://github.com/raviy06/pi-minimal/pull/12
|
|
23
|
+
* Frame fenced code, align chrome with outputPad (0.2.2) by @raviy06 in https://github.com/raviy06/pi-minimal/pull/13
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
**Full Changelog**: https://github.com/raviy06/pi-minimal/compare/v0.2.1...v0.2.2
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
**Install / upgrade**
|
|
30
|
+
```bash
|
|
31
|
+
pi install npm:pi-minimal@0.2.2 # or: pi update --extensions
|
|
32
|
+
```
|
package/src/activity.ts
CHANGED
|
@@ -10,7 +10,3 @@ export function rowIsRunning(context: ActivityContext): boolean {
|
|
|
10
10
|
if (context.isPartial === false) return false
|
|
11
11
|
return true
|
|
12
12
|
}
|
|
13
|
-
|
|
14
|
-
export function activityBullet(running: boolean, frame: 0 | 1): "◆" | "◇" {
|
|
15
|
-
return running && frame === 1 ? "◇" : "◆"
|
|
16
|
-
}
|
package/src/chrome.ts
CHANGED
|
@@ -1 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Horizontal margin for pi-minimal's chrome (status line, footer, turn rule, tool rows).
|
|
3
|
+
* Follows pi's `outputPad` setting so chrome lines up with transcript text. Pi's editor uses
|
|
4
|
+
* the separate `editorPaddingX` setting (default 0); set it to the same value for a flush
|
|
5
|
+
* left edge.
|
|
6
|
+
*/
|
|
7
|
+
const KEY = Symbol.for("pi-minimal.chrome-margin")
|
|
8
|
+
const store = globalThis as unknown as Record<symbol, number | undefined>
|
|
9
|
+
|
|
10
|
+
/** Kept for modules linked against an older copy of this file across a hot reload. */
|
|
1
11
|
export const CHROME_MARGIN = 1
|
|
12
|
+
|
|
13
|
+
export function chromeMargin(): number {
|
|
14
|
+
return store[KEY] ?? CHROME_MARGIN
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function setChromeMargin(value: number): void {
|
|
18
|
+
store[KEY] = value === 0 ? 0 : 1
|
|
19
|
+
}
|
package/src/code-box.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Markdown, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* pi-tui renders fenced code as literal ``` lines. Pi exposes no hook for the code token, so
|
|
5
|
+
* pi-minimal post-processes the rendered lines of every `code` token into a closed box:
|
|
6
|
+
*
|
|
7
|
+
* ╭─ json ──────────────╮
|
|
8
|
+
* │ { "a": 1 } │
|
|
9
|
+
* ╰─────────────────────╯
|
|
10
|
+
*
|
|
11
|
+
* Code lines longer than the box are wrapped inside it so the right edge stays intact.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
type CodeToken = { type?: string; lang?: string; text?: string }
|
|
15
|
+
type MarkdownThemeLike = {
|
|
16
|
+
codeBlockBorder: (text: string) => string
|
|
17
|
+
codeBlockIndent?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const PATCHED = Symbol.for("pi-minimal.code-box")
|
|
21
|
+
/** Current framing implementation. Lives on globalThis so a /reload swaps the behaviour of the
|
|
22
|
+
* wrapper that an earlier load already installed on pi-tui's shared Markdown prototype. */
|
|
23
|
+
const FRAME_SLOT = Symbol.for("pi-minimal.code-box.frame")
|
|
24
|
+
|
|
25
|
+
type Framer = (lines: string[], lang: string | undefined, width: number, theme: MarkdownThemeLike) => string[]
|
|
26
|
+
const slots = globalThis as unknown as Record<symbol, Framer | undefined>
|
|
27
|
+
|
|
28
|
+
export function frameCodeLines(lines: string[], lang: string | undefined, width: number, theme: MarkdownThemeLike): string[] {
|
|
29
|
+
if (lines.length < 2) return lines
|
|
30
|
+
// Trailing spacer ("" after the closing fence) is preserved as-is.
|
|
31
|
+
let end = lines.length
|
|
32
|
+
while (end > 0 && lines[end - 1] === "") end -= 1
|
|
33
|
+
if (end < 2) return lines
|
|
34
|
+
const trailer = lines.slice(end)
|
|
35
|
+
const body = lines.slice(1, end - 1)
|
|
36
|
+
const indent = theme.codeBlockIndent ?? " "
|
|
37
|
+
const outer = Math.max(6, width)
|
|
38
|
+
// "│ " + content + " │"
|
|
39
|
+
const innerWidth = outer - 4
|
|
40
|
+
|
|
41
|
+
const label = lang ? ` ${lang} ` : ""
|
|
42
|
+
const topRule = "─".repeat(Math.max(1, outer - 3 - visibleWidth(label)))
|
|
43
|
+
const top = theme.codeBlockBorder(`╭─${label}${topRule}╮`)
|
|
44
|
+
const bottom = theme.codeBlockBorder(`╰${"─".repeat(outer - 2)}╯`)
|
|
45
|
+
const edge = theme.codeBlockBorder("│")
|
|
46
|
+
|
|
47
|
+
const framed: string[] = []
|
|
48
|
+
for (const line of body) {
|
|
49
|
+
const content = line.startsWith(indent) ? line.slice(indent.length) : line
|
|
50
|
+
for (const piece of wrapTextWithAnsi(content, innerWidth)) {
|
|
51
|
+
const pad = Math.max(0, innerWidth - visibleWidth(piece))
|
|
52
|
+
framed.push(`${edge} ${piece}${" ".repeat(pad)} ${edge}`)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (framed.length === 0) framed.push(`${edge} ${" ".repeat(innerWidth)} ${edge}`)
|
|
56
|
+
return [top, ...framed, bottom, ...trailer]
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
type RenderToken = (this: { theme: MarkdownThemeLike }, token: CodeToken, width: number, next?: string, style?: unknown) => string[]
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Patch pi-tui's Markdown renderer. The prototype wrapper is installed once per process; every
|
|
63
|
+
* call (including after /reload) points it at this module's `frameCodeLines`.
|
|
64
|
+
* Returns false when the renderer is not patchable.
|
|
65
|
+
*/
|
|
66
|
+
export function installCodeBox(): boolean {
|
|
67
|
+
slots[FRAME_SLOT] = frameCodeLines
|
|
68
|
+
const proto = Markdown.prototype as unknown as Record<string | symbol, unknown>
|
|
69
|
+
if (proto[PATCHED]) return true
|
|
70
|
+
const original = proto.renderToken
|
|
71
|
+
if (typeof original !== "function") return false
|
|
72
|
+
const originalRender = original as RenderToken
|
|
73
|
+
proto.renderToken = function (this: { theme: MarkdownThemeLike }, token: CodeToken, width: number, next?: string, style?: unknown) {
|
|
74
|
+
const lines = originalRender.call(this, token, width, next, style)
|
|
75
|
+
if (token?.type !== "code" || !this.theme?.codeBlockBorder) return lines
|
|
76
|
+
const frame = slots[FRAME_SLOT]
|
|
77
|
+
if (!frame) return lines
|
|
78
|
+
try {
|
|
79
|
+
return frame(lines, token.lang, width, this.theme)
|
|
80
|
+
} catch {
|
|
81
|
+
return lines
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
proto[PATCHED] = true
|
|
85
|
+
return true
|
|
86
|
+
}
|