pi-weave 0.1.12 → 0.1.13

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.
Files changed (71) hide show
  1. package/README.md +8 -37
  2. package/package.json +1 -2
  3. package/src/core/concurrency.ts +3 -6
  4. package/src/core/frontmatter.ts +0 -53
  5. package/src/core/graph/build.ts +6 -7
  6. package/src/core/graph/current.ts +2 -4
  7. package/src/core/graph/model.ts +1 -1
  8. package/src/core/graph/wikilinks.ts +3 -3
  9. package/src/core/index.ts +26 -27
  10. package/src/core/paths.ts +0 -7
  11. package/src/core/vault.ts +16 -681
  12. package/src/core/view/detail.ts +1 -1
  13. package/src/core/view/health.ts +1 -1
  14. package/src/core/view/tree.ts +1 -1
  15. package/src/pi/index.ts +6 -85
  16. package/src/pi/summarize.ts +2 -2
  17. package/src/pi/viewer/tui/bodyStore.ts +4 -7
  18. package/src/pi/viewer/tui/branding.ts +7 -148
  19. package/src/pi/viewer/tui/run.ts +3 -17
  20. package/src/pi/viewer/tui/surface/base.ts +24 -3
  21. package/src/pi/viewer/tui/surface/explore.ts +41 -6
  22. package/src/pi/viewer/tui/workspace.ts +23 -351
  23. package/src/pi/viewer/tui/workspaceRoot.ts +31 -172
  24. package/src/pi/viewer/web/run.ts +7 -117
  25. package/src/web/client/api.dom.ts +2 -2
  26. package/src/web/client/api.ts +14 -223
  27. package/src/web/client/bootstrap.ts +5 -14
  28. package/src/web/client/context/context.model.ts +9 -11
  29. package/src/web/client/dist/app.js +93 -219
  30. package/src/web/client/graph/dynamics.ts +5 -65
  31. package/src/web/client/graph/renderer.dom.ts +7 -8
  32. package/src/web/client/graph/renderer.ts +9 -35
  33. package/src/web/client/main.tsx +1 -1
  34. package/src/web/client/note/Note.tsx +21 -63
  35. package/src/web/client/search/SearchPalette.tsx +45 -36
  36. package/src/web/client/search/search.model.ts +33 -454
  37. package/src/web/client/shell/Columns.tsx +13 -83
  38. package/src/web/client/shell/Header.tsx +2 -10
  39. package/src/web/client/shell/Shell.tsx +50 -125
  40. package/src/web/client/shell/StatusBar.tsx +1 -4
  41. package/src/web/client/shell/icons.model.ts +4 -7
  42. package/src/web/client/shell/keys.model.ts +5 -42
  43. package/src/web/client/shell/keys.ts +2 -2
  44. package/src/web/client/shell/shell.model.ts +10 -133
  45. package/src/web/client/shell/theme.model.ts +2 -2
  46. package/src/web/client/shell/theme.ts +33 -157
  47. package/src/web/client/state.ts +9 -89
  48. package/src/web/client/tree/Tree.tsx +25 -575
  49. package/src/web/client/tree/tree.model.ts +8 -162
  50. package/src/web/client/workspace.ts +72 -242
  51. package/src/web/server/page.ts +8 -10
  52. package/src/web/server/routes.ts +30 -563
  53. package/src/web/server/server.ts +6 -145
  54. package/src/web/shared/layout.ts +72 -624
  55. package/src/web/shared/wire.ts +10 -196
  56. package/src/core/sessions.ts +0 -929
  57. package/src/pi/sessionScan.ts +0 -104
  58. package/src/pi/viewer/tui/explorer.ts +0 -586
  59. package/src/web/client/live.model.ts +0 -275
  60. package/src/web/client/live.ts +0 -151
  61. package/src/web/client/note/Editor.tsx +0 -109
  62. package/src/web/client/note/editor.controller.ts +0 -151
  63. package/src/web/client/note/editor.model.ts +0 -686
  64. package/src/web/client/search/search.ts +0 -107
  65. package/src/web/client/shell/Divider.tsx +0 -44
  66. package/src/web/client/shell/cssvars.ts +0 -70
  67. package/src/web/client/shell/drag.model.ts +0 -170
  68. package/src/web/client/shell/layout.model.ts +0 -500
  69. package/src/web/client/shell/viewport.ts +0 -29
  70. package/src/web/server/sse.ts +0 -321
  71. package/src/web/server/watcher.ts +0 -507
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The loopback workspace server — bind, lifecycle, teardown
3
- * (weave-workspace §5.1, §5.4).
3
+ * (weave-workspace §5.3, §5.4, §10).
4
4
  *
5
5
  * This module owns three things and delegates everything else:
6
6
  *
@@ -8,45 +8,18 @@
8
8
  * Never a fixed port: a fixed port is a port another process can squat
9
9
  * before us, and a port a malicious page can guess without scanning.
10
10
  * 2. **Composition.** It builds the {@link RouteDeps} — a
11
- * {@link WorkspaceCache}, a {@link SecurityPolicy}, and whichever hub and
12
- * watcher it was handed — and passes them to `handleRequest`.
13
- * 3. **Lifecycle.** Idle shutdown 30 minutes after the last SSE client
14
- * disconnects, and a `close()` that actually releases the port.
15
- *
16
- * ## Why the hub and the watcher are constructor arguments
17
- *
18
- * `SseHub` and `Watcher` are declared in `routes.ts` (the consumer) and
19
- * arrive here as optional options. That is not ceremony: it means the server
20
- * can be booted with neither — which is exactly what the route tests do, so
21
- * they exercise the real socket and the real security policy without a file
22
- * watcher spinning up on a temp directory, and it means the SSE
23
- * implementation can be written, replaced, or omitted with no edit to this
24
- * file. With no hub, `/events` answers `503` and the server has no idle
25
- * timer to run, because nothing can be idle.
26
- *
27
- * ## Idle shutdown, precisely
28
- *
29
- * The timer starts when the SSE client count reaches zero and is cancelled
30
- * when a client attaches. A workspace with a tab open is never idle; a
31
- * workspace whose last tab closed is reclaimed after
32
- * {@link DEFAULT_IDLE_MS}. `setTimer` is injectable, so the test asserts the
33
- * transition rather than waiting thirty minutes for it, and
34
- * `timer.unref?.()` keeps a pending shutdown from holding the process open.
11
+ * {@link WorkspaceCache} and {@link SecurityPolicy} — and passes them to
12
+ * `handleRequest`.
13
+ * 3. **Lifecycle.** `close()` releases the port.
35
14
  */
36
15
 
37
- import { randomBytes } from "node:crypto";
38
16
  import { createServer as createHttpServer, type Server } from "node:http";
39
17
  import { fileURLToPath } from "node:url";
40
18
  import { WorkspaceCache } from "../../core/cache/workspace";
41
19
  import { resolveVaultRoot } from "../../core/paths";
42
- import { handleRequest, type RouteDeps, type SseHub, type Watcher } from "./routes";
20
+ import { handleRequest, type RouteDeps } from "./routes";
43
21
  import { createSecurityPolicy, type SecurityPolicy } from "./security";
44
22
 
45
- export type { SseHub, Watcher } from "./routes";
46
-
47
- /** §5.4: reclaim the port 30 minutes after the last SSE client disconnects. */
48
- export const DEFAULT_IDLE_MS = 30 * 60 * 1000;
49
-
50
23
  /**
51
24
  * The committed bundle, resolved relative to this module rather than to
52
25
  * `process.cwd()` — the server runs in whatever directory the user invoked
@@ -56,24 +29,10 @@ export function defaultBundlePath(): string {
56
29
  return fileURLToPath(new URL("../client/dist/app.js", import.meta.url));
57
30
  }
58
31
 
59
- /** A `setTimeout` shaped seam, so the idle path is testable in microseconds. */
60
- export interface TimerHandle {
61
- unref?(): unknown;
62
- }
63
- export type SetTimer = (fn: () => void, ms: number) => TimerHandle;
64
- export type ClearTimer = (handle: TimerHandle) => void;
65
-
66
32
  export interface StartWorkspaceServerOptions {
67
33
  cwd: string;
68
34
  /** Defaults to `resolveVaultRoot()`, which honours `PI_WEAVE_VAULT`. */
69
35
  vaultRoot?: string;
70
- /**
71
- * Live-update hub. Absent → `/events` answers `503` and the idle timer
72
- * never runs. Supplied by the caller that also constructs the watcher.
73
- */
74
- sse?: SseHub | undefined;
75
- /** File watcher. Started during boot, closed during teardown. */
76
- watcher?: Watcher | undefined;
77
36
  /** Pre-built cache. Defaults to a fresh one over `cwd` + `vaultRoot`. */
78
37
  cache?: WorkspaceCache | undefined;
79
38
  /** Fixed token, for tests that need to know it before the boot resolves. */
@@ -84,33 +43,6 @@ export interface StartWorkspaceServerOptions {
84
43
  bundlePath?: string | undefined;
85
44
  /** Test seam for `POST /api/open`. */
86
45
  openNote?: ((slug: string) => Promise<boolean>) | undefined;
87
- /** Test seam for the note read; see {@link RouteDeps.readNote}. */
88
- readNote?: RouteDeps["readNote"];
89
- /**
90
- * Self-write suppression (§6): tell the watcher to ignore a path it is
91
- * about to see us change.
92
- *
93
- * A separate option rather than a method on {@link Watcher}, because
94
- * `Watcher` here is the two-method lifecycle contract `routes.ts` declares
95
- * — start and close — and widening it would force every future watcher
96
- * (a poller, a remote FS) to implement suppression whether or not the
97
- * concept applies to it. The caller that constructs the real watcher knows
98
- * it has a `suppress`, and passes it.
99
- *
100
- * Defaults to {@link Watcher.suppress} when the injected watcher has one,
101
- * so the common wiring is automatic and the option exists for a caller
102
- * whose suppression lives somewhere else (or for a test that wants to
103
- * observe it). Absent on both means writes happen unsuppressed, which is
104
- * exactly right for a server booted with no watcher: there is nothing
105
- * listening to feed a loop.
106
- */
107
- suppress?: ((absPath: string) => void) | undefined;
108
- /** Idle shutdown delay. `0` disables it entirely. */
109
- idleMs?: number | undefined;
110
- setTimer?: SetTimer | undefined;
111
- clearTimer?: ClearTimer | undefined;
112
- /** Invoked when the idle timeout fires, after `close()`. */
113
- onIdleShutdown?: (() => void) | undefined;
114
46
  }
115
47
 
116
48
  export interface WorkspaceServer {
@@ -122,37 +54,17 @@ export interface WorkspaceServer {
122
54
  token: string;
123
55
  security: SecurityPolicy;
124
56
  cache: WorkspaceCache;
125
- /** Random per-boot id, also embedded in the page bootstrap. */
126
- session: string;
127
- /**
128
- * Called by the SSE hub whenever a client attaches or detaches, so the
129
- * idle timer can be armed or cancelled. Exposed rather than inferred
130
- * because only the hub knows when a socket actually died.
131
- */
132
- noteActivity(): void;
133
- /** Release the port, the watcher and every stream. Idempotent. */
57
+ /** Release the port. Idempotent. */
134
58
  close(): Promise<void>;
135
59
  }
136
60
 
137
61
  export async function startWorkspaceServer(opts: StartWorkspaceServerOptions): Promise<WorkspaceServer> {
138
62
  const vaultRoot = opts.vaultRoot ?? resolveVaultRoot();
139
63
  const cache = opts.cache ?? new WorkspaceCache({ cwd: opts.cwd, vaultRoot });
140
- const session = randomBytes(8).toString("hex");
141
- const setTimer: SetTimer = opts.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
142
- const clearTimer: ClearTimer = opts.clearTimer ?? ((h) => clearTimeout(h as NodeJS.Timeout));
143
- const idleMs = opts.idleMs ?? DEFAULT_IDLE_MS;
144
-
145
64
  // `deps` is assembled before `listen` because the request handler closes
146
65
  // over it, but the security policy needs the bound port — so the policy
147
66
  // slot is filled after binding and the handler reads it through the
148
67
  // mutable holder rather than capturing a stale value.
149
- // Explicit `suppress` first, else the watcher's own if it has one (§6).
150
- // Bound as a closure rather than passed as a method reference, so a
151
- // watcher implementing `suppress` as a class method keeps its `this`.
152
- const watcherSuppress = opts.watcher?.suppress;
153
- const suppress: ((absPath: string) => void) | null =
154
- opts.suppress ?? (watcherSuppress === undefined ? null : (absPath: string) => watcherSuppress.call(opts.watcher, absPath));
155
-
156
68
  let deps: RouteDeps | null = null;
157
69
  const server: Server = createHttpServer((req, res) => {
158
70
  // Unreachable in practice — `listen` resolves before any connection is
@@ -176,49 +88,11 @@ export async function startWorkspaceServer(opts: StartWorkspaceServerOptions): P
176
88
  });
177
89
 
178
90
  let closed = false;
179
- let idleHandle: TimerHandle | null = null;
180
-
181
- const cancelIdle = (): void => {
182
- if (idleHandle !== null) {
183
- clearTimer(idleHandle);
184
- idleHandle = null;
185
- }
186
- };
187
-
188
- const armIdle = (): void => {
189
- // No hub means `/events` answers 503, so there is no such thing as a
190
- // client here and nothing for a countdown to count. The server stays up
191
- // until its owner closes it.
192
- if (idleMs <= 0 || closed || opts.sse === undefined) return;
193
- const handle = setTimer(() => {
194
- idleHandle = null;
195
- void close().then(() => opts.onIdleShutdown?.());
196
- }, idleMs);
197
- // A pending shutdown must not be the reason the process stays alive.
198
- handle.unref?.();
199
- idleHandle = handle;
200
- };
201
-
202
- const noteActivity = (): void => {
203
- cancelIdle();
204
- // Zero clients right now means the countdown starts now. A hub that
205
- // reports a live client cancels it and says nothing more until that
206
- // client leaves. With no hub at all, `armIdle` is already a no-op — the
207
- // guard lives there rather than being repeated here.
208
- if (opts.sse === undefined || opts.sse.clientCount() === 0) armIdle();
209
- };
210
-
211
91
  const close = async (): Promise<void> => {
212
92
  if (closed) return;
213
93
  closed = true;
214
- cancelIdle();
215
- opts.sse?.close();
216
- await opts.watcher?.close();
217
94
  await new Promise<void>((resolve) => {
218
95
  server.close(() => resolve());
219
- // Without this, an open SSE stream keeps the server's handle alive and
220
- // `close()` never resolves — the exact reason a pi session used to
221
- // hang on shutdown.
222
96
  server.closeAllConnections();
223
97
  });
224
98
  };
@@ -226,23 +100,12 @@ export async function startWorkspaceServer(opts: StartWorkspaceServerOptions): P
226
100
  deps = {
227
101
  cwd: opts.cwd,
228
102
  vaultRoot,
229
- session,
230
103
  cache,
231
104
  security,
232
105
  bundlePath: opts.bundlePath ?? defaultBundlePath(),
233
- ...(opts.sse !== undefined ? { sse: opts.sse } : {}),
234
106
  ...(opts.openNote !== undefined ? { openNote: opts.openNote } : {}),
235
- ...(opts.readNote !== undefined ? { readNote: opts.readNote } : {}),
236
- ...(suppress === null ? {} : { suppress }),
237
- onActivity: noteActivity,
238
107
  };
239
108
 
240
- await opts.watcher?.start();
241
-
242
- // A workspace nobody has connected to yet is already idle: if the browser
243
- // never opens, the port should still be reclaimed.
244
- if (opts.sse !== undefined) armIdle();
245
-
246
109
  return {
247
110
  url: security.origin,
248
111
  entryUrl: security.entryUrl,
@@ -250,8 +113,6 @@ export async function startWorkspaceServer(opts: StartWorkspaceServerOptions): P
250
113
  token: security.token,
251
114
  security,
252
115
  cache,
253
- session,
254
- noteActivity,
255
116
  close,
256
117
  };
257
118
  }