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
@@ -25,19 +25,9 @@
25
25
  * | --- | --- |
26
26
  * | `⌘K` / `Ctrl K` | always — a modifier shortcut is unambiguous even mid-word |
27
27
  * | `⌘1` `⌘2` `⌘3` | always |
28
- * | `⌘E` `⌘S` | always — the editor's two keys (§11 P5.4) |
29
28
  * | `Esc` | an overlay is open, or something is selected. Never otherwise |
30
29
  * | `/` `g` `?` `t` | **only** when focus is not in a text field and no overlay is open |
31
30
  *
32
- * `⌘S` is claimed **even while typing**, which is the one place this map
33
- * deliberately overrides the browser. It has to be: the browser's `⌘S` is
34
- * "save this page as HTML", which in a note editor is never what the user
35
- * meant, and the moment they most want to save is the moment their cursor is
36
- * in the textarea. `⌘E` is claimed on the same grounds — the browser has no
37
- * default for it, and toggling out of the editor is a thing to do *from*
38
- * inside the editor. Neither fires when an overlay is open, because the
39
- * palette owns every key but `Escape`.
40
- *
41
31
  * `j` / `k` are not here at all: they are tree-scoped, so they live in
42
32
  * `tree.model.ts`'s `normalizeTreeKey` where the tree's own handler applies
43
33
  * them — a global `j` would move the tree selection while the user is looking
@@ -51,9 +41,7 @@
51
41
  * `⌘K`, which would otherwise re-open the palette on top of itself.
52
42
  */
53
43
 
54
- import { COLUMNS, columnsAt } from "./layout.model";
55
- import type { ColumnId } from "./layout.model";
56
- import type { OverlayId } from "./shell.model";
44
+ import { COLUMNS, type ColumnId, type OverlayId } from "./shell.model";
57
45
 
58
46
  // --- the event, without the DOM ---------------------------------------------------
59
47
 
@@ -102,25 +90,17 @@ export type ShellAction =
102
90
  | { readonly type: "filterTree" }
103
91
  | { readonly type: "fitGraph" }
104
92
  | { readonly type: "clearSelection" }
105
- /** `⌘E` — toggle the note column between read and edit (§11 P5.4). */
106
- | { readonly type: "toggleEdit" }
107
- /** `⌘S` — save the open draft. */
108
- | { readonly type: "saveNote" }
109
93
  /** `t` — cycle the colour theme: system → light → dark → system. */
110
94
  | { readonly type: "cycleTheme" };
111
95
 
112
96
  /**
113
97
  * The command-key letters, as data.
114
98
  *
115
- * A table for the same reason {@link COLUMN_DIGITS} is one: "every editor key
116
- * has a mapping" becomes a test over this object rather than three
117
- * hand-written cases, and the help sheet is generated from it so it cannot
118
- * document a key the code does not implement.
99
+ * A table for the same reason {@link COLUMN_DIGITS} is one: the help sheet is
100
+ * generated from it so it cannot document a key the code does not implement.
119
101
  */
120
102
  export const COMMAND_KEYS: Readonly<Record<string, ShellAction>> = {
121
103
  k: { type: "openSearch" },
122
- e: { type: "toggleEdit" },
123
- s: { type: "saveNote" },
124
104
  };
125
105
 
126
106
  /**
@@ -140,9 +120,7 @@ export const COLUMN_DIGITS: Readonly<Record<string, ColumnId>> = { "1": "tree",
140
120
  * search button once was. Derived from `columnsAt`, not listed, so the caveat
141
121
  * cannot drift from the breakpoints the sheet is attached to.
142
122
  */
143
- const COLLAPSIBLE_COLUMNS: ReadonlySet<ColumnId> = new Set(
144
- COLUMNS.filter((c) => columnsAt("medium").includes(c) === false || columnsAt("narrow").includes(c) === false),
145
- );
123
+ const COLLAPSIBLE_COLUMNS: ReadonlySet<ColumnId> = new Set(["tree", "graph"]);
146
124
 
147
125
  /**
148
126
  * Whether a modifier combination counts as "the platform's command key".
@@ -249,7 +227,7 @@ export interface KeyTarget {
249
227
  * The slice of `KeyboardEvent` the subscription reads.
250
228
  *
251
229
  * Structural, so the platform's satisfies it without a cast and a fake is an
252
- * object literal — the same port shape `EventSourceLike` and `HttpResponse`
230
+ * object literal — the same port shape as `HttpResponse`
253
231
  * use, for the same reason: there is no DOM test environment (§10).
254
232
  */
255
233
  export interface KeyboardEventLike {
@@ -358,10 +336,6 @@ export interface ShellEffects {
358
336
  fitGraph(): void;
359
337
  /** Write `null` to §1.3's `selectedId`. */
360
338
  clearSelection(): void;
361
- /** `⌘E` — dispatch `toggle` into the note editor. */
362
- toggleEdit(): void;
363
- /** `⌘S` — dispatch `save` into the note editor. */
364
- saveNote(): void;
365
339
  /** `t` — advance the user's theme choice by one step in its cycle. */
366
340
  cycleTheme(): void;
367
341
  }
@@ -385,10 +359,6 @@ export function runShellAction(action: ShellAction, fx: ShellEffects): void {
385
359
  return fx.fitGraph();
386
360
  case "clearSelection":
387
361
  return fx.clearSelection();
388
- case "toggleEdit":
389
- return fx.toggleEdit();
390
- case "saveNote":
391
- return fx.saveNote();
392
362
  case "cycleTheme":
393
363
  return fx.cycleTheme();
394
364
  }
@@ -448,13 +418,6 @@ export function keyHelp(cmd: string): readonly KeyHelpGroup[] {
448
418
  { combo: "Home / End", what: "First / last row" },
449
419
  ],
450
420
  },
451
- {
452
- title: "Note",
453
- entries: [
454
- { combo: `${cmd}E`, what: "Toggle read / edit" },
455
- { combo: `${cmd}S`, what: "Save the open draft" },
456
- ],
457
- },
458
421
  {
459
422
  title: "Graph",
460
423
  entries: [{ combo: "g", what: "Fit the whole graph" }],
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * `keys.model.ts` decides what a keystroke means; this attaches one listener
5
5
  * and performs the answer. Four lines of it are the listener and the rest is
6
- * dispatch, which is deliberate — the same split `viewport.ts` uses, and for
7
- * the same reason: a subscription written inline in a `useEffect` is one no
6
+ * dispatch, which is deliberate — a subscription written inline in a
7
+ * `useEffect` is one no
8
8
  * test can reach.
9
9
  *
10
10
  * ## One listener, on the document, at the bubble phase
@@ -1,9 +1,9 @@
1
1
  /**
2
2
  * Everything the shell *decides*, as pure functions (weave-workspace §1.2).
3
3
  *
4
- * The header's status summary, the connection indicator's wording, the
5
- * per-column empty-state copy and the status bar's text all live here rather
6
- * than inside the components that render them. That is not a stylistic
4
+ * The header's status summary, the per-column empty-state copy and the status
5
+ * bar's text all live here rather than inside the components that render them.
6
+ * That is not a stylistic
7
7
  * preference: §10 forbids adding a DOM test environment, so a conditional
8
8
  * inside a `.tsx` is a conditional that can never be covered, and §14 lists
9
9
  * "coverage gate blocks the UI work" as a live risk whose stated mitigation
@@ -15,9 +15,11 @@
15
15
  */
16
16
 
17
17
  import type { GraphPayload, WireNodeKind, WireStalenessState } from "../../shared/wire";
18
- import type { ConnectionState } from "../state";
19
- import type { ColumnId, DividerId, ResolvedColumn } from "./layout.model";
20
- import { DIVIDERS } from "./layout.model";
18
+ /** The three fixed workspace surfaces. */
19
+ export type ColumnId = "tree" | "note" | "graph";
20
+
21
+ /** Columns in keyboard and visual order. */
22
+ export const COLUMNS: readonly ColumnId[] = ["tree", "note", "graph"];
21
23
 
22
24
  // --- the header summary --------------------------------------------------------
23
25
 
@@ -77,93 +79,22 @@ export function summaryParts(summary: HeaderSummary): readonly string[] {
77
79
  return [`vault:${summary.notes}`, `repo:${repoLabel(summary.repo)}`, `${summary.nodes} nodes`];
78
80
  }
79
81
 
80
- // --- the connection indicator ----------------------------------------------------
81
-
82
- /**
83
- * How a connection state is presented.
84
- *
85
- * `tone` is a class suffix rather than a colour, so the palette stays in the
86
- * stylesheet where §5.2's nonce'd CSS can own it and this module stays free
87
- * of presentation constants it cannot test the appearance of.
88
- */
89
- export interface ConnectionView {
90
- readonly label: string;
91
- readonly tone: "ok" | "warn" | "bad";
92
- /** The `title=` tooltip. Says what the user should expect to happen next. */
93
- readonly hint: string;
94
- }
95
-
96
- const CONNECTION_VIEWS: Readonly<Record<ConnectionState, ConnectionView>> = {
97
- live: { label: "live", tone: "ok", hint: "watching the vault and the repository for changes" },
98
- reconnecting: {
99
- label: "reconnecting",
100
- tone: "warn",
101
- // Naming the recovery matters: §6 has the client refetch everything on
102
- // reopen, so the user's screen will catch up on its own and they should
103
- // not go looking for a reload button.
104
- hint: "the event stream dropped — retrying, and everything refetches when it returns",
105
- },
106
- offline: {
107
- label: "offline",
108
- tone: "bad",
109
- hint: "the workspace server is gone — the header's refresh control retries",
110
- },
111
- };
112
-
113
- /** Present a connection state. Total over the three states of §1.3. */
114
- export function connectionView(state: ConnectionState): ConnectionView {
115
- return CONNECTION_VIEWS[state];
116
- }
117
-
118
82
  // --- empty states ------------------------------------------------------------------
119
83
 
120
- /**
121
- * A column's title, and the placeholder shown while its content is someone
122
- * else's phase.
123
- *
124
- * P1's deliverable for the three columns was an *honest* empty state naming
125
- * the phase that would fill it — more useful than a spinner implying a load,
126
- * and far more useful than a blank pane that reads as a bug.
127
- *
128
- * **As of P3 no column uses one.** All three are built, so `Columns.tsx`
129
- * renders their real surfaces and each has its *own* empty states —
130
- * `treeEmptyMessage`, `noteEmptyMessage` and `graphEmptyMessage`, which
131
- * distinguish "loading" from "filtered to nothing" from "genuinely empty" in a
132
- * way one static sentence cannot.
133
- *
134
- * The table stays because {@link EmptyStateCopy.title} is still every column's
135
- * heading and its `aria-label`. {@link EmptyStateCopy.body} and `phase` are
136
- * now dead for all three, and are kept accurate rather than deleted for one
137
- * reason: `tests/web/client-shell.test.ts` asserts the `phase` values against
138
- * §11, so a column whose phase silently disagreed with the doc would be the
139
- * first sign that this table had stopped tracking reality. `EmptyState.tsx`
140
- * itself now has no caller, and is deleted rather than kept warm — an unused
141
- * component is a thing the next reader has to work out is unused.
142
- */
84
+ /** The title used by a column heading and its `aria-label`. */
143
85
  export interface EmptyStateCopy {
144
86
  readonly title: string;
145
- readonly body: string;
146
- /** The phase that fills this column in, e.g. `"P2"`. */
147
- readonly phase: string;
148
87
  }
149
88
 
150
89
  const EMPTY_STATES: Readonly<Record<ColumnId, EmptyStateCopy>> = {
151
90
  tree: {
152
91
  title: "Tree",
153
- // Built in P2; body and phase are no longer rendered. See the header.
154
- body: "The vault and repository outline, expandable, filterable, with provenance markers.",
155
- phase: "P2",
156
92
  },
157
93
  note: {
158
94
  title: "Note",
159
- body: "Select anything to read it here — rendered Markdown, front matter, tags and wikilinks.",
160
- phase: "P2",
161
95
  },
162
96
  graph: {
163
97
  title: "Graph",
164
- // Built in P3; body and phase are no longer rendered. See the header.
165
- body: "The knowledge graph, laid out and navigable, with the selection's neighbourhood highlighted.",
166
- phase: "P3",
167
98
  },
168
99
  };
169
100
 
@@ -172,18 +103,9 @@ export function emptyStateFor(column: ColumnId): EmptyStateCopy {
172
103
  return EMPTY_STATES[column];
173
104
  }
174
105
 
175
- /**
176
- * The context rail's title.
177
- *
178
- * Built in P2.5, so `body`/`phase` are no longer rendered either — the rail's
179
- * real empty states are `RAIL_EMPTY` in `context/context.model.ts`, which
180
- * separates "loading" from "nothing selected" from "this node is isolated".
181
- * `title` is still the rail's heading and `aria-label`.
182
- */
106
+ /** The context rail's heading and `aria-label`. */
183
107
  export const CONTEXT_EMPTY: EmptyStateCopy = {
184
108
  title: "Context",
185
- body: "Links, backlinks and mentions for whatever is selected.",
186
- phase: "P2",
187
109
  };
188
110
 
189
111
  // --- the status bar -------------------------------------------------------------------
@@ -200,7 +122,6 @@ export const CONTEXT_EMPTY: EmptyStateCopy = {
200
122
  export interface StatusBarModel {
201
123
  readonly cwd: string;
202
124
  readonly selection: string;
203
- readonly connection: ConnectionView;
204
125
  /** `null` before the first successful graph fetch. */
205
126
  readonly stamp: string | null;
206
127
  }
@@ -212,13 +133,11 @@ export const NO_VALUE = "—";
212
133
  export function statusBarModel(
213
134
  cwd: string,
214
135
  selectedId: string | null,
215
- connection: ConnectionState,
216
136
  stamp: string | null,
217
137
  ): StatusBarModel {
218
138
  return {
219
139
  cwd: cwd === "" ? NO_VALUE : cwd,
220
140
  selection: selectedId ?? "nothing selected",
221
- connection: connectionView(connection),
222
141
  stamp,
223
142
  };
224
143
  }
@@ -239,48 +158,6 @@ export function shortStamp(stamp: string | null): string {
239
158
  return match?.[1] ?? stamp;
240
159
  }
241
160
 
242
- // --- pairing columns with dividers ---------------------------------------------------
243
-
244
- /**
245
- * One rendered column, plus the divider that follows it (if any).
246
- *
247
- * `divider` is `null` for the last column: there are three columns and two
248
- * dividers, and the trailing edge of the grid is the window, not a handle.
249
- */
250
- export interface ColumnSlot {
251
- readonly column: ResolvedColumn;
252
- readonly divider: DividerId | null;
253
- }
254
-
255
- /**
256
- * Interleave resolved columns with the dividers between them.
257
- *
258
- * This exists as a model function rather than an `index + 1 < length` check
259
- * inside the JSX for two reasons. The first is coverage: that check is a
260
- * branch, and a branch in a `.tsx` is a branch no test can reach (§10). The
261
- * second is that the naive version is *wrong at a breakpoint* — at `"medium"`
262
- * only tree and note render, so the divider after `note` must not appear even
263
- * though `DIVIDERS` contains one. Deriving the pairing from the columns
264
- * actually being rendered, rather than from the static divider list, makes
265
- * that impossible to get wrong.
266
- */
267
- export function columnSlots(resolved: readonly ResolvedColumn[]): readonly ColumnSlot[] {
268
- return resolved.map((column, index) => ({
269
- column,
270
- // A divider is *named by the column to its left* (`layout.model.ts`'s
271
- // `dividerPair`), so the id is the column's own — no index arithmetic into
272
- // `DIVIDERS`, which would need an unreachable `?? null` to satisfy
273
- // `noUncheckedIndexedAccess` and would leave a branch no test can cover.
274
- // It exists only where a next column follows to resize against.
275
- divider: index < resolved.length - 1 && isDivider(column.id) ? column.id : null,
276
- }));
277
- }
278
-
279
- /** Whether a column has a divider named after it — i.e. is not the last one. */
280
- function isDivider(column: ColumnId): column is DividerId {
281
- return (DIVIDERS as readonly ColumnId[]).includes(column);
282
- }
283
-
284
161
  // --- the refresh button ------------------------------------------------------------
285
162
 
286
163
  /**
@@ -45,7 +45,7 @@ export type ThemeChoice = "system" | "light" | "dark";
45
45
  /** The choices in cycle order. */
46
46
  export const THEME_CHOICES: readonly ThemeChoice[] = ["system", "light", "dark"];
47
47
 
48
- /** The `localStorage` key. Namespaced and versioned, like the layout's. */
48
+ /** The `localStorage` key. Namespaced and versioned. */
49
49
  export const THEME_STORAGE_KEY = "pi-weave.theme.v1";
50
50
 
51
51
  /** The slice of `Storage` this module needs. */
@@ -144,4 +144,4 @@ export function themeButton(choice: ThemeChoice): ThemeButtonView {
144
144
  glyph: THEME_GLYPHS[choice],
145
145
  hint: `Colour theme: ${choice} — click for ${cycleTheme(choice)}`,
146
146
  };
147
- }
147
+ }