pi-weave 0.1.7 → 0.1.9
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 +123 -37
- package/package.json +17 -5
- package/skills/weave-notepad/SKILL.md +13 -4
- package/src/core/cache/workspace.ts +466 -0
- package/src/core/concurrency.ts +36 -0
- package/src/core/frontmatter.ts +270 -23
- package/src/core/git.ts +19 -0
- package/src/core/graph/build.ts +97 -5
- package/src/core/graph/current.ts +41 -28
- package/src/core/graph/mentions.ts +170 -0
- package/src/core/graph/model.ts +39 -0
- package/src/core/graph/wikilinks.ts +5 -1
- package/src/core/index.ts +14 -0
- package/src/core/openInEditor.ts +69 -0
- package/src/core/paths.ts +7 -0
- package/src/core/sessions.ts +929 -0
- package/src/core/summaries.ts +1 -19
- package/src/core/types.ts +40 -0
- package/src/core/vault.ts +739 -57
- package/src/core/view/cluster.ts +262 -0
- package/src/core/view/detail.ts +118 -0
- package/src/core/view/focus.ts +109 -0
- package/src/core/view/health.ts +156 -0
- package/src/core/view/index.ts +15 -0
- package/src/core/view/links.ts +105 -0
- package/src/core/view/time.ts +47 -0
- package/src/core/view/tree.ts +269 -0
- package/src/core/view/types.ts +39 -0
- package/src/core/workspace.ts +3 -3
- package/src/pi/index.ts +248 -48
- package/src/pi/sessionScan.ts +104 -0
- package/src/pi/summarize.ts +24 -4
- package/src/pi/tools/noteTool.ts +13 -4
- package/src/pi/viewer/tui/branding.ts +8 -7
- package/src/pi/viewer/tui/explorer.ts +5 -3
- package/src/pi/viewer/tui/model.ts +46 -667
- package/src/pi/viewer/tui/openNote.ts +7 -56
- package/src/pi/viewer/tui/surface/explore.ts +4 -2
- package/src/pi/viewer/web/run.ts +331 -0
- package/src/web/client/api.dom.ts +40 -0
- package/src/web/client/api.ts +472 -0
- package/src/web/client/bootstrap.ts +58 -0
- package/src/web/client/context/context.model.ts +313 -0
- package/src/web/client/dist/app.js +764 -0
- package/src/web/client/graph/Graph.tsx +209 -0
- package/src/web/client/graph/column.model.ts +372 -0
- package/src/web/client/graph/dynamics.ts +176 -0
- package/src/web/client/graph/graph.model.ts +546 -0
- package/src/web/client/graph/positions.ts +380 -0
- package/src/web/client/graph/project.ts +153 -0
- package/src/web/client/graph/renderer.dom.ts +52 -0
- package/src/web/client/graph/renderer.ts +339 -0
- package/src/web/client/graph/scheme.ts +44 -0
- package/src/web/client/live.model.ts +275 -0
- package/src/web/client/live.ts +151 -0
- package/src/web/client/main.tsx +27 -0
- package/src/web/client/note/Editor.tsx +102 -0
- package/src/web/client/note/Note.tsx +113 -0
- package/src/web/client/note/editor.controller.ts +151 -0
- package/src/web/client/note/editor.model.ts +636 -0
- package/src/web/client/note/note.model.ts +738 -0
- package/src/web/client/search/SearchPalette.tsx +105 -0
- package/src/web/client/search/search.model.ts +588 -0
- package/src/web/client/search/search.ts +107 -0
- package/src/web/client/selection.storage.ts +69 -0
- package/src/web/client/shell/Columns.tsx +161 -0
- package/src/web/client/shell/ContextRail.tsx +87 -0
- package/src/web/client/shell/Divider.tsx +44 -0
- package/src/web/client/shell/FocusTrap.tsx +56 -0
- package/src/web/client/shell/Header.tsx +64 -0
- package/src/web/client/shell/HelpOverlay.tsx +70 -0
- package/src/web/client/shell/Shell.tsx +210 -0
- package/src/web/client/shell/StatusBar.tsx +28 -0
- package/src/web/client/shell/cssvars.ts +70 -0
- package/src/web/client/shell/drag.model.ts +170 -0
- package/src/web/client/shell/focus.model.ts +100 -0
- package/src/web/client/shell/keys.model.ts +453 -0
- package/src/web/client/shell/keys.ts +59 -0
- package/src/web/client/shell/layout.model.ts +526 -0
- package/src/web/client/shell/shell.model.ts +333 -0
- package/src/web/client/shell/theme.ts +490 -0
- package/src/web/client/shell/viewport.ts +29 -0
- package/src/web/client/state.ts +89 -0
- package/src/web/client/tree/Tree.tsx +141 -0
- package/src/web/client/tree/tree.model.ts +674 -0
- package/src/web/client/workspace.ts +278 -0
- package/src/web/server/page.ts +258 -0
- package/src/web/server/routes.ts +987 -0
- package/src/web/server/security.ts +361 -0
- package/src/web/server/server.ts +275 -0
- package/src/web/server/sse.ts +321 -0
- package/src/web/server/watcher.ts +507 -0
- package/src/web/shared/graph.ts +213 -0
- package/src/web/shared/layout.ts +314 -0
- package/src/web/shared/logo.ts +11 -0
- package/src/web/shared/metrics.ts +136 -0
- package/src/web/shared/view.ts +200 -0
- package/src/web/shared/wire.ts +358 -0
package/src/pi/index.ts
CHANGED
|
@@ -11,8 +11,10 @@ import {
|
|
|
11
11
|
} from "../core";
|
|
12
12
|
import { registerNoteTool } from "./tools/noteTool";
|
|
13
13
|
import { registerRepoTool } from "./tools/repoTool";
|
|
14
|
+
import { formatSessionScanResult, scanPiSessions } from "./sessionScan";
|
|
14
15
|
import { deepScanRepository, formatDeepScanResult } from "./summarize";
|
|
15
16
|
import { runWeaveViewTui } from "./viewer/tui/run";
|
|
17
|
+
import { WebWorkspaceController } from "./viewer/web/run";
|
|
16
18
|
|
|
17
19
|
/**
|
|
18
20
|
* pi-weave — an agent-native knowledge workspace (docs/design.md):
|
|
@@ -27,13 +29,21 @@ export default function piWeave(pi: ExtensionAPI): void {
|
|
|
27
29
|
registerNoteTool(pi);
|
|
28
30
|
registerRepoTool(pi);
|
|
29
31
|
|
|
30
|
-
// /weave-view
|
|
31
|
-
//
|
|
32
|
-
//
|
|
32
|
+
// /weave-view opens the browser workspace by default (weave-workspace §13);
|
|
33
|
+
// `tui` selects the in-terminal explorer. The browser workspace owns a
|
|
34
|
+
// server, a file watcher and live SSE streams for the rest of the session,
|
|
35
|
+
// so unlike the TUI it has a real teardown — see `session_shutdown` below.
|
|
33
36
|
let lastCtx: ExtensionContext | ExtensionCommandContext | null = null;
|
|
34
37
|
let lastStatusText: string | undefined = undefined;
|
|
35
38
|
let isActive = false;
|
|
36
39
|
|
|
40
|
+
const web = new WebWorkspaceController({
|
|
41
|
+
exec: (command, args) => pi.exec(command, args),
|
|
42
|
+
// A boot or an idle shutdown changes what the status line should say, and
|
|
43
|
+
// neither happens inside a command handler that could refresh it itself.
|
|
44
|
+
onStateChange: () => updateStatus(),
|
|
45
|
+
});
|
|
46
|
+
|
|
37
47
|
function updateStatus(ctx?: ExtensionContext | ExtensionCommandContext, text?: string): void {
|
|
38
48
|
if (ctx) lastCtx = ctx;
|
|
39
49
|
const c = ctx || lastCtx;
|
|
@@ -41,24 +51,50 @@ export default function piWeave(pi: ExtensionAPI): void {
|
|
|
41
51
|
if (text !== undefined) {
|
|
42
52
|
lastStatusText = text;
|
|
43
53
|
}
|
|
44
|
-
|
|
54
|
+
|
|
55
|
+
// A running browser workspace is a background process the user cannot
|
|
56
|
+
// otherwise see — an open port on their machine — so it always gets a
|
|
57
|
+
// marker. Resolved *before* the "nothing to show" test, because the port
|
|
58
|
+
// is itself something to show.
|
|
59
|
+
//
|
|
60
|
+
// This ordering is the fix for a real bug: the early return used to sit
|
|
61
|
+
// above this lookup, so a workspace started in a session that never
|
|
62
|
+
// emitted `session_start` (no seeded `lastStatusText`) did not merely
|
|
63
|
+
// fail to gain the marker — it actively cleared the status line while a
|
|
64
|
+
// server was listening. The browser-launch-failure path is exactly that
|
|
65
|
+
// shape, which is why it was the case that caught it.
|
|
66
|
+
const port = web.port();
|
|
67
|
+
const suffix = port === null ? "" : ` · web:${port}`;
|
|
68
|
+
|
|
69
|
+
// Clear only when there is genuinely nothing to say: no base text *and*
|
|
70
|
+
// no server.
|
|
71
|
+
if (!lastStatusText && port === null) {
|
|
45
72
|
c.ui.setStatus("weave", undefined);
|
|
46
73
|
return;
|
|
47
74
|
}
|
|
75
|
+
|
|
48
76
|
let theme: { fg?: (slot: string, text: string) => string } | undefined;
|
|
49
77
|
try {
|
|
50
78
|
theme = (c.ui as unknown as { theme?: { fg?: (slot: string, text: string) => string } })?.theme;
|
|
51
79
|
} catch {
|
|
52
80
|
// ignore
|
|
53
81
|
}
|
|
54
|
-
const indicator = (isActive || inFlightDeepScans.size > 0)
|
|
82
|
+
const indicator = (isActive || inFlightDeepScans.size > 0 || inFlightSessionScans.size > 0)
|
|
55
83
|
? (theme?.fg ? theme.fg("accent", "●") : "●")
|
|
56
84
|
: (theme?.fg ? theme.fg("dim", "○") : "○");
|
|
57
|
-
|
|
85
|
+
// With no base text the marker stands alone (`○ web:51234`) rather than
|
|
86
|
+
// rendering a leading ` · ` separator against nothing.
|
|
87
|
+
const body = lastStatusText ? `${lastStatusText}${suffix}` : `web:${port}`;
|
|
88
|
+
c.ui.setStatus("weave", `${indicator} ${body}`);
|
|
58
89
|
}
|
|
59
90
|
|
|
60
91
|
pi.on("session_shutdown", async () => {
|
|
61
|
-
// The TUI explorer owns
|
|
92
|
+
// The TUI explorer owns nothing session-scoped, but the browser workspace
|
|
93
|
+
// owns an HTTP server, a recursive file watcher and any number of open SSE
|
|
94
|
+
// streams. `close()` releases all three (server.ts closes the hub and
|
|
95
|
+
// awaits the watcher); a leaked listener outliving the pi session is a
|
|
96
|
+
// real bug, not a tidiness concern.
|
|
97
|
+
await web.close();
|
|
62
98
|
});
|
|
63
99
|
|
|
64
100
|
pi.on("agent_start", async (_event, ctx) => {
|
|
@@ -97,20 +133,45 @@ export default function piWeave(pi: ExtensionAPI): void {
|
|
|
97
133
|
});
|
|
98
134
|
|
|
99
135
|
pi.registerCommand("weave-view", {
|
|
100
|
-
description: "Open the in-terminal
|
|
136
|
+
description: "Open the knowledge workspace in your browser ('tui' for the in-terminal explorer, '--no-open' to just print the URL)",
|
|
101
137
|
handler: async (args, ctx) => {
|
|
102
|
-
const
|
|
103
|
-
if (
|
|
104
|
-
ctx.ui.notify(
|
|
138
|
+
const parsed = parseWeaveViewArgs(args);
|
|
139
|
+
if (parsed === null) {
|
|
140
|
+
ctx.ui.notify(WEAVE_VIEW_USAGE, "warning");
|
|
105
141
|
return;
|
|
106
142
|
}
|
|
107
|
-
|
|
143
|
+
if (parsed.surface === "tui") {
|
|
144
|
+
await runWeaveViewTui(ctx);
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const outcome = await web.run(ctx, { open: parsed.open });
|
|
148
|
+
// A browser that would not launch on a machine that has a terminal
|
|
149
|
+
// still leaves the user somewhere to go. The server stays up — the URL
|
|
150
|
+
// in the notification remains valid — and the TUI opens on top of it.
|
|
151
|
+
if (outcome.fallbackToTui && ctx.mode === "tui") await runWeaveViewTui(ctx);
|
|
152
|
+
// Unconditionally, and last: the TUI writes its own status line on
|
|
153
|
+
// close, which would otherwise drop the `· web:PORT` marker for a
|
|
154
|
+
// server that is still running.
|
|
155
|
+
updateStatus(ctx);
|
|
108
156
|
},
|
|
109
157
|
});
|
|
110
158
|
|
|
111
159
|
pi.registerCommand("weave-scan", {
|
|
112
|
-
description:
|
|
160
|
+
description:
|
|
161
|
+
"Build or refresh the repository knowledge index (.okf); 'deep' also summarizes files with the session model; 'sessions' summarizes pi session history into the vault",
|
|
113
162
|
handler: async (args, ctx) => {
|
|
163
|
+
const mode = args.trim().toLowerCase();
|
|
164
|
+
if (mode === "sessions") {
|
|
165
|
+
// Repo-agnostic by definition: no git requirement, works from any cwd.
|
|
166
|
+
if (inFlightSessionScans.size > 0) {
|
|
167
|
+
ctx.ui.notify("pi-weave: a session scan is already running — run /weave-scan-cancel to stop it.", "warning");
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const status = await getWorkspaceStatus(ctx.cwd);
|
|
171
|
+
startSessionScan(ctx, status, updateStatus);
|
|
172
|
+
return; // the background scan owns the status line until it settles
|
|
173
|
+
}
|
|
174
|
+
|
|
114
175
|
const root = await findGitRoot(ctx.cwd);
|
|
115
176
|
if (!root) {
|
|
116
177
|
ctx.ui.notify("pi-weave: not inside a git repository.", "warning");
|
|
@@ -124,7 +185,7 @@ export default function piWeave(pi: ExtensionAPI): void {
|
|
|
124
185
|
await writeRepoIndex(root, index);
|
|
125
186
|
ctx.ui.notify(`pi-weave: index refreshed\n${summarizeIndex(index).join("\n")}`, "info");
|
|
126
187
|
|
|
127
|
-
if (
|
|
188
|
+
if (mode === "deep") {
|
|
128
189
|
if (inFlightDeepScans.has(root)) {
|
|
129
190
|
ctx.ui.notify("pi-weave: a deep scan is already running for this repository — run /weave-scan-cancel to stop it.", "warning");
|
|
130
191
|
} else {
|
|
@@ -142,20 +203,68 @@ export default function piWeave(pi: ExtensionAPI): void {
|
|
|
142
203
|
});
|
|
143
204
|
|
|
144
205
|
pi.registerCommand("weave-scan-cancel", {
|
|
145
|
-
description: "Cancel an in-flight /weave-scan deep run",
|
|
206
|
+
description: "Cancel an in-flight /weave-scan deep or sessions run",
|
|
146
207
|
handler: async (_args, ctx) => {
|
|
147
208
|
const root = await findGitRoot(ctx.cwd);
|
|
148
|
-
const
|
|
149
|
-
|
|
209
|
+
const deep = root ? inFlightDeepScans.get(root) : undefined;
|
|
210
|
+
const sessions = inFlightSessionScans.get(SESSIONS_SCAN_KEY);
|
|
211
|
+
if (!deep && !sessions) {
|
|
150
212
|
ctx.ui.notify("pi-weave: no deep scan is currently running.", "info");
|
|
151
213
|
return;
|
|
152
214
|
}
|
|
153
|
-
|
|
154
|
-
|
|
215
|
+
deep?.controller.abort();
|
|
216
|
+
sessions?.controller.abort();
|
|
217
|
+
ctx.ui.notify("pi-weave: scan cancellation requested.", "info");
|
|
155
218
|
},
|
|
156
219
|
});
|
|
157
220
|
}
|
|
158
221
|
|
|
222
|
+
/* ------------------------------------------------------------------ */
|
|
223
|
+
/* /weave-view argument parsing */
|
|
224
|
+
/* ------------------------------------------------------------------ */
|
|
225
|
+
|
|
226
|
+
export const WEAVE_VIEW_USAGE = "usage: /weave-view [tui|web] [--no-open]";
|
|
227
|
+
|
|
228
|
+
/** Which explorer `/weave-view` was asked for. */
|
|
229
|
+
export interface WeaveViewArgs {
|
|
230
|
+
surface: "tui" | "web";
|
|
231
|
+
/** `false` for `--no-open`. Meaningless for `tui`, which never opens one. */
|
|
232
|
+
open: boolean;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Parse `/weave-view`'s argument string, or `null` for "print the usage".
|
|
237
|
+
*
|
|
238
|
+
* Bare `/weave-view` is the **browser** workspace (weave-workspace §13):
|
|
239
|
+
* the default is the thing most people want most of the time, and `tui` is
|
|
240
|
+
* one word away for the SSH case. `--no-open` is accepted on the web
|
|
241
|
+
* surface only — silently ignoring it after `tui` would be a lie about what
|
|
242
|
+
* happened.
|
|
243
|
+
*
|
|
244
|
+
* Exported because a parser is exactly the kind of thing that should be
|
|
245
|
+
* tested as a table rather than through eight command invocations.
|
|
246
|
+
*/
|
|
247
|
+
export function parseWeaveViewArgs(args: string): WeaveViewArgs | null {
|
|
248
|
+
const tokens = args.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
249
|
+
let surface: "tui" | "web" = "web";
|
|
250
|
+
let open = true;
|
|
251
|
+
let sawSurface = false;
|
|
252
|
+
for (const token of tokens) {
|
|
253
|
+
if (token === "--no-open") {
|
|
254
|
+
// Rejecting the repeat costs nothing and keeps "did I typo?" honest.
|
|
255
|
+
if (!open) return null;
|
|
256
|
+
open = false;
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
if (sawSurface) return null;
|
|
260
|
+
if (token !== "tui" && token !== "web") return null;
|
|
261
|
+
surface = token;
|
|
262
|
+
sawSurface = true;
|
|
263
|
+
}
|
|
264
|
+
if (surface === "tui" && !open) return null;
|
|
265
|
+
return { surface, open };
|
|
266
|
+
}
|
|
267
|
+
|
|
159
268
|
/* ------------------------------------------------------------------ */
|
|
160
269
|
/* In-flight deep scans (background, cancellable) */
|
|
161
270
|
/* ------------------------------------------------------------------ */
|
|
@@ -169,60 +278,151 @@ interface InFlightDeepScan {
|
|
|
169
278
|
/** In-flight deep scans keyed by repo root — the /weave-scan-cancel target. */
|
|
170
279
|
const inFlightDeepScans = new Map<string, InFlightDeepScan>();
|
|
171
280
|
|
|
281
|
+
/**
|
|
282
|
+
* The in-flight session scan, under a reserved key that cannot collide with
|
|
283
|
+
* a git root (absolute paths always start with `/`).
|
|
284
|
+
*/
|
|
285
|
+
export const SESSIONS_SCAN_KEY = "(pi-weave:sessions)";
|
|
286
|
+
const inFlightSessionScans = new Map<string, InFlightDeepScan>();
|
|
287
|
+
|
|
172
288
|
/** Test seam: resolve when the in-flight deep scan for `root` settles. */
|
|
173
289
|
export async function deepScanDone(root: string): Promise<void | undefined> {
|
|
174
290
|
const canonical = await findGitRoot(root).catch(() => null);
|
|
175
291
|
return inFlightDeepScans.get(canonical ?? root)?.done;
|
|
176
292
|
}
|
|
177
293
|
|
|
294
|
+
/** Test seam: resolve when the background session scan settles. */
|
|
295
|
+
export async function sessionScanDone(): Promise<void | undefined> {
|
|
296
|
+
return inFlightSessionScans.get(SESSIONS_SCAN_KEY)?.done;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
interface SettledMessage {
|
|
300
|
+
text: string;
|
|
301
|
+
level: "info" | "warning";
|
|
302
|
+
}
|
|
303
|
+
|
|
178
304
|
/**
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
305
|
+
* Shared background-scan lifecycle for the LLM-backed modes: the scan runs
|
|
306
|
+
* off the command handler so the user keeps control of the session (a
|
|
307
|
+
* blocking command can't be cancelled in the TUI — Esc only aborts
|
|
308
|
+
* streaming/bash). Progress is pushed to the status line; the completion
|
|
309
|
+
* message is notified; the settled status is restored when the scan settles
|
|
310
|
+
* (`settledStatus` lets a scan recompute it — a session scan grows the vault,
|
|
311
|
+
* so its restored line should say so).
|
|
184
312
|
*/
|
|
185
|
-
function
|
|
186
|
-
|
|
313
|
+
function startBackgroundScan(
|
|
314
|
+
store: Map<string, InFlightDeepScan>,
|
|
315
|
+
key: string,
|
|
187
316
|
ctx: ExtensionCommandContext,
|
|
188
317
|
baseStatus: WorkspaceStatus,
|
|
189
318
|
updateStatus: (ctx?: ExtensionContext | ExtensionCommandContext, text?: string) => void,
|
|
319
|
+
run: (signal: AbortSignal) => Promise<SettledMessage | null>,
|
|
320
|
+
settledStatus: (() => Promise<WorkspaceStatus>) | undefined = undefined,
|
|
190
321
|
): void {
|
|
191
322
|
const controller = new AbortController();
|
|
192
323
|
let doneResolve: () => void;
|
|
193
324
|
const done = new Promise<void>((resolve) => {
|
|
194
325
|
doneResolve = resolve;
|
|
195
326
|
});
|
|
196
|
-
|
|
327
|
+
store.set(key, { controller, done });
|
|
197
328
|
|
|
198
329
|
void (async () => {
|
|
199
330
|
try {
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
onProgress: ({ current, total, path }) => {
|
|
203
|
-
const pct = total > 0 ? Math.round((current / total) * 100) : 100;
|
|
204
|
-
updateStatus(ctx, `🧵 deep scan: ${current}/${total} (${pct}%) — ${path}`);
|
|
205
|
-
},
|
|
206
|
-
signal: controller.signal,
|
|
207
|
-
});
|
|
208
|
-
if (controller.signal.aborted) {
|
|
209
|
-
ctx.ui.notify("pi-weave: deep scan cancelled.", "warning");
|
|
210
|
-
} else if (outcome.kind === "no-model") {
|
|
211
|
-
ctx.ui.notify(
|
|
212
|
-
"pi-weave: deep scan needs an active session model — none configured. Light index only.",
|
|
213
|
-
"warning",
|
|
214
|
-
);
|
|
215
|
-
} else if (outcome.kind === "ok") {
|
|
216
|
-
ctx.ui.notify(`pi-weave: deep scan complete — ${formatDeepScanResult(outcome.result)}`, "info");
|
|
217
|
-
}
|
|
331
|
+
const settled = await run(controller.signal);
|
|
332
|
+
if (settled !== null) ctx.ui.notify(settled.text, settled.level);
|
|
218
333
|
} catch {
|
|
219
334
|
// session ended or extension torn down — stop quietly
|
|
220
335
|
} finally {
|
|
221
336
|
// Restore the settled status before removing the map entry, so a caller
|
|
222
|
-
// awaiting
|
|
223
|
-
|
|
224
|
-
|
|
337
|
+
// awaiting the done seam observes the settled status line.
|
|
338
|
+
const final = settledStatus
|
|
339
|
+
? await settledStatus().catch(() => baseStatus)
|
|
340
|
+
: baseStatus;
|
|
341
|
+
store.delete(key);
|
|
342
|
+
updateStatus(ctx, formatStatusLine(final));
|
|
225
343
|
doneResolve!();
|
|
226
344
|
}
|
|
227
345
|
})();
|
|
228
346
|
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Kick off a deep scan in the background. `baseStatus` is the workspace
|
|
350
|
+
* status captured before the scan and restored when it settles.
|
|
351
|
+
*/
|
|
352
|
+
function startDeepScan(
|
|
353
|
+
root: string,
|
|
354
|
+
ctx: ExtensionCommandContext,
|
|
355
|
+
baseStatus: WorkspaceStatus,
|
|
356
|
+
updateStatus: (ctx?: ExtensionContext | ExtensionCommandContext, text?: string) => void,
|
|
357
|
+
): void {
|
|
358
|
+
startBackgroundScan(inFlightDeepScans, root, ctx, baseStatus, updateStatus, async (signal) => {
|
|
359
|
+
updateStatus(ctx, "🕸️ deep scan: starting…");
|
|
360
|
+
const outcome = await deepScanRepository(root, ctx, {
|
|
361
|
+
onProgress: ({ current, total, path }) => {
|
|
362
|
+
const pct = total > 0 ? Math.round((current / total) * 100) : 100;
|
|
363
|
+
updateStatus(ctx, `🕸️ deep scan: ${current}/${total} (${pct}%) — ${path}`);
|
|
364
|
+
},
|
|
365
|
+
signal,
|
|
366
|
+
});
|
|
367
|
+
if (signal.aborted) {
|
|
368
|
+
return { text: "pi-weave: deep scan cancelled.", level: "warning" };
|
|
369
|
+
}
|
|
370
|
+
if (outcome.kind === "no-model") {
|
|
371
|
+
return {
|
|
372
|
+
text: "pi-weave: deep scan needs an active session model — none configured. Light index only.",
|
|
373
|
+
level: "warning",
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
if (outcome.kind === "ok") {
|
|
377
|
+
return { text: `pi-weave: deep scan complete — ${formatDeepScanResult(outcome.result)}`, level: "info" };
|
|
378
|
+
}
|
|
379
|
+
return null;
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Kick off a session scan (docs/session-scan.md) in the background — same
|
|
385
|
+
* lifecycle as deep scans; keyed globally, not per repo.
|
|
386
|
+
*/
|
|
387
|
+
function startSessionScan(
|
|
388
|
+
ctx: ExtensionCommandContext,
|
|
389
|
+
baseStatus: WorkspaceStatus,
|
|
390
|
+
updateStatus: (ctx?: ExtensionContext | ExtensionCommandContext, text?: string) => void,
|
|
391
|
+
): void {
|
|
392
|
+
startBackgroundScan(
|
|
393
|
+
inFlightSessionScans,
|
|
394
|
+
SESSIONS_SCAN_KEY,
|
|
395
|
+
ctx,
|
|
396
|
+
baseStatus,
|
|
397
|
+
updateStatus,
|
|
398
|
+
async (signal) => {
|
|
399
|
+
updateStatus(ctx, "🕸️ session scan: starting…");
|
|
400
|
+
const outcome = await scanPiSessions(ctx, {
|
|
401
|
+
onProgress: ({ current, total, path }) => {
|
|
402
|
+
const pct = total > 0 ? Math.round((current / total) * 100) : 100;
|
|
403
|
+
updateStatus(ctx, `🕸️ session scan: ${current}/${total} (${pct}%) — ${path}`);
|
|
404
|
+
},
|
|
405
|
+
signal,
|
|
406
|
+
});
|
|
407
|
+
if (signal.aborted) {
|
|
408
|
+
return { text: "pi-weave: session scan cancelled.", level: "warning" };
|
|
409
|
+
}
|
|
410
|
+
if (outcome.kind === "no-model") {
|
|
411
|
+
return {
|
|
412
|
+
text: "pi-weave: session scan needs an active session model — none configured.",
|
|
413
|
+
level: "warning",
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
const result = outcome.result;
|
|
417
|
+
if (result.discovered === 0) {
|
|
418
|
+
return { text: "pi-weave: session scan complete — no pi sessions found.", level: "info" };
|
|
419
|
+
}
|
|
420
|
+
return { text: `pi-weave: session scan complete — ${formatSessionScanResult(result)}`, level: "info" };
|
|
421
|
+
},
|
|
422
|
+
// Unlike the deep scan (whose settled status is precomputed to avoid git
|
|
423
|
+
// contention), the session scan writes vault notes and takes no git lock:
|
|
424
|
+
// recompute the workspace status so the settled line counts the notes it
|
|
425
|
+
// just wrote.
|
|
426
|
+
() => getWorkspaceStatus(ctx.cwd),
|
|
427
|
+
);
|
|
428
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pi adapter for the session scan (docs/session-scan.md): summarize pi
|
|
3
|
+
* session transcripts into the vault as incremental memory notes.
|
|
4
|
+
*
|
|
5
|
+
* Model wiring is shared with the deep scan (`createModelSummarizer` — the
|
|
6
|
+
* session's already-configured model, auth via `modelRegistry`); only the
|
|
7
|
+
* prompt differs, because a session transcript wants different sentences
|
|
8
|
+
* than a source file. `deps.complete` is the test seam — no network in unit
|
|
9
|
+
* tests.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
13
|
+
import {
|
|
14
|
+
resolveSessionsRoot,
|
|
15
|
+
resolveVaultRoot,
|
|
16
|
+
runSessionScan,
|
|
17
|
+
type SessionScanOptions,
|
|
18
|
+
type SessionScanResult,
|
|
19
|
+
type SummarizeFn,
|
|
20
|
+
} from "../core";
|
|
21
|
+
import { createModelSummarizer, type SummarizerDeps } from "./summarize";
|
|
22
|
+
|
|
23
|
+
const SESSION_SYSTEM_PROMPT = [
|
|
24
|
+
"You write durable memory notes that compact coding-agent sessions into their bottom line.",
|
|
25
|
+
"Answer, in order: what happened, what we did, what shipped (features, files, commands,",
|
|
26
|
+
"decisions), and what went less well (dead ends, breakage, anything left unfinished).",
|
|
27
|
+
"Technical sessions get specifics — real file paths, commands, outcomes — not generalities.",
|
|
28
|
+
"3–6 sentences, past tense. No preamble, no headings, no code fences.",
|
|
29
|
+
].join("\n");
|
|
30
|
+
|
|
31
|
+
const SESSION_MAX_OUTPUT_TOKENS = 400;
|
|
32
|
+
|
|
33
|
+
/** Session summarizer over the session model, or null when none is active. */
|
|
34
|
+
export function createSessionSummarizer(
|
|
35
|
+
ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
|
|
36
|
+
deps: SummarizerDeps = {},
|
|
37
|
+
): { summarize: SummarizeFn; label: string } | null {
|
|
38
|
+
return createModelSummarizer(ctx, SESSION_SYSTEM_PROMPT, SESSION_MAX_OUTPUT_TOKENS, deps);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export type SessionScanOutcome =
|
|
42
|
+
| { kind: "ok"; result: SessionScanResult }
|
|
43
|
+
| { kind: "no-model" };
|
|
44
|
+
|
|
45
|
+
export interface SessionScanAdapterOptions {
|
|
46
|
+
complete?: SummarizerDeps["complete"];
|
|
47
|
+
maxSessions?: SessionScanOptions["maxSessions"];
|
|
48
|
+
maxFileBytes?: SessionScanOptions["maxFileBytes"];
|
|
49
|
+
concurrency?: SessionScanOptions["concurrency"];
|
|
50
|
+
sessionsRoot?: SessionScanOptions["sessionsRoot"];
|
|
51
|
+
vaultRoot?: SessionScanOptions["vaultRoot"];
|
|
52
|
+
now?: SessionScanOptions["now"];
|
|
53
|
+
onProgress?: SessionScanOptions["onProgress"];
|
|
54
|
+
signal?: SessionScanOptions["signal"];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Scan pi sessions into the vault using the session model. Roots default to
|
|
59
|
+
* the real locations (PI_WEAVE_SESSIONS / PI_WEAVE_VAULT overrides apply);
|
|
60
|
+
* tests inject both.
|
|
61
|
+
*/
|
|
62
|
+
export async function scanPiSessions(
|
|
63
|
+
ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
|
|
64
|
+
deps: SessionScanAdapterOptions = {},
|
|
65
|
+
): Promise<SessionScanOutcome> {
|
|
66
|
+
const llm = createSessionSummarizer(
|
|
67
|
+
ctx,
|
|
68
|
+
deps.complete !== undefined ? { complete: deps.complete } : {},
|
|
69
|
+
);
|
|
70
|
+
if (!llm) return { kind: "no-model" };
|
|
71
|
+
// exactOptionalPropertyTypes: only present keys may be spread in.
|
|
72
|
+
const result = await runSessionScan({
|
|
73
|
+
sessionsRoot: deps.sessionsRoot ?? resolveSessionsRoot(),
|
|
74
|
+
vaultRoot: deps.vaultRoot ?? resolveVaultRoot(),
|
|
75
|
+
summarize: llm.summarize,
|
|
76
|
+
model: llm.label,
|
|
77
|
+
...(deps.maxSessions !== undefined ? { maxSessions: deps.maxSessions } : {}),
|
|
78
|
+
...(deps.maxFileBytes !== undefined ? { maxFileBytes: deps.maxFileBytes } : {}),
|
|
79
|
+
...(deps.concurrency !== undefined ? { concurrency: deps.concurrency } : {}),
|
|
80
|
+
...(deps.now !== undefined ? { now: deps.now } : {}),
|
|
81
|
+
...(deps.onProgress !== undefined ? { onProgress: deps.onProgress } : {}),
|
|
82
|
+
...(deps.signal !== undefined ? { signal: deps.signal } : {}),
|
|
83
|
+
});
|
|
84
|
+
return { kind: "ok", result };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** One-line human summary of a session-scan result (for notify output). */
|
|
88
|
+
export function formatSessionScanResult(result: SessionScanResult): string {
|
|
89
|
+
const parts = [
|
|
90
|
+
`${result.written} summarized (${result.created} new, ${result.updated} updated)`,
|
|
91
|
+
`${result.skippedFresh} unchanged`,
|
|
92
|
+
];
|
|
93
|
+
if (result.skippedEmpty > 0) parts.push(`${result.skippedEmpty} empty`);
|
|
94
|
+
if (result.skippedTooBig > 0) parts.push(`${result.skippedTooBig} skipped (size)`);
|
|
95
|
+
if (result.skippedUnreadable > 0) parts.push(`${result.skippedUnreadable} unreadable`);
|
|
96
|
+
let text = `${parts.join(", ")} — ${result.considered} sessions considered`;
|
|
97
|
+
if (result.failed.length > 0) {
|
|
98
|
+
const [failed0] = result.failed;
|
|
99
|
+
if (failed0) {
|
|
100
|
+
text += `; ${result.failed.length} failed, first: ${failed0.path}: ${failed0.error}`;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return text;
|
|
104
|
+
}
|
package/src/pi/summarize.ts
CHANGED
|
@@ -44,9 +44,21 @@ const SYSTEM_PROMPT = [
|
|
|
44
44
|
const MAX_OUTPUT_TOKENS = 220;
|
|
45
45
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
46
46
|
|
|
47
|
-
/**
|
|
48
|
-
|
|
47
|
+
/**
|
|
48
|
+
* The shared session-model wiring behind every pi-weave summarizer (deep
|
|
49
|
+
* scan, session scan): resolve the session's already-configured model — no
|
|
50
|
+
* extra keys or providers (docs/scan-modes.md) — drive completion through
|
|
51
|
+
* `ctx.modelRegistry`, which owns auth, and reject empty outputs so a
|
|
52
|
+
* degraded model cannot silently blank a note.
|
|
53
|
+
*
|
|
54
|
+
* Returns null when the session has no active model (headless runs without a
|
|
55
|
+
* provider, etc.) — callers fall back or notify. `deps.complete` is the test
|
|
56
|
+
* seam: unit tests inject a fake and never touch a network.
|
|
57
|
+
*/
|
|
58
|
+
export function createModelSummarizer(
|
|
49
59
|
ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
|
|
60
|
+
systemPrompt: string,
|
|
61
|
+
maxOutputTokens: number,
|
|
50
62
|
deps: SummarizerDeps = {},
|
|
51
63
|
): LlmSummarizer | null {
|
|
52
64
|
const model = ctx.model;
|
|
@@ -58,7 +70,7 @@ export function createLlmSummarizer(
|
|
|
58
70
|
const message = await complete(
|
|
59
71
|
model,
|
|
60
72
|
{
|
|
61
|
-
systemPrompt
|
|
73
|
+
systemPrompt,
|
|
62
74
|
messages: [
|
|
63
75
|
{
|
|
64
76
|
role: "user",
|
|
@@ -67,7 +79,7 @@ export function createLlmSummarizer(
|
|
|
67
79
|
},
|
|
68
80
|
],
|
|
69
81
|
},
|
|
70
|
-
{ maxTokens:
|
|
82
|
+
{ maxTokens: maxOutputTokens, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) },
|
|
71
83
|
);
|
|
72
84
|
const text = contentText(message.content).trim();
|
|
73
85
|
if (text.length === 0) {
|
|
@@ -78,6 +90,14 @@ export function createLlmSummarizer(
|
|
|
78
90
|
return { summarize, label };
|
|
79
91
|
}
|
|
80
92
|
|
|
93
|
+
/** Create the file summarizer for deep scans, or null when no model is active. */
|
|
94
|
+
export function createLlmSummarizer(
|
|
95
|
+
ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
|
|
96
|
+
deps: SummarizerDeps = {},
|
|
97
|
+
): LlmSummarizer | null {
|
|
98
|
+
return createModelSummarizer(ctx, SYSTEM_PROMPT, MAX_OUTPUT_TOKENS, deps);
|
|
99
|
+
}
|
|
100
|
+
|
|
81
101
|
export type DeepScanOutcome =
|
|
82
102
|
| { kind: "ok"; result: DeepScanResult }
|
|
83
103
|
| { kind: "no-model" }
|
package/src/pi/tools/noteTool.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Type } from "typebox";
|
|
|
5
5
|
import {
|
|
6
6
|
addNote,
|
|
7
7
|
appendToNote,
|
|
8
|
+
extractRawTail,
|
|
8
9
|
finalizeNote,
|
|
9
10
|
formatNote,
|
|
10
11
|
formatRawAppend,
|
|
@@ -30,7 +31,8 @@ export function registerNoteTool(pi: ExtensionAPI): void {
|
|
|
30
31
|
description:
|
|
31
32
|
"Read and write notes in the pi-weave vault — a persistent, human-readable knowledge base " +
|
|
32
33
|
"of Markdown notes. Actions: list (all notes), get (one note by slug), add (new note), " +
|
|
33
|
-
"append (extend a note
|
|
34
|
+
"append (extend a note; raw=true appends verbatim dictation into the ## Raw tail), " +
|
|
35
|
+
"finalize (restructure a note above its raw tail), search (title/tags/body). " +
|
|
34
36
|
"Use it to remember decisions, facts, and user preferences across sessions.",
|
|
35
37
|
promptSnippet: "Remember and retrieve durable knowledge in the pi-weave vault",
|
|
36
38
|
promptGuidelines: [
|
|
@@ -43,6 +45,7 @@ export function registerNoteTool(pi: ExtensionAPI): void {
|
|
|
43
45
|
text: Type.Optional(Type.String({ description: "Markdown body (add), addition (append), or restructured body above the raw tail (finalize)" })),
|
|
44
46
|
tags: Type.Optional(Type.Array(Type.String(), { description: "Tags (add)" })),
|
|
45
47
|
slug: Type.Optional(Type.String({ description: "Note slug (get, append, finalize)" })),
|
|
48
|
+
raw: Type.Optional(Type.Boolean({ description: "append: add text as verbatim dictation to the ## Raw tail (timestamped fenced block; tail created if missing). Use for dictation/scribbles; omit for structured Markdown additions" })),
|
|
46
49
|
source: Type.Optional(StringEnum(["human", "agent"] as const, { description: "Provenance (add): human for user-scribbled notes, agent for Pi-drafted (default agent)" })),
|
|
47
50
|
query: Type.Optional(Type.String({ description: "Search query (search)" })),
|
|
48
51
|
}),
|
|
@@ -117,12 +120,14 @@ export function registerNoteTool(pi: ExtensionAPI): void {
|
|
|
117
120
|
// Serialized read-modify-write: parallel weave_note appends (and
|
|
118
121
|
// pi's own file tools) targeting the same note would otherwise
|
|
119
122
|
// lose each other's additions.
|
|
120
|
-
const note = await withMutationQueue(path, () =>
|
|
123
|
+
const note = await withMutationQueue(path, () =>
|
|
124
|
+
appendToNote(vault, slug, text, new Date(), params.raw ? { raw: true } : {}),
|
|
125
|
+
);
|
|
121
126
|
if (!note) {
|
|
122
127
|
return { content: [{ type: "text", text: `No note found with slug '${params.slug}'.` }], details: { action: "append", found: false } };
|
|
123
128
|
}
|
|
124
129
|
return {
|
|
125
|
-
content: [{ type: "text", text: `Appended to ${note.slug} (updated ${note.updated}).` }],
|
|
130
|
+
content: [{ type: "text", text: params.raw ? `Appended verbatim to the ## Raw tail of ${note.slug} (updated ${note.updated}).` : `Appended to ${note.slug} (updated ${note.updated}).` }],
|
|
126
131
|
details: { action: "append", found: true, note },
|
|
127
132
|
};
|
|
128
133
|
}
|
|
@@ -150,8 +155,12 @@ export function registerNoteTool(pi: ExtensionAPI): void {
|
|
|
150
155
|
if (!note) {
|
|
151
156
|
return { content: [{ type: "text", text: `No note found with slug '${params.slug}'.` }], details: { action: "finalize", found: false } };
|
|
152
157
|
}
|
|
158
|
+
// Be truthful about what was preserved: a note with no `## Raw`
|
|
159
|
+
// marker yet gets its whole pre-finalize body preserved as a new
|
|
160
|
+
// raw tail (never silently dropped).
|
|
161
|
+
const preserved = extractRawTail(note.body) !== "";
|
|
153
162
|
return {
|
|
154
|
-
content: [{ type: "text", text: `Finalized ${note.slug} (updated ${note.updated}). Raw
|
|
163
|
+
content: [{ type: "text", text: `Finalized ${note.slug} (updated ${note.updated}). ${preserved ? "Raw tail preserved beneath the structured body." : "Note body was empty — nothing to preserve."}` }],
|
|
155
164
|
details: { action: "finalize", found: true, note },
|
|
156
165
|
};
|
|
157
166
|
}
|