@gavin-hu/my-pi-agent 0.16.0 → 0.18.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 +29 -15
- package/README.zh-CN.md +27 -15
- package/extensions/bytefly/README.md +195 -115
- 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/notify/types.ts +1 -1
- 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 +29 -39
- 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 +89 -22
- package/extensions/file-browse/chrome.ts +83 -0
- package/extensions/file-browse/client.ts +74 -459
- package/extensions/file-browse/config.ts +12 -3
- package/extensions/file-browse/diff-html.ts +94 -0
- package/extensions/file-browse/html.ts +26 -152
- package/extensions/file-browse/live-client.ts +47 -1
- package/extensions/file-browse/live.ts +35 -14
- package/extensions/file-browse/palette.ts +224 -0
- package/extensions/file-browse/rev.ts +42 -0
- package/extensions/file-browse/router.ts +27 -6
- package/extensions/file-browse/server.ts +7 -0
- package/extensions/file-browse/styles.ts +10 -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/plan-route.ts +8 -2
- package/extensions/goal/tools.ts +6 -11
- package/extensions/job/README.md +7 -5
- package/extensions/job/index.ts +35 -52
- package/extensions/job/tools.ts +51 -77
- package/extensions/memory/README.md +3 -3
- package/extensions/memory/tools.ts +4 -6
- package/extensions/memory/tui.ts +5 -1
- package/extensions/plan/README.md +333 -84
- package/extensions/plan/commands.ts +73 -7
- package/extensions/plan/feedback.ts +300 -63
- package/extensions/plan/index.ts +1 -1
- package/extensions/plan/plan-index.ts +4 -2
- package/extensions/plan/plan-text.ts +18 -1
- package/extensions/plan/plans.ts +184 -58
- package/extensions/plan/review-prompt.ts +93 -0
- package/extensions/plan/review.ts +95 -95
- package/extensions/plan/runtime.ts +4 -2
- package/extensions/plan/split.ts +87 -0
- package/extensions/plan/steps.ts +123 -107
- package/extensions/plan/tools/cancel.ts +79 -0
- package/extensions/plan/tools/enter.ts +17 -12
- package/extensions/plan/tools/exit.ts +148 -59
- package/extensions/plan/tools/steps.ts +7 -19
- package/extensions/plan/tools/write.ts +29 -31
- package/extensions/plan/tools.ts +6 -3
- package/extensions/plan/tui.ts +60 -15
- package/extensions/plan/types.ts +45 -3
- package/extensions/plan/web-chrome.ts +102 -0
- package/extensions/plan/web-client.ts +177 -446
- package/extensions/plan/web-collab.ts +207 -0
- package/extensions/plan/web-comments.ts +269 -0
- package/extensions/plan/web-markdown.ts +222 -0
- package/extensions/plan/web-page.ts +131 -19
- package/extensions/plan/web-router.ts +148 -32
- package/extensions/plan/web-share-session.ts +197 -0
- package/extensions/plan/web-share.ts +169 -0
- package/extensions/plan/web-streams.ts +124 -0
- package/extensions/plan/web-tokens.ts +63 -0
- package/extensions/plan/web.ts +193 -45
- package/extensions/question/README.md +3 -0
- package/extensions/question/dialogs.ts +91 -1
- package/extensions/question/tools.ts +2 -1
- 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/session-bus/README.md +247 -0
- package/extensions/session-bus/commands.ts +165 -0
- package/extensions/session-bus/format.ts +124 -0
- package/extensions/session-bus/index.ts +69 -0
- package/extensions/session-bus/notice.ts +58 -0
- package/extensions/session-bus/protocol.ts +134 -0
- package/extensions/session-bus/result.ts +243 -0
- package/extensions/session-bus/rows.ts +169 -0
- package/extensions/session-bus/runtime.ts +575 -0
- package/extensions/session-bus/store.ts +329 -0
- package/extensions/session-bus/team.ts +102 -0
- package/extensions/session-bus/tools.ts +227 -0
- package/extensions/session-bus/tui.ts +394 -0
- package/extensions/session-bus/types.ts +98 -0
- package/extensions/subagent/README.md +26 -13
- 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/README.md +9 -5
- package/extensions/watch/index.ts +25 -44
- package/extensions/watch/tools.ts +31 -59
- package/extensions/wechat/README.md +45 -36
- package/extensions/wechat/bridge.ts +46 -48
- package/extensions/wechat/client.ts +1 -4
- package/extensions/wechat/commands.ts +3 -3
- package/extensions/wechat/deliver.ts +57 -37
- package/extensions/wechat/interaction.ts +54 -18
- package/extensions/wechat/login-screen.ts +2 -3
- package/extensions/wechat/notices.ts +9 -7
- package/extensions/wechat/peer-worker.ts +19 -34
- package/extensions/wechat/presence.ts +3 -4
- package/extensions/wechat/prompt.ts +53 -23
- package/extensions/wechat/reply-pipeline.ts +12 -4
- 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/git.ts +5 -0
- package/extensions/worktree/lifecycle.ts +9 -1
- package/extensions/worktree/registry.ts +11 -5
- package/extensions/worktree/screen.ts +263 -0
- package/extensions/worktree/status.ts +84 -40
- package/lib/README.md +12 -7
- package/lib/claims/overlap.ts +43 -0
- package/lib/config.ts +12 -0
- package/lib/format.ts +6 -2
- package/lib/http/router.ts +40 -13
- package/lib/http/server.ts +27 -13
- package/lib/interaction.ts +28 -1
- package/lib/notice.ts +29 -2
- package/lib/plan-entry.ts +7 -2
- package/lib/plans/sections.ts +172 -0
- package/lib/plans/setting.ts +52 -0
- package/lib/plans/split.ts +215 -0
- package/lib/plans/store.ts +19 -0
- package/lib/plans/team.ts +151 -0
- package/lib/qr.ts +64 -0
- package/lib/repo-key.ts +74 -0
- package/lib/report.ts +117 -25
- package/lib/requirements.ts +1 -2
- package/lib/tool-result.ts +54 -4
- package/lib/tui/README.md +30 -8
- package/lib/tui/chrome.ts +27 -6
- package/lib/tui/dock-screen.ts +91 -19
- package/lib/tui/gauge.ts +9 -5
- package/lib/tui/line-pane.ts +15 -4
- package/lib/tui/tool.ts +23 -4
- 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 +5 -2
- package/skills/changelog/SKILL.md +57 -45
- package/skills/init/SKILL.md +80 -45
- package/skills/init/references/agents-md-quality.md +26 -2
- package/skills/init/references/interview-questions.md +36 -14
- package/skills/next/SKILL.md +209 -0
- package/skills/parallel-dev/SKILL.md +150 -0
- package/extensions/wechat/qrcode.ts +0 -46
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The browse page's sidebar: the path filter, the fold-all control, the lazy
|
|
3
|
+
* tree, and the tree's keyboard model.
|
|
4
|
+
*
|
|
5
|
+
* It is a fragment of the same IIFE `client.ts` ships (`TREE_CLIENT_JS` is
|
|
6
|
+
* interpolated into `CLIENT_JS`), kept in its own module because the sidebar is
|
|
7
|
+
* one concern with its own state: the filter query, each folder's `data-open`
|
|
8
|
+
* intent, and the one row the keyboard is on.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { SIDEBAR_COLLAPSE_MAX } from "../../lib/web/index.ts";
|
|
12
|
+
|
|
13
|
+
export const TREE_CLIENT_JS = ` var tree = document.querySelector("nav.bf-tree");
|
|
14
|
+
var filter = document.getElementById("filter");
|
|
15
|
+
var foldAllButton = document.getElementById("fold-all");
|
|
16
|
+
var query = "";
|
|
17
|
+
|
|
18
|
+
function matches(text) {
|
|
19
|
+
return !query || text.toLowerCase().indexOf(query) !== -1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// A row is found from inside it (an event target) or from its own list item:
|
|
23
|
+
// closest() only looks upwards, so paint() cannot start from the li.
|
|
24
|
+
function rowOf(node) {
|
|
25
|
+
var row = node && node.closest ? node.closest(".bf-tree__row") : null;
|
|
26
|
+
return row && tree && tree.contains(row) ? row : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function rowFor(li) {
|
|
30
|
+
return li ? li.querySelector(":scope > .bf-tree__row") : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function rowLink(row) {
|
|
34
|
+
return row ? row.querySelector("a") : null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
38
|
+
|
|
39
|
+
function svgIcon(entry) {
|
|
40
|
+
var svg = document.createElementNS(SVG_NS, "svg");
|
|
41
|
+
svg.setAttribute("class", "browse-cat " + (entry.colorClass || ""));
|
|
42
|
+
svg.setAttribute("aria-hidden", "true");
|
|
43
|
+
svg.setAttribute("title", entry.label || "");
|
|
44
|
+
var use = document.createElementNS(SVG_NS, "use");
|
|
45
|
+
use.setAttribute("href", entry.iconHref);
|
|
46
|
+
svg.appendChild(use);
|
|
47
|
+
return svg;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildNode(entry) {
|
|
51
|
+
var li = document.createElement("li");
|
|
52
|
+
var row = document.createElement("div");
|
|
53
|
+
row.className = "bf-tree__row";
|
|
54
|
+
row.setAttribute("data-path", entry.path || "");
|
|
55
|
+
var twisty = document.createElement(entry.isDir ? "button" : "span");
|
|
56
|
+
twisty.className = "bf-tree__tw";
|
|
57
|
+
row.appendChild(twisty);
|
|
58
|
+
row.appendChild(svgIcon(entry));
|
|
59
|
+
if (entry.isDir) {
|
|
60
|
+
li.dataset.open = "false";
|
|
61
|
+
twisty.classList.add("bf-tree__tw--toggle");
|
|
62
|
+
twisty.type = "button";
|
|
63
|
+
twisty.tabIndex = -1;
|
|
64
|
+
twisty.setAttribute("aria-hidden", "true");
|
|
65
|
+
twisty.textContent = "\\u25B8";
|
|
66
|
+
var dir = document.createElement("a");
|
|
67
|
+
dir.href = entry.href;
|
|
68
|
+
dir.tabIndex = -1;
|
|
69
|
+
dir.setAttribute("aria-expanded", "false");
|
|
70
|
+
dir.textContent = entry.name + "/";
|
|
71
|
+
row.appendChild(dir);
|
|
72
|
+
} else if (entry.broken) {
|
|
73
|
+
twisty.className = "bf-tree__tw bf-tree__tw--spacer";
|
|
74
|
+
twisty.setAttribute("aria-hidden", "true");
|
|
75
|
+
var broken = document.createElement("span");
|
|
76
|
+
broken.className = "browse-broken";
|
|
77
|
+
broken.title = "broken symbolic link";
|
|
78
|
+
broken.textContent = entry.name;
|
|
79
|
+
row.appendChild(broken);
|
|
80
|
+
var mark = document.createElement("span");
|
|
81
|
+
mark.className = "browse-link-mark";
|
|
82
|
+
mark.textContent = "\\u21A9";
|
|
83
|
+
row.appendChild(mark);
|
|
84
|
+
} else {
|
|
85
|
+
twisty.className = "bf-tree__tw bf-tree__tw--spacer";
|
|
86
|
+
twisty.setAttribute("aria-hidden", "true");
|
|
87
|
+
var file = document.createElement("a");
|
|
88
|
+
file.href = entry.href;
|
|
89
|
+
file.tabIndex = -1;
|
|
90
|
+
file.textContent = entry.name;
|
|
91
|
+
row.appendChild(file);
|
|
92
|
+
}
|
|
93
|
+
li.appendChild(row);
|
|
94
|
+
return li;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function filterNode(li) {
|
|
98
|
+
var row = li.querySelector(":scope > .bf-tree__row");
|
|
99
|
+
var own = matches(row ? row.getAttribute("data-path") || row.textContent : "");
|
|
100
|
+
var childMatch = false;
|
|
101
|
+
Array.prototype.forEach.call(li.querySelectorAll(":scope > ul > li"), function (child) {
|
|
102
|
+
if (filterNode(child)) childMatch = true;
|
|
103
|
+
});
|
|
104
|
+
var visible = own || childMatch;
|
|
105
|
+
li.hidden = !visible;
|
|
106
|
+
return visible;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function applyFilter() {
|
|
110
|
+
var empty = document.getElementById("tree-empty");
|
|
111
|
+
var anyTree = false;
|
|
112
|
+
if (tree) {
|
|
113
|
+
Array.prototype.forEach.call(tree.querySelectorAll(":scope > ul > li"), function (li) {
|
|
114
|
+
if (filterNode(li)) anyTree = true;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (empty) empty.hidden = !tree || anyTree;
|
|
118
|
+
Array.prototype.forEach.call(document.querySelectorAll("table.bf-table tbody tr"), function (row) {
|
|
119
|
+
var path = row.getAttribute("data-path") || row.getAttribute("data-name") || "";
|
|
120
|
+
row.hidden = !matches(path);
|
|
121
|
+
});
|
|
122
|
+
// Lists are repainted too: while a query is active every list is shown, and
|
|
123
|
+
// clearing the query brings the folds back.
|
|
124
|
+
if (tree) Array.prototype.forEach.call(tree.querySelectorAll("li[data-open]"), paint);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (filter) {
|
|
128
|
+
filter.addEventListener("input", function () {
|
|
129
|
+
query = filter.value.trim().toLowerCase();
|
|
130
|
+
applyFilter();
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Visibility is derived: the intent in data-open, opened up while a filter is
|
|
135
|
+
// active so a fold cannot hide a match.
|
|
136
|
+
function isOpen(li) {
|
|
137
|
+
return query !== "" || li.dataset.open === "true";
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function paint(li) {
|
|
141
|
+
var row = rowFor(li);
|
|
142
|
+
var twisty = row ? row.querySelector(":scope > .bf-tree__tw") : null;
|
|
143
|
+
if (!twisty || twisty.tagName !== "BUTTON") return;
|
|
144
|
+
var open = isOpen(li);
|
|
145
|
+
// A pending load owns the glyph until it lands or fails.
|
|
146
|
+
if (!li.dataset.loading && !li.dataset.error) twisty.textContent = open ? "\\u25BE" : "\\u25B8";
|
|
147
|
+
var link = rowLink(row);
|
|
148
|
+
if (link) link.setAttribute("aria-expanded", open ? "true" : "false");
|
|
149
|
+
var list = li.querySelector(":scope > ul");
|
|
150
|
+
if (list) list.hidden = !open;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function setOpen(li, open) {
|
|
154
|
+
li.dataset.open = open ? "true" : "false";
|
|
155
|
+
paint(li);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Every dir with rendered children: what the fold-all control acts on.
|
|
159
|
+
function foldableDirs() {
|
|
160
|
+
var dirs = [];
|
|
161
|
+
if (!tree) return dirs;
|
|
162
|
+
Array.prototype.forEach.call(tree.querySelectorAll("li"), function (li) {
|
|
163
|
+
if (li.querySelector(":scope > ul")) dirs.push(li);
|
|
164
|
+
});
|
|
165
|
+
return dirs;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function syncFoldButton() {
|
|
169
|
+
if (!foldAllButton) return;
|
|
170
|
+
var dirs = foldableDirs();
|
|
171
|
+
var open = 0;
|
|
172
|
+
dirs.forEach(function (li) {
|
|
173
|
+
if (li.dataset.open === "true") open++;
|
|
174
|
+
});
|
|
175
|
+
foldAllButton.hidden = dirs.length === 0;
|
|
176
|
+
foldAllButton.textContent = open > 0 ? "fold all" : "expand all";
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function setAll(open) {
|
|
180
|
+
foldableDirs().forEach(function (li) {
|
|
181
|
+
setOpen(li, open);
|
|
182
|
+
});
|
|
183
|
+
syncFoldButton();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function loadChildren(li, twisty, path) {
|
|
187
|
+
var link = rowLink(rowFor(li));
|
|
188
|
+
var name = link ? link.textContent.slice(0, -1) : path;
|
|
189
|
+
li.dataset.loading = "true";
|
|
190
|
+
twisty.textContent = "\\u2026";
|
|
191
|
+
if (link) link.setAttribute("aria-busy", "true");
|
|
192
|
+
fetch("/api/tree?path=" + encodeURIComponent(path))
|
|
193
|
+
.then(function (response) {
|
|
194
|
+
if (!response.ok) throw new Error("load failed");
|
|
195
|
+
return response.json();
|
|
196
|
+
})
|
|
197
|
+
.then(function (data) {
|
|
198
|
+
var built = document.createElement("ul");
|
|
199
|
+
(data.entries || []).forEach(function (entry) {
|
|
200
|
+
built.appendChild(buildNode(entry));
|
|
201
|
+
});
|
|
202
|
+
li.appendChild(built);
|
|
203
|
+
delete li.dataset.loading;
|
|
204
|
+
delete li.dataset.error;
|
|
205
|
+
setOpen(li, true);
|
|
206
|
+
if (link) link.setAttribute("aria-busy", "false");
|
|
207
|
+
applyFilter();
|
|
208
|
+
})
|
|
209
|
+
.catch(function () {
|
|
210
|
+
delete li.dataset.loading;
|
|
211
|
+
li.dataset.error = "true";
|
|
212
|
+
li.dataset.open = "false";
|
|
213
|
+
twisty.textContent = "\\u26A0";
|
|
214
|
+
twisty.title = "Could not load " + name + ". Press to retry.";
|
|
215
|
+
if (link) {
|
|
216
|
+
link.setAttribute("aria-busy", "false");
|
|
217
|
+
link.setAttribute("aria-expanded", "false");
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (tree) {
|
|
223
|
+
tree.addEventListener("click", function (event) {
|
|
224
|
+
var twisty = event.target.closest(".bf-tree__tw");
|
|
225
|
+
if (!twisty || twisty.tagName !== "BUTTON" || !tree.contains(twisty)) return;
|
|
226
|
+
event.preventDefault();
|
|
227
|
+
var li = twisty.closest("li");
|
|
228
|
+
if (!li) return;
|
|
229
|
+
var list = li.querySelector(":scope > ul");
|
|
230
|
+
if (li.dataset.open === "true") {
|
|
231
|
+
setOpen(li, false);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (list) {
|
|
235
|
+
setOpen(li, true);
|
|
236
|
+
applyFilter();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
var row = li.querySelector(":scope > .bf-tree__row");
|
|
240
|
+
var path = row ? row.getAttribute("data-path") : null;
|
|
241
|
+
if (!path) return;
|
|
242
|
+
loadChildren(li, twisty, path);
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// The tree is one tab stop. Arrows move between the rows a reader can see,
|
|
247
|
+
// Right opens a folder (reading it first when it has never been loaded),
|
|
248
|
+
// Left closes it or steps out to the parent, Space toggles, and Enter follows
|
|
249
|
+
// the focused link itself — the row is a link, so the browser owns activation.
|
|
250
|
+
function treeRows() {
|
|
251
|
+
return tree ? Array.prototype.slice.call(tree.querySelectorAll(".bf-tree__row")) : [];
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function visibleRows() {
|
|
255
|
+
return treeRows().filter(function (row) { return row.getClientRects().length > 0; });
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function setTabStop(row) {
|
|
259
|
+
treeRows().forEach(function (other) {
|
|
260
|
+
var link = rowLink(other);
|
|
261
|
+
if (link) link.tabIndex = other === row ? 0 : -1;
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function focusRow(row) {
|
|
266
|
+
if (!row) return;
|
|
267
|
+
setTabStop(row);
|
|
268
|
+
var link = rowLink(row);
|
|
269
|
+
if (link) link.focus();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function moveFocus(row, delta) {
|
|
273
|
+
var rows = visibleRows();
|
|
274
|
+
var at = rows.indexOf(row);
|
|
275
|
+
if (at >= 0) focusRow(rows[at + delta]);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function parentRowOf(row) {
|
|
279
|
+
var li = row.parentNode;
|
|
280
|
+
var list = li ? li.parentNode : null;
|
|
281
|
+
var parentLi = list && list.closest ? list.closest("li") : null;
|
|
282
|
+
return parentLi ? parentLi.querySelector(":scope > .bf-tree__row") : null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// A folder that has never been expanded has no child list yet; opening it is
|
|
286
|
+
// the same request the twisty makes, so the keyboard reuses it.
|
|
287
|
+
function openFolder(row) {
|
|
288
|
+
var li = row.parentNode;
|
|
289
|
+
var twisty = row.querySelector(".bf-tree__tw--toggle");
|
|
290
|
+
if (!li || !twisty) return null;
|
|
291
|
+
var list = li.querySelector(":scope > ul");
|
|
292
|
+
if (list) return list;
|
|
293
|
+
var path = row.getAttribute("data-path");
|
|
294
|
+
if (path) loadChildren(li, twisty, path);
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function expandRow(row) {
|
|
299
|
+
var li = row.parentNode;
|
|
300
|
+
if (!li || !row.querySelector(".bf-tree__tw--toggle")) return;
|
|
301
|
+
var list = openFolder(row);
|
|
302
|
+
if (!list) return;
|
|
303
|
+
if (!isOpen(li)) {
|
|
304
|
+
setOpen(li, true);
|
|
305
|
+
applyFilter();
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
var rows = visibleRows();
|
|
309
|
+
var next = rows[rows.indexOf(row) + 1];
|
|
310
|
+
if (next && parentRowOf(next) === row) focusRow(next);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function collapseRow(row) {
|
|
314
|
+
var li = row.parentNode;
|
|
315
|
+
if (li && li.querySelector(":scope > ul") && isOpen(li)) {
|
|
316
|
+
setOpen(li, false);
|
|
317
|
+
applyFilter();
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
focusRow(parentRowOf(row));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function toggleRow(row) {
|
|
324
|
+
var li = row.parentNode;
|
|
325
|
+
if (!li || !row.querySelector(".bf-tree__tw--toggle")) return;
|
|
326
|
+
if (!openFolder(row)) return;
|
|
327
|
+
setOpen(li, li.dataset.open !== "true");
|
|
328
|
+
applyFilter();
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
if (tree) {
|
|
332
|
+
tree.addEventListener("keydown", function (event) {
|
|
333
|
+
var row = rowOf(event.target);
|
|
334
|
+
if (!row || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
335
|
+
var rows = visibleRows();
|
|
336
|
+
if (event.key === "ArrowDown") moveFocus(row, 1);
|
|
337
|
+
else if (event.key === "ArrowUp") moveFocus(row, -1);
|
|
338
|
+
else if (event.key === "Home") focusRow(rows[0]);
|
|
339
|
+
else if (event.key === "End") focusRow(rows[rows.length - 1]);
|
|
340
|
+
else if (event.key === "ArrowRight") expandRow(row);
|
|
341
|
+
else if (event.key === "ArrowLeft") collapseRow(row);
|
|
342
|
+
else if (event.key === " " && row.querySelector(".bf-tree__tw--toggle")) toggleRow(row);
|
|
343
|
+
else return;
|
|
344
|
+
// The page keymap treats a bare key as its own; the tree keeps its own.
|
|
345
|
+
event.preventDefault();
|
|
346
|
+
event.stopPropagation();
|
|
347
|
+
});
|
|
348
|
+
// A pointer click lands on one row, so that row becomes the tab stop the
|
|
349
|
+
// next Tab returns to.
|
|
350
|
+
tree.addEventListener("focusin", function (event) {
|
|
351
|
+
var row = rowOf(event.target);
|
|
352
|
+
if (row) setTabStop(row);
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// The current page's row, else the first one, is the way into the tree.
|
|
357
|
+
setTabStop(tree && tree.querySelector(".bf-tree__row[aria-current]") || (tree ? treeRows()[0] : null));
|
|
358
|
+
|
|
359
|
+
var panel = document.querySelector("details.bf-app__sidebar");
|
|
360
|
+
if (panel && window.matchMedia) {
|
|
361
|
+
var mq = window.matchMedia("(max-width: ${SIDEBAR_COLLAPSE_MAX}px)");
|
|
362
|
+
var syncSide = function () { panel.open = !mq.matches; };
|
|
363
|
+
syncSide();
|
|
364
|
+
if (mq.addEventListener) mq.addEventListener("change", syncSide);
|
|
365
|
+
else if (mq.addListener) mq.addListener(syncSide);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (foldAllButton) {
|
|
369
|
+
foldAllButton.addEventListener("click", function () {
|
|
370
|
+
// The control folds when anything is open, and expands when nothing is.
|
|
371
|
+
var open = false;
|
|
372
|
+
foldableDirs().forEach(function (li) {
|
|
373
|
+
if (li.dataset.open === "true") open = true;
|
|
374
|
+
});
|
|
375
|
+
setAll(!open);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// The control starts in the state the rendered tree implies.
|
|
380
|
+
syncFoldButton();
|
|
381
|
+
`;
|
|
@@ -201,7 +201,10 @@ config defaults, and error listing all derive from it.
|
|
|
201
201
|
2. Add `extensions/file-read/extract/<fmt>.ts` exporting a `FormatExtractor` that
|
|
202
202
|
`await import(...)`s its library, reads the bytes with `readFormatBytes`, and
|
|
203
203
|
normalizes the output. Keep the parsing in a pure, exported helper so it is
|
|
204
|
-
testable without a real file.
|
|
204
|
+
testable without a real file. **The import goes inside the function**, not at
|
|
205
|
+
module scope: `formats.ts` imports every extractor when the tool registers, so
|
|
206
|
+
a module-scope import loads that library on every start — `test/dependencies.test.ts`
|
|
207
|
+
fails on exactly that.
|
|
205
208
|
3. Add a `ReadFormat` entry to `FORMATS` and widen the `id` union.
|
|
206
209
|
4. Add the new extractor to `extract/extract.test.ts` with a real minimal
|
|
207
210
|
fixture, and extend `tool.test.ts` for format selection and config.
|
|
@@ -5,11 +5,11 @@
|
|
|
5
5
|
* unzips it with `fflate`, parses the XML with `fast-xml-parser`, and walks the
|
|
6
6
|
* body for text: paragraphs and headings become lines, table cells are
|
|
7
7
|
* tab-separated, and `text:s` / `text:tab` / `text:line-break` become their
|
|
8
|
-
* whitespace.
|
|
8
|
+
* whitespace. Both libraries load on the first ODT read, never at registration,
|
|
9
|
+
* so a session that never opens one pays nothing for them; the walk over the
|
|
10
|
+
* parsed tree is pure and tested directly.
|
|
9
11
|
*/
|
|
10
12
|
|
|
11
|
-
import { strFromU8, unzipSync } from "fflate";
|
|
12
|
-
import { XMLParser } from "fast-xml-parser";
|
|
13
13
|
import { normalizeBodyText, readFormatBytes, type FormatExtractor } from "./file.ts";
|
|
14
14
|
|
|
15
15
|
/** Elements that end a line. */
|
|
@@ -68,12 +68,13 @@ function collect(nodes: unknown, out: string[], inText: boolean): void {
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
/**
|
|
71
|
-
* Extract plain text from the body of an ODT `content.xml`.
|
|
71
|
+
* Extract plain text from the body of an ODT `content.xml`.
|
|
72
72
|
*
|
|
73
73
|
* Trailing tabs and spaces at line ends, runs of blank lines, and the final
|
|
74
74
|
* newline are stripped so the tool's paging sees ordinary text.
|
|
75
75
|
*/
|
|
76
|
-
export function odtXmlToText(xml: string): string {
|
|
76
|
+
export async function odtXmlToText(xml: string): Promise<string> {
|
|
77
|
+
const { XMLParser } = await import("fast-xml-parser");
|
|
77
78
|
const parser = new XMLParser({
|
|
78
79
|
preserveOrder: true,
|
|
79
80
|
ignoreAttributes: false,
|
|
@@ -94,6 +95,7 @@ export function odtXmlToText(xml: string): string {
|
|
|
94
95
|
|
|
95
96
|
/** Extract the body text from an ODT file. */
|
|
96
97
|
export const extractOdtText: FormatExtractor = async (file) => {
|
|
98
|
+
const { strFromU8, unzipSync } = await import("fflate");
|
|
97
99
|
const archive = unzipSync(await readFormatBytes(file));
|
|
98
100
|
const content = archive["content.xml"];
|
|
99
101
|
if (!content) throw new Error("ODT archive has no content.xml.");
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
|
|
20
20
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
21
21
|
import { isPursuedGoal } from "../../lib/goal-state.ts";
|
|
22
|
-
import {
|
|
22
|
+
import { isRemoteTurn, remoteLanguage } from "../../lib/interaction.ts";
|
|
23
|
+
import { PLAN_CONSENT, PLAN_CONSENT_SHORT, PLAN_CONSENT_SHORT_ZH, requestPlanMode } from "../../lib/plan-entry.ts";
|
|
23
24
|
import { isPlanModeEnabled } from "../../lib/plan-state.ts";
|
|
24
25
|
import { PLAN_ENTER_TOOL } from "../../lib/tool-names.ts";
|
|
25
26
|
import type { Goal, GoalStatus } from "./types.ts";
|
|
@@ -45,7 +46,12 @@ export async function goalPhase(
|
|
|
45
46
|
if (!options.ask || !ctx.hasUI) return "active";
|
|
46
47
|
// Do not ask about a phase this session cannot enter.
|
|
47
48
|
if (!pi.getAllTools().some((tool) => tool.name === PLAN_ENTER_TOOL)) return "active";
|
|
48
|
-
const
|
|
49
|
+
const consent = isRemoteTurn(ctx)
|
|
50
|
+
? remoteLanguage(ctx) === "zh"
|
|
51
|
+
? PLAN_CONSENT_SHORT_ZH
|
|
52
|
+
: PLAN_CONSENT_SHORT
|
|
53
|
+
: PLAN_CONSENT;
|
|
54
|
+
const approved = await ctx.ui.confirm(PLAN_ASK, consent);
|
|
49
55
|
if (!approved) return "active";
|
|
50
56
|
return (await requestPlanMode(pi, ctx)) ? "planning" : "active";
|
|
51
57
|
}
|
package/extensions/goal/tools.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import type { JsonValue } from "@earendil-works/pi-ai";
|
|
11
11
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { GOAL_TOOL } from "../../lib/tool-names.ts";
|
|
13
|
+
import { failedResult, textResult } from "../../lib/tool-result.ts";
|
|
13
14
|
import { callLine, detailsOrLine } from "../../lib/tui/tool.ts";
|
|
14
15
|
import { reuseText, toolDetails } from "../../lib/tui/chrome.ts";
|
|
15
16
|
import { paint } from "../../lib/tui/tokens.ts";
|
|
@@ -52,17 +53,11 @@ function toStructuredContent(details: GoalDetails): Record<string, JsonValue> {
|
|
|
52
53
|
*/
|
|
53
54
|
function goalResult(details: GoalDetails, options: { error?: string; openTodos?: number } = {}) {
|
|
54
55
|
const { error, openTodos = 0 } = options;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
},
|
|
61
|
-
],
|
|
62
|
-
details,
|
|
63
|
-
structuredContent: toStructuredContent(details),
|
|
64
|
-
...(error === undefined ? {} : { isError: true as const }),
|
|
65
|
-
};
|
|
56
|
+
const text = error === undefined ? formatGoalText(details.goal, openTodos) : `Error: ${error}`;
|
|
57
|
+
const structuredContent = toStructuredContent(details);
|
|
58
|
+
return error === undefined
|
|
59
|
+
? textResult(text, details, structuredContent)
|
|
60
|
+
: failedResult(text, details, structuredContent);
|
|
66
61
|
}
|
|
67
62
|
|
|
68
63
|
export function registerTools(pi: ExtensionAPI, runtime: GoalRuntime): void {
|
package/extensions/job/README.md
CHANGED
|
@@ -37,7 +37,8 @@ How the `job` call and its result look in the transcript — the call line as a
|
|
|
37
37
|
chip for this session's jobs, plus a `/jobs` screen for selecting, tailing,
|
|
38
38
|
killing, and clearing.
|
|
39
39
|
- Reports finished jobs to the model at the start of the next turn, or — for
|
|
40
|
-
jobs started with `wake: true` — by triggering one turn
|
|
40
|
+
jobs started with `wake: true` — by triggering one turn as soon as the agent is
|
|
41
|
+
idle, including right after a running turn settles.
|
|
41
42
|
- Kills session-owned jobs on shutdown so no process is left behind; on POSIX,
|
|
42
43
|
start a job with `detached: true` to keep it running and reattach next session.
|
|
43
44
|
A detached job points its own output straight at its log file, so it keeps
|
|
@@ -338,13 +339,14 @@ rewritten.
|
|
|
338
339
|
extension keeps only what is job-shaped — the durable registry, rotation,
|
|
339
340
|
stdin, and the job vocabulary. The report coordinator is also why a wake can
|
|
340
341
|
never stack two turns: it owns the guard for the window between the triggered
|
|
341
|
-
send and `agent_start
|
|
342
|
+
send and `agent_start`, and it retries a wake the running turn suppressed.
|
|
342
343
|
- **Notify by default, wake opt-in.** A failed job raises one `ctx.ui.notify`
|
|
343
344
|
(a clean exit is left to the list and chips). Finished jobs are also posted
|
|
344
345
|
as a shared transcript notice — model-facing text plus the notice block — at the
|
|
345
|
-
next `before_agent_start
|
|
346
|
-
triggers one turn
|
|
347
|
-
|
|
346
|
+
next `before_agent_start`, or in the wake turn it raises. A `wake: true` job (or
|
|
347
|
+
`wakeOnFinish`) additionally triggers one turn as soon as the agent is idle,
|
|
348
|
+
including right after a running turn settles; auto-wake is not the default
|
|
349
|
+
because it easily loops and burns tokens.
|
|
348
350
|
- **Width-1 glyphs and cached latest line.** The running chip is `▸ N` and the
|
|
349
351
|
failure chip `✗ N`, each a two-token badge under its own status key, so the
|
|
350
352
|
status bar compacts each independently (`▸N` / `✗N`) and an unreported
|
package/extensions/job/index.ts
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
* processes with their output streamed to log files; the status chip shows
|
|
6
6
|
* running and unreported-failure counts, and finished jobs are reported to the
|
|
7
7
|
* model — and shown as a bytefly notice — at the next turn, or by triggering one
|
|
8
|
-
* turn for a `wake` job
|
|
8
|
+
* turn for a `wake` job as soon as the agent is idle, including right after a
|
|
9
|
+
* running turn settles.
|
|
9
10
|
*
|
|
10
11
|
* Load with: pi --extension ./extensions/job
|
|
11
12
|
*/
|
|
@@ -13,8 +14,8 @@
|
|
|
13
14
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
14
15
|
import { isExtensionEnabled } from "../../lib/env.ts";
|
|
15
16
|
import { JOB_FINISHED_CHANNEL, type JobFinishedEvent } from "../../lib/events.ts";
|
|
16
|
-
import { noticeMessage, type NoticeMessage } from "../../lib/notice.ts";
|
|
17
|
-
import { createPendingReporter } from "../../lib/report.ts";
|
|
17
|
+
import { burstText, noticeMessage, type NoticeMessage } from "../../lib/notice.ts";
|
|
18
|
+
import { createPendingReporter, registerPendingChannel } from "../../lib/report.ts";
|
|
18
19
|
import { registerCommands } from "./commands.ts";
|
|
19
20
|
import { formatCompletion, jobOutcome, shortLabel } from "./format.ts";
|
|
20
21
|
import { createJobsRuntime, type JobsRuntime } from "./runtime.ts";
|
|
@@ -25,8 +26,13 @@ import type { JobRecord } from "./types.ts";
|
|
|
25
26
|
const JOB_CONTEXT_MARKER = "[BACKGROUND JOBS]";
|
|
26
27
|
|
|
27
28
|
function buildContext(jobs: JobRecord[]): string {
|
|
28
|
-
const
|
|
29
|
-
return
|
|
29
|
+
const count = jobs.length;
|
|
30
|
+
return burstText({
|
|
31
|
+
marker: JOB_CONTEXT_MARKER,
|
|
32
|
+
headline: `${count} background job${count === 1 ? "" : "s"} finished`,
|
|
33
|
+
lines: jobs.map((job) => formatCompletion(job)),
|
|
34
|
+
hint: "Run `job logs <id>` for output.",
|
|
35
|
+
});
|
|
30
36
|
}
|
|
31
37
|
|
|
32
38
|
/** The shared notice for pending completions: model text plus renderer details. */
|
|
@@ -66,65 +72,42 @@ export default function jobs(pi: ExtensionAPI, deps: JobsDeps = {}): void {
|
|
|
66
72
|
if (!isExtensionEnabled("job")) return;
|
|
67
73
|
const runtime = deps.runtime ?? createJobsRuntime();
|
|
68
74
|
|
|
69
|
-
// Opt-in wake: a `wake` job finished
|
|
70
|
-
//
|
|
71
|
-
//
|
|
75
|
+
// Opt-in wake: a `wake` job finished. The reporter reports *all* pending
|
|
76
|
+
// completions, not just the waking one, so a job that finished earlier is not
|
|
77
|
+
// silently marked seen and dropped, and it retries a completion that arrived
|
|
78
|
+
// mid-turn once the run settles.
|
|
72
79
|
const reporter = createPendingReporter<JobRecord>({
|
|
73
80
|
wake: (notice) => pi.sendMessage(notice, { triggerTurn: true }),
|
|
74
81
|
pending: () => runtime.pendingCompletions(),
|
|
75
82
|
markReported: (records) => runtime.markReported(records.map((job) => job.id)),
|
|
76
83
|
buildNotice: jobNotice,
|
|
77
84
|
source: "job",
|
|
85
|
+
wakeRequested: () => runtime.pendingCompletions().some((job) => job.wake),
|
|
78
86
|
});
|
|
79
87
|
|
|
80
88
|
registerTools(pi, runtime);
|
|
81
89
|
registerCommands(pi, runtime);
|
|
82
90
|
|
|
83
|
-
pi
|
|
84
|
-
reporter
|
|
85
|
-
runtime
|
|
91
|
+
registerPendingChannel(pi, {
|
|
92
|
+
reporter,
|
|
93
|
+
runtime,
|
|
86
94
|
// `shutdown` clears these; a reload or session replacement reuses the
|
|
87
95
|
// runtime, so re-arm them on every session start.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
pi.on("agent_start", () => {
|
|
106
|
-
reporter.agentStart();
|
|
107
|
-
});
|
|
108
|
-
pi.on("agent_settled", () => {
|
|
109
|
-
reporter.agentSettled();
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// Report completions to the model at the start of the next turn.
|
|
113
|
-
pi.on("before_agent_start", (_event, ctx) => {
|
|
114
|
-
const reported = reporter.beforeAgentStart();
|
|
115
|
-
if (!reported) return undefined;
|
|
116
|
-
runtime.setStatus(ctx);
|
|
117
|
-
return reported;
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
// Keep stale completion notices out of later turns. Only this extension's
|
|
121
|
-
// notices are pruned; another source's are left for its own handler.
|
|
122
|
-
pi.on("context", (event) => reporter.filterContext(event.messages));
|
|
123
|
-
|
|
124
|
-
// `shutdown` detaches the UI controller, which clears both chips.
|
|
125
|
-
pi.on("session_shutdown", async () => {
|
|
126
|
-
runtime.onFinish = undefined;
|
|
127
|
-
runtime.onComplete = undefined;
|
|
128
|
-
await runtime.shutdown();
|
|
96
|
+
onSessionStart: () => {
|
|
97
|
+
runtime.onFinish = () => reporter.report();
|
|
98
|
+
// Notify-listening extensions follow completions on the shared bus; the
|
|
99
|
+
// runtime only knows the callback, never the channel.
|
|
100
|
+
runtime.onComplete = (job) => {
|
|
101
|
+
try {
|
|
102
|
+
pi.events.emit(JOB_FINISHED_CHANNEL, toFinishedEvent(job));
|
|
103
|
+
} catch {
|
|
104
|
+
// The event bus is optional.
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
onSessionShutdown: () => {
|
|
109
|
+
runtime.onFinish = undefined;
|
|
110
|
+
runtime.onComplete = undefined;
|
|
111
|
+
},
|
|
129
112
|
});
|
|
130
113
|
}
|