@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,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The quick-open palette: a fuzzy path search over `/api/files`.
|
|
3
|
+
*
|
|
4
|
+
* It is a fragment of the same IIFE `client.ts` ships (`PALETTE_CLIENT_JS` is
|
|
5
|
+
* interpolated into `CLIENT_JS`). The page's keymap opens it through
|
|
6
|
+
* `window.bfPalette`, the same contract shape as `window.bfKeys`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export const PALETTE_CLIENT_JS = ` // Quick open: a fuzzy path palette backed by /api/files.
|
|
10
|
+
var quickOpenButton = document.getElementById("quick-open");
|
|
11
|
+
var paletteBackdrop = null;
|
|
12
|
+
var paletteInput = null;
|
|
13
|
+
var paletteList = null;
|
|
14
|
+
var paletteStatus = null;
|
|
15
|
+
var paletteResults = [];
|
|
16
|
+
var paletteActive = -1;
|
|
17
|
+
var paletteTimer = null;
|
|
18
|
+
|
|
19
|
+
function paletteOpen() {
|
|
20
|
+
return paletteBackdrop !== null && paletteBackdrop.hidden === false;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function encodeSegments(path) {
|
|
24
|
+
return path.split("/").map(encodeURIComponent).join("/");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function closestRole(node, role) {
|
|
28
|
+
while (node && node !== paletteList) {
|
|
29
|
+
if (node.getAttribute && node.getAttribute("role") === role) return node;
|
|
30
|
+
node = node.parentNode;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildPalette() {
|
|
36
|
+
if (paletteBackdrop) return;
|
|
37
|
+
paletteBackdrop = document.createElement("div");
|
|
38
|
+
paletteBackdrop.className = "bf-dialog";
|
|
39
|
+
paletteBackdrop.hidden = true;
|
|
40
|
+
var dialog = document.createElement("div");
|
|
41
|
+
dialog.className = "bf-dialog__panel";
|
|
42
|
+
dialog.setAttribute("role", "dialog");
|
|
43
|
+
dialog.setAttribute("aria-modal", "true");
|
|
44
|
+
dialog.setAttribute("aria-label", "Quick open a file");
|
|
45
|
+
paletteInput = document.createElement("input");
|
|
46
|
+
paletteInput.type = "text";
|
|
47
|
+
paletteInput.className = "bf-dialog__input";
|
|
48
|
+
paletteInput.id = "palette-input";
|
|
49
|
+
paletteInput.setAttribute("role", "combobox");
|
|
50
|
+
paletteInput.setAttribute("aria-expanded", "true");
|
|
51
|
+
paletteInput.setAttribute("aria-controls", "palette-list");
|
|
52
|
+
paletteInput.setAttribute("aria-autocomplete", "list");
|
|
53
|
+
paletteInput.setAttribute("autocomplete", "off");
|
|
54
|
+
paletteInput.setAttribute("placeholder", "Type a file path…");
|
|
55
|
+
paletteInput.setAttribute("aria-label", "Quick open a file");
|
|
56
|
+
paletteList = document.createElement("ul");
|
|
57
|
+
paletteList.className = "bf-dialog__list";
|
|
58
|
+
paletteList.id = "palette-list";
|
|
59
|
+
paletteList.setAttribute("role", "listbox");
|
|
60
|
+
paletteStatus = document.createElement("p");
|
|
61
|
+
paletteStatus.className = "bf-dialog__status";
|
|
62
|
+
paletteStatus.setAttribute("role", "status");
|
|
63
|
+
dialog.appendChild(paletteInput);
|
|
64
|
+
dialog.appendChild(paletteList);
|
|
65
|
+
dialog.appendChild(paletteStatus);
|
|
66
|
+
paletteBackdrop.appendChild(dialog);
|
|
67
|
+
document.body.appendChild(paletteBackdrop);
|
|
68
|
+
|
|
69
|
+
paletteBackdrop.addEventListener("mousedown", function (event) {
|
|
70
|
+
if (event.target === paletteBackdrop) closePalette();
|
|
71
|
+
});
|
|
72
|
+
paletteInput.addEventListener("input", function () {
|
|
73
|
+
if (paletteTimer) window.clearTimeout(paletteTimer);
|
|
74
|
+
paletteTimer = window.setTimeout(runSearch, 80);
|
|
75
|
+
});
|
|
76
|
+
paletteInput.addEventListener("keydown", function (event) {
|
|
77
|
+
if (event.key === "Tab") {
|
|
78
|
+
event.preventDefault();
|
|
79
|
+
} else if (event.key === "ArrowDown" || (event.ctrlKey && event.key === "n")) {
|
|
80
|
+
event.preventDefault();
|
|
81
|
+
moveActive(1);
|
|
82
|
+
} else if (event.key === "ArrowUp" || (event.ctrlKey && event.key === "p")) {
|
|
83
|
+
event.preventDefault();
|
|
84
|
+
moveActive(-1);
|
|
85
|
+
} else if (event.key === "Enter") {
|
|
86
|
+
event.preventDefault();
|
|
87
|
+
openActive();
|
|
88
|
+
} else if (event.key === "Escape") {
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
closePalette();
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
paletteList.addEventListener("mousemove", function (event) {
|
|
94
|
+
var option = closestRole(event.target, "option");
|
|
95
|
+
if (option) setActive(Number(option.getAttribute("data-index")));
|
|
96
|
+
});
|
|
97
|
+
paletteList.addEventListener("click", function (event) {
|
|
98
|
+
var option = closestRole(event.target, "option");
|
|
99
|
+
if (option) {
|
|
100
|
+
setActive(Number(option.getAttribute("data-index")));
|
|
101
|
+
openActive();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function openPalette() {
|
|
107
|
+
buildPalette();
|
|
108
|
+
paletteBackdrop.hidden = false;
|
|
109
|
+
document.body.classList.add("palette-open");
|
|
110
|
+
paletteInput.value = "";
|
|
111
|
+
renderResults([], "Searching…");
|
|
112
|
+
paletteInput.focus();
|
|
113
|
+
runSearch();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function closePalette() {
|
|
117
|
+
if (!paletteBackdrop) return;
|
|
118
|
+
paletteBackdrop.hidden = true;
|
|
119
|
+
document.body.classList.remove("palette-open");
|
|
120
|
+
if (quickOpenButton) quickOpenButton.focus();
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function runSearch() {
|
|
124
|
+
var query = paletteInput.value.trim();
|
|
125
|
+
fetch("/api/files?q=" + encodeURIComponent(query))
|
|
126
|
+
.then(function (response) {
|
|
127
|
+
if (!response.ok) throw new Error("search failed");
|
|
128
|
+
return response.json();
|
|
129
|
+
})
|
|
130
|
+
.then(function (data) {
|
|
131
|
+
if (paletteInput.value.trim() !== query) return;
|
|
132
|
+
if (query === "") {
|
|
133
|
+
renderResults([], data.total + " files indexed" + (data.truncated ? " (truncated)" : ""));
|
|
134
|
+
} else {
|
|
135
|
+
renderResults(data.results || [], null);
|
|
136
|
+
}
|
|
137
|
+
})
|
|
138
|
+
.catch(function () {
|
|
139
|
+
renderResults([], "Search failed");
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function appendHighlighted(target, path, positions) {
|
|
144
|
+
var marks = {};
|
|
145
|
+
(positions || []).forEach(function (index) { marks[index] = true; });
|
|
146
|
+
var buffer = "";
|
|
147
|
+
var marked = false;
|
|
148
|
+
function flush() {
|
|
149
|
+
if (buffer === "") return;
|
|
150
|
+
if (marked) {
|
|
151
|
+
var mark = document.createElement("mark");
|
|
152
|
+
mark.textContent = buffer;
|
|
153
|
+
target.appendChild(mark);
|
|
154
|
+
} else {
|
|
155
|
+
target.appendChild(document.createTextNode(buffer));
|
|
156
|
+
}
|
|
157
|
+
buffer = "";
|
|
158
|
+
}
|
|
159
|
+
for (var i = 0; i < path.length; i++) {
|
|
160
|
+
var isMatch = marks[i] === true;
|
|
161
|
+
if (isMatch !== marked) { flush(); marked = isMatch; }
|
|
162
|
+
buffer += path.charAt(i);
|
|
163
|
+
}
|
|
164
|
+
flush();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function renderResults(results, hint) {
|
|
168
|
+
paletteResults = results;
|
|
169
|
+
paletteActive = results.length > 0 ? 0 : -1;
|
|
170
|
+
paletteList.textContent = "";
|
|
171
|
+
results.forEach(function (result, index) {
|
|
172
|
+
var option = document.createElement("li");
|
|
173
|
+
option.className = "bf-dialog__option";
|
|
174
|
+
option.id = "palette-option-" + index;
|
|
175
|
+
option.setAttribute("role", "option");
|
|
176
|
+
option.setAttribute("data-index", String(index));
|
|
177
|
+
appendHighlighted(option, result.path, result.positions);
|
|
178
|
+
paletteList.appendChild(option);
|
|
179
|
+
});
|
|
180
|
+
if (hint) paletteStatus.textContent = hint;
|
|
181
|
+
else if (results.length === 0) paletteStatus.textContent = "No matches";
|
|
182
|
+
else paletteStatus.textContent = results.length + (results.length === 1 ? " match" : " matches");
|
|
183
|
+
updateActive();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function updateActive() {
|
|
187
|
+
Array.prototype.forEach.call(paletteList.children, function (option, index) {
|
|
188
|
+
option.setAttribute("aria-selected", index === paletteActive ? "true" : "false");
|
|
189
|
+
});
|
|
190
|
+
var active = paletteList.children[paletteActive];
|
|
191
|
+
if (paletteActive >= 0 && active) {
|
|
192
|
+
paletteInput.setAttribute("aria-activedescendant", "palette-option-" + paletteActive);
|
|
193
|
+
if (active.scrollIntoView) active.scrollIntoView({ block: "nearest" });
|
|
194
|
+
} else {
|
|
195
|
+
paletteInput.removeAttribute("aria-activedescendant");
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function setActive(index) {
|
|
200
|
+
if (index < 0 || index >= paletteResults.length) return;
|
|
201
|
+
paletteActive = index;
|
|
202
|
+
updateActive();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function moveActive(delta) {
|
|
206
|
+
if (paletteResults.length === 0) return;
|
|
207
|
+
var next = paletteActive + delta;
|
|
208
|
+
if (next < 0) next = paletteResults.length - 1;
|
|
209
|
+
if (next >= paletteResults.length) next = 0;
|
|
210
|
+
setActive(next);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function openActive() {
|
|
214
|
+
var result = paletteResults[paletteActive];
|
|
215
|
+
if (result) window.location.assign("/view/" + encodeSegments(result.path));
|
|
216
|
+
}
|
|
217
|
+
var quickOpenButton = document.getElementById("quick-open");
|
|
218
|
+
if (quickOpenButton) quickOpenButton.addEventListener("click", openPalette);
|
|
219
|
+
|
|
220
|
+
window.bfPalette = {
|
|
221
|
+
open: openPalette,
|
|
222
|
+
close: closePalette,
|
|
223
|
+
isOpen: paletteOpen
|
|
224
|
+
};`;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Revisions for the browse live status.
|
|
3
|
+
*
|
|
4
|
+
* A revision is a short digest of something a page already drew — the change set
|
|
5
|
+
* a frame carries, or the names a listing holds — so a client can tell "nothing
|
|
6
|
+
* moved" from "paint again" without comparing the payload it was sent. A
|
|
7
|
+
* collision costs one skipped repaint and is corrected by the next frame or a
|
|
8
|
+
* reload, which is the trade a 12-character digest makes for a small frame.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A digest of the parts, cut to 12 hex characters. The parts are joined with NUL,
|
|
15
|
+
* which no filename can contain, so two different lists cannot run together into
|
|
16
|
+
* the same text.
|
|
17
|
+
*/
|
|
18
|
+
function digest(parts: readonly string[]): string {
|
|
19
|
+
return createHash("sha1").update(parts.join("\u0000")).digest("hex").slice(0, 12);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** The change set a frame carries: its totals and paths, in a stable order. */
|
|
23
|
+
export interface RevisionInput {
|
|
24
|
+
count: number;
|
|
25
|
+
insertions: number;
|
|
26
|
+
deletions: number;
|
|
27
|
+
files: readonly string[];
|
|
28
|
+
dirs: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The revision of a change set. The totals are part of it, so an edit that moves
|
|
33
|
+
* no path still moves the revision and the chip's `+A −D` stays honest.
|
|
34
|
+
*/
|
|
35
|
+
export function changeRevision(changes: RevisionInput): string {
|
|
36
|
+
return digest([`${changes.count}:${changes.insertions}:${changes.deletions}`, ...changes.files, ...changes.dirs]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The revision of a listing's names, independent of the order they arrived in. */
|
|
40
|
+
export function listingRevision(names: readonly string[]): string {
|
|
41
|
+
return digest([...names].sort());
|
|
42
|
+
}
|
|
@@ -20,8 +20,9 @@ import {
|
|
|
20
20
|
scriptHeaders,
|
|
21
21
|
type ServeResponse,
|
|
22
22
|
} from "../../lib/http/index.ts";
|
|
23
|
-
import { THEME_JS } from "../../lib/web/index.ts";
|
|
23
|
+
import { THEME_JS, THEME_JS_PATH } from "../../lib/web/index.ts";
|
|
24
24
|
import { CLIENT_JS } from "./client.ts";
|
|
25
|
+
import type { LivePage } from "./chrome.ts";
|
|
25
26
|
import type { ServeConfig } from "./config.ts";
|
|
26
27
|
import { isDiffMode, type DiffMode } from "./diff.ts";
|
|
27
28
|
import type { DiffList, GitDiffProvider, GitStatus, GitStatusProvider } from "./git.ts";
|
|
@@ -43,6 +44,7 @@ import { fuzzySearch } from "./fuzzy.ts";
|
|
|
43
44
|
import { renderDirectoryPage, renderDiffPage, renderErrorPage, renderFileDiffPage, renderFilePage } from "./html.ts";
|
|
44
45
|
import type { ChangeSummary } from "./html.ts";
|
|
45
46
|
import { encodePath, HttpError, relativePath, resolveDecodedPath, resolveRequestPath } from "./paths.ts";
|
|
47
|
+
import { listingRevision } from "./rev.ts";
|
|
46
48
|
|
|
47
49
|
export interface ServeContext {
|
|
48
50
|
root: string;
|
|
@@ -108,9 +110,13 @@ function summaryOf(list: DiffList): ChangeSummary {
|
|
|
108
110
|
};
|
|
109
111
|
}
|
|
110
112
|
|
|
111
|
-
/**
|
|
112
|
-
|
|
113
|
-
|
|
113
|
+
/**
|
|
114
|
+
* The live chrome a page renders, or `undefined` when live status is off. A
|
|
115
|
+
* listing page also passes the revision of what it drew, which is what its
|
|
116
|
+
* reload control compares against; every other page has nothing to compare.
|
|
117
|
+
*/
|
|
118
|
+
function liveOf(ctx: ServeContext, folder?: { dir: string; rev: string }): LivePage | undefined {
|
|
119
|
+
return ctx.config.live && ctx.live ? { intervalMs: ctx.config.liveIntervalMs, ...folder } : undefined;
|
|
114
120
|
}
|
|
115
121
|
|
|
116
122
|
/** The `head` change summary, for the header chip, tree markers, and file action. */
|
|
@@ -184,7 +190,10 @@ async function directoryResponse(ctx: ServeContext, rel: string): Promise<ServeR
|
|
|
184
190
|
maxThumbBytes: ctx.config.maxThumbBytes,
|
|
185
191
|
git,
|
|
186
192
|
summary: changes,
|
|
187
|
-
live: liveOf(
|
|
193
|
+
live: liveOf(
|
|
194
|
+
ctx,
|
|
195
|
+
listing.truncated ? undefined : { dir: rel, rev: listingRevision(listing.entries.map((entry) => entry.name)) },
|
|
196
|
+
),
|
|
188
197
|
}),
|
|
189
198
|
);
|
|
190
199
|
}
|
|
@@ -312,6 +321,17 @@ async function treeApi(ctx: ServeContext, decodedPath: string): Promise<ServeRes
|
|
|
312
321
|
return jsonResponse(200, { path: rel, entries: listing.entries.map(treeApiEntry) });
|
|
313
322
|
}
|
|
314
323
|
|
|
324
|
+
/** The revision of one folder's listing: what a page's reload control compares against. */
|
|
325
|
+
async function dirRevApi(ctx: ServeContext, decodedPath: string): Promise<ServeResponse> {
|
|
326
|
+
const abs = await resolveDecodedPath(ctx.root, decodedPath);
|
|
327
|
+
const rel = relativePath(ctx.root, abs);
|
|
328
|
+
const listing = await listDirectory(ctx.root, rel, ctx.config.maxDirEntries);
|
|
329
|
+
// A truncated listing is a prefix whose boundary moves, so its revision is not
|
|
330
|
+
// comparable with what the page drew: `null` says "cannot tell".
|
|
331
|
+
const names = listing.entries.map((entry) => entry.name);
|
|
332
|
+
return jsonResponse(200, { path: rel, rev: listing.truncated ? null : listingRevision(names) });
|
|
333
|
+
}
|
|
334
|
+
|
|
315
335
|
/** Fuzzy file search for the quick-open palette, over the cached index. */
|
|
316
336
|
async function filesApi(ctx: ServeContext, query: string): Promise<ServeResponse> {
|
|
317
337
|
const index = ctx.index ? await ctx.index() : await buildFileIndex(ctx.root, ctx.config.maxIndexEntries);
|
|
@@ -352,9 +372,10 @@ export async function handleRequest(
|
|
|
352
372
|
|
|
353
373
|
try {
|
|
354
374
|
if (pathname === "/app.js") return { status: 200, headers: scriptHeaders(), body: CLIENT_JS };
|
|
355
|
-
if (pathname ===
|
|
375
|
+
if (pathname === THEME_JS_PATH) return { status: 200, headers: scriptHeaders(), body: THEME_JS };
|
|
356
376
|
if (pathname === "/favicon.ico") return { status: 204, headers: pageHeaders(), body: "" };
|
|
357
377
|
if (pathname === "/api/tree") return await treeApi(ctx, search.get("path") ?? "");
|
|
378
|
+
if (pathname === "/api/dir-rev") return await dirRevApi(ctx, search.get("path") ?? "");
|
|
358
379
|
if (pathname === "/api/files") return await filesApi(ctx, search.get("q") ?? "");
|
|
359
380
|
if (pathname === "/events") return liveEvents(ctx);
|
|
360
381
|
const rawMode = search.get("mode");
|
|
@@ -103,6 +103,13 @@ export async function createFileServer(options: FileServerOptions): Promise<File
|
|
|
103
103
|
respond(request, response, result);
|
|
104
104
|
return;
|
|
105
105
|
}
|
|
106
|
+
// Checked before the head is written: a refusal must not leave a stream the
|
|
107
|
+
// client would wait on. `204` is the one status an event source does not
|
|
108
|
+
// reconnect after, so a page beyond the cap goes quiet instead of retrying.
|
|
109
|
+
if (live.atCapacity()) {
|
|
110
|
+
respond(request, response, { status: 204, headers: { "Cache-Control": "no-store" }, body: "" });
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
106
113
|
openEventStream(response, result);
|
|
107
114
|
const detach = live.attach({
|
|
108
115
|
write: (frame) => {
|
|
@@ -19,9 +19,6 @@ export const BROWSE_CSS = `${WEB_BASE_CSS}
|
|
|
19
19
|
${WEB_THEME_CSS}
|
|
20
20
|
${WEB_LAYOUT_CSS}
|
|
21
21
|
${WEB_COMPONENTS_CSS}
|
|
22
|
-
${WEB_THEME_CSS}
|
|
23
|
-
${WEB_LAYOUT_CSS}
|
|
24
|
-
${WEB_COMPONENTS_CSS}
|
|
25
22
|
.browse-side{padding:var(--space-3)}
|
|
26
23
|
.browse-root{flex:1 1 auto;direction:rtl;text-align:left}
|
|
27
24
|
.browse-git{font-family:var(--font-mono)}
|
|
@@ -50,6 +47,8 @@ body.palette-open{overflow:hidden}
|
|
|
50
47
|
swap only changes the colour, and the footer line reads as one muted clause. */
|
|
51
48
|
.browse-live{font-family:var(--font-mono);border:1px solid currentColor;border-radius:var(--radius-pill);padding:2px 10px}
|
|
52
49
|
.browse-live__glyph{font-size:9px;line-height:1}
|
|
50
|
+
.browse-reload{border:1px solid currentColor;border-radius:var(--radius-pill);padding:2px 10px;cursor:pointer;font-family:var(--font-mono)}
|
|
51
|
+
.browse-reload:hover{text-decoration:underline}
|
|
53
52
|
.browse-live-line{color:var(--muted);font-variant-numeric:tabular-nums}
|
|
54
53
|
.browse-live-line::after{content:" · "}
|
|
55
54
|
.browse-diff-totals{font-family:var(--font-mono)}
|
|
@@ -121,4 +120,12 @@ body.palette-open{overflow:hidden}
|
|
|
121
120
|
}
|
|
122
121
|
@media (max-width:520px){
|
|
123
122
|
.bf-toolbar .bf-theme-toggle{margin-left:auto}
|
|
123
|
+
}
|
|
124
|
+
/* A printed page scrolls nothing: the card and its body open up, the pinned hunk
|
|
125
|
+
header stops pinning, and the progress hairline belongs to the screen. */
|
|
126
|
+
@media print{
|
|
127
|
+
.bf-card{border:0}
|
|
128
|
+
.bf-card,.bf-app__main,.bf-code,.bf-md,.bf-diff,.browse-listing{height:auto!important;max-height:none!important;overflow:visible!important}
|
|
129
|
+
.bf-diff .bf-diff-line--hunk{position:static}
|
|
130
|
+
.browse-progress{display:none}
|
|
124
131
|
}`;
|