@gavin-hu/my-pi-agent 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -8
- package/README.zh-CN.md +14 -9
- package/extensions/bytefly/README.md +174 -101
- package/extensions/bytefly/footer/collect/entries.ts +1 -6
- package/extensions/bytefly/footer/collect/snapshot.ts +23 -9
- package/extensions/bytefly/footer/price/price.ts +12 -8
- package/extensions/bytefly/footer/register.ts +13 -1
- package/extensions/bytefly/footer/types.ts +11 -0
- package/extensions/bytefly/footer/unpriced.ts +36 -0
- package/extensions/bytefly/header/component.ts +15 -29
- package/extensions/bytefly/header/identity.ts +71 -0
- package/extensions/bytefly/header/register.ts +9 -3
- package/extensions/bytefly/index.ts +13 -5
- package/extensions/bytefly/message/format.ts +20 -13
- package/extensions/bytefly/message/register.ts +8 -7
- package/extensions/bytefly/panel/commands.ts +46 -25
- package/extensions/bytefly/panel/format.ts +49 -14
- package/extensions/bytefly/panel/join.ts +35 -9
- package/extensions/bytefly/panel/settings.ts +10 -27
- package/extensions/bytefly/panel/skills.ts +51 -0
- package/extensions/bytefly/panel/tui/extensions-view.ts +7 -61
- package/extensions/bytefly/panel/tui/frame.ts +9 -1
- package/extensions/bytefly/panel/tui/list-view.ts +72 -0
- package/extensions/bytefly/panel/tui/nav.ts +71 -3
- package/extensions/bytefly/panel/tui/settings-section.ts +250 -0
- package/extensions/bytefly/panel/tui/skills-view.ts +50 -0
- package/extensions/bytefly/panel/tui.ts +223 -331
- package/extensions/bytefly/panel/types.ts +26 -8
- package/extensions/bytefly/picker/discovery.ts +32 -27
- package/extensions/bytefly/picker/resources.ts +16 -8
- package/extensions/bytefly/picker/runtime.ts +62 -34
- package/extensions/bytefly/picker/toggle.ts +16 -10
- package/extensions/bytefly/picker/types.ts +31 -13
- package/extensions/bytefly/update/format.ts +66 -8
- package/extensions/bytefly/update/patch.ts +66 -0
- package/extensions/bytefly/update/register.ts +4 -11
- package/extensions/bytefly/update/release.ts +65 -0
- package/extensions/bytefly/update/state.ts +14 -0
- package/extensions/bytefly/update/suppress.ts +47 -29
- package/extensions/bytefly/update/types.ts +15 -5
- package/extensions/check/tui.ts +2 -0
- package/extensions/context/README.md +21 -11
- package/extensions/context/format.ts +10 -8
- package/extensions/context/report.ts +21 -8
- package/extensions/context/tui.ts +26 -10
- package/extensions/context/types.ts +5 -3
- package/extensions/file-browse/README.md +29 -1
- package/extensions/file-browse/client.ts +16 -463
- package/extensions/file-browse/html.ts +7 -5
- package/extensions/file-browse/palette.ts +224 -0
- package/extensions/file-browse/router.ts +2 -2
- package/extensions/file-browse/styles.ts +8 -3
- package/extensions/file-browse/tree.ts +381 -0
- package/extensions/file-read/README.md +4 -1
- package/extensions/file-read/extract/odt.ts +7 -5
- package/extensions/goal/tools.ts +6 -11
- package/extensions/job/index.ts +20 -45
- package/extensions/job/tools.ts +51 -77
- package/extensions/memory/tools.ts +4 -6
- package/extensions/memory/tui.ts +5 -1
- package/extensions/plan/README.md +88 -33
- package/extensions/plan/commands.ts +19 -4
- package/extensions/plan/config.ts +27 -0
- package/extensions/plan/plan-index.ts +3 -1
- package/extensions/plan/plans.ts +31 -13
- package/extensions/plan/review.ts +34 -14
- package/extensions/plan/runtime.ts +3 -1
- package/extensions/plan/steps.ts +43 -14
- package/extensions/plan/tools/cancel.ts +79 -0
- package/extensions/plan/tools/enter.ts +10 -10
- package/extensions/plan/tools/exit.ts +54 -43
- package/extensions/plan/tools/steps.ts +7 -19
- package/extensions/plan/tools/write.ts +22 -29
- package/extensions/plan/tools.ts +5 -2
- package/extensions/plan/tui.ts +9 -7
- package/extensions/plan/types.ts +8 -0
- package/extensions/plan/web-chrome.ts +101 -0
- package/extensions/plan/web-client.ts +98 -311
- package/extensions/plan/web-markdown.ts +222 -0
- package/extensions/plan/web-page.ts +28 -2
- package/extensions/plan/web-router.ts +2 -2
- package/extensions/rewind/checklist.ts +4 -0
- package/extensions/rewind/review-tui.ts +3 -1
- package/extensions/rewind/rewind-tui.ts +4 -0
- package/extensions/subagent/README.md +25 -12
- package/extensions/subagent/format.ts +72 -25
- package/extensions/subagent/index.ts +21 -47
- package/extensions/subagent/orchestrate.ts +18 -41
- package/extensions/subagent/render.ts +29 -42
- package/extensions/subagent/stream.ts +24 -18
- package/extensions/subagent/types.ts +7 -0
- package/extensions/todo/tools.ts +5 -6
- package/extensions/todo/tui.ts +5 -1
- package/extensions/watch/index.ts +10 -35
- package/extensions/watch/tools.ts +31 -59
- package/extensions/wechat/README.md +40 -18
- package/extensions/wechat/bridge.ts +38 -40
- package/extensions/wechat/client.ts +1 -4
- package/extensions/wechat/commands.ts +1 -1
- package/extensions/wechat/interaction.ts +25 -4
- package/extensions/wechat/login-screen.ts +2 -3
- package/extensions/wechat/notices.ts +7 -5
- package/extensions/wechat/prompt.ts +19 -4
- package/extensions/wechat/qrcode.ts +9 -9
- package/extensions/wechat/slash.ts +14 -0
- package/extensions/wechat/status-screen.ts +2 -2
- package/extensions/wechat/tools.ts +3 -11
- package/extensions/worktree/README.md +48 -12
- package/extensions/worktree/commands.ts +109 -65
- package/extensions/worktree/enter.ts +1 -1
- package/extensions/worktree/exit.ts +11 -1
- package/extensions/worktree/lifecycle.ts +9 -1
- package/extensions/worktree/screen.ts +263 -0
- package/extensions/worktree/status.ts +84 -38
- package/lib/README.md +7 -6
- package/lib/config.ts +12 -0
- package/lib/format.ts +6 -2
- package/lib/interaction.ts +18 -1
- package/lib/report.ts +75 -2
- package/lib/tool-result.ts +54 -4
- package/lib/tui/README.md +16 -8
- package/lib/tui/chrome.ts +27 -6
- package/lib/tui/dock-screen.ts +6 -2
- package/lib/tui/gauge.ts +9 -5
- package/lib/usage.ts +26 -0
- package/lib/web/README.md +131 -0
- package/lib/web/base.ts +19 -2
- package/lib/web/components.ts +10 -6
- package/lib/web/layout.ts +2 -0
- package/lib/web/markup.ts +2 -1
- package/lib/web/runtime.ts +103 -25
- package/lib/web/tokens.ts +23 -7
- package/package.json +3 -1
- package/skills/changelog/SKILL.md +57 -45
- package/skills/next/SKILL.md +184 -0
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Markdown half of the review client script.
|
|
3
|
+
*
|
|
4
|
+
* It is a fragment of the same IIFE `web-client.ts` ships (`WEB_MARKDOWN_JS` is
|
|
5
|
+
* interpolated into `REVIEW_CLIENT_JS`), kept in its own module so the client's
|
|
6
|
+
* event wiring and its document rendering are read one at a time. Every value it
|
|
7
|
+
* writes is a DOM text node: plan and comment text never becomes markup, and the
|
|
8
|
+
* only HTML it copies is the server's own `tok-*` spans.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const WEB_MARKDOWN_JS = String.raw` // A link is rendered only for an http(s) or mailto target. Resolving against
|
|
12
|
+
// the review origin is what catches a target that only looks relative: the
|
|
13
|
+
// URL parser strips a tab or newline, so "/\t/evil.com" is cross-origin once
|
|
14
|
+
// it is parsed, and a disguised target must stay text.
|
|
15
|
+
function safeHref(url, base) {
|
|
16
|
+
var trimmed = String(url).trim();
|
|
17
|
+
if (!trimmed || /^\/\//.test(trimmed)) return null;
|
|
18
|
+
var resolved;
|
|
19
|
+
try { resolved = new URL(trimmed, base); } catch (error) { return null; }
|
|
20
|
+
if (resolved.protocol === "mailto:") return trimmed;
|
|
21
|
+
if (resolved.protocol !== "http:" && resolved.protocol !== "https:") return null;
|
|
22
|
+
if (resolved.username || resolved.password) return null;
|
|
23
|
+
if (resolved.origin === new URL(base).origin) return resolved.href;
|
|
24
|
+
return /^https?:\/\//i.test(trimmed) ? resolved.href : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Inline token order matters: code, emphasis, and links are matched before a
|
|
28
|
+
// bare URL, so a URL inside a link target or a code span is never re-split.
|
|
29
|
+
var INLINE = /(\x60[^\x60]+\x60|\*\*[^*]+\*\*|\*[^*]+\*|\[[^\]]+\]\([^)]+\)|(?:https?:\/\/|www\.)[^\s<]+)/g;
|
|
30
|
+
|
|
31
|
+
function renderInline(container, text) {
|
|
32
|
+
String(text).split(INLINE).forEach(function (part) {
|
|
33
|
+
if (!part) return;
|
|
34
|
+
var match;
|
|
35
|
+
if ((match = /^\x60([^\x60]+)\x60$/.exec(part))) {
|
|
36
|
+
var code = el("code"); code.textContent = match[1]; container.appendChild(code); return;
|
|
37
|
+
}
|
|
38
|
+
if ((match = /^\*\*([^*]+)\*\*$/.exec(part))) {
|
|
39
|
+
var strong = el("strong"); strong.textContent = match[1]; container.appendChild(strong); return;
|
|
40
|
+
}
|
|
41
|
+
if ((match = /^\*([^*]+)\*$/.exec(part))) {
|
|
42
|
+
var em = el("em"); em.textContent = match[1]; container.appendChild(em); return;
|
|
43
|
+
}
|
|
44
|
+
if ((match = /^\[([^\]]+)\]\(([^)]+)\)$/.exec(part))) {
|
|
45
|
+
var href = safeHref(match[2], location.href);
|
|
46
|
+
if (!href) { container.appendChild(document.createTextNode(part)); return; }
|
|
47
|
+
var link = el("a"); link.textContent = match[1]; link.href = href; link.target = "_blank"; link.rel = "noreferrer noopener";
|
|
48
|
+
container.appendChild(link); return;
|
|
49
|
+
}
|
|
50
|
+
// GFM extended autolink: a bare http(s) or www. URL in running text.
|
|
51
|
+
if ((match = /^(?:https?:\/\/|www\.)[^\s<]+$/.exec(part))) {
|
|
52
|
+
var url = match[0].replace(/[.,;:!?*_~]+$/, "");
|
|
53
|
+
while (url.charAt(url.length - 1) === ")" && (url.match(/\(/g) || []).length < (url.match(/\)/g) || []).length) {
|
|
54
|
+
url = url.slice(0, -1).replace(/[.,;:!?*_~]+$/, "");
|
|
55
|
+
}
|
|
56
|
+
var dot = url.indexOf(".");
|
|
57
|
+
var target = /^www\./i.test(url) ? "http://" + url : url;
|
|
58
|
+
var safe = dot > 0 && /\.[^./]/.test(url.slice(dot)) ? safeHref(target, location.href) : null;
|
|
59
|
+
if (safe) {
|
|
60
|
+
var auto = el("a");
|
|
61
|
+
auto.textContent = url;
|
|
62
|
+
auto.href = safe;
|
|
63
|
+
auto.target = "_blank";
|
|
64
|
+
auto.rel = "noreferrer noopener";
|
|
65
|
+
container.appendChild(auto);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
container.appendChild(document.createTextNode(part));
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Copy only text and tok-* spans out of server-highlighted HTML, so a code
|
|
74
|
+
// line can never introduce markup even though it arrives as a string.
|
|
75
|
+
function copyTokens(from, to) {
|
|
76
|
+
Array.prototype.forEach.call(from.childNodes, function (node) {
|
|
77
|
+
if (node.nodeType === 3) { to.appendChild(document.createTextNode(node.nodeValue)); return; }
|
|
78
|
+
if (node.nodeType === 1 && node.tagName === "SPAN" && /^tok-/.test(node.className || "")) {
|
|
79
|
+
var span = el("span", node.className);
|
|
80
|
+
copyTokens(node, span);
|
|
81
|
+
to.appendChild(span);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
to.appendChild(document.createTextNode(node.textContent || ""));
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function codeBody(lines) {
|
|
89
|
+
var body = Array.isArray(lines) ? lines.slice() : [];
|
|
90
|
+
if (body.length && /^ {0,3}(\x60{3,}|~{3,})/.test(body[0])) body.shift();
|
|
91
|
+
if (body.length && /^ {0,3}(\x60{3,}|~{3,})/.test(body[body.length - 1])) body.pop();
|
|
92
|
+
return body;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function renderCode(wrap, block) {
|
|
96
|
+
var body = codeBody(block.lines);
|
|
97
|
+
var highlighted = data.code && data.code[String(block.start)];
|
|
98
|
+
var pre = el("pre", "bf-code");
|
|
99
|
+
var code = el("code");
|
|
100
|
+
if (Array.isArray(highlighted)) {
|
|
101
|
+
body.forEach(function (line, index) {
|
|
102
|
+
var span = el("span", "bf-code__line");
|
|
103
|
+
var html = highlighted[index];
|
|
104
|
+
if (html) {
|
|
105
|
+
try { copyTokens(new DOMParser().parseFromString(html, "text/html").body, span); }
|
|
106
|
+
catch (error) { span.textContent = line; }
|
|
107
|
+
} else {
|
|
108
|
+
span.textContent = line;
|
|
109
|
+
}
|
|
110
|
+
code.appendChild(span);
|
|
111
|
+
if (index < body.length - 1) code.appendChild(document.createTextNode("\n"));
|
|
112
|
+
});
|
|
113
|
+
} else {
|
|
114
|
+
code.textContent = body.join("\n");
|
|
115
|
+
}
|
|
116
|
+
pre.appendChild(code);
|
|
117
|
+
wrap.appendChild(pre);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// A GFM table: split each pipe row into cells and honour the alignment row.
|
|
121
|
+
function splitRow(line) {
|
|
122
|
+
var text = String(line).trim();
|
|
123
|
+
if (text.charAt(0) === "|") text = text.slice(1);
|
|
124
|
+
if (text.charAt(text.length - 1) === "|") text = text.slice(0, -1);
|
|
125
|
+
return text.split("|").map(function (cell) { return cell.trim(); });
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function columnAlign(delim) {
|
|
129
|
+
return splitRow(delim).map(function (cell) {
|
|
130
|
+
var left = cell.charAt(0) === ":";
|
|
131
|
+
var right = cell.charAt(cell.length - 1) === ":";
|
|
132
|
+
return left && right ? "center" : right ? "right" : left ? "left" : "";
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function renderTable(wrap, block) {
|
|
137
|
+
var rows = block.lines.map(splitRow);
|
|
138
|
+
var align = columnAlign(block.lines[1] || "");
|
|
139
|
+
var table = el("table", "review-table");
|
|
140
|
+
var thead = el("thead");
|
|
141
|
+
var headRow = el("tr");
|
|
142
|
+
rows[0].forEach(function (cell, index) {
|
|
143
|
+
var th = el("th");
|
|
144
|
+
if (align[index]) th.style.textAlign = align[index];
|
|
145
|
+
renderInline(th, cell);
|
|
146
|
+
headRow.appendChild(th);
|
|
147
|
+
});
|
|
148
|
+
thead.appendChild(headRow);
|
|
149
|
+
table.appendChild(thead);
|
|
150
|
+
var tbody = el("tbody");
|
|
151
|
+
rows.slice(2).forEach(function (cells) {
|
|
152
|
+
var tr = el("tr");
|
|
153
|
+
rows[0].forEach(function (unused, index) {
|
|
154
|
+
var td = el("td");
|
|
155
|
+
if (align[index]) td.style.textAlign = align[index];
|
|
156
|
+
renderInline(td, cells[index] || "");
|
|
157
|
+
tr.appendChild(td);
|
|
158
|
+
});
|
|
159
|
+
tbody.appendChild(tr);
|
|
160
|
+
});
|
|
161
|
+
table.appendChild(tbody);
|
|
162
|
+
var scroll = el("div", "review-table-wrap");
|
|
163
|
+
scroll.appendChild(table);
|
|
164
|
+
wrap.appendChild(scroll);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function renderBlockContent(wrap, block) {
|
|
168
|
+
var lines = Array.isArray(block.lines) ? block.lines : [];
|
|
169
|
+
if (block.kind === "heading") {
|
|
170
|
+
var text = lines.join(" ").replace(/^\s*#{1,6}\s*/, "");
|
|
171
|
+
var level = Math.min(6, Math.max(1, (lines[0].match(/^ {0,3}(#{1,6})/) || ["", "#"])[1].length));
|
|
172
|
+
var heading = el("h" + level);
|
|
173
|
+
renderInline(heading, text);
|
|
174
|
+
wrap.appendChild(heading);
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
if (block.kind === "code") { renderCode(wrap, block); return; }
|
|
178
|
+
if (block.kind === "table") { renderTable(wrap, block); return; }
|
|
179
|
+
if (block.kind === "hr") { wrap.appendChild(el("hr")); return; }
|
|
180
|
+
if (block.kind === "quote") {
|
|
181
|
+
var quote = el("blockquote");
|
|
182
|
+
renderInline(quote, lines.map(function (line) { return line.replace(/^\s*>\s?/, ""); }).join("\n"));
|
|
183
|
+
wrap.appendChild(quote);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
if (block.kind === "list") {
|
|
187
|
+
var items = [];
|
|
188
|
+
lines.forEach(function (line) {
|
|
189
|
+
var match = /^(\s*)([-*+]|\d+[.)])\s+(.*)$/.exec(line);
|
|
190
|
+
if (!match) {
|
|
191
|
+
// A following unmarked line is a CommonMark lazy continuation of the
|
|
192
|
+
// item above; join it so the paragraph hangs under the item's text.
|
|
193
|
+
var last = items[items.length - 1];
|
|
194
|
+
if (last) { if (line.trim()) last.rest += " " + line.trim(); return; }
|
|
195
|
+
var plain = el("p"); renderInline(plain, line); wrap.appendChild(plain); return;
|
|
196
|
+
}
|
|
197
|
+
var row = el("div", "bf-block__li");
|
|
198
|
+
row.style.paddingLeft = String(Math.floor(match[1].replace(/\t/g, " ").length / 2) * 16) + "px";
|
|
199
|
+
var markerText = match[2];
|
|
200
|
+
var rest = match[3];
|
|
201
|
+
var box = /^\[([ xX])\]\s*(.*)$/.exec(rest);
|
|
202
|
+
var marker = el("span", "marker");
|
|
203
|
+
if (box) {
|
|
204
|
+
var done = box[1].toLowerCase() === "x";
|
|
205
|
+
markerText = done ? "\u2713" : "\u2610";
|
|
206
|
+
marker.className = done ? "bf-block__marker bf-block__marker--done" : "bf-block__marker";
|
|
207
|
+
rest = box[2];
|
|
208
|
+
}
|
|
209
|
+
marker.textContent = markerText;
|
|
210
|
+
var body = el("span");
|
|
211
|
+
row.appendChild(marker); row.appendChild(body); wrap.appendChild(row);
|
|
212
|
+
items.push({ body: body, rest: rest });
|
|
213
|
+
});
|
|
214
|
+
// Render each body once, after its continuation lines are collected.
|
|
215
|
+
items.forEach(function (item) { renderInline(item.body, item.rest); });
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
var paragraph = el("p");
|
|
219
|
+
renderInline(paragraph, lines.join(" "));
|
|
220
|
+
wrap.appendChild(paragraph);
|
|
221
|
+
}
|
|
222
|
+
`;
|
|
@@ -179,7 +179,16 @@ ${WEB_COMPONENTS_CSS}
|
|
|
179
179
|
}
|
|
180
180
|
@media (prefers-reduced-motion:no-preference){.bf-block{transition:background var(--motion-fast) var(--motion-ease)}}
|
|
181
181
|
@media (hover:none){.bf-block__add{opacity:1}}
|
|
182
|
-
|
|
182
|
+
/* On paper the document is the whole page: no comment rail to hover, no composer,
|
|
183
|
+
no reading progress, and the comments stay with the blocks they belong to. */
|
|
184
|
+
@media print{
|
|
185
|
+
.bf-review{padding:0}
|
|
186
|
+
.bf-review>.bf-block{max-width:none;padding:0 0 6px}
|
|
187
|
+
.bf-block__rail,.bf-composer,.bf-block__add,.review-progress{display:none}
|
|
188
|
+
.bf-block:hover,.bf-block--active{background:none;box-shadow:none}
|
|
189
|
+
.bf-comments{padding-left:0}
|
|
190
|
+
.bf-comment{break-inside:avoid}
|
|
191
|
+
}`.trim();
|
|
183
192
|
|
|
184
193
|
/**
|
|
185
194
|
* The sidebar's one-line keyboard reminder. It names the keys a reviewer reaches
|
|
@@ -206,6 +215,22 @@ function reviewKeys(canApprove: boolean): KeyHint[] {
|
|
|
206
215
|
return rows;
|
|
207
216
|
}
|
|
208
217
|
|
|
218
|
+
/**
|
|
219
|
+
* The approval confirmation. Approving leaves plan mode and runs the plan, which
|
|
220
|
+
* cannot be undone from the page, so it is one deliberate dialog rather than the
|
|
221
|
+
* header button's single click. The shared `bf-dialog` styles and the shared
|
|
222
|
+
* dialog runtime supply the backdrop, the Tab trap, and `Esc`.
|
|
223
|
+
*/
|
|
224
|
+
function approveDialog(): string {
|
|
225
|
+
return `<div class="bf-dialog" id="approve-dialog" hidden>
|
|
226
|
+
<div class="bf-dialog__panel" role="dialog" aria-modal="true" aria-labelledby="approve-title">
|
|
227
|
+
<div class="bf-dialog__head"><h2 id="approve-title">Approve and run the plan?</h2></div>
|
|
228
|
+
<div class="bf-dialog__body"><p>Pi leaves plan mode and starts the work. The comments you added are saved with the plan.</p><p class="bf-dialog__hint">Choose <strong>Approve & run</strong> to go ahead, or <strong>Cancel</strong> to keep reviewing.</p></div>
|
|
229
|
+
<div class="bf-dialog__actions"><button type="button" class="bf-btn" id="approve-cancel">Cancel</button><button type="button" class="bf-btn bf-btn--primary" id="approve-confirm">Approve & run</button></div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>`;
|
|
232
|
+
}
|
|
233
|
+
|
|
209
234
|
/**
|
|
210
235
|
* Render the review page for `data`. The markup is a fixed shell; the client
|
|
211
236
|
* fills every dynamic field, so this function is the only place that needs the
|
|
@@ -258,6 +283,7 @@ ${saveButton}
|
|
|
258
283
|
${outlinePanel}<main class="bf-app__main bf-review" id="plan" aria-label="Plan"></main>
|
|
259
284
|
</div>
|
|
260
285
|
${keysSheet(reviewKeys(data.canApprove))}
|
|
286
|
+
${data.canApprove ? approveDialog() : ""}
|
|
261
287
|
<script type="application/json" id="review-data">${island}</script>
|
|
262
288
|
<script src="/client.js" defer></script>
|
|
263
289
|
</body>
|
|
@@ -271,7 +297,7 @@ ${keysSheet(reviewKeys(data.canApprove))}
|
|
|
271
297
|
*/
|
|
272
298
|
function reviewFacts(): string {
|
|
273
299
|
return `<div class="review-facts" id="review-facts">
|
|
274
|
-
<div class="review-fact"><span>Steps</span><span class="bf-progress"><span class="bf-progress__track"><span class="bf-progress__fill" id="step-fill"></span></span><span class="review-fact__value" id="step-count">0/0</span></span></div>
|
|
300
|
+
<div class="review-fact" id="step-fact"><span>Steps</span><span class="bf-progress"><span class="bf-progress__track"><span class="bf-progress__fill" id="step-fill"></span></span><span class="review-fact__value" id="step-count">0/0</span></span></div>
|
|
275
301
|
<div class="review-fact"><span>Comments</span><span class="review-fact__value" id="comment-total">0</span></div>
|
|
276
302
|
</div>`;
|
|
277
303
|
}
|
|
@@ -23,7 +23,7 @@ import {
|
|
|
23
23
|
scriptHeaders,
|
|
24
24
|
type ServeResponse,
|
|
25
25
|
} from "../../lib/http/index.ts";
|
|
26
|
-
import { THEME_JS } from "../../lib/web/index.ts";
|
|
26
|
+
import { THEME_JS, THEME_JS_PATH } from "../../lib/web/index.ts";
|
|
27
27
|
import { coerceAnnotations, type PlanAnnotation } from "./feedback.ts";
|
|
28
28
|
import { REVIEW_CLIENT_JS } from "./web-client.ts";
|
|
29
29
|
import { renderReviewPage, type ReviewPageData } from "./web-page.ts";
|
|
@@ -109,7 +109,7 @@ export async function handleReviewRequest(
|
|
|
109
109
|
const { pathname, search } = parsed;
|
|
110
110
|
|
|
111
111
|
if (pathname === "/favicon.ico") return { status: 204, headers: { "Cache-Control": "no-store" } };
|
|
112
|
-
if (pathname ===
|
|
112
|
+
if (pathname === THEME_JS_PATH) return { status: 200, headers: scriptHeaders(), body: THEME_JS };
|
|
113
113
|
if (pathname === "/client.js") return { status: 200, headers: scriptHeaders(), body: REVIEW_CLIENT_JS };
|
|
114
114
|
|
|
115
115
|
if (search.get("token") !== ctx.token) return jsonError(403, "Invalid or missing token.");
|
|
@@ -152,6 +152,8 @@ export abstract class ChecklistScreen<T> implements Component {
|
|
|
152
152
|
const rows = this.rows();
|
|
153
153
|
if (rows.length === 0 && this.options.emptyLabel !== undefined) {
|
|
154
154
|
return dockListFrame({
|
|
155
|
+
theme: this.theme,
|
|
156
|
+
width: w,
|
|
155
157
|
header,
|
|
156
158
|
body: emptyState(this.theme, w, { label: this.options.emptyLabel }),
|
|
157
159
|
hint: this.hint(w),
|
|
@@ -172,6 +174,8 @@ export abstract class ChecklistScreen<T> implements Component {
|
|
|
172
174
|
}
|
|
173
175
|
|
|
174
176
|
return dockListFrame({
|
|
177
|
+
theme: this.theme,
|
|
178
|
+
width: w,
|
|
175
179
|
header,
|
|
176
180
|
summary: clip(` ${paint(this.theme, "meta", this.summaryText())}`, w),
|
|
177
181
|
body,
|
|
@@ -28,7 +28,7 @@ import { SELECTION_MARKS, type PathChange, type ReviewEntry } from "./paths.ts";
|
|
|
28
28
|
|
|
29
29
|
/** Rows a diff screen shows when the terminal height is unknown. */
|
|
30
30
|
const DIFF_DEFAULT_ITEMS = 20;
|
|
31
|
-
/** Header, summary, blank, blank, hint, and
|
|
31
|
+
/** Header, summary, blank, blank, hint, and closing-rule rows around the diff. */
|
|
32
32
|
const DIFF_CHROME_ROWS = 6;
|
|
33
33
|
|
|
34
34
|
/** Lines one wheel notch moves in the diff pane. */
|
|
@@ -294,6 +294,8 @@ export class ReviewDiffComponent implements Component {
|
|
|
294
294
|
if (body.length === 0) body.push(...emptyState(this.theme, w, { label: "No textual diff." }));
|
|
295
295
|
|
|
296
296
|
return dockListFrame({
|
|
297
|
+
theme: this.theme,
|
|
298
|
+
width: w,
|
|
297
299
|
header,
|
|
298
300
|
summary,
|
|
299
301
|
body,
|
|
@@ -79,6 +79,8 @@ export class RewindListComponent implements Component {
|
|
|
79
79
|
|
|
80
80
|
if (this.points.length === 0) {
|
|
81
81
|
return dockListFrame({
|
|
82
|
+
theme: this.theme,
|
|
83
|
+
width: w,
|
|
82
84
|
header,
|
|
83
85
|
body: emptyState(this.theme, w, { label: "No prompts on this branch yet." }),
|
|
84
86
|
hint: this.hint(w),
|
|
@@ -122,6 +124,8 @@ export class RewindListComponent implements Component {
|
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
return dockListFrame({
|
|
127
|
+
theme: this.theme,
|
|
128
|
+
width: w,
|
|
125
129
|
header,
|
|
126
130
|
summary: clip(` ${paint(this.theme, "meta", `${count}${SEPARATORS.item}newest first`)}`, w),
|
|
127
131
|
body,
|
|
@@ -275,9 +275,18 @@ child's system prompt for behavior; the tool list is the enforcement.
|
|
|
275
275
|
→ grep /retry/ in ~/src
|
|
276
276
|
→ read ~/src/http.ts:40-120
|
|
277
277
|
Retries are configured in http.ts:64...
|
|
278
|
-
|
|
278
|
+
|
|
279
|
+
2 turns ↑12.4k ↓1.1k R8.0k ctx:18.2k claude-sonnet-4-5
|
|
279
280
|
```
|
|
280
281
|
|
|
282
|
+
The footnote is the same in every mode: a blank line, the faint usage row, then —
|
|
283
|
+
when the run has more to show — the expand affordance, so the hint is always the
|
|
284
|
+
block's last line. The row carries turns, token counts, context size, and the
|
|
285
|
+
model — never money. A price needs a currency and a tariff, and those belong to
|
|
286
|
+
the status bar, which shows every child run as its own `↳` roll-up priced in the
|
|
287
|
+
configured currency (see [Non-goals](#non-goals)). A parallel or chain `Total:`
|
|
288
|
+
row sums the counters, and names a model only when every task ran the same one.
|
|
289
|
+
|
|
281
290
|
**Expanded** (the `app.tools.expand` key, `ctrl+o` by default): the full task,
|
|
282
291
|
every intermediate assistant message and tool call, the final output rendered as
|
|
283
292
|
Markdown, and per-step/per-task usage with a total.
|
|
@@ -296,7 +305,9 @@ the first failing step also reports `stopped at step N`. Running tasks show an
|
|
|
296
305
|
elapsed-time label on the shared 1s repaint timer (the package's common
|
|
297
306
|
duration format, e.g. `12.0s`), and a finished run keeps its duration. A result
|
|
298
307
|
that hit tool errors appends `N tool errors` to its
|
|
299
|
-
usage line (and to the multi `Total:`).
|
|
308
|
+
usage line (and to the multi `Total:`). A usage payload the child reports with a
|
|
309
|
+
non-number (a string, `NaN`) contributes nothing, so a malformed event cannot
|
|
310
|
+
corrupt the row.
|
|
300
311
|
|
|
301
312
|
A chain step's expanded `Task` section shows the step's original instruction,
|
|
302
313
|
with `{previous}` left in place, rather than the substituted prior output that
|
|
@@ -317,15 +328,17 @@ step's `Output`.
|
|
|
317
328
|
details; keeping it on disk is opt-in, parent-written, and bounded by age and
|
|
318
329
|
count. It is an audit and handoff record, not a resume point: replaying a run
|
|
319
330
|
whose writes already landed is not attempted.
|
|
320
|
-
- **No cost attribution to the parent session.** A subagent's
|
|
331
|
+
- **No cost attribution to the parent session.** A subagent's token counts are
|
|
321
332
|
captured in the tool result and shown in the transcript, but they belong to a
|
|
322
|
-
separate process and are not added to the dispatching session's totals.
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
333
|
+
separate process and are not added to the dispatching session's totals. The
|
|
334
|
+
child's own dollar cost stays in the tool details (and in a run's `meta.json`)
|
|
335
|
+
and is never printed in the transcript: only the status bar prices a delegated
|
|
336
|
+
run, in the configured currency and tariff. Each finished run publishes
|
|
337
|
+
`my-pi-agent/subagent-finished` (`SubagentFinishedEvent` in `lib/events.ts`)
|
|
338
|
+
and bytefly's footer shows the session's child runs as their own `↳` roll-up,
|
|
339
|
+
priced separately from the session total (the last 500 runs). The roll-up is
|
|
340
|
+
per process: a resumed session starts from zero, and a run that never produced
|
|
341
|
+
an assistant message publishes nothing.
|
|
329
342
|
|
|
330
343
|
## Limitations
|
|
331
344
|
|
|
@@ -421,7 +434,7 @@ step's `Output`.
|
|
|
421
434
|
| `agents.ts` | Built-in agent definitions, external markdown discovery, and pool resolution. |
|
|
422
435
|
| `schema.ts` | TypeBox params, limits, and `resolveMode`. |
|
|
423
436
|
| `invocation.ts` | `pi` subprocess invocation and argument construction. |
|
|
424
|
-
| `stream.ts` | JSON-event parsing, usage accounting, output helpers (pure). |
|
|
437
|
+
| `stream.ts` | JSON-event parsing, guarded usage accounting, output helpers (pure). |
|
|
425
438
|
| `transcripts.ts` | Per-run transcript buffer, run ids, the store directory, and pruning. |
|
|
426
439
|
| `config.ts` | The `subagent` section of `bytefly.json`: deadline, transcripts, retention. |
|
|
427
440
|
| `run.ts` | Runs one subagent process and captures its result (including the background abort-as-result path). |
|
|
@@ -429,6 +442,6 @@ step's `Output`.
|
|
|
429
442
|
| `background-tui.ts` | The `/agents` board: a `DockListScreen` with a live output pane and width-aware facts. |
|
|
430
443
|
| `commands.ts` | The `/agents` command. |
|
|
431
444
|
| `ui.ts` | The running-count footer chip and finish notices. |
|
|
432
|
-
| `format.ts` | Token/usage/tool-call formatting (pure). |
|
|
445
|
+
| `format.ts` | Token/usage/total line and tool-call formatting (pure, money-free). |
|
|
433
446
|
| `render.ts` | `renderCall` / `renderResult` transcript rendering. |
|
|
434
447
|
| `types.ts` | Shared result and process types. |
|
|
@@ -1,46 +1,93 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure formatting helpers shared by the transcript renderers.
|
|
3
|
+
*
|
|
4
|
+
* The usage line keeps tokens, turns, model, and context — the facts the child
|
|
5
|
+
* reported — and never money: a price needs a currency and a tariff, which the
|
|
6
|
+
* bytefly footer owns for the whole session. See `usageLine` / `totalUsageLine`.
|
|
3
7
|
*/
|
|
4
8
|
|
|
5
9
|
import { stripTerminalSequences, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
6
10
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
7
11
|
import { formatDuration, formatTokens, sanitizeLine, shortenPath } from "../../lib/format.ts";
|
|
12
|
+
import { USAGE_COUNTERS, zeroUsage } from "../../lib/usage.ts";
|
|
8
13
|
import type { BackgroundRun } from "./background.ts";
|
|
9
|
-
import type { UsageStats } from "./types.ts";
|
|
14
|
+
import type { UsageSource, UsageStats } from "./types.ts";
|
|
10
15
|
|
|
11
|
-
/**
|
|
12
|
-
function
|
|
13
|
-
if (
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
/** `2 turns` / `1 turn`, or "" when no turn was counted. */
|
|
17
|
+
function turnPart(turns: number | undefined): string {
|
|
18
|
+
if (!turns) return "";
|
|
19
|
+
return `${turns} turn${turns > 1 ? "s" : ""}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A prefix and a compact count, or "" when the counter is zero. */
|
|
23
|
+
function tokenPart(prefix: string, count: number | undefined): string {
|
|
24
|
+
return count ? `${prefix}${formatTokens(count)}` : "";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** `ctx:18.2k`, or "" when no context was reported. */
|
|
28
|
+
function contextPart(tokens: number | undefined): string {
|
|
29
|
+
const count = tokens ?? 0;
|
|
30
|
+
return count > 0 ? `ctx:${formatTokens(count)}` : "";
|
|
16
31
|
}
|
|
17
32
|
|
|
18
33
|
/** A space-separated usage line, omitting zero fields. */
|
|
19
34
|
export function formatUsageStats(usage: Partial<UsageStats>, model?: string): string {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
35
|
+
return [
|
|
36
|
+
turnPart(usage.turns),
|
|
37
|
+
tokenPart("↑", usage.input),
|
|
38
|
+
tokenPart("↓", usage.output),
|
|
39
|
+
tokenPart("R", usage.cacheRead),
|
|
40
|
+
tokenPart("W", usage.cacheWrite),
|
|
41
|
+
contextPart(usage.contextTokens),
|
|
42
|
+
model ?? "",
|
|
43
|
+
]
|
|
44
|
+
.filter(Boolean)
|
|
45
|
+
.join(" ");
|
|
30
46
|
}
|
|
31
47
|
|
|
32
48
|
/** Sum usage across results, for the chain/parallel totals line. */
|
|
33
|
-
export function aggregateUsage(results: { usage: UsageStats }[]): Partial<UsageStats> {
|
|
34
|
-
const
|
|
49
|
+
export function aggregateUsage(results: readonly { usage: UsageStats }[]): Partial<UsageStats> {
|
|
50
|
+
const counters = zeroUsage();
|
|
51
|
+
let turns = 0;
|
|
35
52
|
for (const { usage } of results) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
total.cacheRead += usage.cacheRead;
|
|
39
|
-
total.cacheWrite += usage.cacheWrite;
|
|
40
|
-
total.cost += usage.cost;
|
|
41
|
-
total.turns += usage.turns;
|
|
53
|
+
for (const counter of USAGE_COUNTERS) counters[counter] += usage[counter];
|
|
54
|
+
turns += usage.turns;
|
|
42
55
|
}
|
|
43
|
-
return
|
|
56
|
+
return { ...counters, turns };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** "2 tool errors", "1 tool error", or "" when the run hit none. */
|
|
60
|
+
function toolErrorsLabel(count: number | undefined): string {
|
|
61
|
+
if (!count) return "";
|
|
62
|
+
return `${count} tool error${count > 1 ? "s" : ""}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The one model every result ran, or undefined when they differ.
|
|
67
|
+
*
|
|
68
|
+
* A total may name a model only when the name is true of all of it: a batch whose
|
|
69
|
+
* tasks pinned different models gets no label rather than one task's.
|
|
70
|
+
*/
|
|
71
|
+
function sharedModel(sources: readonly UsageSource[]): string | undefined {
|
|
72
|
+
const models = new Set(sources.map((source) => source.model).filter((model): model is string => Boolean(model)));
|
|
73
|
+
return models.size === 1 ? [...models][0] : undefined;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** One result's usage line, with its tool-error count when it hit any. */
|
|
77
|
+
export function usageLine(source: UsageSource): string {
|
|
78
|
+
return [formatUsageStats(source.usage, source.model), toolErrorsLabel(source.toolErrors)].filter(Boolean).join(" ");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The `Total:` line for a multi run: summed counters, the shared model when there
|
|
83
|
+
* is one, and summed tool errors. A total carries no `ctx:` — every child has its
|
|
84
|
+
* own context window, so a sum of them would mean nothing.
|
|
85
|
+
*/
|
|
86
|
+
export function totalUsageLine(sources: readonly UsageSource[]): string {
|
|
87
|
+
const usage = formatUsageStats(aggregateUsage(sources), sharedModel(sources));
|
|
88
|
+
const errors = sources.reduce((sum, source) => sum + (source.toolErrors ?? 0), 0);
|
|
89
|
+
const line = [usage, toolErrorsLabel(errors)].filter(Boolean).join(" ");
|
|
90
|
+
return line ? `Total: ${line}` : "";
|
|
44
91
|
}
|
|
45
92
|
|
|
46
93
|
/** Display-width-aware truncation with a trailing ellipsis (keeps wide chars/surrogates intact). */
|