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
@@ -33,7 +33,7 @@ export interface DetailModel {
33
33
  backlinks: DetailLinkRow[];
34
34
  }
35
35
 
36
- /** Ordered meta keys shown in the detail header (weave-view-tui-design §5.2). */
36
+ /** Ordered meta keys shown in the detail header. */
37
37
  const META_ORDER = [
38
38
  "path",
39
39
  "slug",
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * healthModel — staleness + link health, derived exclusively from the
3
- * GraphModel (weave-view-tui-design §5.4 / weave-workspace §3).
3
+ * GraphModel (weave-workspace §3).
4
4
  *
5
5
  * Zero new server/core fields: everything here is a projection of nodes,
6
6
  * edges, and `model.staleness`.
@@ -269,7 +269,7 @@ export function treeRows(model: GraphModel, state: TreeState): TreeRow[] {
269
269
  return rows;
270
270
  }
271
271
 
272
- /** Empty-state hint for the tree surface (weave-view-tui-design §5.1). */
272
+ /** Empty-state hint for the tree surface. */
273
273
  export function treeEmptyHint(model: GraphModel): string | null {
274
274
  const vault = model.nodes.find((n) => n.kind === "vault");
275
275
  if (!vault) return null;
package/src/pi/index.ts CHANGED
@@ -11,7 +11,6 @@ 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";
15
14
  import { deepScanRepository, formatDeepScanResult } from "./summarize";
16
15
  import { runWeaveViewTui } from "./viewer/tui/run";
17
16
  import { WebWorkspaceController } from "./viewer/web/run";
@@ -79,7 +78,7 @@ export default function piWeave(pi: ExtensionAPI): void {
79
78
  } catch {
80
79
  // ignore
81
80
  }
82
- const indicator = (isActive || inFlightDeepScans.size > 0 || inFlightSessionScans.size > 0)
81
+ const indicator = (isActive || inFlightDeepScans.size > 0)
83
82
  ? (theme?.fg ? theme.fg("accent", "●") : "●")
84
83
  : (theme?.fg ? theme.fg("dim", "○") : "○");
85
84
  // With no base text the marker stands alone (`○ web:51234`) rather than
@@ -158,20 +157,9 @@ export default function piWeave(pi: ExtensionAPI): void {
158
157
 
159
158
  pi.registerCommand("weave-scan", {
160
159
  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",
160
+ "Build or refresh the repository knowledge index (.okf); 'deep' also summarizes files with the session model",
162
161
  handler: async (args, ctx) => {
163
162
  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
-
175
163
  const root = await findGitRoot(ctx.cwd);
176
164
  if (!root) {
177
165
  ctx.ui.notify("pi-weave: not inside a git repository.", "warning");
@@ -203,17 +191,15 @@ export default function piWeave(pi: ExtensionAPI): void {
203
191
  });
204
192
 
205
193
  pi.registerCommand("weave-scan-cancel", {
206
- description: "Cancel an in-flight /weave-scan deep or sessions run",
194
+ description: "Cancel an in-flight /weave-scan deep run",
207
195
  handler: async (_args, ctx) => {
208
196
  const root = await findGitRoot(ctx.cwd);
209
197
  const deep = root ? inFlightDeepScans.get(root) : undefined;
210
- const sessions = inFlightSessionScans.get(SESSIONS_SCAN_KEY);
211
- if (!deep && !sessions) {
198
+ if (!deep) {
212
199
  ctx.ui.notify("pi-weave: no deep scan is currently running.", "info");
213
200
  return;
214
201
  }
215
202
  deep?.controller.abort();
216
- sessions?.controller.abort();
217
203
  ctx.ui.notify("pi-weave: scan cancellation requested.", "info");
218
204
  },
219
205
  });
@@ -278,24 +264,12 @@ interface InFlightDeepScan {
278
264
  /** In-flight deep scans keyed by repo root — the /weave-scan-cancel target. */
279
265
  const inFlightDeepScans = new Map<string, InFlightDeepScan>();
280
266
 
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
-
288
267
  /** Test seam: resolve when the in-flight deep scan for `root` settles. */
289
268
  export async function deepScanDone(root: string): Promise<void | undefined> {
290
269
  const canonical = await findGitRoot(root).catch(() => null);
291
270
  return inFlightDeepScans.get(canonical ?? root)?.done;
292
271
  }
293
272
 
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
273
  interface SettledMessage {
300
274
  text: string;
301
275
  level: "info" | "warning";
@@ -306,9 +280,7 @@ interface SettledMessage {
306
280
  * off the command handler so the user keeps control of the session (a
307
281
  * blocking command can't be cancelled in the TUI — Esc only aborts
308
282
  * 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).
283
+ * message is notified; the settled status is restored when the scan settles.
312
284
  */
313
285
  function startBackgroundScan(
314
286
  store: Map<string, InFlightDeepScan>,
@@ -317,7 +289,6 @@ function startBackgroundScan(
317
289
  baseStatus: WorkspaceStatus,
318
290
  updateStatus: (ctx?: ExtensionContext | ExtensionCommandContext, text?: string) => void,
319
291
  run: (signal: AbortSignal) => Promise<SettledMessage | null>,
320
- settledStatus: (() => Promise<WorkspaceStatus>) | undefined = undefined,
321
292
  ): void {
322
293
  const controller = new AbortController();
323
294
  let doneResolve: () => void;
@@ -335,11 +306,8 @@ function startBackgroundScan(
335
306
  } finally {
336
307
  // Restore the settled status before removing the map entry, so a caller
337
308
  // awaiting the done seam observes the settled status line.
338
- const final = settledStatus
339
- ? await settledStatus().catch(() => baseStatus)
340
- : baseStatus;
341
309
  store.delete(key);
342
- updateStatus(ctx, formatStatusLine(final));
310
+ updateStatus(ctx, formatStatusLine(baseStatus));
343
311
  doneResolve!();
344
312
  }
345
313
  })();
@@ -379,50 +347,3 @@ function startDeepScan(
379
347
  return null;
380
348
  });
381
349
  }
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
- }
@@ -45,8 +45,8 @@ const MAX_OUTPUT_TOKENS = 220;
45
45
  const REQUEST_TIMEOUT_MS = 30_000;
46
46
 
47
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
48
+ * The shared model wiring behind the deep scan: resolve the session's
49
+ * already-configured model — no
50
50
  * extra keys or providers (docs/scan-modes.md) — drive completion through
51
51
  * `ctx.modelRegistry`, which owns auth, and reject empty outputs so a
52
52
  * degraded model cannot silently blank a note.
@@ -1,15 +1,12 @@
1
1
  /**
2
2
  * BodyStore — shared note/.okf body cache (weave-view-tui-v2 §6, §9.1).
3
3
  *
4
- * The v1 WeaveExplorer cached bodies in private maps. v2 lifts that cache into
5
- * a per-session store so every pane (e.g. two Detail panes reading the same
6
- * note) shares one fetch per node id. Behavior is identical to v1's
7
- * per-explorer cache: a body load is kicked off once per id, in-flight loads
8
- * are deduped, and a refresh busts the cache so the next read re-fetches.
4
+ * Every pane (e.g. two Detail panes reading the same note) shares one fetch
5
+ * per node id. A body load is kicked off once per id, in-flight loads are
6
+ * deduped, and a refresh busts the cache so the next read re-fetches.
9
7
  *
10
8
  * The store is harness-free (takes injected loaders + an optional
11
- * onChange callback), so it is unit-tested with fake loaders exactly like
12
- * v1's body tests.
9
+ * onChange callback).
13
10
  */
14
11
 
15
12
  import type { ViewNote } from "../../../core/graph/current";
@@ -1,155 +1,14 @@
1
- /**
2
- * branding.ts — weave-view TUI logo & wordmark tiers (weave-view-tui-v2 §5).
3
- *
4
- * Renders the brand as a small, casual in-bar mark. Three tiers, auto-selected
5
- * once per session from terminal capability and cached:
6
- *
7
- * 1. "kitty" — the real raster logo via pi-tui's `Image` component (a small
8
- * ~2×2-cell favicon). The raw JPG is bundled and base64'd once.
9
- * 2. "glyph" — a tiny curated Unicode glyph derived from the logo silhouette.
10
- * 3. "plain" — `🕸️` + wordmark, forced by PI_WEAVE_TUI_PLAIN.
11
- *
12
- * The line-art/unicode constants contain no ESC by construction (decision 1:
13
- * the representation fallback is a curated constant, never generated at runtime).
14
- */
15
-
16
- import { detectCapabilities, getCapabilities, Image, type Component } from "@earendil-works/pi-tui";
1
+ import { visibleWidth } from "@earendil-works/pi-tui";
17
2
  import type { ThemeSlot } from "./theme";
18
3
 
19
- /** Logo render tier. */
20
- export type LogoTier = "kitty" | "glyph" | "plain";
21
-
22
- /** A tiny Unicode mark derived from the logo silhouette (decision 1 fallback). */
4
+ /** The TUI uses one terminal-safe mark. */
23
5
  export const MARK_GLYPH = "◈";
24
- /** Absolute last-resort mark (also the forced plain tier). */
25
- export const PLAIN_MARK = "🕸️";
26
- /** The wordmark shown after the mark. */
27
6
  export const WORDMARK = "weave view";
28
7
 
29
- /** Env var that forces the plain tier. */
30
- export const PLAIN_ENV = "PI_WEAVE_TUI_PLAIN";
31
-
32
- /** MIME of the bundled raster logo asset (downscaled transparent PNG of the logo). */
33
- export const LOGO_MIME = "image/png";
34
-
35
- /**
36
- * Bundled downscaled raster copy of `docs/pi-weave-logo.png` (the transparent-
37
- * background spider, 32×32, ~1.7 KB). Decision 1 (§15): ship a small raster copy
38
- * and embed it via pi-tui's `Image`
39
- * (Kitty graphics) as a small ~2×2-cell favicon in the header strip — a casual
40
- * in-bar mark, not a hero splash. Stored base64 so the render path needs no
41
- * fs/path resolution and the asset is trivially unit-testable.
42
- */
43
- export const LOGO_B64 =
44
- "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";
45
-
46
- /** Terminal capability surface branding probes. */
47
- export interface BrandCapabilities {
48
- /** Kitty graphics protocol available. */
49
- kitty: boolean;
50
- }
51
-
52
- /** Pure tier selection; exported for direct unit testing. */
53
- export function logoTierFor(caps: BrandCapabilities, forcePlain: boolean): LogoTier {
54
- if (forcePlain) return "plain";
55
- if (caps.kitty) return "kitty";
56
- return "glyph";
57
- }
58
-
59
- /**
60
- * Resolve the environment (PI_WEAVE_TUI_PLAIN) to a plain flag. Exported as a
61
- * small pure function so the env read is testable in isolation.
62
- */
63
- export function plainEnv(env: Record<string, string | undefined> = process.env): boolean {
64
- const v = env[PLAIN_ENV];
65
- return v !== undefined && v !== "" && v !== "0";
66
- }
67
-
68
- // Session-level probe cache (probed once, reused for the whole session §5.1).
69
- let cachedTier: LogoTier | null = null;
70
- let cachedCaps: BrandCapabilities | null = null;
71
-
72
- /**
73
- * Return the session-cached capabilities (probed once). The probe runs at most
74
- * once per process; subsequent calls reuse the cache. `getCaps` is injectable
75
- * for tests (defaults to a live kitty probe). Never throws.
76
- */
77
- export function probeGraphics(getCaps: () => BrandCapabilities): BrandCapabilities {
78
- if (cachedCaps) return cachedCaps;
79
- try {
80
- cachedCaps = getCaps();
81
- } catch {
82
- cachedCaps = { kitty: false };
83
- }
84
- return cachedCaps;
85
- }
86
-
87
- /**
88
- * Resolve the session logo tier, probing terminal capability once and caching
89
- * for the process. `env` is injectable for tests.
90
- */
91
- export function logoTier(env: Record<string, string | undefined> = process.env): LogoTier {
92
- if (cachedTier) return cachedTier;
93
- const caps = probeGraphics(() => getBrandCapabilities());
94
- cachedTier = logoTierFor(caps, plainEnv(env));
95
- return cachedTier;
96
- }
97
-
98
- /** Reset the session probe cache (test seam). */
99
- export function resetBrandCache(): void {
100
- cachedTier = null;
101
- cachedCaps = null;
102
- }
103
-
104
- /** A live kitty-capability probe backed by pi-tui's terminal capability query. */
105
- export function getBrandCapabilities(): BrandCapabilities {
106
- return { kitty: detectCapabilities().images === "kitty" };
107
- }
108
-
109
- /**
110
- * The mark rendered as a single header line (≤ width). This is the string the
111
- * header strip / empty state embed. For the kitty tier the mark glyph is used
112
- * in the text strip; the real raster is emitted via `logoImage()` when a kitty
113
- * surface is available.
114
- */
115
- export function renderMark(tier: LogoTier, theme: { fg: (slot: ThemeSlot, text: string) => string }, width: number): string {
116
- let mark: string;
117
- if (tier === "plain") {
118
- mark = theme.fg("muted", PLAIN_MARK);
119
- } else {
120
- // kitty and glyph tiers share the curated glyph in the text strip.
121
- mark = theme.fg("accent", MARK_GLYPH);
122
- }
123
- return mark.slice(0, Math.max(1, width));
124
- }
125
-
126
- /**
127
- * A small pi-tui `Image` component for the kitty tier (decision 1 favicon).
128
- * Takes the base64 JPG + mime; renders at ~2×2 cells. Returns a component that
129
- * emits the Kitty sequence when the terminal supports it, else a text fallback.
130
- */
131
- export function logoImage(
132
- base64Data: string,
133
- mimeType: string,
134
- theme: { fg: (slot: ThemeSlot, text: string) => string },
135
- ): Component {
136
- return new Image(base64Data, mimeType, { fallbackColor: (t) => theme.fg("text", t) }, {
137
- maxWidthCells: 2,
138
- maxHeightCells: 2,
139
- });
140
- }
141
-
142
- /**
143
- * Build the bundled kitty raster logo `Image` (decision 1 favicon). Returns a
144
- * component only when the terminal supports the Kitty graphics protocol;
145
- * `null` otherwise so the caller keeps the one-line glyph header. The render
146
- * path splices this component's lines onto their own row(s) — never inlined
147
- * into a styled text line. The base64 asset is a compile-time constant, so the
148
- * only fallback needed is the Kitty gate (no fs/path reads at runtime).
149
- */
150
- export function bundledLogoImage(
8
+ export function renderMark(
151
9
  theme: { fg: (slot: ThemeSlot, text: string) => string },
152
- ): Component | null {
153
- if (getCapabilities().images === "kitty") return logoImage(LOGO_B64, LOGO_MIME, theme);
154
- return null;
10
+ width: number,
11
+ ): string {
12
+ const mark = theme.fg("accent", MARK_GLYPH);
13
+ return visibleWidth(mark) <= width ? mark : "";
155
14
  }
@@ -1,9 +1,9 @@
1
1
  /**
2
- * runWeaveViewTui — wires the WeaveExplorer into a pi session
2
+ * runWeaveViewTui — wires the workspace explorer into a pi session
3
3
  * (weave-view-tui-design §2, §4.1, §3.2).
4
4
  *
5
5
  * Guards (interactive terminal only), builds the graph from disk in the
6
- * handler, then hands a ready WeaveExplorer to `ctx.ui.custom` so the
6
+ * handler, then hands a ready workspace explorer to `ctx.ui.custom` so the
7
7
  * explorer owns input for its whole lifetime. After `done(null)` resolves,
8
8
  * the workspace status line is refreshed. Dependencies are injected so the
9
9
  * component never touches a real terminal directly.
@@ -15,12 +15,10 @@ import {
15
15
  readNoteForView,
16
16
  readOkfFileForView,
17
17
  resolveVaultRoot,
18
- type GraphModel,
19
18
  } from "../../../core";
20
19
  import { openNoteInEditor } from "./openNote";
21
- import { bundledLogoImage, logoTier, renderMark } from "./branding";
22
20
  import { WeaveWorkspace } from "./workspaceRoot";
23
- import type { WeaveLoaders, WeaveTheme, WeaveTui } from "./explorer";
21
+ import type { WeaveLoaders, WeaveTheme, WeaveTui } from "./surface/base";
24
22
  import { getWorkspaceStatus, formatStatusLine } from "../../../core";
25
23
 
26
24
  /** Open the in-terminal knowledge explorer. Returns when the explorer closes. */
@@ -47,11 +45,6 @@ export async function runWeaveViewTui(ctx: ExtensionCommandContext): Promise<voi
47
45
 
48
46
  await ctx.ui.custom(
49
47
  (tui, theme, _keybindings, done) => {
50
- const tier = logoTier();
51
- const logo = renderMark(tier, theme as unknown as WeaveTheme, 20);
52
- // bundledLogoImage gates on Kitty support itself and returns null (glyph
53
- // header) when unavailable.
54
- const logoImage = bundledLogoImage(theme as unknown as WeaveTheme);
55
48
  const explorer = new WeaveWorkspace({
56
49
  model,
57
50
  theme: theme as unknown as WeaveTheme,
@@ -59,8 +52,6 @@ export async function runWeaveViewTui(ctx: ExtensionCommandContext): Promise<voi
59
52
  loaders,
60
53
  done,
61
54
  rows: tui.terminal.rows,
62
- logo,
63
- logoImage,
64
55
  });
65
56
  return explorer;
66
57
  },
@@ -78,8 +69,3 @@ export async function runWeaveViewTui(ctx: ExtensionCommandContext): Promise<voi
78
69
  const indicator = theme?.fg ? theme.fg("dim", "○") : "○";
79
70
  ctx.ui.setStatus("weave", `${indicator} ${formatStatusLine(status)}`);
80
71
  }
81
-
82
- /** Test seam: build the model the explorer opens with, without a terminal. */
83
- export async function buildTuiModel(cwd: string, vaultRoot: string = resolveVaultRoot()): Promise<GraphModel> {
84
- return buildCurrentGraph(cwd, vaultRoot);
85
- }
@@ -16,10 +16,31 @@
16
16
  import { truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
17
17
  import type { GraphModel } from "../../../../core/graph/model";
18
18
  import { BodyStore } from "../bodyStore";
19
- import type { WeaveLoaders, WeaveTheme } from "../explorer";
19
+ import type { ViewNote } from "../../../../core/graph/current";
20
20
  import type { SurfaceKind } from "../workspace";
21
21
  import type { ThemeSlot } from "../theme";
22
22
 
23
+ /** Minimal theme surface shared by the workspace root and its surfaces. */
24
+ export interface WeaveTheme {
25
+ fg(slot: ThemeSlot, text: string): string;
26
+ bg(slot: "selectedBg", text: string): string;
27
+ bold(text: string): string;
28
+ }
29
+
30
+ /** Minimal TUI surface shared by the workspace root and its adapter. */
31
+ export interface WeaveTui {
32
+ requestRender(force?: boolean): void;
33
+ terminal: { rows: number; columns: number };
34
+ }
35
+
36
+ /** Injected readers/actions bound to the vault and repository by run.ts. */
37
+ export interface WeaveLoaders {
38
+ loadNote: (slug: string) => Promise<ViewNote | null>;
39
+ loadOkf: (rel: string) => Promise<{ path: string; body: string } | null>;
40
+ openNote: (slug: string) => Promise<boolean>;
41
+ rebuild: () => Promise<GraphModel>;
42
+ }
43
+
23
44
  /** A cross-pane navigation request a surface emits (resolved by the root). */
24
45
  export type SurfaceEvent =
25
46
  | { type: "openDetail"; id: string }
@@ -151,7 +172,7 @@ export class Pane implements Component {
151
172
  const inner = Math.max(2, width - 2);
152
173
  // The surface already windows + selection-marks its own lines (each
153
174
  // surface owns its scroll/selection state). The pane only wraps them in a
154
- // border and PADS to fill its allocated height so the split looks clean —
175
+ // border and PADS to fill its allocated height so the multi-pane view looks clean —
155
176
  // it must NOT re-window (that double-applies the indent/marker and corrupts
156
177
  // the layout). Each body line is sliced/padded to `inner` so borders align.
157
178
  const surfaceLines = this.surface.render(inner);
@@ -164,7 +185,7 @@ export class Pane implements Component {
164
185
  const titleVis = visibleWidth(titleText);
165
186
  const titleLine = `${this.borderFn(borderSlot, "│")}${this.theme.bold(titleText)}${" ".repeat(Math.max(0, inner - titleVis))}${this.borderFn(borderSlot, "│")}`;
166
187
  // top + title + bottom = 3 fixed lines; body fills the rest so both panes
167
- // in a split render the same height.
188
+ // in a multi-pane render the same height.
168
189
  const bodyRows = Math.max(0, this.rows - 3);
169
190
  const out = [top, titleLine];
170
191
  for (let i = 0; i < bodyRows; i++) {
@@ -8,8 +8,7 @@
8
8
  * the workspace root resolves (e.g. into the nearest Detail pane).
9
9
  */
10
10
 
11
- import { truncateToWidth } from "@earendil-works/pi-tui";
12
- import { decodeAction } from "../explorer";
11
+ import { matchesKey, parseKey, truncateToWidth } from "@earendil-works/pi-tui";
13
12
  import {
14
13
  formatTreeMeta,
15
14
  graphRoots,
@@ -25,6 +24,43 @@ import type { GraphModel } from "../../../../core/graph/model";
25
24
  import type { NoteSource } from "../../../../core/types";
26
25
  import { windowLines, type Surface, type SurfaceEvent, type SurfaceInit, type SurfaceRender } from "./base";
27
26
 
27
+ /** Decode terminal input into the Explore surface's state-machine actions. */
28
+ export function decodeAction(data: string, state: Pick<ExplorerState, "searching">): Action | null {
29
+ if (matchesKey(data, "up")) return { type: "up" };
30
+ if (matchesKey(data, "down")) return { type: "down" };
31
+ if (matchesKey(data, "left")) return { type: "left" };
32
+ if (matchesKey(data, "right")) return { type: "right" };
33
+ if (matchesKey(data, "enter")) return { type: "enter" };
34
+ if (matchesKey(data, "escape")) return { type: "esc" };
35
+ if (matchesKey(data, "pageUp")) return { type: "pageUp" };
36
+ if (matchesKey(data, "pageDown")) return { type: "pageDown" };
37
+ if (matchesKey(data, "home")) return { type: "home" };
38
+ if (matchesKey(data, "end")) return { type: "end" };
39
+ if (state.searching) {
40
+ if (matchesKey(data, "backspace")) return { type: "searchBackspace" };
41
+ const ch = parseKey(data);
42
+ if (ch === undefined) return null;
43
+ if (ch === "space") return { type: "searchChar", ch: " " };
44
+ if (ch.length === 1) return { type: "searchChar", ch };
45
+ return null;
46
+ }
47
+ if (data === "k") return { type: "up" };
48
+ if (data === "j") return { type: "down" };
49
+ if (data === "h") return { type: "left" };
50
+ if (data === "l") return { type: "right" };
51
+ if (data === "/") return { type: "searchStart" };
52
+ if (data === "p") return { type: "cycleProvenance" };
53
+ if (data === "i") return { type: "toggleInternals" };
54
+ if (data === "f") return { type: "focus" };
55
+ if (data === "g") return { type: "focusExit" };
56
+ if (data === "1") return { type: "surfaceTree" };
57
+ if (data === "2") return { type: "surfaceHealth" };
58
+ if (data === "r") return { type: "refresh" };
59
+ if (data === "?") return { type: "toggleHelp" };
60
+ if (data === "q") return { type: "quit" };
61
+ return null;
62
+ }
63
+
28
64
  /** The Explore surface's own state (wraps the v1 ExplorerState for the tree). */
29
65
  export interface ExploreSurfaceState {
30
66
  selectedId: string | null;
@@ -77,11 +113,10 @@ export class ExploreSurface implements Surface {
77
113
  this.renderCache.clear();
78
114
  }
79
115
 
80
- /** Navigate the tree with a key sequence (delegates to v1 decodeAction + reduce). */
116
+ /** Navigate the tree with a key sequence. */
81
117
  handleInput(data: string): void {
82
- // o opens the selected node in the external editor (parity with v1's
83
- // WeaveExplorer and the Detail surface; the workspace root resolves the
84
- // event into loaders.openNote/openFile).
118
+ // o opens the selected node in the external editor; the workspace root
119
+ // resolves the event into loaders.openNote/openFile.
85
120
  if (data === "o" && !this.state.searching) {
86
121
  if (this.state.selectedId) this.onEvent?.({ type: "openEditor", id: this.state.selectedId });
87
122
  return;