pi-weave 0.1.11 → 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 (75) hide show
  1. package/README.md +148 -132
  2. package/package.json +1 -2
  3. package/src/core/cache/workspace.ts +26 -9
  4. package/src/core/concurrency.ts +3 -6
  5. package/src/core/frontmatter.ts +0 -53
  6. package/src/core/graph/build.ts +12 -8
  7. package/src/core/graph/current.ts +4 -6
  8. package/src/core/graph/model.ts +1 -1
  9. package/src/core/graph/wikilinks.ts +3 -3
  10. package/src/core/index.ts +26 -27
  11. package/src/core/paths.ts +0 -7
  12. package/src/core/slug.ts +13 -0
  13. package/src/core/types.ts +1 -0
  14. package/src/core/vault.ts +45 -467
  15. package/src/core/view/detail.ts +1 -1
  16. package/src/core/view/health.ts +1 -1
  17. package/src/core/view/tree.ts +15 -3
  18. package/src/pi/index.ts +6 -85
  19. package/src/pi/summarize.ts +2 -2
  20. package/src/pi/viewer/tui/bodyStore.ts +4 -7
  21. package/src/pi/viewer/tui/branding.ts +7 -148
  22. package/src/pi/viewer/tui/run.ts +3 -17
  23. package/src/pi/viewer/tui/surface/base.ts +24 -3
  24. package/src/pi/viewer/tui/surface/explore.ts +41 -6
  25. package/src/pi/viewer/tui/workspace.ts +23 -351
  26. package/src/pi/viewer/tui/workspaceRoot.ts +31 -172
  27. package/src/pi/viewer/web/run.ts +7 -117
  28. package/src/web/client/api.dom.ts +2 -2
  29. package/src/web/client/api.ts +14 -176
  30. package/src/web/client/bootstrap.ts +5 -14
  31. package/src/web/client/context/context.model.ts +9 -11
  32. package/src/web/client/dist/app.js +77 -167
  33. package/src/web/client/graph/dynamics.ts +5 -65
  34. package/src/web/client/graph/renderer.dom.ts +7 -8
  35. package/src/web/client/graph/renderer.ts +9 -35
  36. package/src/web/client/main.tsx +1 -1
  37. package/src/web/client/note/Note.tsx +37 -60
  38. package/src/web/client/note/note.model.ts +23 -0
  39. package/src/web/client/search/SearchPalette.tsx +45 -36
  40. package/src/web/client/search/search.model.ts +33 -454
  41. package/src/web/client/shell/Columns.tsx +13 -81
  42. package/src/web/client/shell/Header.tsx +2 -10
  43. package/src/web/client/shell/Shell.tsx +50 -123
  44. package/src/web/client/shell/StatusBar.tsx +1 -4
  45. package/src/web/client/shell/icons.model.ts +4 -7
  46. package/src/web/client/shell/keys.model.ts +5 -42
  47. package/src/web/client/shell/keys.ts +2 -2
  48. package/src/web/client/shell/shell.model.ts +10 -133
  49. package/src/web/client/shell/theme.model.ts +2 -2
  50. package/src/web/client/shell/theme.ts +33 -121
  51. package/src/web/client/state.ts +9 -89
  52. package/src/web/client/tree/Tree.tsx +24 -97
  53. package/src/web/client/tree/tree.model.ts +8 -56
  54. package/src/web/client/workspace.ts +72 -242
  55. package/src/web/server/page.ts +8 -10
  56. package/src/web/server/routes.ts +30 -415
  57. package/src/web/server/server.ts +6 -145
  58. package/src/web/shared/layout.ts +72 -624
  59. package/src/web/shared/wire.ts +10 -178
  60. package/src/core/sessions.ts +0 -929
  61. package/src/pi/sessionScan.ts +0 -104
  62. package/src/pi/viewer/tui/explorer.ts +0 -586
  63. package/src/web/client/live.model.ts +0 -275
  64. package/src/web/client/live.ts +0 -151
  65. package/src/web/client/note/Editor.tsx +0 -109
  66. package/src/web/client/note/editor.controller.ts +0 -151
  67. package/src/web/client/note/editor.model.ts +0 -686
  68. package/src/web/client/search/search.ts +0 -107
  69. package/src/web/client/shell/Divider.tsx +0 -44
  70. package/src/web/client/shell/cssvars.ts +0 -70
  71. package/src/web/client/shell/drag.model.ts +0 -170
  72. package/src/web/client/shell/layout.model.ts +0 -500
  73. package/src/web/client/shell/viewport.ts +0 -29
  74. package/src/web/server/sse.ts +0 -321
  75. package/src/web/server/watcher.ts +0 -507
@@ -1,19 +1,10 @@
1
- /**
2
- * The tree column (weave-workspace §1.2, P2.3).
3
- *
4
- * Props in, JSX out. Every string, glyph, branch and key binding comes from
5
- * `tree.model.ts`; what is left here is a `useState`, a `map` and four
6
- * handlers that forward into it. §10's rule, and the reason the 95 % gate
7
- * survives a UI phase with no DOM test environment.
8
- */
1
+ /** The read-only tree column. */
9
2
 
10
3
  import { useState } from "preact/hooks";
11
- import { recentIds } from "../state";
12
4
  import { isTextEntry, type KeyTarget } from "../shell/keys.model";
13
5
  import { ICON_BOX, ICON_STROKE, ICONS } from "../shell/icons.model";
14
6
  import type { IconName } from "../shell/icons.model";
15
7
  import type { GraphPayload } from "../../shared/wire";
16
- import type { TreeRowView, TreeViewState } from "./tree.model";
17
8
  import {
18
9
  FILTER_HINT,
19
10
  FILTER_LABEL,
@@ -40,22 +31,11 @@ import {
40
31
  export interface TreeProps {
41
32
  graph: GraphPayload | null;
42
33
  selectedId: string | null;
34
+ recentIds: ReadonlySet<string>;
43
35
  onSelect: (id: string) => void;
44
- /** Epoch ms for relative times. Injected so the render is deterministic. */
45
36
  now: number;
46
37
  }
47
38
 
48
- /**
49
- * One sprite glyph, as a real `<svg>`.
50
- *
51
- * The element is built from {@link ICONS}' path data rather than injected as
52
- * an HTML string — CSP-identical (neither path touches a `script-src` hook),
53
- * but the string form would carry a whole `<svg>` per row and Preact can
54
- * branch the two paint modes with a spread and no `if`. Every attribute here
55
- * is a presentation *attribute*, not a `style` one: `style-src` never sees it.
56
- * Sizing rides the width/height attributes rather than CSS for the same
57
- * reason — the box is part of the icon, not of its context.
58
- */
59
39
  export function Icon({ name, class: className }: { name: IconName; class?: string }) {
60
40
  const def = ICONS[name];
61
41
  return (
@@ -71,20 +51,17 @@ export function Icon({ name, class: className }: { name: IconName; class?: strin
71
51
  stroke-linejoin="round"
72
52
  aria-hidden="true"
73
53
  >
74
- {def.d.map((d) => (
75
- <path d={d} />
76
- ))}
54
+ {def.d.map((d) => <path d={d} />)}
77
55
  </svg>
78
56
  );
79
57
  }
80
58
 
81
- function Row({ view, onSelect, onToggle }: { view: TreeRowView; onSelect: () => void; onToggle: () => void }) {
59
+ function Row({ view, recentIds, onSelect, onToggle }: { view: ReturnType<typeof rowViews>[number]; recentIds: ReadonlySet<string>; onSelect: () => void; onToggle: () => void }) {
82
60
  return (
83
61
  <li
84
62
  id={view.domId}
85
- class={`weave-row weave-row-${view.kind}${view.selected ? " weave-row-on" : ""}${view.muted ? " weave-row-muted" : ""}${
86
- recentIds.value.has(view.id) ? " weave-row-new" : ""
87
- }`}
63
+ data-row-id={view.id}
64
+ class={`weave-row weave-row-${view.kind}${view.selected ? " weave-row-on" : ""}${recentIds.has(view.id) ? " weave-row-new" : ""}`}
88
65
  role="treeitem"
89
66
  aria-level={view.level}
90
67
  aria-posinset={view.posinset}
@@ -94,27 +71,11 @@ function Row({ view, onSelect, onToggle }: { view: TreeRowView; onSelect: () =>
94
71
  style={depthVar(view.depth)}
95
72
  onClick={onSelect}
96
73
  >
97
- {/* The glyphs are decoration: the twisty duplicates `aria-expanded`,
98
- the kind glyph duplicates nothing a screen reader needs, and the
99
- provenance shape is announced through its `title` instead. The
100
- chevron rotates through CSS, so "open" is a class, not a different
101
- sprite. */}
102
- <span
103
- class="weave-twisty"
104
- aria-hidden="true"
105
- onClick={(event) => {
106
- event.stopPropagation();
107
- onToggle();
108
- }}
109
- >
74
+ <span class="weave-twisty" aria-hidden="true" onClick={(event) => { event.stopPropagation(); onToggle(); }}>
110
75
  {view.hasKids ? <Icon name="chevron" class={view.expanded ? "weave-icon weave-icon-open" : "weave-icon"} /> : null}
111
76
  </span>
112
- <span class="weave-kind" aria-hidden="true">
113
- <Icon name={view.kindIcon} class="weave-icon" />
114
- </span>
115
- <span class={`weave-prov weave-prov-${view.provenance ?? "none"}`} title={view.provenanceTitle}>
116
- {view.provenanceGlyph}
117
- </span>
77
+ <span class="weave-kind" aria-hidden="true"><Icon name={view.kindIcon} class="weave-icon" /></span>
78
+ <span class={`weave-prov weave-prov-${view.provenance ?? "none"}`} title={view.provenanceTitle}>{view.provenanceGlyph}</span>
118
79
  <span class="weave-label">{view.label}</span>
119
80
  <span class="weave-meta">{view.meta}</span>
120
81
  </li>
@@ -122,62 +83,28 @@ function Row({ view, onSelect, onToggle }: { view: TreeRowView; onSelect: () =>
122
83
  }
123
84
 
124
85
  export function Tree(props: TreeProps) {
125
- const [state, setState] = useState<TreeViewState>(initialTreeView);
86
+ const [state, setState] = useState(initialTreeView);
126
87
  const rows = rowsFor(props.graph, state);
127
88
  const empty = treeEmptyMessage(props.graph, rows, state);
128
-
129
89
  return (
130
- <div
131
- class="weave-tree"
132
- onKeyDown={(event) => {
133
- // The filter box sits inside this listener, so its keystrokes arrive
134
- // here too: a `j` meant for the query must stay a character, not an
135
- // alias the tree consumes. The model refuses when `typing` is true.
136
- const target = event.target as KeyTarget;
137
- const typing = isTextEntry(target?.tagName ?? null, target?.isContentEditable === true);
138
- const next = treeKey(rows, state, props.selectedId, event.key, typing);
139
- if (!next.handled) return;
140
- event.preventDefault();
141
- setState(next.state);
142
- if (next.selectedId !== null) props.onSelect(next.selectedId);
143
- }}
144
- >
90
+ <div class="weave-tree" onKeyDown={(event) => {
91
+ const target = event.target as KeyTarget;
92
+ const next = treeKey(rows, state, props.selectedId, event.key, isTextEntry(target?.tagName ?? null, target?.isContentEditable === true));
93
+ if (!next.handled) return;
94
+ event.preventDefault();
95
+ setState(next.state);
96
+ if (next.selectedId !== null) props.onSelect(next.selectedId);
97
+ }}>
145
98
  <div class="weave-tree-controls">
146
- <input
147
- type="search"
148
- class="weave-filter"
149
- value={state.query}
150
- placeholder={FILTER_PLACEHOLDER}
151
- aria-label={FILTER_LABEL}
152
- title={FILTER_HINT}
153
- onInput={(event) => setState(setQuery(state, event.currentTarget.value))}
154
- />
155
- <button type="button" class="weave-chip" title={provenanceHint(state.provFilter)} onClick={() => setState(cycleProvenance(state))}>
156
- ◧ {provenanceLabel(state.provFilter)}
157
- </button>
158
- <button type="button" class="weave-chip" title={internalsHint(state.showInternals)} onClick={() => setState(toggleInternals(state))}>
159
- ◧ {internalsLabel(state.showInternals)}
160
- </button>
99
+ <input type="search" class="weave-filter" value={state.query} placeholder={FILTER_PLACEHOLDER} aria-label={FILTER_LABEL} title={FILTER_HINT} onInput={(event) => setState(setQuery(state, event.currentTarget.value))} />
100
+ <button type="button" class="weave-chip" title={provenanceHint(state.provFilter)} onClick={() => setState(cycleProvenance(state))}>◧ {provenanceLabel(state.provFilter)}</button>
101
+ <button type="button" class="weave-chip" title={internalsHint(state.showInternals)} onClick={() => setState(toggleInternals(state))}>◧ {internalsLabel(state.showInternals)}</button>
161
102
  </div>
162
103
  {empty === null ? (
163
- <ul
164
- class="weave-rows"
165
- role="tree"
166
- tabIndex={0}
167
- aria-label={TREE_LABEL}
168
- // Focus stays on the `<ul>` and the *active* row is named by
169
- // reference — the alternative, a roving `tabindex`, would put every
170
- // row in the Tab order and make Tab a fourth way to walk the tree.
171
- // `null` when the selection is not visible; see the model.
172
- aria-activedescendant={treeActiveDescendant(rows, props.selectedId) ?? undefined}
173
- >
174
- {rowViews(rows, props.selectedId, props.now).map((view) => (
175
- <Row key={view.id} view={view} onSelect={() => props.onSelect(view.id)} onToggle={() => setState(toggleExpanded(state, view.id))} />
176
- ))}
104
+ <ul class="weave-rows" role="tree" tabIndex={0} aria-label={TREE_LABEL} aria-activedescendant={treeActiveDescendant(rows, props.selectedId) ?? undefined}>
105
+ {rowViews(rows, props.selectedId, props.now).map((view) => <Row key={view.id} view={view} recentIds={props.recentIds} onSelect={() => { props.onSelect(view.id); if (view.hasKids) setState((current) => toggleExpanded(current, view.id)); }} onToggle={() => setState(toggleExpanded(state, view.id))} />)}
177
106
  </ul>
178
- ) : (
179
- <p class="weave-tree-empty">{empty}</p>
180
- )}
107
+ ) : <p class="weave-tree-empty">{empty}</p>}
181
108
  <p class="weave-tree-count">{rowCountLabel(rows)}</p>
182
109
  </div>
183
110
  );
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Everything the tree column *decides* (weave-workspace §1.2, §3, §10).
2
+ * Everything the tree column *decides*.
3
3
  *
4
4
  * The column itself is `treeRows` with a different renderer — that is §3's
5
5
  * whole claim, and this module is what makes it true in the browser: it holds
@@ -40,7 +40,7 @@ import type { IconName } from "../shell/icons.model";
40
40
  * It is **not** `src/web/client/state.ts`'s `TreeState`. That one is a P1
41
41
  * placeholder carrying only an expanded-id list, and it stays where it is:
42
42
  * this state is owned by the tree column, lives in the component that renders
43
- * it, and never crosses the context bus. §1.3's bus is `selectedId` — which
43
+ * it, and never crosses the context bus. 's bus is `selectedId` — which
44
44
  * rows happen to be open is not something the note column or the graph has any
45
45
  * business reacting to.
46
46
  */
@@ -81,7 +81,7 @@ export function initialTreeView(roots: readonly string[] = ["vault", "repository
81
81
  *
82
82
  * `WireGraphModel` is `Omit<GraphModel, "danglingLinks">` — the payload hoists
83
83
  * that map to its own top-level `dangling` rather than shipping it twice
84
- * (§4.2) — so exactly one field has to be put back before `treeRows` or
84
+ * (§3) — so exactly one field has to be put back before `treeRows` or
85
85
  * `detailModel` will accept it. The door (`shared/view.ts`) deliberately does
86
86
  * not do this: it is a wire concern, and a door that carried a transformation
87
87
  * would be a second implementation rather than a re-export.
@@ -248,7 +248,7 @@ export function parentOf(rows: readonly TreeRow[], id: string): string | null {
248
248
  }
249
249
 
250
250
  /**
251
- * Vim-ish aliases: `j` is `ArrowDown`, `k` is `ArrowUp` (§11 P4).
251
+ * Vim-ish aliases: `j` is `ArrowDown`, `k` is `ArrowUp` ( P4).
252
252
  *
253
253
  * A *normalizer* rather than two more branches in {@link treeKey}, because
254
254
  * the aliasing and the navigation are separate concerns and folding them
@@ -257,7 +257,7 @@ export function parentOf(rows: readonly TreeRow[], id: string): string | null {
257
257
  *
258
258
  * ## Why the aliases are tree-scoped and not global
259
259
  *
260
- * §11 says "vim-ish `j/k` **in the tree**", and the qualifier is load-bearing
260
+ * says "vim-ish `j/k` **in the tree**", and the qualifier is load-bearing
261
261
  * on both sides. A global `j` would move the tree's cursor while the user is
262
262
  * reading the note column — an invisible change to a surface they are not
263
263
  * looking at — and it would make `j` untypeable in the graph's depth control.
@@ -368,45 +368,6 @@ export function kindIcon(kind: WireNodeKind): IconName {
368
368
  return KIND_ICONS[kind];
369
369
  }
370
370
 
371
- // --- the session fold -----------------------------------------------------------------
372
-
373
- /**
374
- * The synthesized folder session memory lives under.
375
- *
376
- * Core's graph builder (`src/core/graph/build.ts`) nests notes whose slug has
377
- * a directory under a synthesized `vfolder:<dir>` node — kind `module`,
378
- * because reusing the tree's containment chain needs no client change. That
379
- * reuse is why there is no `session` node *kind*: a session note is an
380
- * ordinary `note` that happens to be filed there, and this client recognises
381
- * it by path, not by kind.
382
- */
383
- export const SESSION_DIR = "sessions";
384
-
385
- /** True when a node id names a note under the {@link SESSION_DIR} fold. */
386
- export function isSessionNote(id: string): boolean {
387
- return id.startsWith(`note:${SESSION_DIR}/`);
388
- }
389
-
390
- /**
391
- * Whether a row renders a notch quieter.
392
- *
393
- * `sessions/<n>.md` notes are machine-written memory that accrues by the
394
- * dozens with near-duplicate titles, so at any real vault size they are most
395
- * of the tree's rows — and rows that all look alike at full weight make the
396
- * six notes a human actually wrote harder to find. So session rows sit in
397
- * `--weave-dim` until hovered or selected, which is how Obsidian treats its
398
- * own long tails.
399
- *
400
- * The *never* half is the load-bearing part: `selectedId` is the §1.3 bus, so
401
- * the selection is decided somewhere the tree does not own, and a rule that
402
- * could dim the selected row would be a rule the graph's click could silently
403
- * break. The class-coverage gate does not see this — it is asserted here,
404
- * which is where §10 says it belongs.
405
- */
406
- export function isMuted(row: TreeRow, selectedId: string | null): boolean {
407
- return row.id !== selectedId && isSessionNote(row.id);
408
- }
409
-
410
371
  /**
411
372
  * Provenance → glyph.
412
373
  *
@@ -557,14 +518,6 @@ export interface TreeRowView {
557
518
  readonly hasKids: boolean;
558
519
  readonly expanded: boolean;
559
520
  readonly selected: boolean;
560
- /**
561
- * Render a notch quieter — the session rows of {@link isMuted}.
562
- *
563
- * A view-model flag rather than a `.tsx` branch for the same reason every
564
- * other branch is here, and the stylesheet keys off it with
565
- * `.weave-row-muted`.
566
- */
567
- readonly muted: boolean;
568
521
  /** The trailing annotation, already formatted against `now`. */
569
522
  readonly meta: string;
570
523
  /** ARIA `aria-level`, which is 1-based where `depth` is 0-based. */
@@ -600,7 +553,6 @@ export function rowView(row: TreeRow, selectedId: string | null, now: number, po
600
553
  hasKids: row.hasKids,
601
554
  expanded: row.expanded,
602
555
  selected: row.id === selectedId,
603
- muted: isMuted(row, selectedId),
604
556
  meta: formatTreeMeta(row.meta, now),
605
557
  level: row.depth + 1,
606
558
  };
@@ -675,7 +627,7 @@ export const FILTER_PLACEHOLDER = "filter…";
675
627
  * Names *what is in it* rather than what it is: "Tree" is already the
676
628
  * column's heading and the role announces the widget type, so a second
677
629
  * "tree" would be read three times. "Vault and repository" is the sentence
678
- * §1.1 uses for the same thing.
630
+ * uses for the same thing.
679
631
  */
680
632
  export const TREE_LABEL = "Vault and repository";
681
633
 
@@ -699,7 +651,7 @@ export const FILTER_HINT = "Filter the tree (/)";
699
651
  * The obvious order — "no rows? then work out why" — is wrong here, and it is
700
652
  * wrong in the case that matters most. A brand-new vault is not a graph with
701
653
  * no rows: `treeRows` still emits the `vault` root, so the column renders one
702
- * word and nothing else, and a user's first ever session says nothing about
654
+ * word and nothing else, and a user's first ever use says nothing about
703
655
  * how to add a note. `treeEmptyHint` is core's answer to exactly that
704
656
  * question, and it is deliberately narrow — it returns a string only for a
705
657
  * vault with no notes *and* no repository — so consulting it first cannot
@@ -708,7 +660,7 @@ export const FILTER_HINT = "Filter the tree (/)";
708
660
  * It also outranks the filter message, for the same reason: when the vault is
709
661
  * genuinely empty, "nothing matches this filter" is true and useless.
710
662
  *
711
- * Using core's sentence rather than writing one here is §3: the TUI's empty
663
+ * Using core's sentence rather than writing one here is : the TUI's empty
712
664
  * tree and the browser's say the same thing about the same vault, because
713
665
  * there is one sentence.
714
666
  */