@gavin-hu/my-pi-agent 0.12.0 → 0.13.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 +28 -21
- package/README.zh-CN.md +30 -22
- package/extensions/bytefly/README.md +139 -67
- package/extensions/bytefly/chrome.ts +7 -2
- package/extensions/bytefly/doctor/register.ts +6 -0
- package/extensions/bytefly/doctor/runtime.ts +36 -6
- package/extensions/bytefly/footer/collect/snapshot.ts +12 -2
- package/extensions/bytefly/footer/config.ts +7 -1
- package/extensions/bytefly/footer/delegation.ts +100 -0
- package/extensions/bytefly/footer/register.ts +13 -2
- package/extensions/bytefly/footer/render/lines.ts +2 -0
- package/extensions/bytefly/footer/render/segments.ts +27 -0
- package/extensions/bytefly/footer/types.ts +11 -0
- package/extensions/bytefly/index.ts +4 -3
- package/extensions/bytefly/notify/config.ts +2 -0
- package/extensions/bytefly/notify/format.ts +19 -1
- package/extensions/bytefly/notify/register.ts +37 -13
- package/extensions/bytefly/notify/types.ts +2 -0
- package/extensions/bytefly/panel/commands.ts +34 -13
- package/extensions/bytefly/panel/settings.ts +98 -16
- package/extensions/bytefly/update/cache.ts +73 -0
- package/extensions/bytefly/update/format.ts +65 -10
- package/extensions/bytefly/update/register.ts +99 -13
- package/extensions/bytefly/update/state.ts +13 -1
- package/extensions/document/README.md +22 -12
- package/extensions/document/config.ts +4 -0
- package/extensions/document/deadline.ts +61 -0
- package/extensions/document/tool.ts +13 -2
- package/extensions/file-browse/README.md +12 -8
- package/extensions/file-browse/client.ts +6 -7
- package/extensions/file-browse/files.ts +0 -4
- package/extensions/file-browse/git.ts +9 -10
- package/extensions/file-browse/html.ts +17 -7
- package/extensions/file-browse/router.ts +24 -15
- package/extensions/goal/README.md +7 -1
- package/extensions/goal/schema.ts +6 -2
- package/extensions/goal/types.ts +6 -2
- package/extensions/job/README.md +16 -12
- package/extensions/job/runtime.ts +23 -6
- package/extensions/job/status.ts +4 -2
- package/extensions/memory/README.md +55 -13
- package/extensions/memory/commands.ts +20 -2
- package/extensions/memory/doctor.ts +137 -0
- package/extensions/memory/format.ts +24 -7
- package/extensions/memory/runtime.ts +49 -0
- package/extensions/memory/store.ts +31 -0
- package/extensions/memory/tui.ts +136 -12
- package/extensions/plan/README.md +60 -29
- package/extensions/plan/commands.ts +67 -8
- package/extensions/plan/index.ts +35 -11
- package/extensions/plan/plan-index.ts +234 -0
- package/extensions/plan/plan-text.ts +95 -0
- package/extensions/plan/plans.ts +90 -1
- package/extensions/plan/review.ts +12 -9
- package/extensions/plan/runtime.ts +72 -27
- package/extensions/plan/steps.ts +35 -25
- package/extensions/plan/todos.ts +24 -2
- package/extensions/plan/tools.ts +124 -24
- package/extensions/plan/tui.ts +7 -28
- package/extensions/plan/types.ts +9 -0
- package/extensions/plan/web-page.ts +2 -2
- package/extensions/plan/web.ts +12 -11
- package/extensions/rewind/README.md +28 -8
- package/extensions/rewind/commands.ts +1 -1
- package/extensions/rewind/format.ts +4 -4
- package/extensions/rewind/git.ts +56 -18
- package/extensions/rewind/index.ts +3 -5
- package/extensions/rewind/path-picker.ts +198 -0
- package/extensions/rewind/paths.ts +128 -0
- package/extensions/rewind/restore.ts +69 -28
- package/extensions/rewind/rewind.ts +80 -3
- package/extensions/rewind/runtime.ts +13 -5
- package/extensions/rewind/snapshot-run.ts +5 -2
- package/extensions/rewind/types.ts +2 -0
- package/extensions/subagent/README.md +7 -1
- package/extensions/subagent/accounting.ts +34 -0
- package/extensions/subagent/index.ts +23 -1
- package/extensions/subagent/stream.ts +2 -0
- package/extensions/subagent/types.ts +2 -0
- package/extensions/watch/README.md +8 -3
- package/extensions/watch/index.ts +19 -0
- package/extensions/web-access/README.md +43 -13
- package/extensions/web-access/charset.ts +58 -0
- package/extensions/web-access/fetch/find.ts +11 -7
- package/extensions/web-access/fetch/format.ts +64 -18
- package/extensions/web-access/fetch/page.ts +5 -1
- package/extensions/web-access/fetch/tool.ts +19 -18
- package/extensions/web-access/http.ts +49 -8
- package/extensions/web-access/search/facts.ts +38 -0
- package/extensions/web-access/search/tool.ts +13 -0
- package/extensions/web-access/text.ts +50 -11
- package/extensions/worktree/README.md +74 -15
- package/extensions/worktree/config.ts +11 -0
- package/extensions/worktree/exit.ts +35 -3
- package/extensions/worktree/gaps.ts +74 -0
- package/extensions/worktree/guard.ts +50 -4
- package/extensions/worktree/handoff.ts +55 -0
- package/extensions/worktree/index.ts +12 -2
- package/extensions/worktree/runtime.ts +20 -0
- package/extensions/worktree/status.ts +15 -2
- package/extensions/worktree/tools.ts +3 -2
- package/lib/README.md +10 -8
- package/lib/events.ts +86 -1
- package/lib/goal-state.ts +95 -0
- package/lib/requirements.ts +282 -62
- package/lib/tui/README.md +91 -0
- package/lib/tui/glyphs.ts +1 -0
- package/lib/web/layout.ts +12 -2
- package/package.json +5 -3
- package/themes/bytefly-dark.json +1 -1
- package/assets/chrome.png +0 -0
- package/assets/file-browse.png +0 -0
- package/assets/job.png +0 -0
- package/assets/plan-review.png +0 -0
- package/assets/question.png +0 -0
- package/assets/rewind.png +0 -0
- package/assets/todo.png +0 -0
- package/assets/tool-renderings.png +0 -0
- package/assets/tools/ask_user_question-cancelled.png +0 -0
- package/assets/tools/ask_user_question.png +0 -0
- package/assets/tools/bash.png +0 -0
- package/assets/tools/edit.png +0 -0
- package/assets/tools/enter_plan_mode.png +0 -0
- package/assets/tools/enter_worktree.png +0 -0
- package/assets/tools/exit_plan_mode.png +0 -0
- package/assets/tools/exit_worktree.png +0 -0
- package/assets/tools/find.png +0 -0
- package/assets/tools/goal.png +0 -0
- package/assets/tools/grep.png +0 -0
- package/assets/tools/job.png +0 -0
- package/assets/tools/list_worktrees.png +0 -0
- package/assets/tools/ls.png +0 -0
- package/assets/tools/memory-long-subject.png +0 -0
- package/assets/tools/memory.png +0 -0
- package/assets/tools/notice.png +0 -0
- package/assets/tools/open_path.png +0 -0
- package/assets/tools/prune_worktrees.png +0 -0
- package/assets/tools/read.png +0 -0
- package/assets/tools/read_doc-running.png +0 -0
- package/assets/tools/read_doc.png +0 -0
- package/assets/tools/run_doctor.png +0 -0
- package/assets/tools/send_wechat.png +0 -0
- package/assets/tools/set_plan_steps.png +0 -0
- package/assets/tools/subagent.png +0 -0
- package/assets/tools/todo.png +0 -0
- package/assets/tools/turn-rule.png +0 -0
- package/assets/tools/user-message.png +0 -0
- package/assets/tools/watch.png +0 -0
- package/assets/tools/web_fetch-rejected.png +0 -0
- package/assets/tools/web_fetch.png +0 -0
- package/assets/tools/web_search.png +0 -0
- package/assets/tools/write.png +0 -0
- package/assets/tools/write_plan.png +0 -0
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* and `bun build --no-bundle` needs no separate asset.
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { THEME_TOGGLE_JS } from "../../lib/web/index.ts";
|
|
14
|
+
import { SIDEBAR_COLLAPSE_MAX, THEME_TOGGLE_JS } from "../../lib/web/index.ts";
|
|
15
15
|
|
|
16
16
|
export const CLIENT_JS = `(function () {
|
|
17
17
|
"use strict";
|
|
@@ -183,7 +183,7 @@ export const CLIENT_JS = `(function () {
|
|
|
183
183
|
|
|
184
184
|
var panel = document.querySelector("details.bf-app__sidebar");
|
|
185
185
|
if (panel && window.matchMedia) {
|
|
186
|
-
var mq = window.matchMedia("(max-width:
|
|
186
|
+
var mq = window.matchMedia("(max-width: ${SIDEBAR_COLLAPSE_MAX}px)");
|
|
187
187
|
var syncSide = function () { panel.open = !mq.matches; };
|
|
188
188
|
syncSide();
|
|
189
189
|
if (mq.addEventListener) mq.addEventListener("change", syncSide);
|
|
@@ -289,11 +289,11 @@ export const CLIENT_JS = `(function () {
|
|
|
289
289
|
});
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
-
function flash(button, label) {
|
|
292
|
+
function flash(button, label, message) {
|
|
293
293
|
var original = button.textContent;
|
|
294
294
|
button.textContent = label;
|
|
295
295
|
var status = document.getElementById("copy-status");
|
|
296
|
-
if (status) status.textContent =
|
|
296
|
+
if (status) status.textContent = message;
|
|
297
297
|
window.setTimeout(function () { button.textContent = original; }, 1200);
|
|
298
298
|
}
|
|
299
299
|
|
|
@@ -310,8 +310,8 @@ export const CLIENT_JS = `(function () {
|
|
|
310
310
|
}
|
|
311
311
|
source
|
|
312
312
|
.then(function (text) { return copyText(text); })
|
|
313
|
-
.then(function () { flash(button, "copied"); })
|
|
314
|
-
.catch(function () { flash(button, "failed"); });
|
|
313
|
+
.then(function () { flash(button, "copied", "Copied to clipboard"); })
|
|
314
|
+
.catch(function () { flash(button, "failed", "Copy failed"); });
|
|
315
315
|
});
|
|
316
316
|
});
|
|
317
317
|
|
|
@@ -483,7 +483,6 @@ export const CLIENT_JS = `(function () {
|
|
|
483
483
|
option.id = "palette-option-" + index;
|
|
484
484
|
option.setAttribute("role", "option");
|
|
485
485
|
option.setAttribute("data-index", String(index));
|
|
486
|
-
option.setAttribute("aria-selected", index === 0 ? "true" : "false");
|
|
487
486
|
appendHighlighted(option, result.path, result.positions);
|
|
488
487
|
paletteList.appendChild(option);
|
|
489
488
|
});
|
|
@@ -46,7 +46,6 @@ const TEXT_EXTS = new Set([
|
|
|
46
46
|
".ini",
|
|
47
47
|
".cfg",
|
|
48
48
|
".conf",
|
|
49
|
-
".env",
|
|
50
49
|
".xml",
|
|
51
50
|
".csv",
|
|
52
51
|
".tsv",
|
|
@@ -89,9 +88,6 @@ const TEXT_EXTS = new Set([
|
|
|
89
88
|
".vue",
|
|
90
89
|
".svelte",
|
|
91
90
|
".astro",
|
|
92
|
-
".gitignore",
|
|
93
|
-
".gitattributes",
|
|
94
|
-
".editorconfig",
|
|
95
91
|
]);
|
|
96
92
|
const TEXT_NAMES = new Set([
|
|
97
93
|
"license",
|
|
@@ -137,8 +137,8 @@ interface RepoState {
|
|
|
137
137
|
const INSERTIONS = /(\d+) insertions?\(\+\)/;
|
|
138
138
|
const DELETIONS = /(\d+) deletions?\(-\)/;
|
|
139
139
|
|
|
140
|
-
/** A count from a
|
|
141
|
-
function
|
|
140
|
+
/** A count from a diffstat summary line, or `0` when the line does not carry it. */
|
|
141
|
+
function summaryCount(text: string, pattern: RegExp): number {
|
|
142
142
|
const match = pattern.exec(text);
|
|
143
143
|
return match ? Number(match[1]) : 0;
|
|
144
144
|
}
|
|
@@ -218,17 +218,16 @@ export function createGitDiff(
|
|
|
218
218
|
const current = await state();
|
|
219
219
|
if (!current) return undefined;
|
|
220
220
|
const args = diffArgs(effectiveMode(mode, current.hasCommits));
|
|
221
|
-
const [stat,
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
const summary = shortstat.code === 0 ? shortstat.stdout : "";
|
|
221
|
+
const stat = await run(["diff", "--no-color", "--stat", ...args, "--"], { cwd: root });
|
|
222
|
+
const output = stat.code === 0 ? stat.stdout : "";
|
|
223
|
+
const trimmed = output.trimEnd();
|
|
224
|
+
const summary = trimmed.slice(trimmed.lastIndexOf("\n") + 1);
|
|
226
225
|
return {
|
|
227
226
|
mode,
|
|
228
227
|
files: current.files.filter((file) => inMode(file, mode)),
|
|
229
|
-
stat:
|
|
230
|
-
insertions:
|
|
231
|
-
deletions:
|
|
228
|
+
stat: output.trim(),
|
|
229
|
+
insertions: summaryCount(summary, INSERTIONS),
|
|
230
|
+
deletions: summaryCount(summary, DELETIONS),
|
|
232
231
|
};
|
|
233
232
|
},
|
|
234
233
|
async file(rel, mode) {
|
|
@@ -12,6 +12,7 @@ import { GLYPHS } from "../../lib/tui/glyphs.ts";
|
|
|
12
12
|
import {
|
|
13
13
|
brandLockup,
|
|
14
14
|
docHead,
|
|
15
|
+
SIDEBAR_COLLAPSE_MAX,
|
|
15
16
|
WEB_BASE_CSS,
|
|
16
17
|
WEB_COMPONENTS_CSS,
|
|
17
18
|
WEB_LAYOUT_CSS,
|
|
@@ -61,7 +62,7 @@ export interface ChangeSummary {
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
/** Whether a path carries a change marker: a changed file, or a directory above one. */
|
|
64
|
-
|
|
65
|
+
function isChanged(summary: ChangeSummary | undefined, rel: string, isDir: boolean): boolean {
|
|
65
66
|
if (!summary || !rel) return false;
|
|
66
67
|
return isDir ? summary.dirs.has(rel) : summary.files.has(rel);
|
|
67
68
|
}
|
|
@@ -169,6 +170,16 @@ body.palette-open{overflow:hidden}
|
|
|
169
170
|
.browse-keys dt{white-space:nowrap}
|
|
170
171
|
.browse-keys dd{margin:0;color:var(--muted);font-size:var(--fs-sm)}
|
|
171
172
|
.browse-kbd{display:inline-block;min-width:20px;padding:1px 6px;border:1px solid var(--border);border-bottom-width:2px;border-radius:var(--radius-sm);background:var(--surface);color:var(--text);font:var(--fs-xs) var(--font-mono);text-align:center}
|
|
173
|
+
/* Wide screens: pin quick open and the filter, and scroll the tree on its own
|
|
174
|
+
box. The sidebar is a details element, whose content sits in an anonymous
|
|
175
|
+
wrapper with no definite height, so the panel fills the sidebar by absolute
|
|
176
|
+
positioning; below the sidebar breakpoint the disclosure keeps normal page
|
|
177
|
+
scroll. */
|
|
178
|
+
@media not (max-width:${SIDEBAR_COLLAPSE_MAX}px){
|
|
179
|
+
.browse-sidebar{position:relative;overflow:hidden}
|
|
180
|
+
.browse-side{position:absolute;inset:0;display:flex;flex-direction:column;overflow:hidden}
|
|
181
|
+
.browse-side .bf-tree{flex:1 1 auto;min-height:0;overflow:auto}
|
|
182
|
+
}
|
|
172
183
|
@media (max-width:820px){
|
|
173
184
|
.browse-root{order:9;flex:1 1 100%;direction:ltr;text-align:left}
|
|
174
185
|
}
|
|
@@ -238,7 +249,7 @@ ${SPRITE}
|
|
|
238
249
|
<button type="button" class="bf-btn bf-btn--icon bf-theme-toggle" id="theme-toggle" aria-label="Toggle light and dark theme" title="Toggle light and dark theme"><span class="theme-glyph" aria-hidden="true">☾</span></button>
|
|
239
250
|
</div><div class="browse-progress" aria-hidden="true"><span class="browse-progress__fill" id="progress-fill"></span></div></header>
|
|
240
251
|
<div class="bf-app__body">
|
|
241
|
-
<details class="bf-app__sidebar" open>
|
|
252
|
+
<details class="bf-app__sidebar browse-sidebar" open>
|
|
242
253
|
<summary class="bf-panel__summary">Files</summary>
|
|
243
254
|
<aside class="bf-panel browse-side">
|
|
244
255
|
<button type="button" class="browse-quick-open" id="quick-open" aria-haspopup="dialog"><span>quick open</span><kbd>/</kbd></button>
|
|
@@ -252,7 +263,7 @@ ${SPRITE}
|
|
|
252
263
|
${layout.main}
|
|
253
264
|
</main>
|
|
254
265
|
</div>
|
|
255
|
-
<footer class="bf-app__footer">served from ${escapeHtml(layout.rootLabel)} · read-only · ${layout.footerNote}</footer>
|
|
266
|
+
<footer class="bf-app__footer">served from ${escapeHtml(layout.rootLabel)} · read-only · ${escapeHtml(layout.footerNote)}</footer>
|
|
256
267
|
${layout.keys && layout.keys.length > 0 ? renderKeysSheet(layout.keys) : ""}
|
|
257
268
|
<script src="/app.js" defer></script>
|
|
258
269
|
</body>
|
|
@@ -283,7 +294,7 @@ function renderTreeNode(node: TreeNode, currentRel: string, summary?: ChangeSumm
|
|
|
283
294
|
const expanded = node.children.length > 0;
|
|
284
295
|
const isDir = node.isDir;
|
|
285
296
|
const spec = isDir ? FOLDER_ICON : languageOf(node.name);
|
|
286
|
-
const href =
|
|
297
|
+
const href = hrefFor(node);
|
|
287
298
|
const current = node.rel === currentRel ? ` aria-current="page"` : "";
|
|
288
299
|
const twisty = isDir
|
|
289
300
|
? `<button type="button" class="bf-tree__tw bf-tree__tw--toggle" aria-expanded="${expanded}" aria-label="${escapeHtml(node.name)}">${expanded ? "▾" : "▸"}</button>`
|
|
@@ -495,7 +506,7 @@ export function renderFilePage(page: FilePage): string {
|
|
|
495
506
|
mainClass: rendered ? "browse-page browse-measure" : "browse-page",
|
|
496
507
|
keys,
|
|
497
508
|
main: ` <div class="bf-card">${head}${content}</div>\n ${note}`,
|
|
498
|
-
footerNote:
|
|
509
|
+
footerNote: page.rel,
|
|
499
510
|
git: page.git,
|
|
500
511
|
summary: page.summary,
|
|
501
512
|
});
|
|
@@ -715,7 +726,7 @@ export function renderFileDiffPage(page: FileDiffPage): string {
|
|
|
715
726
|
mainClass: "browse-page",
|
|
716
727
|
keys,
|
|
717
728
|
main: ` <div class="bf-card">${head}${content}</div>\n ${note}`,
|
|
718
|
-
footerNote: `diff · ${
|
|
729
|
+
footerNote: `diff · ${modeScope(page.mode)}`,
|
|
719
730
|
git: page.git,
|
|
720
731
|
summary: page.summary,
|
|
721
732
|
});
|
|
@@ -726,7 +737,6 @@ export interface ErrorPage {
|
|
|
726
737
|
status: number;
|
|
727
738
|
message: string;
|
|
728
739
|
tree: TreeNode;
|
|
729
|
-
rel: string;
|
|
730
740
|
git?: GitStatus;
|
|
731
741
|
}
|
|
732
742
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* tested without a socket.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
+
import type { Stats } from "node:fs";
|
|
10
11
|
import { stat } from "node:fs/promises";
|
|
11
12
|
import { THEME_JS } from "../../lib/web/index.ts";
|
|
12
13
|
import { CLIENT_JS } from "./client.ts";
|
|
@@ -87,8 +88,8 @@ function jsonResponse(status: number, value: unknown): ServeResponse {
|
|
|
87
88
|
};
|
|
88
89
|
}
|
|
89
90
|
|
|
90
|
-
async function treeFor(ctx: ServeContext,
|
|
91
|
-
return buildTree(ctx.root,
|
|
91
|
+
async function treeFor(ctx: ServeContext, currentRel: string): Promise<TreeNode> {
|
|
92
|
+
return buildTree(ctx.root, currentRel, ctx.config.maxDirEntries);
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
/** Resolve the header's git context, never failing the request. */
|
|
@@ -153,18 +154,30 @@ async function errorResponse(
|
|
|
153
154
|
status,
|
|
154
155
|
message,
|
|
155
156
|
tree,
|
|
156
|
-
rel: "",
|
|
157
157
|
git: await gitFor(ctx),
|
|
158
158
|
});
|
|
159
159
|
return { status, headers: { ...HTML_SECURITY, ...extra }, body };
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
|
|
162
|
+
/** Resolve a request path once and render the directory or file page it names. */
|
|
163
|
+
async function renderPath(ctx: ServeContext, rawRel: string, raw: boolean): Promise<ServeResponse> {
|
|
163
164
|
const abs = await resolveRequestPath(ctx.root, rawRel);
|
|
164
165
|
const rel = relativePath(ctx.root, abs);
|
|
165
|
-
const info = await stat(abs).catch(() => undefined);
|
|
166
|
-
if (
|
|
166
|
+
const info: Stats | undefined = await stat(abs).catch(() => undefined);
|
|
167
|
+
if (info?.isDirectory()) return directoryResponse(ctx, rel);
|
|
168
|
+
if (!info) throw new HttpError(404, "That path does not exist.");
|
|
169
|
+
return fileResponse(ctx, rel, raw);
|
|
170
|
+
}
|
|
167
171
|
|
|
172
|
+
async function browse(ctx: ServeContext, rawRel: string): Promise<ServeResponse> {
|
|
173
|
+
return renderPath(ctx, rawRel, false);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function view(ctx: ServeContext, rawRel: string, raw: boolean): Promise<ServeResponse> {
|
|
177
|
+
return renderPath(ctx, rawRel, raw);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function directoryResponse(ctx: ServeContext, rel: string): Promise<ServeResponse> {
|
|
168
181
|
const listing = await listDirectory(ctx.root, rel, ctx.config.maxDirEntries);
|
|
169
182
|
const tree = await treeFor(ctx, rel);
|
|
170
183
|
const git = await gitFor(ctx);
|
|
@@ -184,13 +197,7 @@ async function browse(ctx: ServeContext, rawRel: string): Promise<ServeResponse>
|
|
|
184
197
|
);
|
|
185
198
|
}
|
|
186
199
|
|
|
187
|
-
async function
|
|
188
|
-
const abs = await resolveRequestPath(ctx.root, rawRel);
|
|
189
|
-
const rel = relativePath(ctx.root, abs);
|
|
190
|
-
const info = await stat(abs).catch(() => undefined);
|
|
191
|
-
if (info?.isDirectory()) return browse(ctx, rawRel);
|
|
192
|
-
if (!info) throw new HttpError(404, "That path does not exist.");
|
|
193
|
-
|
|
200
|
+
async function fileResponse(ctx: ServeContext, rel: string, raw: boolean): Promise<ServeResponse> {
|
|
194
201
|
const file = await readFileView(ctx.root, rel, ctx.config.maxFileBytes, ctx.config.maxTextLines, ctx.config.markdown);
|
|
195
202
|
const tree = await treeFor(ctx, rel);
|
|
196
203
|
const git = await gitFor(ctx);
|
|
@@ -375,7 +382,9 @@ export async function handleRequest(
|
|
|
375
382
|
if (pathname.startsWith("/raw/")) return await raw(ctx, pathname.slice("/raw/".length));
|
|
376
383
|
return errorResponse(ctx, 404, "That page does not exist.");
|
|
377
384
|
} catch (error) {
|
|
378
|
-
|
|
379
|
-
|
|
385
|
+
const status = error instanceof HttpError ? error.status : 500;
|
|
386
|
+
const message = error instanceof HttpError ? error.message : "Something went wrong reading that path.";
|
|
387
|
+
if (pathname.startsWith("/api/")) return jsonResponse(status, { error: message });
|
|
388
|
+
return errorResponse(ctx, status, message);
|
|
380
389
|
}
|
|
381
390
|
}
|
|
@@ -93,6 +93,10 @@ the branch, so the note appears whether the todos were written by the model, by
|
|
|
93
93
|
|
|
94
94
|
`goal` is allowlisted in `plan-mode` (alongside `todo`), so the model can record
|
|
95
95
|
or update the objective during read-only exploration without leaving plan mode.
|
|
96
|
+
Plan mode also reads the goal: it restates an active one in its injected context,
|
|
97
|
+
and when a plan is approved with no goal at all it offers to record the plan's
|
|
98
|
+
title as the goal (`lib/goal-state.ts` is the reader). An existing goal — active
|
|
99
|
+
or achieved — is never replaced by that offer.
|
|
96
100
|
|
|
97
101
|
### `/goal` command
|
|
98
102
|
|
|
@@ -232,7 +236,9 @@ depends on config.
|
|
|
232
236
|
todo list still holds open items appends one line to the model-facing result
|
|
233
237
|
naming the count. The list is replayed from the branch by
|
|
234
238
|
`lib/todo-state.ts` (shared with plan's approval-time merge, which reads the
|
|
235
|
-
same channel), never by importing the `todo` extension.
|
|
239
|
+
same channel), never by importing the `todo` extension. The same reasoning
|
|
240
|
+
applies to the goal itself: `lib/goal-state.ts` owns the entry type and the
|
|
241
|
+
objective limit, and plan reads the published goal through it. Only the text is
|
|
236
242
|
affected: `details` and `structuredContent` stay the goal alone, and the
|
|
237
243
|
transcript result is still the status header.
|
|
238
244
|
- **One verb table.** `GOAL_VERBS` is the only list of reserved words: the
|
|
@@ -9,10 +9,14 @@
|
|
|
9
9
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
10
10
|
import { Type, type Static } from "typebox";
|
|
11
11
|
import { sanitizeLine } from "../../lib/format.ts";
|
|
12
|
+
import { MAX_GOAL_OBJECTIVE } from "../../lib/goal-state.ts";
|
|
12
13
|
import { GOAL_ACTIONS, GOAL_STATUSES, type Goal, type GoalStatus } from "./types.ts";
|
|
13
14
|
|
|
14
|
-
/**
|
|
15
|
-
|
|
15
|
+
/**
|
|
16
|
+
* Maximum length of the objective. Owned by `lib/goal-state.ts`, so a reader of
|
|
17
|
+
* the published state applies the same limit as this validator.
|
|
18
|
+
*/
|
|
19
|
+
export const MAX_OBJECTIVE = MAX_GOAL_OBJECTIVE;
|
|
16
20
|
|
|
17
21
|
const GoalStatusEnum = StringEnum(GOAL_STATUSES, {
|
|
18
22
|
description: 'Whether the goal is still being pursued. Defaults to "active".',
|
package/extensions/goal/types.ts
CHANGED
|
@@ -15,8 +15,12 @@ export const GOAL_ACTIONS = ["set", "achieve", "clear"] as const;
|
|
|
15
15
|
|
|
16
16
|
export type GoalAction = (typeof GOAL_ACTIONS)[number];
|
|
17
17
|
|
|
18
|
-
/**
|
|
19
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Custom-entry type used to persist a goal set outside the tool (the `/goal`
|
|
20
|
+
* command). Owned by `lib/goal-state.ts` because another extension's reader
|
|
21
|
+
* dispatches on it; re-exported here so goal's own modules keep one import.
|
|
22
|
+
*/
|
|
23
|
+
export { GOAL_ENTRY_TYPE } from "../../lib/goal-state.ts";
|
|
20
24
|
|
|
21
25
|
/** How an achieved goal renders in the persistent widget. */
|
|
22
26
|
export type AchievedStyle = "show" | "hide";
|
package/extensions/job/README.md
CHANGED
|
@@ -38,10 +38,11 @@ How the `job` call and its result look in the transcript — the call line as a
|
|
|
38
38
|
killing, and clearing.
|
|
39
39
|
- Reports finished jobs to the model at the start of the next turn, or — for
|
|
40
40
|
jobs started with `wake: true` — by triggering one turn.
|
|
41
|
-
- Kills session-owned jobs on shutdown so no process is left behind;
|
|
42
|
-
job with `detached: true` to keep it running and reattach next session.
|
|
43
|
-
detached
|
|
44
|
-
logging after this process ends.
|
|
41
|
+
- Kills session-owned jobs on shutdown so no process is left behind; on POSIX,
|
|
42
|
+
start a job with `detached: true` to keep it running and reattach next session.
|
|
43
|
+
A detached job points its own output straight at its log file, so it keeps
|
|
44
|
+
logging after this process ends. Windows has neither a process group to leave
|
|
45
|
+
nor a shell `EXIT` trap, so it refuses the detach instead of faking one.
|
|
45
46
|
|
|
46
47
|
## Tool
|
|
47
48
|
|
|
@@ -53,7 +54,7 @@ How the `job` call and its result look in the transcript — the call line as a
|
|
|
53
54
|
| `cwd` | Working directory (`start`); defaults to the session cwd. |
|
|
54
55
|
| `label` | Short display label (`start`); defaults to the command. |
|
|
55
56
|
| `wake` | Wake the agent with one triggered turn when it finishes (`start`). |
|
|
56
|
-
| `detached` | Leave it running when the session ends (`start`). |
|
|
57
|
+
| `detached` | Leave it running when the session ends (`start`); POSIX only, refused on Windows. |
|
|
57
58
|
| `stdin` | Give the job a stdin pipe so `send` can write to it (`start`); not with `detached`. |
|
|
58
59
|
| `id` | Job id such as `j1` (`status`/`logs`/`send`/`kill`/`wait`/`clear`). |
|
|
59
60
|
| `lines` | Log lines to return (`logs`, 1–2000). |
|
|
@@ -191,7 +192,7 @@ document, project wins):
|
|
|
191
192
|
| `enabled` | `true` | Track jobs at all. |
|
|
192
193
|
| `showStatus` | `true` | Show the `▸ N` / `✗ N` status chips. |
|
|
193
194
|
| `wakeOnFinish` | `false` | Wake the agent when any job finishes. |
|
|
194
|
-
| `detachedByDefault` | `false` | Leave jobs running when the session ends. |
|
|
195
|
+
| `detachedByDefault` | `false` | Leave jobs running when the session ends; inert on Windows. |
|
|
195
196
|
| `maxJobs` | `20` | Finished jobs kept before the oldest are pruned. |
|
|
196
197
|
| `maxLogLines` | `100` | Log lines returned by `logs`. |
|
|
197
198
|
| `maxLogBytes` | `8388608` | Bytes a log segment may reach before it rotates to `<id>.log.1`; `0` keeps logs unbounded. |
|
|
@@ -214,8 +215,8 @@ rewritten.
|
|
|
214
215
|
- **`unknown` still has no exit code on some paths.** A POSIX job is wrapped in
|
|
215
216
|
an `EXIT` trap that writes `$?` to a per-job `.status` file, so a job
|
|
216
217
|
reattached after its session died reports its real exit code. `exec cmd`
|
|
217
|
-
replaces the shell and bypasses the trap, and Windows has no equivalent,
|
|
218
|
-
those
|
|
218
|
+
replaces the shell and bypasses the trap, and a Windows job has no equivalent,
|
|
219
|
+
so those settle as `unknown`.
|
|
219
220
|
- **Another session's jobs are shown, but not managed here.** Records are merged
|
|
220
221
|
from every session file so ids stay unique and a peer's job is not reaped, and
|
|
221
222
|
`list`/`/jobs` show them tagged `peer` (a dead session's records are adopted
|
|
@@ -236,10 +237,13 @@ rewritten.
|
|
|
236
237
|
keep appending to the moved inode, or lose everything. Its log therefore grows
|
|
237
238
|
without the `maxLogBytes` cap; `logs` still reads a bounded tail, and the file
|
|
238
239
|
can be emptied by hand.
|
|
239
|
-
- **Windows
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
240
|
+
- **Windows cannot detach a job.** There is no process group to leave and no
|
|
241
|
+
shell `EXIT` trap, so `detached: true` is refused with the three alternatives
|
|
242
|
+
(`detached: false`, WSL, a `subagent`) rather than starting a job that would
|
|
243
|
+
die with the session while claiming to outlive it. `job.detachedByDefault` is
|
|
244
|
+
inert there instead of failing every start with a config set on another
|
|
245
|
+
machine, and `doctor` names both facts. Tree-kill is still `taskkill /T`, and a
|
|
246
|
+
job reported only after its process is gone settles as `unknown`.
|
|
243
247
|
|
|
244
248
|
## Non-goals
|
|
245
249
|
|
|
@@ -127,6 +127,8 @@ export interface RuntimeOptions {
|
|
|
127
127
|
killTree?: KillTreeFn;
|
|
128
128
|
startToken?: StartTokenFn;
|
|
129
129
|
now?: () => number;
|
|
130
|
+
/** Platform seam: whether this is Windows. Defaults to the running platform. */
|
|
131
|
+
isWindows?: () => boolean;
|
|
130
132
|
/** Fixed config, for tests; production loads it per cwd. */
|
|
131
133
|
config?: JobsConfig;
|
|
132
134
|
}
|
|
@@ -202,6 +204,7 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
|
|
|
202
204
|
const killTree = options.killTree ?? defaultKillTree;
|
|
203
205
|
const startToken = options.startToken ?? defaultStartToken;
|
|
204
206
|
const now = options.now ?? (() => Date.now());
|
|
207
|
+
const isWindows = options.isWindows ?? (() => process.platform === "win32");
|
|
205
208
|
|
|
206
209
|
const jobs = new Map<string, Job>();
|
|
207
210
|
const handles = new Map<string, Handle>();
|
|
@@ -717,7 +720,20 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
|
|
|
717
720
|
if (!config.enabled) throw new Error("jobs are disabled.");
|
|
718
721
|
ui.attach(ctx);
|
|
719
722
|
const wantsStdin = startOptions.stdin === true;
|
|
720
|
-
|
|
723
|
+
// Windows has no process group to leave and no shell `EXIT` trap to record an
|
|
724
|
+
// exit code, so a job there cannot outlive the session: refusing an explicit
|
|
725
|
+
// detach is the honest answer, and it happens before any id is reserved.
|
|
726
|
+
// `detachedByDefault` stays inert instead of failing every start with a config
|
|
727
|
+
// the user set on another machine; `doctor` names that.
|
|
728
|
+
const windows = isWindows();
|
|
729
|
+
if (windows && startOptions.detached === true) {
|
|
730
|
+
throw new Error(
|
|
731
|
+
"A detached job cannot outlive the session on Windows: there is no process group to leave " +
|
|
732
|
+
"and no shell EXIT trap to record its exit code. Start it with `detached: false`, " +
|
|
733
|
+
"run it under WSL, or move the work into a `subagent`.",
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
const detached = windows ? false : (startOptions.detached ?? config.detachedByDefault);
|
|
721
737
|
// A detached job outlives this process, so a stdin pipe owned by this session
|
|
722
738
|
// would hand it EOF the moment the session ends; refuse instead of silently
|
|
723
739
|
// dropping the pipe (or the detach).
|
|
@@ -732,14 +748,15 @@ export function createJobsRuntime(options: RuntimeOptions = {}): JobsRuntime {
|
|
|
732
748
|
// so a peer that advanced the shared counter cannot hand us a live job's id.
|
|
733
749
|
const id = store.nextId(jobs.keys());
|
|
734
750
|
const logPath = store.logPath(id);
|
|
735
|
-
|
|
736
|
-
//
|
|
737
|
-
//
|
|
738
|
-
const
|
|
751
|
+
// Only a POSIX shell has an `EXIT` trap to record a status, and only a detached
|
|
752
|
+
// job points its own output at the log so it keeps logging after this process
|
|
753
|
+
// ends (which on Windows is never: `detached` is false there).
|
|
754
|
+
const statusPath = windows ? null : store.statusPath(id);
|
|
755
|
+
const detachOutput = detached;
|
|
739
756
|
|
|
740
757
|
// Wrap the command so a POSIX shell records its exit status; the path travels
|
|
741
758
|
// in the environment so the shell never has to quote it.
|
|
742
|
-
const program = withExitTrap(startOptions.command,
|
|
759
|
+
const program = withExitTrap(startOptions.command, windows, detachOutput);
|
|
743
760
|
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
744
761
|
if (statusPath) env[STATUS_ENV] = statusPath;
|
|
745
762
|
if (detachOutput) env[LOG_ENV] = logPath;
|
package/extensions/job/status.ts
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* shell never has to quote it). A later session reads that file when it finds a
|
|
8
8
|
* dead pid and reports `exited`/`failed` instead of `unknown`.
|
|
9
9
|
*
|
|
10
|
-
* Windows shells have no equivalent trap
|
|
11
|
-
*
|
|
10
|
+
* Windows shells have no equivalent trap. A Windows job is never detached (the
|
|
11
|
+
* runtime refuses it), so the missing trap is not a second limitation: a job
|
|
12
|
+
* reported only after its process is gone still settles as `unknown`, which
|
|
13
|
+
* `doctor` names.
|
|
12
14
|
*/
|
|
13
15
|
|
|
14
16
|
import { closeSync, existsSync, openSync, readSync, statSync } from "node:fs";
|
|
@@ -39,9 +39,15 @@ How the `memory` call and its result look in the transcript — the call line as
|
|
|
39
39
|
- Re-reads each store when its file changes on disk (compared by mtime and size),
|
|
40
40
|
so an external edit lands in the same session without a restart.
|
|
41
41
|
- Exposes `/memory`: in the TUI it opens a picker to add, edit, or forget a
|
|
42
|
-
single note,
|
|
43
|
-
`/memory path`, `edit`, and `clear` print the file paths,
|
|
44
|
-
editor, and clear a scope
|
|
42
|
+
single note, merge two notes into one, move a note to the other scope, filter
|
|
43
|
+
by scope, and reload; elsewhere it lists the store. `/memory path`, `edit`, and `clear` print the file paths,
|
|
44
|
+
open the multi-line editor, and clear a scope; `/memory doctor` reports store
|
|
45
|
+
health.
|
|
46
|
+
- Reports store health with `/memory doctor`: note counts and file bytes per
|
|
47
|
+
scope, how much of the `maxInjectBytes` budget the injection uses and which
|
|
48
|
+
scope overflowed it, and the near-duplicate pairs the conservative key already
|
|
49
|
+
considers one fact. It is read-only — the report names the pair and the picker
|
|
50
|
+
merges it.
|
|
45
51
|
- Loads and writes the project store only when the project is trusted, sanitizes
|
|
46
52
|
every note to one terminal-safe line, and writes atomically.
|
|
47
53
|
|
|
@@ -91,24 +97,49 @@ or stem both survive.
|
|
|
91
97
|
|
|
92
98
|
| Command | What it does |
|
|
93
99
|
|---|---|
|
|
94
|
-
| `/memory` | In the TUI, open the picker (add, edit, or
|
|
100
|
+
| `/memory` | In the TUI, open the picker (add, edit, forget, merge, or move notes; `s`/Tab filters by scope; `r` reloads). Other modes notify the stored notes. |
|
|
95
101
|
| `/memory list` | Notify the stored notes and counts. |
|
|
96
102
|
| `/memory path` | Notify the two file paths and whether the project store is trusted. |
|
|
97
103
|
| `/memory reload` | Re-read the stores from disk and report what changed. |
|
|
104
|
+
| `/memory doctor` | Report counts, file bytes, the injection budget with the notes it omits, and near-duplicate pairs. Read-only. |
|
|
98
105
|
| `/memory edit [project\|global]` | Open the multi-line editor prefilled with the file and save the result. |
|
|
99
106
|
| `/memory clear [project\|global]` | Clear a scope after a confirmation. |
|
|
100
107
|
|
|
101
108
|
Bulk deletion lives only in the command, behind a confirm — the model cannot wipe
|
|
102
109
|
memory in one call.
|
|
103
110
|
|
|
111
|
+
### Picker keys
|
|
112
|
+
|
|
113
|
+
| Key | Action |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `Enter` / `e` | Edit the focused note in the shared inline editor. |
|
|
116
|
+
| `a` | Add a note to the active scope. |
|
|
117
|
+
| `d` | Forget the focused note after a `y`/`N` confirm. |
|
|
118
|
+
| `m` | Mark the focused note as a merge source; `m` on a second note in the same scope opens the editor pre-filled with `"<first>; <second>"`. `m` on the marked note unmarks it. |
|
|
119
|
+
| `p` | Move the focused note to the other scope: project → global, or global → project. No confirmation; `p` again moves it back, and a move into an untrusted project is refused. |
|
|
120
|
+
| `s` / `Tab` | Cycle the scope filter. |
|
|
121
|
+
| `r` | Re-read the stores. |
|
|
122
|
+
| `Esc` | Cancel the mark, the editor, or the screen, in that order. |
|
|
123
|
+
|
|
124
|
+
A merge is one write: the store replaces the first note in place and drops the
|
|
125
|
+
second, so a cancelled or failed merge leaves both notes as they were. Two notes
|
|
126
|
+
in different scopes are refused — the stores are separate files.
|
|
127
|
+
|
|
128
|
+
A move is two writes across those files, ordered target first: the note is added
|
|
129
|
+
to the destination and only then removed from the source. If the second write
|
|
130
|
+
fails the note is in both scopes — a duplicate `/memory doctor` reports and `m`
|
|
131
|
+
merges — rather than gone. A note the destination already holds is not
|
|
132
|
+
duplicated, and the move still removes it from the source.
|
|
133
|
+
|
|
104
134
|
## Behaviour by mode
|
|
105
135
|
|
|
106
136
|
The tool and the context injection work in every mode (`tui`, `rpc`, `json`,
|
|
107
|
-
`print`)
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
137
|
+
`print`), and `/memory doctor` reports through `notify` everywhere. The picker
|
|
138
|
+
opened by a bare `/memory` requires `ctx.mode === "tui"`, and `/memory edit` and
|
|
139
|
+
`/memory clear` require `ctx.hasUI`; in JSON and print sessions the command
|
|
140
|
+
reports that the interactive form needs a TUI session. Memory itself is external
|
|
141
|
+
state, so it is not reconstructed from the session tree and tree navigation does
|
|
142
|
+
not change it.
|
|
112
143
|
|
|
113
144
|
## Configuration
|
|
114
145
|
|
|
@@ -152,10 +183,11 @@ skill. Mitigations:
|
|
|
152
183
|
- The near-duplicate guard on `add` is deliberately conservative: it folds
|
|
153
184
|
case, whitespace runs, and trailing sentence punctuation only, never word
|
|
154
185
|
order, stemming, or internal punctuation. Two paraphrases can therefore still
|
|
155
|
-
both be stored;
|
|
186
|
+
both be stored; `/memory doctor` reports the pairs the key does consider equal,
|
|
187
|
+
and `m` in the picker merges them under wording you write.
|
|
156
188
|
- `update` replaces every exact match of `text`; a replacement that already
|
|
157
189
|
exists under a different note can leave a duplicate. Forget the old note first
|
|
158
|
-
when consolidating.
|
|
190
|
+
when consolidating, or merge the pair in the picker afterward.
|
|
159
191
|
- Only one note per line; multi-line notes are collapsed to a single line.
|
|
160
192
|
|
|
161
193
|
## Non-goals
|
|
@@ -180,6 +212,15 @@ when consolidating.
|
|
|
180
212
|
|
|
181
213
|
## Design notes
|
|
182
214
|
|
|
215
|
+
- **Health reads the injection it describes.** `injectionPlan` is the same
|
|
216
|
+
cap arithmetic `buildInjection` joins into the context body, so the doctor
|
|
217
|
+
cannot claim notes fit while recall drops them; `buildInjection` is a join over
|
|
218
|
+
that plan, which is why the report needed no second budget model.
|
|
219
|
+
- **A merge is one transform.** `applyMerge` replaces the first note and drops
|
|
220
|
+
the second inside the same read-modify-write the other mutations use, so a
|
|
221
|
+
cancelled or failed merge cannot leave one note gone and the other not. The
|
|
222
|
+
editor is pre-filled with both notes, but the wording saved is the user's — the
|
|
223
|
+
duplicate rule stays conservative and nothing is folded automatically.
|
|
183
224
|
- **External storage, deliberately.** `goal`/`todo` keep state in tool-result
|
|
184
225
|
`details` because it follows the active branch. Memory is the opposite: it must
|
|
185
226
|
outlive the branch and be editable outside Pi, so it is plain files read at
|
|
@@ -207,11 +248,12 @@ when consolidating.
|
|
|
207
248
|
| `types.ts` | `MemoryScope`, `MemoryAction`, `MemoryDetails`, the context type. |
|
|
208
249
|
| `config.ts` | Defaults and the `memory` section of `bytefly.json` loading/clamping. |
|
|
209
250
|
| `schema.ts` | TypeBox params/result and pure validation of action, scope, and text. |
|
|
210
|
-
| `store.ts` | Paths, parse/read, file stamps, add/update/forget/clear transforms, atomic queued write. |
|
|
251
|
+
| `store.ts` | Paths, parse/read, file stamps, add/update/forget/merge/clear transforms, atomic queued write. |
|
|
252
|
+
| `doctor.ts` | The health report (pure): counts, file bytes, the injection plan's omissions, and near-duplicate pairs. |
|
|
211
253
|
| `runtime.ts` | Session view of both stores, stamp-based refresh, trust gating, mutations. |
|
|
212
254
|
| `format.ts` | Pure call/result/notice text and the bounded injection body. |
|
|
213
255
|
| `errors.ts` | `messageOf`, the shared error-text extraction for the tool and command. |
|
|
214
|
-
| `tui.ts` | The `/memory` picker component (note list, inline editor, scope filter), drawn from the `lib/tui/` kit (`titleRule`, `dockListFrame`, `listRow`, `emptyState`). |
|
|
256
|
+
| `tui.ts` | The `/memory` picker component (note list, inline editor, merge mark, scope move, scope filter), drawn from the `lib/tui/` kit (`titleRule`, `dockListFrame`, `listRow`, `emptyState`). |
|
|
215
257
|
| `tools.ts` | `registerTool("memory", …)`. |
|
|
216
258
|
| `commands.ts` | `registerCommand("memory", …)`. |
|
|
217
259
|
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
13
13
|
import { withRailsSuppressed } from "../../lib/rails.ts";
|
|
14
14
|
import { messageOf } from "./errors.ts";
|
|
15
|
+
import { formatHealth, memoryHealth } from "./doctor.ts";
|
|
15
16
|
import { formatNotice, scopeLabel } from "./format.ts";
|
|
16
17
|
import type { MemoryRuntime } from "./runtime.ts";
|
|
17
18
|
import { MemoryPickerComponent } from "./tui.ts";
|
|
@@ -30,10 +31,10 @@ function notify(ctx: ExtensionCommandContext, message: string, kind: "info" | "w
|
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
/** `/memory` subcommands and the scope argument accepted by `edit`/`clear`. */
|
|
33
|
-
const MEMORY_SUBCOMMANDS = ["list", "path", "reload", "edit", "clear"];
|
|
34
|
+
const MEMORY_SUBCOMMANDS = ["list", "path", "reload", "doctor", "edit", "clear"];
|
|
34
35
|
|
|
35
36
|
/** Usage line shared by the command description and the unknown-subcommand warning. */
|
|
36
|
-
const MEMORY_USAGE = "/memory [list | path | reload | edit [project|global] | clear [project|global]]";
|
|
37
|
+
const MEMORY_USAGE = "/memory [list | path | reload | doctor | edit [project|global] | clear [project|global]]";
|
|
37
38
|
|
|
38
39
|
/** Subcommand on the first token, scope on the second for `edit`/`clear`. */
|
|
39
40
|
function completeMemoryArg(prefix: string): Array<{ value: string; label: string }> | null {
|
|
@@ -74,6 +75,8 @@ async function openPicker(pi: ExtensionAPI, runtime: MemoryRuntime, ctx: Extensi
|
|
|
74
75
|
add: (scope, text) => runtime.add(scope, text),
|
|
75
76
|
update: (scope, from, to) => runtime.update(scope, from, to),
|
|
76
77
|
forget: (scope, entry) => runtime.forget(scope, entry),
|
|
78
|
+
merge: (scope, first, second, merged) => runtime.merge(scope, first, second, merged),
|
|
79
|
+
move: (from, to, text) => runtime.move(from, to, text),
|
|
77
80
|
},
|
|
78
81
|
() => {
|
|
79
82
|
stopTimer();
|
|
@@ -140,6 +143,21 @@ export function registerCommands(pi: ExtensionAPI, runtime: MemoryRuntime): void
|
|
|
140
143
|
);
|
|
141
144
|
return;
|
|
142
145
|
}
|
|
146
|
+
case "doctor": {
|
|
147
|
+
// Read-only: the report names what to fix, and `m` in the picker merges a pair.
|
|
148
|
+
notify(
|
|
149
|
+
ctx,
|
|
150
|
+
formatHealth(
|
|
151
|
+
memoryHealth({
|
|
152
|
+
entries: runtime.entries(),
|
|
153
|
+
sizes: runtime.sizes(),
|
|
154
|
+
trusted: runtime.trusted(),
|
|
155
|
+
config: runtime.config(),
|
|
156
|
+
}),
|
|
157
|
+
),
|
|
158
|
+
);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
143
161
|
case "edit": {
|
|
144
162
|
if (!ctx.hasUI) {
|
|
145
163
|
notify(ctx, "Editing memory requires an interactive session.", "warning");
|