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
@@ -1,57 +1,27 @@
1
1
  /**
2
- * The three-column grid, its dividers and the context rail
3
- * (weave-workspace §1.2).
4
- *
5
- * The one interesting line is the `useLayoutEffect`: column widths reach the
6
- * DOM as **custom properties**, written through `applyVars` →
7
- * `style.setProperty`. That is the CSSOM path, which `style-src 'nonce-…'`
8
- * does not govern — see `cssvars.ts` for the verified reasoning — and it
9
- * leaves the actual `grid-template-columns` rule in the nonce'd stylesheet
10
- * where it can be read.
11
- *
12
- * `useLayoutEffect` rather than `useEffect` so the widths land before paint;
13
- * with `useEffect` the first frame renders at whatever the CSS fallback says
14
- * and then jumps.
15
- *
16
- * The column/divider pairing is {@link columnSlots}, not an index check here:
17
- * it is breakpoint-sensitive and therefore worth a test.
18
- *
19
- * All three columns are built as of P3, so {@link Column} dispatches on the
20
- * column id. That ternary chain is the one piece of branching in the file and
21
- * it is a *routing* decision, not a product one: each arm is a bare element,
22
- * and everything those components then decide lives in their own `.model.ts`.
2
+ * The fixed three-column grid and context rail. CSS owns responsive hiding;
3
+ * keeping all three surfaces in the DOM preserves keyboard focus targets when
4
+ * the viewport changes without a resize listener or persisted layout state.
23
5
  */
24
6
 
25
- import { Fragment } from "preact";
26
- import { useLayoutEffect, useRef } from "preact/hooks";
27
7
  import { Graph } from "../graph/Graph";
28
8
  import type { ColorScheme } from "../graph/graph.model";
29
9
  import type { PositionStorage } from "../graph/positions";
30
10
  import type { RendererFactory } from "../graph/renderer";
31
11
  import type { SchemeHost } from "../graph/scheme";
32
- import type { FetchLike } from "../api";
33
- import type { EditorEvent, EditorPrompt, EditorToolbar } from "../note/editor.model";
34
12
  import { Note } from "../note/Note";
35
13
  import { Tree } from "../tree/Tree";
36
14
  import type { GraphPayload, NotePayload } from "../../shared/wire";
37
- import { applyVars } from "./cssvars";
38
- import type { ColumnId, DividerId, ResolvedColumn } from "./layout.model";
39
- import { columnVars } from "./layout.model";
15
+ import type { ColumnId } from "./shell.model";
40
16
  import { ContextRail } from "./ContextRail";
41
- import { Divider } from "./Divider";
42
- import type { ColumnSlot } from "./shell.model";
43
- import { columnSlots, emptyStateFor } from "./shell.model";
17
+ import { emptyStateFor } from "./shell.model";
44
18
 
45
19
  export interface ColumnsProps {
46
- resolved: readonly ResolvedColumn[];
47
- onDown: (divider: DividerId, clientX: number, pointerId: number) => void;
48
- onMove: (clientX: number) => void;
49
- onUp: () => void;
50
- onKey: (divider: DividerId, key: string) => void;
51
20
  /** The §1.3 context bus, as the columns see it. */
52
21
  graph: GraphPayload | null;
53
22
  note: NotePayload | null;
54
23
  selectedId: string | null;
24
+ recentIds: ReadonlySet<string>;
55
25
  /**
56
26
  * The §1.3 context bus.
57
27
  *
@@ -62,6 +32,7 @@ export interface ColumnsProps {
62
32
  * a handler accepting the wider type takes without a cast.
63
33
  */
64
34
  onSelect: (id: string | null) => void;
35
+ onOpen: (slug: string) => void;
65
36
  /** Epoch ms for relative times, read once per render by the shell. */
66
37
  now: number;
67
38
  /**
@@ -75,7 +46,6 @@ export interface ColumnsProps {
75
46
  renderer: RendererFactory;
76
47
  storage: PositionStorage;
77
48
  host: SchemeHost;
78
- fetch?: FetchLike | undefined;
79
49
  /**
80
50
  * The scheme the shell resolved from the user's theme choice
81
51
  * (`shell/theme.model.ts`'s `effectiveScheme`), or `null` to let the column
@@ -88,18 +58,6 @@ export interface ColumnsProps {
88
58
  bootFailed: boolean;
89
59
  /** Slot the graph column fills with its `fit`, for the global `g` key. */
90
60
  fit: { current: (() => void) | null };
91
- /**
92
- * The note editor's view model (§11 P5).
93
- *
94
- * Derived by the shell from the one `EditorHandle`, because the editor's
95
- * state outlives the note column's mount: below 800 px the column can be
96
- * unmounted by a resize, and an editor owned by the component would lose
97
- * an unsaved draft to a window drag.
98
- */
99
- toolbar: EditorToolbar | null;
100
- prompt: EditorPrompt | null;
101
- draft: string;
102
- send: (event: EditorEvent) => void;
103
61
  }
104
62
 
105
63
  /** One column: a titled region and whichever surface fills it. */
@@ -108,7 +66,7 @@ function Column({ id, props }: { id: ColumnId; props: ColumnsProps }) {
108
66
  return (
109
67
  <section class={`weave-col weave-col-${id}`} aria-label={copy.title}>
110
68
  <h2 class="weave-col-title">{copy.title}</h2>
111
- {id === "tree" ? <Tree graph={props.graph} selectedId={props.selectedId} onSelect={props.onSelect} now={props.now} fetch={props.fetch} /> : null}
69
+ {id === "tree" ? <Tree graph={props.graph} selectedId={props.selectedId} recentIds={props.recentIds} onSelect={props.onSelect} now={props.now} /> : null}
112
70
  {id === "note" ? (
113
71
  <Note
114
72
  note={props.note}
@@ -116,10 +74,7 @@ function Column({ id, props }: { id: ColumnId; props: ColumnsProps }) {
116
74
  selectedId={props.selectedId}
117
75
  onSelect={props.onSelect}
118
76
  now={props.now}
119
- toolbar={props.toolbar}
120
- prompt={props.prompt}
121
- draft={props.draft}
122
- send={props.send}
77
+ onOpen={props.onOpen}
123
78
  />
124
79
  ) : null}
125
80
  {id === "graph" ? (
@@ -140,37 +95,12 @@ function Column({ id, props }: { id: ColumnId; props: ColumnsProps }) {
140
95
  );
141
96
  }
142
97
 
143
- /** A column and, where one follows it, its divider. */
144
- function Slot({ slot, props }: { slot: ColumnSlot; props: ColumnsProps }) {
145
- const divider = slot.divider;
146
- return (
147
- <Fragment>
148
- <Column id={slot.column.id} props={props} />
149
- {divider === null ? null : (
150
- <Divider
151
- id={divider}
152
- label={`Resize ${slot.column.id} column`}
153
- onDown={(x, pointerId) => props.onDown(divider, x, pointerId)}
154
- onMove={props.onMove}
155
- onUp={props.onUp}
156
- onKey={(key) => props.onKey(divider, key)}
157
- />
158
- )}
159
- </Fragment>
160
- );
161
- }
162
-
163
98
  export function Columns(props: ColumnsProps) {
164
- const grid = useRef<HTMLDivElement | null>(null);
165
- useLayoutEffect(() => {
166
- applyVars(grid.current, columnVars(props.resolved));
167
- }, [props.resolved]);
168
-
169
99
  return (
170
- <div class="weave-grid" ref={grid} data-columns={props.resolved.length}>
171
- {columnSlots(props.resolved).map((slot) => (
172
- <Slot key={slot.column.id} slot={slot} props={props} />
173
- ))}
100
+ <div class="weave-grid">
101
+ <Column id="tree" props={props} />
102
+ <Column id="note" props={props} />
103
+ <Column id="graph" props={props} />
174
104
  </div>
175
105
  );
176
106
  }
@@ -2,8 +2,7 @@
2
2
  * The header bar (weave-workspace §1.2).
3
3
  *
4
4
  * Title, the `⌘K` search button, the `vault:N · repo:… · N nodes` summary,
5
- * the refresh button and the connection dot. Every string and every tone
6
- * comes from `shell.model.ts`.
5
+ * the refresh button. Every string comes from `shell.model.ts`.
7
6
  *
8
7
  * The search control was `disabled` through P1–P3 because search was P4 and
9
8
  * an affordance that looks live but is not would be the dishonest option. P4
@@ -16,12 +15,11 @@
16
15
  import { useState } from "preact/hooks";
17
16
  import { LOGO_MARK_B64, LOGO_MARK_MIME } from "../../shared/logo";
18
17
  import type { ThemeButtonView } from "./theme.model";
19
- import type { ConnectionView, HeaderSummary } from "./shell.model";
18
+ import type { HeaderSummary } from "./shell.model";
20
19
  import { REFRESH_ICON_PATHS, SEARCH_PLACEHOLDER, searchHint, summaryParts } from "./shell.model";
21
20
 
22
21
  export interface HeaderProps {
23
22
  summary: HeaderSummary;
24
- connection: ConnectionView;
25
23
  shortcut: string;
26
24
  onRefresh: () => void;
27
25
  /** Opens the ⌘K palette. The same action the global key performs. */
@@ -116,12 +114,6 @@ export function Header(props: HeaderProps) {
116
114
  {props.theme.glyph}
117
115
  </button>
118
116
  <RefreshButton onRefresh={props.onRefresh} />
119
- <span class={`weave-conn weave-conn-${props.connection.tone}`} title={props.connection.hint}>
120
- <span class="weave-conn-dot" aria-hidden="true">
121
- ●
122
- </span>
123
- {props.connection.label}
124
- </span>
125
117
  </header>
126
118
  );
127
119
  }
@@ -1,63 +1,49 @@
1
1
  /**
2
- * The workspace shell (weave-workspace §1.2).
2
+ * The workspace shell.
3
3
  *
4
- * Header, three resizable columns, context rail, status bar. This component
5
- * holds the wiring and nothing else — every value it renders comes from a
6
- * pure function in `shell.model.ts`, `layout.model.ts` or `drag.model.ts`,
7
- * and the fetch/SSE loop is `workspace.ts`. What is left here is hooks:
8
- * signals in, callbacks out.
4
+ * Header, three fixed columns, context rail, status bar. This component holds
5
+ * the wiring and nothing else — every value it renders comes from pure
6
+ * functions in the shell models,
7
+ * and the fetch/poll loop is `workspace.ts`. What is left here is hooks:
8
+ * state in, callbacks out.
9
9
  *
10
- * The three effects, all one-liners over injected units:
10
+ * The main effects are one-liners over injected units:
11
11
  *
12
- * 1. **mount** — `startWorkspace` fetches the graph and opens the stream;
13
- * the returned `stop` is the cleanup, so a hot reload cannot leak a
14
- * socket.
15
- * 2. **resize** — `watchViewport` keeps `width` current, because the width
16
- * picks the breakpoint, which decides how many columns exist.
17
- * 3. **keys** — `watchKeys` attaches the one global `keydown` listener
12
+ * 1. **mount** — `startWorkspace` fetches the graph and starts polling;
13
+ * the returned `stop` is the cleanup, so a hot reload cannot leak a timer.
14
+ * 2. **keys** — `watchKeys` attaches the one global `keydown` listener
18
15
  * (§11 P4). Its context is read through `live`, because a listener
19
16
  * registered at mount outlives every render and a captured overlay flag
20
17
  * would let `⌘K` stack a palette on top of itself.
21
- *
22
- * Layout persistence is not an effect: `dividerHandlers` writes to
23
- * `localStorage` on release and on a keyboard nudge, never per frame.
24
18
  */
25
19
 
26
- import { useEffect, useMemo, useRef, useState } from "preact/hooks";
20
+ import { useEffect, useRef, useState } from "preact/hooks";
27
21
  import { fetchJson } from "../api.dom";
28
- import { deleteNote } from "../api";
22
+ import { openNote } from "../api";
29
23
  import type { ColorScheme } from "../graph/graph.model";
30
24
  import { schemeOf, watchScheme } from "../graph/scheme";
31
25
  import { createSigmaRenderer } from "../graph/renderer.dom";
32
- import { domEventSource } from "../live";
33
- import { createEditor, watchUnload, type EditorHandle } from "../note/editor.controller";
34
- import { editorPrompt, editorToolbar, initialEditorState, shouldBlockUnload } from "../note/editor.model";
35
26
  import { SearchPalette } from "../search/SearchPalette";
36
27
  import { restoreSelection, saveSelection } from "../selection.storage";
37
- import { connection, graph, graphFailed, noteBody, selectedId } from "../state";
28
+ import { initialWorkspaceState } from "../state";
29
+ import type { WorkspaceState } from "../state";
38
30
  import type { WorkspaceHandle } from "../workspace";
39
- import { observeNotes, select, startWorkspace } from "../workspace";
31
+ import { startWorkspace } from "../workspace";
40
32
  import { Columns } from "./Columns";
41
33
  import { deeplinkSelection, formatHash } from "./deeplink.model";
42
- import { dividerHandlers } from "./drag.model";
43
34
  import { Header } from "./Header";
44
35
  import { HelpOverlay } from "./HelpOverlay";
45
36
  import { watchKeys } from "./keys";
46
37
  import { focusSelector, runShellAction } from "./keys.model";
47
- import type { LayoutState } from "./layout.model";
48
- import { breakpointFor, loadLayout, resolveColumns, saveLayout } from "./layout.model";
49
38
  import { StatusBar } from "./StatusBar";
50
39
  import type { OverlayId } from "./shell.model";
51
- import { TICK_MS, connectionView, looksApple, searchShortcut, statusBarModel, summarize } from "./shell.model";
40
+ import { TICK_MS, looksApple, searchShortcut, statusBarModel, summarize } from "./shell.model";
52
41
  import { cycleTheme, effectiveScheme, loadTheme, saveTheme, themeAttr, themeButton } from "./theme.model";
53
42
  import type { ThemeChoice } from "./theme.model";
54
- import { watchViewport } from "./viewport";
55
43
 
56
44
  /**
57
45
  * Write the choice onto `<html>`, so the sheet's attribute branch and the
58
- * media query agree on who is in charge (see `theme.model.ts`'s header). The
59
- * one DOM write the theme needs, and it is here rather than in the model for
60
- * the same reason `applyVars` is: it is an effect, not a decision.
46
+ * media query agree on who is in charge (see `theme.model.ts`'s header).
61
47
  */
62
48
  function applyThemeAttr(choice: ReturnType<typeof themeAttr>): void {
63
49
  if (choice === null) delete document.documentElement.dataset.weaveTheme;
@@ -67,17 +53,13 @@ function applyThemeAttr(choice: ReturnType<typeof themeAttr>): void {
67
53
  export interface ShellProps {
68
54
  /** From the page bootstrap. Shown in the status bar. */
69
55
  cwd: string;
70
- /** `window.innerWidth` at mount. Injected so the first render is testable. */
71
- initialWidth: number;
72
56
  /** `navigator.platform`, for the `⌘K` vs `Ctrl K` hint. */
73
57
  platform: string;
74
58
  }
75
59
 
76
60
  export function Shell(props: ShellProps) {
77
- const [width, setWidth] = useState(props.initialWidth);
61
+ const [workspaceState, setWorkspaceState] = useState<WorkspaceState>(initialWorkspaceState);
78
62
  const [overlay, setOverlay] = useState<OverlayId>(null);
79
- const [layout, setLayout] = useState<LayoutState>(() => loadLayout(localStorage, props.initialWidth));
80
- const [editorState, setEditorState] = useState(initialEditorState);
81
63
  // The theme: what the user picked, and what the OS is currently saying. Two
82
64
  // states because they answer different questions — `theme` changes on a
83
65
  // button press or the `t` key, `systemScheme` on an OS flip while the user
@@ -88,21 +70,13 @@ export function Shell(props: ShellProps) {
88
70
  // Filled by the graph column at mount, cleared on unmount. The global `g`
89
71
  // key's only route to the renderer — see `Graph.tsx`'s `fit` prop.
90
72
  const fit = useRef<(() => void) | null>(null);
91
- // The gesture and the global key listener read through these so a handler
92
- // built on an early render still sees current state — see `DragHost`.
93
- const live = useRef({ layout, width, overlay });
94
- live.current = { layout, width, overlay };
95
-
96
- // Built once and owned by the shell, not by the note column. The column is
97
- // unmounted by a resize below 800 px, and an editor whose lifetime was the
98
- // column's would lose an unsaved draft to a window drag.
99
- const editor: EditorHandle = useMemo(
100
- () => createEditor({ fetch: fetchJson, select: (id) => void select(fetchJson, id), onChange: setEditorState }),
101
- [],
102
- );
73
+ // The global key listener reads through this so a handler registered at
74
+ // mount still sees the current overlay.
75
+ const live = useRef({ overlay, selectedId: workspaceState.selectedId });
76
+ live.current = { overlay, selectedId: workspaceState.selectedId };
103
77
 
104
78
  useEffect(() => {
105
- const handle = startWorkspace({ fetch: fetchJson, open: domEventSource });
79
+ const handle = startWorkspace({ fetch: fetchJson, state: workspaceState, setState: setWorkspaceState });
106
80
  workspace.current = handle;
107
81
  return () => handle.stop();
108
82
  }, []);
@@ -117,43 +91,31 @@ export function Shell(props: ShellProps) {
117
91
  const bootHash = useRef(location.hash);
118
92
  useEffect(() => {
119
93
  if (!selectionRestored.current) return;
120
- saveSelection(localStorage, selectedId.value);
94
+ saveSelection(localStorage, workspaceState.selectedId);
121
95
  // replaceState, not pushState: the address bar mirrors the note on
122
96
  // screen, and reading three notes is not three history entries. The
123
97
  // *string* is the model's (`formatHash`); this is the write itself.
124
- history.replaceState(null, "", formatHash(selectedId.value));
125
- }, [selectedId.value]);
98
+ history.replaceState(null, "", formatHash(workspaceState.selectedId));
99
+ }, [workspaceState.selectedId]);
126
100
  useEffect(() => {
127
- if (selectionRestored.current || graph.value === null) return;
101
+ if (selectionRestored.current || workspaceState.graph === null) return;
128
102
  selectionRestored.current = true;
129
- if (selectedId.value !== null) return;
103
+ if (workspaceState.selectedId !== null) return;
130
104
  // A link wins over storage: the address bar is an explicit instruction,
131
105
  // the saved note is a habit. A link to a note the graph does not hold is
132
106
  // refused (see `deeplink.model.ts`) and continuity falls through.
133
- const linked = deeplinkSelection(bootHash.current, graph.value);
134
- if (linked !== null) void select(fetchJson, linked);
107
+ const linked = deeplinkSelection(bootHash.current, workspaceState.graph);
108
+ if (linked !== null) void workspace.current?.select(linked);
135
109
  else {
136
- const saved = restoreSelection(graph.value, localStorage);
137
- if (saved !== null) void select(fetchJson, saved);
110
+ const saved = restoreSelection(workspaceState.graph, localStorage);
111
+ if (saved !== null) void workspace.current?.select(saved);
138
112
  }
139
- }, [graph.value]);
140
-
141
- // Every note that arrives, from any of the three directions it can arrive
142
- // from (mount, selection, SSE refetch), so the editor can decide whether it
143
- // is news or an interruption — see `editor.model.ts`'s header.
144
- useEffect(() => observeNotes((payload) => editor.send({ type: "loaded", payload })), []);
145
-
146
- // Read through a thunk, not captured: a listener registered at mount
147
- // outlives every render, so a captured state would always look clean and
148
- // the guard would be installed but inert.
149
- useEffect(() => watchUnload(window, () => shouldBlockUnload(editor.state())), []);
150
-
151
- useEffect(() => watchViewport(window, setWidth), []);
113
+ }, [workspaceState.graph]);
152
114
 
153
115
  // The relative-time clock. Every "8h ago" in the tree and the note meta is
154
116
  // computed from a `now` stamped per render, and a resting workspace never
155
117
  // re-renders on its own — so the minutes used to go stale until the next
156
- // SSE frame. One interval at `TICK_MS` (the reasoning is in `shell.model.ts`);
118
+ // poll. One interval at `TICK_MS` (the reasoning is in `shell.model.ts`);
157
119
  // `setNow` with a new value re-renders, and a re-render is all the copy
158
120
  // needs to catch up.
159
121
  const [now, setNow] = useState(() => Date.now());
@@ -177,45 +139,23 @@ export function Shell(props: ShellProps) {
177
139
  useEffect(
178
140
  () =>
179
141
  watchKeys(document, {
180
- context: () => ({ overlay: live.current.overlay, hasSelection: selectedId.value !== null }),
142
+ context: () => ({ overlay: live.current.overlay, hasSelection: live.current.selectedId !== null }),
181
143
  run: (action) =>
182
144
  runShellAction(action, {
183
145
  setOverlay,
184
146
  focusSelector: (selector) => focusSelector(document, selector),
185
147
  fitGraph: () => fit.current?.(),
186
- // Through the editor, not straight to `select`: clearing the
187
- // selection while a draft is dirty must be refused like any
188
- // other navigation, and `Esc` is the easiest way to do it by
189
- // accident.
190
- clearSelection: () => editor.send({ type: "navigate", id: null }),
191
- toggleEdit: () => editor.send({ type: "toggle" }),
192
- saveNote: () => editor.send({ type: "save" }),
148
+ clearSelection: () => { void workspace.current?.select(null); },
193
149
  cycleTheme: () => setTheme((current) => cycleTheme(current)),
194
150
  }),
195
151
  }),
196
152
  [],
197
153
  );
198
154
 
199
- const resolved = useMemo(() => resolveColumns(layout, width, breakpointFor(width)), [layout, width]);
200
-
201
- // Built once: the handlers read state through `live`, so they never go
202
- // stale and never need to be rebuilt.
203
- const drag = useMemo(
204
- () =>
205
- dividerHandlers({
206
- layout: () => live.current.layout,
207
- width: () => live.current.width,
208
- setLayout,
209
- persist: (next) => void saveLayout(localStorage, next),
210
- }),
211
- [],
212
- );
213
-
214
155
  return (
215
156
  <>
216
157
  <Header
217
- summary={summarize(graph.value)}
218
- connection={connectionView(connection.value)}
158
+ summary={summarize(workspaceState.graph)}
219
159
  shortcut={searchShortcut(looksApple(props.platform))}
220
160
  onRefresh={() => workspace.current?.refresh()}
221
161
  onSearch={() => setOverlay("search")}
@@ -223,54 +163,39 @@ export function Shell(props: ShellProps) {
223
163
  onTheme={() => setTheme((current) => cycleTheme(current))}
224
164
  />
225
165
  <Columns
226
- resolved={resolved}
227
- onDown={drag.onDown}
228
- onMove={drag.onMove}
229
- onUp={drag.onUp}
230
- onKey={drag.onKey}
231
166
  scheme={effectiveScheme(theme, systemScheme)}
232
- bootFailed={graphFailed.value}
233
- graph={graph.value}
234
- note={noteBody.value}
235
- selectedId={selectedId.value}
236
- // Through the editor: a selection made while a draft is dirty is
237
- // parked rather than performed, and the column then asks. Every
238
- // column's `onSelect` routes here, so there is one guarded door
239
- // rather than three that each had to remember.
240
- onSelect={(id) => editor.send({ type: "navigate", id })}
167
+ bootFailed={workspaceState.graphFailed}
168
+ graph={workspaceState.graph}
169
+ note={workspaceState.note}
170
+ selectedId={workspaceState.selectedId}
171
+ recentIds={workspaceState.recentIds}
172
+ onSelect={(id) => void workspace.current?.select(id)}
173
+ onOpen={(slug) => void openNote(fetchJson, slug)}
241
174
  now={now}
242
- toolbar={editorToolbar(editorState)}
243
- prompt={editorPrompt(editorState)}
244
- draft={editorState.draft}
245
- send={editor.send}
246
175
  // The graph column's three ports (§7.5, §10). Supplied here, at the
247
176
  // one place that is already allowed to name browser globals, so
248
177
  // `Graph.tsx` and everything under it takes its world as parameters.
249
178
  renderer={createSigmaRenderer}
250
179
  storage={localStorage}
251
180
  host={window}
252
- fetch={fetchJson}
253
181
  fit={fit}
254
182
  />
255
183
  {/*
256
184
  `model.generatedAt`, not `stamp`. The two used to be the same string;
257
- since §15.6 `stamp` is a content digest (a hex validator for the ETag
258
- and the SSE dedupe) and would render as `a3f9c2…` under a label that
185
+ since §15.6 `stamp` is the ETag's content digest and would render as
186
+ `a3f9c2…` under a label that
259
187
  says "data as of". `generatedAt` kept the data-as-of job, which is
260
188
  exactly what this bar wants.
261
189
  */}
262
190
  <StatusBar
263
- model={statusBarModel(props.cwd, selectedId.value, connection.value, graph.value?.model.generatedAt ?? null)}
191
+ model={statusBarModel(props.cwd, workspaceState.selectedId, workspaceState.graph?.model.generatedAt ?? null)}
264
192
  />
265
193
  {overlay === "search" ? (
266
194
  <SearchPalette
267
- graph={graph.value}
268
- // Through the editor, like every column's `onSelect`: choosing a hit
269
- // while a draft is dirty must park behind the UNSAVED prompt, not
270
- // wipe it. This was the one navigation path that skipped the guard.
271
- onSelect={(id) => editor.send({ type: "navigate", id })}
195
+ graph={workspaceState.graph}
196
+ onSelect={(id) => void workspace.current?.select(id)}
272
197
  onClose={() => setOverlay(null)}
273
- ports={{ fetch: fetchJson, now: Date.now, delay: (run, ms) => void setTimeout(run, ms) }}
198
+ ports={{ fetch: fetchJson }}
274
199
  />
275
200
  ) : null}
276
201
  {overlay === "help" ? (
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The status bar (weave-workspace §1.2).
3
3
  *
4
- * Working directory, current selection, data-as-of stamp, connection. The
4
+ * Working directory, current selection, and data-as-of stamp. The
5
5
  * model is built by `statusBarModel`; this renders it.
6
6
  */
7
7
 
@@ -20,9 +20,6 @@ export function StatusBar({ model }: { model: StatusBarModel }) {
20
20
  <span class="weave-status-stamp" title="data as of">
21
21
  {shortStamp(model.stamp)}
22
22
  </span>
23
- <span class={`weave-conn weave-conn-${model.connection.tone}`} title={model.connection.hint}>
24
- {model.connection.label}
25
- </span>
26
23
  </footer>
27
24
  );
28
25
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * The inline icon sprite (Tier 6, §8 P6.4).
2
+ * The inline icon sprite (Tier 6, P6.4).
3
3
  *
4
4
  * ## Why a sprite of path data
5
5
  *
@@ -26,7 +26,7 @@
26
26
  *
27
27
  * ## Why the renderer is not here
28
28
  *
29
- * §10: this module decides (which glyphs exist, which are filled, how fat a
29
+ * : this module decides (which glyphs exist, which are filled, how fat a
30
30
  * dot is), and the `.tsx` renders (`<svg>` JSX from {@link IconDef}). That
31
31
  * also keeps this file free of JSX and DOM types, so the root `tsconfig.json`
32
32
  * project compiles its tests.
@@ -43,8 +43,7 @@ export type IconName =
43
43
  | "entryPoint"
44
44
  | "gitState"
45
45
  | "external"
46
- | "file"
47
- | "session";
46
+ | "file";
48
47
 
49
48
  /** The box every path is drawn in, and the stroke the sheet's aesthetic asks for. */
50
49
  export const ICON_BOX = 16;
@@ -82,8 +81,6 @@ export const ICONS: Readonly<Record<IconName, IconDef>> = {
82
81
  vault: { d: ["M8 2.6 13.4 8 8 13.4 2.6 8Z"], filled: true },
83
82
  /** A note page with a folded corner — the vault's contents. */
84
83
  note: { d: ["M4.5 2.75h4.75l2.25 2.25v8.25h-7Z", "M9.25 2.75V5h2.25"], filled: false },
85
- /** A session memory page: a rounded speech bubble with a tail. */
86
- session: { d: ["M3 3.75h10v6.5H6.75L3.75 13v-2.75H3Z"], filled: false },
87
84
  /** The repository root, a framed card with a header rule — the TUI's ▣. */
88
85
  repository: { d: ["M2.75 4.25h10.5v7.5H2.75Z", "M2.75 6.75h10.5"], filled: false },
89
86
  /** A folder — the tree's module / synthesized vault directory. */
@@ -98,4 +95,4 @@ export const ICONS: Readonly<Record<IconName, IconDef>> = {
98
95
  external: { d: ["M4.5 11.5l7-7", "M6.75 4.5h4.75v4.75"], filled: false },
99
96
  /** A plain repo file, an unfolded page. */
100
97
  file: { d: ["M4.5 2.75h7v10.5h-7Z"], filled: false },
101
- };
98
+ };