pi-weave 0.3.8 → 0.3.10

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 (80) hide show
  1. package/README.md +21 -17
  2. package/package.json +4 -3
  3. package/skills/weave-explore/SKILL.md +3 -5
  4. package/skills/weave-notepad/SKILL.md +22 -2
  5. package/skills/weave-notepad/references/second-thoughts.md +57 -0
  6. package/src/core/graph/build.ts +1 -1
  7. package/src/core/graph/current.ts +10 -8
  8. package/src/core/history.ts +417 -0
  9. package/src/core/index.ts +19 -0
  10. package/src/core/links/repair.ts +5 -68
  11. package/src/core/noteAction.ts +72 -2
  12. package/src/core/openInEditor.ts +3 -8
  13. package/src/core/repoAction.ts +20 -61
  14. package/src/core/repoIndex.ts +38 -0
  15. package/src/core/trail.ts +124 -0
  16. package/src/core/types.ts +1 -3
  17. package/src/core/vault.ts +116 -29
  18. package/src/core/view/focus.ts +2 -2
  19. package/src/core/view/index.ts +2 -3
  20. package/src/core/view/rawTail.ts +153 -0
  21. package/src/core/workspace.ts +20 -26
  22. package/src/core/workspaceCommands.ts +14 -14
  23. package/src/opencode/tools.ts +3 -3
  24. package/src/opencode/tui.ts +1 -7
  25. package/src/pi/index.ts +68 -80
  26. package/src/pi/tools/noteTool.ts +1 -1
  27. package/src/pi/tools/repoTool.ts +3 -6
  28. package/src/pi/viewer/web/run.ts +6 -8
  29. package/src/web/client/api.ts +103 -5
  30. package/src/web/client/context/context.model.ts +5 -0
  31. package/src/web/client/dist/app.js +499 -129
  32. package/src/web/client/graph/Graph.tsx +166 -14
  33. package/src/web/client/graph/column.model.ts +59 -2
  34. package/src/web/client/graph/graph.model.ts +183 -7
  35. package/src/web/client/graph/renderer.ts +39 -9
  36. package/src/web/client/graph/spotlight.model.ts +472 -0
  37. package/src/web/client/note/Note.tsx +370 -66
  38. package/src/web/client/note/Replay.tsx +139 -0
  39. package/src/web/client/note/drafts.ts +115 -11
  40. package/src/web/client/note/editbar.model.ts +153 -0
  41. package/src/web/client/note/note.model.ts +82 -1
  42. package/src/web/client/note/replay.model.ts +248 -0
  43. package/src/web/client/search/SearchPalette.tsx +103 -34
  44. package/src/web/client/search/search.model.ts +533 -26
  45. package/src/web/client/shell/ContextRail.tsx +7 -0
  46. package/src/web/client/shell/Settings.tsx +21 -1
  47. package/src/web/client/shell/Shell.tsx +93 -27
  48. package/src/web/client/shell/StatusBar.tsx +1 -0
  49. package/src/web/client/shell/icons.model.ts +68 -1
  50. package/src/web/client/shell/keys.model.ts +31 -8
  51. package/src/web/client/shell/shell.model.ts +48 -1
  52. package/src/web/client/shell/theme.model.ts +3 -0
  53. package/src/web/client/shell/theme.ts +354 -32
  54. package/src/web/client/shell/workspace.css.ts +40 -9
  55. package/src/web/client/trail/TrailStrip.tsx +55 -0
  56. package/src/web/client/trail/trail.model.ts +195 -0
  57. package/src/web/client/trail/trail.ts +45 -0
  58. package/src/web/client/tree/Tree.tsx +7 -3
  59. package/src/web/client/tree/tree.model.ts +2 -5
  60. package/src/web/server/page.ts +1 -1
  61. package/src/web/server/routes.ts +83 -3
  62. package/src/web/shared/logo.ts +1 -1
  63. package/src/web/shared/themes.ts +1 -1
  64. package/src/web/shared/view.ts +10 -4
  65. package/src/web/shared/wire.ts +74 -1
  66. package/src/web/shared/workspace.ts +10 -2
  67. package/src/core/view/health.ts +0 -160
  68. package/src/pi/viewer/tui/bodyStore.ts +0 -95
  69. package/src/pi/viewer/tui/branding.ts +0 -14
  70. package/src/pi/viewer/tui/model.ts +0 -390
  71. package/src/pi/viewer/tui/openNote.ts +0 -11
  72. package/src/pi/viewer/tui/run.ts +0 -71
  73. package/src/pi/viewer/tui/surface/base.ts +0 -203
  74. package/src/pi/viewer/tui/surface/detail.ts +0 -315
  75. package/src/pi/viewer/tui/surface/explore.ts +0 -241
  76. package/src/pi/viewer/tui/surface/focus.ts +0 -139
  77. package/src/pi/viewer/tui/surface/health.ts +0 -102
  78. package/src/pi/viewer/tui/theme.ts +0 -90
  79. package/src/pi/viewer/tui/workspace.ts +0 -53
  80. package/src/pi/viewer/tui/workspaceRoot.ts +0 -389
@@ -1,241 +0,0 @@
1
- /**
2
- * surface/explore.ts — the Explore surface as a Component (weave-view-tui-v2 §6).
3
- *
4
- * Wraps the v1 tree view-model (`treeRows` + `reduce`/`ExplorerState`) so each
5
- * Explore pane owns its own selection, expansion, filter, provenance cycle,
6
- * and search sub-mode — independent of other panes. Cross-pane navigation
7
- * (enter → open detail, f → open focus) is emitted as a `SurfaceEvent` that
8
- * the workspace root resolves (e.g. into the nearest Detail pane).
9
- */
10
-
11
- import { matchesKey, parseKey, truncateToWidth } from "@earendil-works/pi-tui";
12
- import {
13
- formatTreeMeta,
14
- graphRoots,
15
- initialState,
16
- reduce,
17
- treeEmptyHint,
18
- treeRows,
19
- type Action,
20
- type ExplorerState,
21
- } from "../model";
22
- import { chevron, kindStyle, provenanceStyle, SELECTION_MARKER, type ThemeSlot } from "../theme";
23
- import type { GraphModel } from "../../../../core/graph/model";
24
- import type { NoteSource } from "../../../../core/types";
25
- import { windowLines, type Surface, type SurfaceEvent, type SurfaceInit, type SurfaceRender } from "./base";
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
-
64
- /** The Explore surface's own state (wraps the v1 ExplorerState for the tree). */
65
- export interface ExploreSurfaceState {
66
- selectedId: string | null;
67
- expanded: Set<string>;
68
- showInternals: boolean;
69
- provFilter: NoteSource | null;
70
- query: string;
71
- searching: boolean;
72
- scrollOffset: number;
73
- }
74
-
75
- export function initialExploreState(model: GraphModel): ExploreSurfaceState {
76
- const s = initialState(graphRoots(model));
77
- return {
78
- selectedId: s.selectedId,
79
- expanded: s.expanded,
80
- showInternals: s.showInternals,
81
- provFilter: s.provFilter,
82
- query: s.query,
83
- searching: s.searching,
84
- scrollOffset: 0,
85
- };
86
- }
87
-
88
- export class ExploreSurface implements Surface {
89
- readonly kind = "explore" as const;
90
- private readonly ctx;
91
- private readonly onEvent: ((e: SurfaceEvent) => void) | undefined;
92
- state: ExploreSurfaceState;
93
- private focused = false;
94
- /** Pane viewport height (rows); set by the workspace root. */
95
- paneRows = 24;
96
- private renderCache = new Map<string, string[]>();
97
-
98
- constructor(init: SurfaceInit) {
99
- this.ctx = init.context;
100
- this.onEvent = init.onEvent;
101
- this.state = initialExploreState(this.ctx.model);
102
- }
103
-
104
- title(): string {
105
- return "Explore";
106
- }
107
-
108
- setFocused(focused: boolean): void {
109
- this.focused = focused;
110
- }
111
-
112
- invalidate(): void {
113
- this.renderCache.clear();
114
- }
115
-
116
- /** Navigate the tree with a key sequence. */
117
- handleInput(data: string): void {
118
- // o opens the selected node in the external editor; the workspace root
119
- // resolves the event into loaders.openNote/openFile.
120
- if (data === "o" && !this.state.searching) {
121
- if (this.state.selectedId) this.onEvent?.({ type: "openEditor", id: this.state.selectedId });
122
- return;
123
- }
124
- const action = decodeAction(data, toExplorerState(this.state));
125
- if (!action) return;
126
- this.applyAction(action);
127
- }
128
-
129
- private applyAction(action: Action): void {
130
- // Cross-pane navigation: enter → open detail; f → open focus.
131
- if (action.type === "enter" && !this.state.searching) {
132
- if (this.state.selectedId) this.onEvent?.({ type: "openDetail", id: this.state.selectedId });
133
- return;
134
- }
135
- if (action.type === "focus" && !this.state.searching) {
136
- if (this.state.selectedId) this.onEvent?.({ type: "focusNode", id: this.state.selectedId });
137
- return;
138
- }
139
- const rows = this.renderRows(80);
140
- const prev = toExplorerState(this.state);
141
- const next = reduce(prev, action, { rows, window: Math.max(5, this.paneRows) });
142
- this.state = fromExplorerState(next);
143
- }
144
-
145
- private renderRows(width: number): { id: string; target?: string }[] {
146
- return this.renderSurface(width).rows;
147
- }
148
-
149
- render(width: number): string[] {
150
- const key = `${width}:${this.state.scrollOffset}:${this.state.selectedId}:${this.state.query}:${this.state.provFilter ?? ""}:${this.state.showInternals}:${this.state.searching}`;
151
- const cached = this.renderCache.get(key);
152
- if (cached) return cached;
153
- const out = this.renderSurface(width).lines.map((l) => truncateToWidth(l, width));
154
- this.renderCache.set(key, out);
155
- return out;
156
- }
157
-
158
- private renderSurface(width: number): SurfaceRender {
159
- const t = this.ctx.theme;
160
- const rows = treeRows(this.ctx.model, {
161
- expanded: this.state.expanded,
162
- showInternals: this.state.showInternals,
163
- provFilter: this.state.provFilter,
164
- query: this.state.query,
165
- });
166
- const now = this.ctx.now();
167
- const hint = treeEmptyHint(this.ctx.model);
168
- if (rows.length === 0 && hint) {
169
- return { lines: [t.fg("dim", hint)], rows: [] };
170
- }
171
- const lines: { text: string; id: string }[] = rows.map((r) => {
172
- const chev = chevron(r.expanded, r.hasKids);
173
- const marker = this.rowMarker(r.kind, r.provenance);
174
- const label = sanitize(r.label);
175
- const metaText = formatTreeMeta(r.meta, now);
176
- const meta = metaText ? t.fg("dim", ` ${sanitize(metaText)}`) : "";
177
- const indent = " ".repeat(r.depth);
178
- const body = `${indent}${chev} ${marker}${label}`;
179
- return { text: meta ? `${body}${meta}` : body, id: r.id };
180
- });
181
- const selId = this.state.selectedId;
182
- const selLine = selId ? lines.findIndex((l) => l.id === selId) : -1;
183
- const marker = (text: string) => `${SELECTION_MARKER} ${this.ctx.theme.bg("selectedBg", text)}`;
184
- const out: string[] = windowLines(
185
- lines.map((l) => l.text),
186
- selLine,
187
- this.state.scrollOffset,
188
- Math.max(5, this.paneRows),
189
- marker,
190
- );
191
- if (this.state.searching) {
192
- const prompt = t.fg("accent", "/");
193
- out.push(`${prompt}${sanitize(this.state.query)}`);
194
- }
195
- return { lines: out, rows: rows.map((r) => ({ id: r.id })) };
196
- }
197
-
198
- private rowMarker(kind: string, prov: NoteSource | null): string {
199
- const ks = kindStyle(kind as never);
200
- if (kind === "note") {
201
- const ps = provenanceStyle(prov);
202
- return ps.glyph ? `${ps.glyph} ` : "";
203
- }
204
- return `${this.ctx.theme.fg(ks.slot as ThemeSlot, `${ks.glyph} `)}`;
205
- }
206
- }
207
-
208
- function sanitize(s: string): string {
209
- // eslint-disable-next-line no-control-regex
210
- return s.replace(/\x1b/g, "").replace(/\x9b/g, "").replace(/\x07/g, "").replace(/[\x00-\x08\x0b-\x1f\x7f]/g, "");
211
- }
212
-
213
- function toExplorerState(s: ExploreSurfaceState): ExplorerState {
214
- return {
215
- surface: "tree",
216
- searching: s.searching,
217
- selectedId: s.selectedId,
218
- focusId: null,
219
- detailId: null,
220
- expanded: s.expanded,
221
- showInternals: s.showInternals,
222
- provFilter: s.provFilter,
223
- query: s.query,
224
- helpOpen: false,
225
- refreshing: false,
226
- version: 0,
227
- scrollOffset: s.scrollOffset,
228
- };
229
- }
230
-
231
- function fromExplorerState(s: ExplorerState): ExploreSurfaceState {
232
- return {
233
- selectedId: s.selectedId,
234
- expanded: s.expanded,
235
- showInternals: s.showInternals,
236
- provFilter: s.provFilter,
237
- query: s.query,
238
- searching: s.searching,
239
- scrollOffset: s.scrollOffset,
240
- };
241
- }
@@ -1,139 +0,0 @@
1
- /**
2
- * surface/focus.ts — the Focus surface as a Component (weave-view-tui-v2 §6).
3
- *
4
- * The 1-hop graph neighborhood (`focusModel`). Enter re-centers the focus on
5
- * the selected neighbor, walking the graph. Each Focus pane owns its own
6
- * center node + selection/scroll.
7
- */
8
-
9
- import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
10
- import { focusModel, sanitizeTerminalText } from "../model";
11
- import { kindStyle, provenanceStyle, type ThemeSlot } from "../theme";
12
- import { windowLines, type Surface, type SurfaceEvent, type SurfaceInit } from "./base";
13
- import type { NoteSource } from "../../../../core/types";
14
- import type { GraphModel } from "../../../../core/graph/model";
15
-
16
- export interface FocusSurfaceState {
17
- focusId: string | null;
18
- selectedId: string | null;
19
- scrollOffset: number;
20
- }
21
-
22
- export class FocusSurface implements Surface {
23
- readonly kind = "focus" as const;
24
- private readonly ctx;
25
- private readonly onEvent: ((e: SurfaceEvent) => void) | undefined;
26
- state: FocusSurfaceState;
27
- paneRows = 24;
28
-
29
- constructor(init: SurfaceInit) {
30
- this.ctx = init.context;
31
- this.onEvent = init.onEvent;
32
- this.state = { focusId: null, selectedId: null, scrollOffset: 0 };
33
- }
34
-
35
- /** Bind the focus center node (called when opening/`f`). */
36
- setFocus(nodeId: string): void {
37
- this.state = { focusId: nodeId, selectedId: nodeId, scrollOffset: 0 };
38
- }
39
-
40
- setFocused(_focused: boolean): void {}
41
-
42
- title(): string {
43
- return "Focus";
44
- }
45
-
46
- invalidate(): void {}
47
-
48
- rebind(model: GraphModel): void {
49
- // After a refresh, drop a focus node that no longer exists rather than
50
- // rendering an empty "(no focus node)" pane.
51
- if (this.state.focusId && !model.nodes.some((n) => n.id === this.state.focusId)) {
52
- this.state = { focusId: null, selectedId: null, scrollOffset: 0 };
53
- }
54
- }
55
-
56
- handleInput(data: string): void {
57
- if (matchesKey(data, "up")) return this.move(-1);
58
- if (matchesKey(data, "down")) return this.move(1);
59
- if (matchesKey(data, "pageUp")) return this.movePage(-1);
60
- if (matchesKey(data, "pageDown")) return this.movePage(1);
61
- if (matchesKey(data, "home")) {
62
- this.state = { ...this.state, selectedId: this.rows()[0]?.id ?? null, scrollOffset: 0 };
63
- return;
64
- }
65
- if (matchesKey(data, "end")) {
66
- const r = this.rows();
67
- const last = r[r.length - 1];
68
- this.state = { ...this.state, selectedId: last?.id ?? null, scrollOffset: Math.max(0, r.length - this.paneRows) };
69
- return;
70
- }
71
- if (matchesKey(data, "enter")) {
72
- const rows = this.rows();
73
- const idx = rows.findIndex((r) => r.id === this.state.selectedId);
74
- const row = idx >= 0 ? rows[idx] : undefined;
75
- if (row && row.target && row.target !== this.state.focusId) {
76
- this.state = { focusId: row.target, selectedId: row.target, scrollOffset: 0 };
77
- }
78
- return;
79
- }
80
- if (data === "g") {
81
- this.onEvent?.({ type: "focusNode", id: this.state.focusId ?? "" });
82
- return;
83
- }
84
- }
85
-
86
- private move(delta: 1 | -1): void {
87
- const rows = this.rows();
88
- if (rows.length === 0) return;
89
- const idx = rows.findIndex((r) => r.id === this.state.selectedId);
90
- const nextIdx = Math.max(0, Math.min(rows.length - 1, (idx < 0 ? -1 : idx) + delta));
91
- const row = rows[nextIdx]!;
92
- this.state = { ...this.state, selectedId: row.id };
93
- }
94
-
95
- private movePage(dir: 1 | -1): void {
96
- const rows = this.rows();
97
- const idx = rows.findIndex((r) => r.id === this.state.selectedId);
98
- const target = idx < 0 ? 0 : Math.max(0, Math.min(rows.length - 1, idx + dir * this.paneRows));
99
- const row = rows[target];
100
- if (row) this.state = { ...this.state, selectedId: row.id };
101
- }
102
-
103
- private rows(): { id: string; target?: string }[] {
104
- if (!this.state.focusId) return [];
105
- const f = focusModel(this.ctx.model, this.state.focusId);
106
- const center: { id: string; target?: string } = f.center.target ? { id: f.center.id, target: f.center.target } : { id: f.center.id };
107
- return [center, ...f.groups.flatMap((g) => g.rows.map((r): { id: string; target?: string } => ({ id: r.id, target: r.target! })))];
108
- }
109
-
110
- render(width: number): string[] {
111
- const t = this.ctx.theme;
112
- const id = this.state.focusId;
113
- if (!id) return [t.fg("dim", "(no focus node)")];
114
- const f = focusModel(this.ctx.model, id);
115
- const lines: { text: string; id?: string }[] = [];
116
- const cm = this.rowMarker(f.center.kind, f.center.provenance);
117
- lines.push({ text: t.bold(`${cm}${sanitizeTerminalText(f.center.label)}`) + t.fg("dim", " (focus — g/esc to exit)"), id: f.center.id });
118
- for (const g of f.groups) {
119
- lines.push({ text: t.fg("accent", g.heading) });
120
- for (const r of g.rows) {
121
- lines.push({ text: ` ${this.rowMarker(r.kind, r.provenance)}${sanitizeTerminalText(r.label)}`, id: r.id });
122
- }
123
- }
124
- const selId = this.state.selectedId;
125
- const selLine = selId ? lines.findIndex((l) => l.id === selId) : -1;
126
- const marker = (text: string) => `› ${this.ctx.theme.bg("selectedBg", text)}`;
127
- const windowed = windowLines(lines.map((l) => l.text), selLine, this.state.scrollOffset, Math.max(5, this.paneRows), marker);
128
- return windowed.map((l) => truncateToWidth(l, width));
129
- }
130
-
131
- private rowMarker(kind: string, prov: NoteSource | null): string {
132
- const ks = kindStyle(kind as never);
133
- if (kind === "note") {
134
- const ps = provenanceStyle(prov);
135
- return ps.glyph ? `${ps.glyph} ` : "";
136
- }
137
- return `${this.ctx.theme.fg(ks.slot as ThemeSlot, `${ks.glyph} `)}`;
138
- }
139
- }
@@ -1,102 +0,0 @@
1
- /**
2
- * surface/health.ts — the Health surface as a Component (weave-view-tui-v2 §6).
3
- *
4
- * Renders `healthModel` (staleness + link health). Enter on a health row with
5
- * a target opens that node in detail (emitted as a SurfaceEvent).
6
- */
7
-
8
- import { matchesKey, truncateToWidth } from "@earendil-works/pi-tui";
9
- import { healthModel, sanitizeTerminalText } from "../model";
10
- import { windowLines, type Surface, type SurfaceEvent, type SurfaceInit } from "./base";
11
-
12
- export interface HealthSurfaceState {
13
- selectedId: string | null;
14
- scrollOffset: number;
15
- }
16
-
17
- export class HealthSurface implements Surface {
18
- readonly kind = "health" as const;
19
- private readonly ctx;
20
- private readonly onEvent: ((e: SurfaceEvent) => void) | undefined;
21
- state: HealthSurfaceState;
22
- paneRows = 24;
23
-
24
- constructor(init: SurfaceInit) {
25
- this.ctx = init.context;
26
- this.onEvent = init.onEvent;
27
- this.state = { selectedId: null, scrollOffset: 0 };
28
- }
29
-
30
- title(): string {
31
- return "Health";
32
- }
33
-
34
- setFocused(_focused: boolean): void {}
35
-
36
- invalidate(): void {}
37
-
38
- handleInput(data: string): void {
39
- if (matchesKey(data, "up")) return this.move(-1);
40
- if (matchesKey(data, "down")) return this.move(1);
41
- if (matchesKey(data, "pageUp")) return this.movePage(-1);
42
- if (matchesKey(data, "pageDown")) return this.movePage(1);
43
- if (matchesKey(data, "home")) {
44
- this.state = { ...this.state, selectedId: this.rows()[0]?.id ?? null, scrollOffset: 0 };
45
- return;
46
- }
47
- if (matchesKey(data, "end")) {
48
- const r = this.rows();
49
- const last = r[r.length - 1];
50
- this.state = { ...this.state, selectedId: last?.id ?? null, scrollOffset: Math.max(0, r.length - this.paneRows) };
51
- return;
52
- }
53
- if (matchesKey(data, "enter")) {
54
- const rows = this.rows();
55
- const idx = rows.findIndex((r) => r.id === this.state.selectedId);
56
- const row = idx >= 0 ? rows[idx] : undefined;
57
- if (row && row.target) this.onEvent?.({ type: "openDetail", id: row.target });
58
- return;
59
- }
60
- }
61
-
62
- private move(delta: 1 | -1): void {
63
- const rows = this.rows();
64
- if (rows.length === 0) return;
65
- const idx = rows.findIndex((r) => r.id === this.state.selectedId);
66
- const nextIdx = Math.max(0, Math.min(rows.length - 1, (idx < 0 ? -1 : idx) + delta));
67
- const row = rows[nextIdx]!;
68
- this.state = { ...this.state, selectedId: row.id };
69
- }
70
-
71
- private movePage(dir: 1 | -1): void {
72
- const rows = this.rows();
73
- const idx = rows.findIndex((r) => r.id === this.state.selectedId);
74
- const target = idx < 0 ? 0 : Math.max(0, Math.min(rows.length - 1, idx + dir * this.paneRows));
75
- const row = rows[target];
76
- if (row) this.state = { ...this.state, selectedId: row.id };
77
- }
78
-
79
- private rows(): { id: string; target?: string }[] {
80
- const h = healthModel(this.ctx.model);
81
- return h.sections.flatMap((s) =>
82
- s.rows.filter((r) => r.target).map((r): { id: string; target?: string } => ({ id: r.id, target: r.target! })),
83
- );
84
- }
85
-
86
- render(width: number): string[] {
87
- const t = this.ctx.theme;
88
- const h = healthModel(this.ctx.model);
89
- const lines: { text: string; id?: string }[] = [];
90
- for (const s of h.sections) {
91
- lines.push({ text: t.bold(s.heading) });
92
- for (const r of s.rows) {
93
- lines.push({ text: sanitizeTerminalText(r.text), id: r.id });
94
- }
95
- }
96
- const selId = this.state.selectedId;
97
- const selLine = selId ? lines.findIndex((l) => l.id === selId) : -1;
98
- const marker = (text: string) => `› ${this.ctx.theme.bg("selectedBg", text)}`;
99
- const windowed = windowLines(lines.map((l) => l.text), selLine, this.state.scrollOffset, Math.max(5, this.paneRows), marker);
100
- return windowed.map((l) => truncateToWidth(l, width));
101
- }
102
- }
@@ -1,90 +0,0 @@
1
- /**
2
- * Provenance & kind → glyph + theme color-slot maps for the weave-view TUI
3
- * (weave-view-tui-design §7).
4
- *
5
- * Pure data: imports only core types. Theme slots are names only
6
- * (`theme.fg(slot, …)` applies the actual color), so pi themes restyle
7
- * everything — never hardcoded hex. Style-first per v2 P1: provenance is
8
- * glyph + dimness, color is the weak secondary channel.
9
- */
10
-
11
- import type { NodeKind } from "../../../core/graph/model";
12
- import type { NoteSource } from "../../../core/types";
13
-
14
- /** Theme foreground color-slot names understood by pi's `Theme`. */
15
- export type ThemeSlot =
16
- | "accent"
17
- | "success"
18
- | "warning"
19
- | "dim"
20
- | "muted"
21
- | "text";
22
-
23
- /** Provenance rendering: glyph prefix, whole-row dimness, and theme slot. */
24
- export interface ProvenanceStyle {
25
- glyph: string;
26
- /** When true, the whole row is rendered dim (generated knowledge). */
27
- dim: boolean;
28
- slot: ThemeSlot;
29
- /** Word printed in the provenance cycle / header banner, e.g. "human". */
30
- word: string;
31
- }
32
-
33
- /** The provenance cycle order for the `p` key: all → human → agent → generated → all. */
34
- export const PROVENANCE_CYCLE: readonly (NoteSource | null)[] = [null, "human", "agent", "generated"];
35
-
36
- export function provenanceStyle(prov: NoteSource | null): ProvenanceStyle {
37
- switch (prov) {
38
- case "human":
39
- return { glyph: "●", dim: false, slot: "success", word: "human" };
40
- case "agent":
41
- return { glyph: "◐", dim: false, slot: "accent", word: "agent" };
42
- case "generated":
43
- return { glyph: "○", dim: true, slot: "dim", word: "generated" };
44
- default:
45
- // structural node — no provenance glyph; kind glyph leads.
46
- return { glyph: "", dim: false, slot: "muted", word: "" };
47
- }
48
- }
49
-
50
- export interface KindStyle {
51
- glyph: string;
52
- slot: ThemeSlot;
53
- }
54
-
55
- /** Kind → glyph + slot. Notes defer to their provenance glyph (trust-first). */
56
- export function kindStyle(kind: NodeKind): KindStyle {
57
- switch (kind) {
58
- case "vault":
59
- return { glyph: "◆", slot: "accent" };
60
- case "note":
61
- // provenance glyph leads; kind slot unused for the marker.
62
- return { glyph: "", slot: "text" };
63
- case "repository":
64
- return { glyph: "■", slot: "accent" };
65
- case "module":
66
- return { glyph: "▪", slot: "success" };
67
- case "package":
68
- return { glyph: "▲", slot: "success" };
69
- case "entryPoint":
70
- return { glyph: "▹", slot: "warning" };
71
- case "gitState":
72
- return { glyph: "⎇", slot: "warning" };
73
- case "external":
74
- return { glyph: "↗", slot: "warning" };
75
- case "file":
76
- return { glyph: "·", slot: "dim" };
77
- }
78
- }
79
-
80
- /** Chevron shown for an expandable tree row. */
81
- export function chevron(expanded: boolean, hasKids: boolean): string {
82
- if (!hasKids) return " ";
83
- return expanded ? "▾" : "▸";
84
- }
85
-
86
- /** Selection gutter marker. */
87
- export const SELECTION_MARKER = "›";
88
-
89
- /** Maximum length of the inline search/filter string (design §9.4). */
90
- export const MAX_FILTER_LEN = 200;
@@ -1,53 +0,0 @@
1
- import type { GraphModel } from "../../../core/graph/model";
2
- import { graphRoots } from "./model";
3
-
4
- export type SurfaceKind = "explore" | "detail" | "focus" | "health";
5
-
6
- export interface PaneNode {
7
- id: string;
8
- surface: SurfaceKind;
9
- nodeId: string | null;
10
- }
11
-
12
- export interface Workspace {
13
- name: string;
14
- panes: PaneNode[];
15
- activePaneId: string;
16
- }
17
-
18
- function pane(surface: SurfaceKind, nodeId: string | null = null): PaneNode {
19
- return { id: surface, surface, nodeId };
20
- }
21
-
22
- export function workspacePanes(ws: Workspace): PaneNode[] {
23
- return ws.panes;
24
- }
25
-
26
- export function findPane(panes: PaneNode[], id: string): PaneNode | null {
27
- return panes.find((item) => item.id === id) ?? null;
28
- }
29
-
30
- export function focusNext(ws: Workspace, dir: 1 | -1): Workspace {
31
- if (ws.panes.length <= 1) return ws;
32
- const index = ws.panes.findIndex((item) => item.id === ws.activePaneId);
33
- const next = ws.panes[(index + dir + ws.panes.length) % ws.panes.length];
34
- return next ? { ...ws, activePaneId: next.id } : ws;
35
- }
36
-
37
- /** The only layout: four equal-width read-only surfaces. */
38
- export function defaultWorkspace(model: GraphModel): Workspace {
39
- const panes = [
40
- pane("explore", graphRoots(model)[0] ?? null),
41
- pane("detail"),
42
- pane("focus"),
43
- pane("health"),
44
- ];
45
- return { name: "Explore", panes, activePaneId: panes[0]!.id };
46
- }
47
-
48
- /** Hide inactive panes in a narrow terminal. */
49
- export function collapseForWidth(ws: Workspace, width: number): Workspace {
50
- if (width >= 80) return ws;
51
- const active = findPane(ws.panes, ws.activePaneId);
52
- return active ? { ...ws, panes: [active] } : ws;
53
- }