pi-weave 0.2.3 → 0.3.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.
@@ -1,133 +1,22 @@
1
1
  import { StringEnum } from "@earendil-works/pi-ai";
2
2
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
- import { join } from "node:path";
4
3
  import { Type } from "typebox";
5
4
  import {
6
- addNote,
7
- appendToNote,
8
- extractRawTail,
9
- finalizeNote,
10
- formatNote,
5
+ executeNoteAction,
11
6
  formatRawAppend,
12
- getNote,
13
- listNotes,
14
- NOTES_DIR,
15
- repairVaultLinks,
16
- resolveNotePath,
17
- withMutationQueue,
18
- resolveVaultRoot,
19
- searchNotes,
20
- suggestLinks,
21
- readVault,
22
- type LinkRepairResult,
23
- type SuggestionReport,
7
+ WEAVE_NOTE_DESCRIPTION,
24
8
  } from "../../core";
25
9
 
26
- /** Cap on how many rows of each link-audit category get printed. */
27
- const LINK_REPORT_CAP = 20;
28
-
29
- /** Cap on notes printed by `list`; `details.notes` still carries them all. */
30
- const LIST_CAP = 50;
31
-
32
- function capped<T>(items: readonly T[], render: (item: T) => string): string[] {
33
- const lines = items.slice(0, LINK_REPORT_CAP).map(render);
34
- if (items.length > LINK_REPORT_CAP) lines.push(` … and ${items.length - LINK_REPORT_CAP} more`);
35
- return lines;
36
- }
37
-
38
- /**
39
- * Render suggestions as the text the model reads.
40
- *
41
- * Every row carries the shared terms that earned it. A bare score is
42
- * unreviewable; the evidence is what lets a human accept or reject a
43
- * suggestion without opening both notes.
44
- */
45
- function formatSuggestions(report: SuggestionReport, focus: string | undefined): string {
46
- if (report.suggestions.length === 0) {
47
- return focus === undefined
48
- ? `No unlinked notes share enough distinctive vocabulary to suggest a connection (${report.considered} note(s) considered).`
49
- : `Nothing unlinked looks related to '${focus}' (${report.considered} note(s) considered).`;
50
- }
51
- const head = focus === undefined
52
- ? `${report.suggestions.length} suggested connection(s) across ${report.considered} note(s):`
53
- : `${report.suggestions.length} note(s) look related to '${focus}':`;
54
- const rows = report.suggestions.map((s) => {
55
- const pair = focus === undefined ? `${s.a} ↔ ${s.b}` : s.a === focus ? s.b : s.a;
56
- return ` ${s.score.toFixed(3)} ${pair}\n shared: ${s.shared.join(", ")}`;
57
- });
58
- return [
59
- head,
60
- ...rows,
61
- "",
62
- "These are suggestions, not links — nothing was written. Add a [[wikilink]] to any pair worth keeping.",
63
- ].join("\n");
64
- }
65
-
66
- /** Render a link audit (and any repair) as the text the model reads. */
67
- function formatLinkReport(result: LinkRepairResult, applied: boolean): string {
68
- const { audit } = result;
69
- const stale = audit.total - audit.resolved;
70
- // Occurrences, not rows. `total`/`resolved` count every `[[…]]` in the
71
- // vault, while `fixable`/`unresolvable` are grouped (per note+target, per
72
- // target). Reporting "70 stale" beside "40 unresolvable" with no unit
73
- // invites the reader to subtract them and find 30 phantom links; saying
74
- // what each number counts is the whole fix.
75
- const lines = [
76
- `${audit.total} wiki-link(s): ${audit.resolved} resolved, ${stale} stale.`,
77
- ];
78
- if (applied) {
79
- lines.push(
80
- result.applied.length === 0
81
- ? "Nothing to repair automatically."
82
- : `Repaired ${result.applied.length} link(s) across ${result.notes.length} note(s):`,
83
- ...capped(result.applied, (f) => ` ${f.slug}: [[${f.from}]] → [[${f.to}]] (${f.rule})`),
84
- );
85
- } else if (audit.fixable.length > 0) {
86
- lines.push(
87
- `${audit.fixable.length} auto-fixable (re-run with fix: true):`,
88
- ...capped(audit.fixable, (f) => ` ${f.slug}: [[${f.from}]] → [[${f.to}]] (${f.rule})`),
89
- );
90
- }
91
- if (audit.ambiguous.length > 0) {
92
- lines.push(
93
- `${audit.ambiguous.length} ambiguous link(s) (several candidates — pick one and edit the note):`,
94
- ...capped(audit.ambiguous, (a) => ` ${a.slug}: [[${a.target}]] → ${a.candidates.join(" | ")}`),
95
- );
96
- }
97
- if (audit.unresolvable.length > 0) {
98
- lines.push(
99
- `${audit.unresolvable.length} unresolvable target(s) (no such note — write it or drop the link):`,
100
- ...capped(audit.unresolvable, (u) => ` [[${u.target}]] ← ${u.notes.join(", ")}`),
101
- );
102
- }
103
- if (audit.fixable.length === 0 && audit.ambiguous.length === 0 && audit.unresolvable.length === 0) {
104
- lines.push("Every link resolves.");
105
- }
106
- return lines.join("\n");
107
- }
108
-
109
- /**
110
- * `weave_note` — the smart-notepad tool (design §1: vault knowledge).
111
- *
112
- * The LLM uses this to remember decisions, facts, and preferences as plain
113
- * Markdown notes that humans can read and edit directly on disk.
114
- */
10
+ /** `weave_note` — the smart-notepad tool (design §1: vault knowledge). */
115
11
  export function registerNoteTool(pi: ExtensionAPI): void {
116
12
  pi.registerTool({
117
13
  name: "weave_note",
118
14
  label: "Weave Note",
119
- description:
120
- "Read and write notes in the pi-weave vault — a persistent, human-readable knowledge base " +
121
- "of Markdown notes. Actions: list (all notes — avoid on large vaults, prefer search), get (one note by slug), add (new note), " +
122
- "append (extend a note; raw=true appends verbatim dictation into the ## Raw tail), " +
123
- "finalize (restructure a note above its raw tail), search (title/tags/body), " +
124
- "links (audit stale [[wiki-links]]; fix=true repairs the unambiguous ones), " +
125
- "suggest (rank unlinked notes that share distinctive vocabulary; reports only, never writes). " +
126
- "Use it to remember decisions, facts, and user preferences across sessions.",
15
+ description: WEAVE_NOTE_DESCRIPTION,
127
16
  promptSnippet: "Remember and retrieve durable knowledge in the pi-weave vault",
128
17
  promptGuidelines: [
129
18
  "Use weave_note to store durable knowledge (decisions, preferences, key facts) that should survive the session, marking source as agent-written knowledge.",
130
- "Use weave_note with action=search before answering questions about past decisions, people, or projects; generated notes under sessions/ carry takeaways from earlier sessions.",
19
+ "Use weave_note with action=get when the slug is known; otherwise use one targeted action=search before answering questions about past decisions, people, or projects. A resolved search already contains the full note; do not fetch it again or retry with reformulated queries.",
131
20
  "Use weave_note with action=links to find and repair stale [[wiki-links]] deterministically instead of rereading the vault to reconnect notes by hand; add fix=true to apply the unambiguous repairs.",
132
21
  "Use weave_note with action=suggest to discover notes that belong together but are not linked (optionally scoped to one slug); it only reports — propose the links to the user rather than writing them.",
133
22
  ],
@@ -143,186 +32,9 @@ export function registerNoteTool(pi: ExtensionAPI): void {
143
32
  fix: Type.Optional(Type.Boolean({ description: "links: apply the unambiguous repairs. Omit for a read-only report" })),
144
33
  limit: Type.Optional(Type.Number({ description: "suggest: how many suggestions to return (default 20)" })),
145
34
  }),
146
- async execute(toolCallId, params, signal, onUpdate, ctx) {
147
- const vault = resolveVaultRoot();
148
-
149
- switch (params.action) {
150
- case "list": {
151
- const notes = await listNotes(vault);
152
- if (notes.length === 0) {
153
- return {
154
- content: [{ type: "text", text: `The vault at ${vault} has no notes yet.` }],
155
- details: { action: "list", notes: [] },
156
- };
157
- }
158
- // Truncated, because the whole list is rarely the answer and on a
159
- // large vault it is actively harmful: hundreds of lines of slugs
160
- // crowd out the conversation that prompted the call. Newest first
161
- // (`listNotes` order), so the cap keeps what is most likely wanted,
162
- // and the footer names `search` — the action that answers "is there
163
- // a note about X" without reading the vault aloud.
164
- const shown = notes.slice(0, LIST_CAP);
165
- const lines = shown.map(
166
- (n) => `- ${n.slug}: ${n.title}${n.tags.length > 0 ? ` [${n.tags.join(", ")}]` : ""} (updated ${n.updated}, source: ${n.source})`,
167
- );
168
- if (notes.length > shown.length) {
169
- lines.push(
170
- `… and ${notes.length - shown.length} more (newest ${shown.length} shown) — use action=search to find a specific note.`,
171
- );
172
- }
173
- return {
174
- content: [{ type: "text", text: `${notes.length} note(s) in ${vault}:\n${lines.join("\n")}` }],
175
- details: { action: "list", notes },
176
- };
177
- }
178
-
179
- case "get": {
180
- if (!params.slug) throw new Error("weave_note(get) requires 'slug'");
181
- const note = await getNote(vault, params.slug);
182
- if (!note) {
183
- return { content: [{ type: "text", text: `No note found with slug '${params.slug}'.` }], details: { action: "get", found: false } };
184
- }
185
- return { content: [{ type: "text", text: formatNote(note) }], details: { action: "get", found: true, note } };
186
- }
187
-
188
- case "add": {
189
- if (!params.title) throw new Error("weave_note(add) requires 'title'");
190
- if (!params.text) throw new Error("weave_note(add) requires 'text'");
191
- const title = params.title;
192
- const text = params.text;
193
- // Serialized per vault: parallel adds of the same title must not
194
- // race the unique-slug check and overwrite each other.
195
- const note = await withMutationQueue(join(vault, NOTES_DIR), () =>
196
- addNote(vault, {
197
- title,
198
- body: text,
199
- ...(params.tags ? { tags: params.tags } : {}),
200
- ...(params.source ? { source: params.source } : {}),
201
- }),
202
- );
203
- return {
204
- content: [{ type: "text", text: `Note created: ${note.slug} (${vault})` }],
205
- details: { action: "add", note },
206
- };
207
- }
208
-
209
- case "append": {
210
- if (!params.slug) throw new Error("weave_note(append) requires 'slug'");
211
- if (!params.text) throw new Error("weave_note(append) requires 'text'");
212
- const slug = params.slug;
213
- const text = params.text;
214
- const path = resolveNotePath(vault, slug);
215
- if (!path) {
216
- return {
217
- content: [
218
- {
219
- type: "text",
220
- text: `Invalid note slug '${slug}' — notes are flat files inside the vault (no path separators or '..').`,
221
- },
222
- ],
223
- details: { action: "append", found: false },
224
- };
225
- }
226
- // Serialized read-modify-write: parallel weave_note appends (and
227
- // pi's own file tools) targeting the same note would otherwise
228
- // lose each other's additions.
229
- const note = await withMutationQueue(path, () =>
230
- appendToNote(vault, slug, text, new Date(), params.raw ? { raw: true } : {}),
231
- );
232
- if (!note) {
233
- return { content: [{ type: "text", text: `No note found with slug '${params.slug}'.` }], details: { action: "append", found: false } };
234
- }
235
- return {
236
- 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}).` }],
237
- details: { action: "append", found: true, note },
238
- };
239
- }
240
-
241
- case "finalize": {
242
- if (!params.slug) throw new Error("weave_note(finalize) requires 'slug'");
243
- if (!params.text) throw new Error("weave_note(finalize) requires 'text'");
244
- const slug = params.slug;
245
- const text = params.text;
246
- const path = resolveNotePath(vault, slug);
247
- if (!path) {
248
- return {
249
- content: [
250
- {
251
- type: "text",
252
- text: `Invalid note slug '${slug}' — notes are flat files inside the vault (no path separators or '..').`,
253
- },
254
- ],
255
- details: { action: "finalize", found: false },
256
- };
257
- }
258
- // Serialized read-modify-write, same as append: finalize replaces the
259
- // body above the raw tail, so it must not race other writers.
260
- const note = await withMutationQueue(path, () => finalizeNote(vault, slug, { body: text }));
261
- if (!note) {
262
- return { content: [{ type: "text", text: `No note found with slug '${params.slug}'.` }], details: { action: "finalize", found: false } };
263
- }
264
- // Be truthful about what was preserved: a note with no `## Raw`
265
- // marker yet gets its whole pre-finalize body preserved as a new
266
- // raw tail (never silently dropped).
267
- const preserved = extractRawTail(note.body) !== "";
268
- return {
269
- 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."}` }],
270
- details: { action: "finalize", found: true, note },
271
- };
272
- }
273
-
274
- case "search": {
275
- if (!params.query) throw new Error("weave_note(search) requires 'query'");
276
- const hits = await searchNotes(vault, params.query);
277
- if (hits.length === 0) {
278
- return {
279
- content: [{ type: "text", text: `No notes matched '${params.query}'.` }],
280
- details: { action: "search", hits: [] },
281
- };
282
- }
283
- const lines = hits.map(
284
- (h) => `- ${h.summary.slug}: ${h.summary.title} (score ${h.score})\n ${h.snippet}`,
285
- );
286
- return {
287
- content: [{ type: "text", text: `${hits.length} hit(s) for '${params.query}':\n${lines.join("\n")}` }],
288
- details: { action: "search", hits },
289
- };
290
- }
291
-
292
- case "suggest": {
293
- const { notes } = await readVault(vault);
294
- const report = suggestLinks(
295
- { notes },
296
- {
297
- ...(params.slug ? { slug: params.slug } : {}),
298
- ...(params.limit !== undefined ? { limit: params.limit } : {}),
299
- },
300
- );
301
- return {
302
- content: [{ type: "text", text: formatSuggestions(report, params.slug) }],
303
- details: { action: "suggest", considered: report.considered, suggestions: report.suggestions },
304
- };
305
- }
306
-
307
- case "links": {
308
- const apply = params.fix === true;
309
- const result = await repairVaultLinks(vault, apply ? { apply: true } : {});
310
- return {
311
- content: [{ type: "text", text: formatLinkReport(result, apply) }],
312
- details: {
313
- action: "links",
314
- fixed: apply,
315
- total: result.audit.total,
316
- resolved: result.audit.resolved,
317
- fixable: result.audit.fixable,
318
- ambiguous: result.audit.ambiguous,
319
- unresolvable: result.audit.unresolvable,
320
- applied: result.applied,
321
- notes: result.notes,
322
- },
323
- };
324
- }
325
- }
35
+ async execute(_toolCallId, params) {
36
+ const result = await executeNoteAction(params);
37
+ return { content: [{ type: "text", text: result.text }], details: result.details };
326
38
  },
327
39
  });
328
40
  }
@@ -1,31 +1,14 @@
1
1
  import { StringEnum } from "@earendil-works/pi-ai";
2
2
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
3
  import { Type } from "typebox";
4
- import {
5
- assessStaleness,
6
- buildRepoIndex,
7
- findGitRoot,
8
- readRepoIndex,
9
- repoIndexDir,
10
- summarizeIndex,
11
- writeRepoIndex,
12
- } from "../../core";
4
+ import { executeRepoAction, WEAVE_REPO_DESCRIPTION } from "../../core";
13
5
 
14
- /**
15
- * `weave_repo` — the repository-exploration tool (design §2/§8).
16
- *
17
- * Builds and reads the derived .okf index: structure, modules, packages,
18
- * entry points, plus git-aware staleness.
19
- */
6
+ /** `weave_repo` — the repository-exploration tool (design §2/§8). */
20
7
  export function registerRepoTool(pi: ExtensionAPI): void {
21
8
  pi.registerTool({
22
9
  name: "weave_repo",
23
10
  label: "Weave Repo",
24
- description:
25
- "Explore the current git repository through its pi-weave knowledge index (.okf). " +
26
- "Actions: status (index freshness vs git state), scan (build/refresh the index), " +
27
- "overview (read the indexed structure: languages, packages, modules, entry points). " +
28
- "The index is derived and rebuildable; scanning is always safe.",
11
+ description: WEAVE_REPO_DESCRIPTION,
29
12
  promptSnippet: "Explore the repository's structure via its .okf knowledge index",
30
13
  promptGuidelines: [
31
14
  "Use weave_repo action=overview to learn repository structure before broad code exploration instead of scanning files one by one.",
@@ -34,61 +17,11 @@ export function registerRepoTool(pi: ExtensionAPI): void {
34
17
  parameters: Type.Object({
35
18
  action: StringEnum(["status", "scan", "overview"] as const),
36
19
  }),
37
- async execute(toolCallId, params, signal, onUpdate, ctx) {
38
- const root = await findGitRoot(ctx.cwd);
39
- if (!root) {
40
- return {
41
- content: [{ type: "text", text: "Not inside a git repository — repository knowledge is unavailable here." }],
42
- details: { action: params.action, inRepo: false },
43
- };
44
- }
45
-
46
- switch (params.action) {
47
- case "status": {
48
- const staleness = await assessStaleness(root);
49
- const index = staleness.state !== "missing" ? await readRepoIndex(root) : null;
50
- const lines = [`Index state: ${staleness.state}`];
51
- for (const reason of staleness.reasons) lines.push(`- ${reason}`);
52
- if (index) lines.push(`Indexed at: ${index.updated} by ${index.generator}`);
53
- return {
54
- content: [{ type: "text", text: `Repository ${root}\n${lines.join("\n")}` }],
55
- details: { action: "status", inRepo: true, staleness, indexed: index !== null },
56
- };
57
- }
58
-
59
- case "scan": {
60
- onUpdate?.({ content: [{ type: "text", text: `Scanning ${root}…` }], details: {} });
61
- const index = await buildRepoIndex(root);
62
- if (!index) {
63
- return {
64
- content: [{ type: "text", text: "Cannot build index: the repository has no commits yet." }],
65
- details: { action: "scan", inRepo: true, scanned: false },
66
- };
67
- }
68
- const dir = await writeRepoIndex(root, index);
69
- const summary = summarizeIndex(index).join("\n");
70
- return {
71
- content: [{ type: "text", text: `Knowledge index written to ${dir}\n\n${summary}` }],
72
- details: { action: "scan", inRepo: true, scanned: true, index },
73
- };
74
- }
75
-
76
- case "overview": {
77
- const index = await readRepoIndex(root);
78
- if (!index) {
79
- return {
80
- content: [{ type: "text", text: `No knowledge index at ${repoIndexDir(root)} yet. Use action=scan to build one.` }],
81
- details: { action: "overview", inRepo: true, indexed: false },
82
- };
83
- }
84
- const staleness = await assessStaleness(root);
85
- const header = staleness.state === "fresh" ? "" : `⚠ index is ${staleness.state} (consider rescanning)\n`;
86
- return {
87
- content: [{ type: "text", text: header + summarizeIndex(index).join("\n") }],
88
- details: { action: "overview", inRepo: true, indexed: true, staleness, index },
89
- };
90
- }
91
- }
20
+ async execute(_toolCallId, params, _signal, onUpdate, ctx) {
21
+ const result = await executeRepoAction(params, ctx.cwd, (text) => {
22
+ onUpdate?.({ content: [{ type: "text", text }], details: {} });
23
+ });
24
+ return { content: [{ type: "text", text: result.text }], details: result.details };
92
25
  },
93
26
  });
94
27
  }
@@ -33,43 +33,19 @@
33
33
  * run, rather than that some private function was called.
34
34
  */
35
35
 
36
- import { platform as osPlatform } from "node:os";
37
36
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
38
- import { resolveVaultRoot, WorkspaceCache } from "../../../core";
39
37
  import {
40
- startWorkspaceServer,
41
- type StartWorkspaceServerOptions,
42
- type WorkspaceServer,
43
- } from "../../../web/server/server";
38
+ browserOpenCommand,
39
+ WorkspaceServerController,
40
+ type StartServerFn,
41
+ type WorkspaceServerSession,
42
+ } from "../../../web/server/controller";
44
43
 
45
44
  /** The slice of `ExtensionAPI.exec` this module uses. */
46
45
  export type ExecFn = (command: string, args: string[]) => Promise<{ code: number; stderr: string }>;
47
46
 
48
- /** `startWorkspaceServer`, as a seam so tests can pin `idleMs` and the bundle. */
49
- export type StartServerFn = (opts: StartWorkspaceServerOptions) => Promise<WorkspaceServer>;
50
-
51
- /**
52
- * The command that hands a URL to the desktop's default browser.
53
- *
54
- * `os` is injectable so the whole mapping is unit-testable on any host
55
- * without stubbing globals — the same convention as `openNoteCommand`.
56
- */
57
- export function browserOpenCommand(
58
- url: string,
59
- os: NodeJS.Platform = osPlatform(),
60
- ): { command: string; args: string[] } {
61
- if (os === "darwin") return { command: "open", args: [url] };
62
- // The empty string is `start`'s title argument. Without it, a URL that
63
- // happens to be quoted is consumed as the window title and nothing opens.
64
- if (os === "win32") return { command: "cmd", args: ["/c", "start", "", url] };
65
- return { command: "xdg-open", args: [url] };
66
- }
67
-
68
- /** Everything one running workspace owns. Closed as a unit. */
69
- export interface WebWorkspaceSession {
70
- server: WorkspaceServer;
71
- cache: WorkspaceCache;
72
- }
47
+ export { browserOpenCommand };
48
+ export type WebWorkspaceSession = WorkspaceServerSession;
73
49
 
74
50
  export interface WebWorkspaceDeps {
75
51
  /** `ExtensionAPI.exec`. Used only to launch the browser. */
@@ -115,17 +91,20 @@ export interface RunWebOutcome {
115
91
  */
116
92
  export class WebWorkspaceController {
117
93
  private readonly deps: WebWorkspaceDeps;
118
- private session: WebWorkspaceSession | null = null;
119
- /** The single in-flight boot, so two fast `/weave-view`s do not race. */
120
- private booting: Promise<WebWorkspaceSession> | null = null;
94
+ private readonly server: WorkspaceServerController;
121
95
 
122
96
  constructor(deps: WebWorkspaceDeps) {
123
97
  this.deps = deps;
98
+ this.server = new WorkspaceServerController({
99
+ ...(deps.startServer ? { startServer: deps.startServer } : {}),
100
+ ...(deps.vaultRoot ? { vaultRoot: deps.vaultRoot } : {}),
101
+ ...(deps.onStateChange ? { onStateChange: deps.onStateChange } : {}),
102
+ });
124
103
  }
125
104
 
126
105
  /** Bound port while a workspace is running, else `null`. Drives the status line. */
127
106
  port(): number | null {
128
- return this.session?.server.port ?? null;
107
+ return this.server.port();
129
108
  }
130
109
 
131
110
  /**
@@ -136,9 +115,7 @@ export class WebWorkspaceController {
136
115
  * rather than a server they cannot find.
137
116
  */
138
117
  async run(ctx: ExtensionCommandContext, opts: RunWebOptions): Promise<RunWebOutcome> {
139
- const existing = this.session;
140
- const session = existing ?? (await this.boot(ctx));
141
- const started = existing === null;
118
+ const { session, started } = await this.server.run(ctx.cwd);
142
119
 
143
120
  // Only a session with a UI has a browser worth spawning: over `--mode
144
121
  // rpc` or a plain SSH pipe there is no desktop on this side of the
@@ -154,45 +131,7 @@ export class WebWorkspaceController {
154
131
 
155
132
  /** Stop the workspace server. Idempotent. */
156
133
  async close(): Promise<void> {
157
- // A close arriving mid-boot must still close what that boot produced,
158
- // or `session_shutdown` during a slow start leaks the whole stack.
159
- const booting = this.booting;
160
- if (booting !== null) await booting.catch(() => undefined);
161
- const session = this.session;
162
- if (session === null) return;
163
- this.session = null;
164
- await session.server.close();
165
- this.deps.onStateChange?.();
166
- }
167
-
168
- // --- internals --------------------------------------------------------------
169
-
170
- private async boot(ctx: ExtensionCommandContext): Promise<WebWorkspaceSession> {
171
- if (this.booting !== null) return this.booting;
172
- const boot = this.bootOnce(ctx);
173
- this.booting = boot;
174
- try {
175
- const session = await boot;
176
- this.session = session;
177
- this.deps.onStateChange?.();
178
- return session;
179
- } finally {
180
- this.booting = null;
181
- }
182
- }
183
-
184
- private async bootOnce(ctx: ExtensionCommandContext): Promise<WebWorkspaceSession> {
185
- const cwd = ctx.cwd;
186
- const vaultRoot = (this.deps.vaultRoot ?? resolveVaultRoot)();
187
- const cache = new WorkspaceCache({ cwd, vaultRoot });
188
- const startServer = this.deps.startServer ?? startWorkspaceServer;
189
- const server = await startServer({
190
- cwd,
191
- vaultRoot,
192
- cache,
193
- });
194
-
195
- return { server, cache };
134
+ await this.server.close();
196
135
  }
197
136
 
198
137
  /** Spawn the browser, reporting failure rather than throwing it. */