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.
- package/README.md +8 -37
- package/package.json +1 -2
- package/src/core/concurrency.ts +3 -6
- package/src/core/frontmatter.ts +0 -53
- package/src/core/graph/build.ts +6 -7
- package/src/core/graph/current.ts +2 -4
- package/src/core/graph/model.ts +1 -1
- package/src/core/graph/wikilinks.ts +3 -3
- package/src/core/index.ts +26 -27
- package/src/core/paths.ts +0 -7
- package/src/core/vault.ts +16 -681
- package/src/core/view/detail.ts +1 -1
- package/src/core/view/health.ts +1 -1
- package/src/core/view/tree.ts +1 -1
- package/src/pi/index.ts +6 -85
- package/src/pi/summarize.ts +2 -2
- package/src/pi/viewer/tui/bodyStore.ts +4 -7
- package/src/pi/viewer/tui/branding.ts +7 -148
- package/src/pi/viewer/tui/run.ts +3 -17
- package/src/pi/viewer/tui/surface/base.ts +24 -3
- package/src/pi/viewer/tui/surface/explore.ts +41 -6
- package/src/pi/viewer/tui/workspace.ts +23 -351
- package/src/pi/viewer/tui/workspaceRoot.ts +31 -172
- package/src/pi/viewer/web/run.ts +7 -117
- package/src/web/client/api.dom.ts +2 -2
- package/src/web/client/api.ts +14 -223
- package/src/web/client/bootstrap.ts +5 -14
- package/src/web/client/context/context.model.ts +9 -11
- package/src/web/client/dist/app.js +93 -219
- package/src/web/client/graph/dynamics.ts +5 -65
- package/src/web/client/graph/renderer.dom.ts +7 -8
- package/src/web/client/graph/renderer.ts +9 -35
- package/src/web/client/main.tsx +1 -1
- package/src/web/client/note/Note.tsx +21 -63
- package/src/web/client/search/SearchPalette.tsx +45 -36
- package/src/web/client/search/search.model.ts +33 -454
- package/src/web/client/shell/Columns.tsx +13 -83
- package/src/web/client/shell/Header.tsx +2 -10
- package/src/web/client/shell/Shell.tsx +50 -125
- package/src/web/client/shell/StatusBar.tsx +1 -4
- package/src/web/client/shell/icons.model.ts +4 -7
- package/src/web/client/shell/keys.model.ts +5 -42
- package/src/web/client/shell/keys.ts +2 -2
- package/src/web/client/shell/shell.model.ts +10 -133
- package/src/web/client/shell/theme.model.ts +2 -2
- package/src/web/client/shell/theme.ts +33 -157
- package/src/web/client/state.ts +9 -89
- package/src/web/client/tree/Tree.tsx +25 -575
- package/src/web/client/tree/tree.model.ts +8 -162
- package/src/web/client/workspace.ts +72 -242
- package/src/web/server/page.ts +8 -10
- package/src/web/server/routes.ts +30 -563
- package/src/web/server/server.ts +6 -145
- package/src/web/shared/layout.ts +72 -624
- package/src/web/shared/wire.ts +10 -196
- package/src/core/sessions.ts +0 -929
- package/src/pi/sessionScan.ts +0 -104
- package/src/pi/viewer/tui/explorer.ts +0 -586
- package/src/web/client/live.model.ts +0 -275
- package/src/web/client/live.ts +0 -151
- package/src/web/client/note/Editor.tsx +0 -109
- package/src/web/client/note/editor.controller.ts +0 -151
- package/src/web/client/note/editor.model.ts +0 -686
- package/src/web/client/search/search.ts +0 -107
- package/src/web/client/shell/Divider.tsx +0 -44
- package/src/web/client/shell/cssvars.ts +0 -70
- package/src/web/client/shell/drag.model.ts +0 -170
- package/src/web/client/shell/layout.model.ts +0 -500
- package/src/web/client/shell/viewport.ts +0 -29
- package/src/web/server/sse.ts +0 -321
- package/src/web/server/watcher.ts +0 -507
package/src/core/view/detail.ts
CHANGED
package/src/core/view/health.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* healthModel — staleness + link health, derived exclusively from the
|
|
3
|
-
* GraphModel (weave-
|
|
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`.
|
package/src/core/view/tree.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
}
|
package/src/pi/summarize.ts
CHANGED
|
@@ -45,8 +45,8 @@ const MAX_OUTPUT_TOKENS = 220;
|
|
|
45
45
|
const REQUEST_TIMEOUT_MS = 30_000;
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
|
-
* The shared
|
|
49
|
-
*
|
|
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
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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)
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
10
|
+
width: number,
|
|
11
|
+
): string {
|
|
12
|
+
const mark = theme.fg("accent", MARK_GLYPH);
|
|
13
|
+
return visibleWidth(mark) <= width ? mark : "";
|
|
155
14
|
}
|
package/src/pi/viewer/tui/run.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* runWeaveViewTui — wires the
|
|
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
|
|
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 "./
|
|
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 {
|
|
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
|
|
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
|
|
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
|
|
116
|
+
/** Navigate the tree with a key sequence. */
|
|
81
117
|
handleInput(data: string): void {
|
|
82
|
-
// o opens the selected node in the external editor
|
|
83
|
-
//
|
|
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;
|