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,56 +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 { EditorEvent, EditorPrompt, EditorToolbar } from "../note/editor.model";
33
12
  import { Note } from "../note/Note";
34
13
  import { Tree } from "../tree/Tree";
35
14
  import type { GraphPayload, NotePayload } from "../../shared/wire";
36
- import { applyVars } from "./cssvars";
37
- import type { ColumnId, DividerId, ResolvedColumn } from "./layout.model";
38
- import { columnVars } from "./layout.model";
15
+ import type { ColumnId } from "./shell.model";
39
16
  import { ContextRail } from "./ContextRail";
40
- import { Divider } from "./Divider";
41
- import type { ColumnSlot } from "./shell.model";
42
- import { columnSlots, emptyStateFor } from "./shell.model";
17
+ import { emptyStateFor } from "./shell.model";
43
18
 
44
19
  export interface ColumnsProps {
45
- resolved: readonly ResolvedColumn[];
46
- onDown: (divider: DividerId, clientX: number, pointerId: number) => void;
47
- onMove: (clientX: number) => void;
48
- onUp: () => void;
49
- onKey: (divider: DividerId, key: string) => void;
50
20
  /** The §1.3 context bus, as the columns see it. */
51
21
  graph: GraphPayload | null;
52
22
  note: NotePayload | null;
53
23
  selectedId: string | null;
24
+ recentIds: ReadonlySet<string>;
54
25
  /**
55
26
  * The §1.3 context bus.
56
27
  *
@@ -61,6 +32,7 @@ export interface ColumnsProps {
61
32
  * a handler accepting the wider type takes without a cast.
62
33
  */
63
34
  onSelect: (id: string | null) => void;
35
+ onOpen: (slug: string) => void;
64
36
  /** Epoch ms for relative times, read once per render by the shell. */
65
37
  now: number;
66
38
  /**
@@ -86,18 +58,6 @@ export interface ColumnsProps {
86
58
  bootFailed: boolean;
87
59
  /** Slot the graph column fills with its `fit`, for the global `g` key. */
88
60
  fit: { current: (() => void) | null };
89
- /**
90
- * The note editor's view model (§11 P5).
91
- *
92
- * Derived by the shell from the one `EditorHandle`, because the editor's
93
- * state outlives the note column's mount: below 800 px the column can be
94
- * unmounted by a resize, and an editor owned by the component would lose
95
- * an unsaved draft to a window drag.
96
- */
97
- toolbar: EditorToolbar | null;
98
- prompt: EditorPrompt | null;
99
- draft: string;
100
- send: (event: EditorEvent) => void;
101
61
  }
102
62
 
103
63
  /** One column: a titled region and whichever surface fills it. */
@@ -106,7 +66,7 @@ function Column({ id, props }: { id: ColumnId; props: ColumnsProps }) {
106
66
  return (
107
67
  <section class={`weave-col weave-col-${id}`} aria-label={copy.title}>
108
68
  <h2 class="weave-col-title">{copy.title}</h2>
109
- {id === "tree" ? <Tree graph={props.graph} selectedId={props.selectedId} onSelect={props.onSelect} now={props.now} /> : null}
69
+ {id === "tree" ? <Tree graph={props.graph} selectedId={props.selectedId} recentIds={props.recentIds} onSelect={props.onSelect} now={props.now} /> : null}
110
70
  {id === "note" ? (
111
71
  <Note
112
72
  note={props.note}
@@ -114,10 +74,7 @@ function Column({ id, props }: { id: ColumnId; props: ColumnsProps }) {
114
74
  selectedId={props.selectedId}
115
75
  onSelect={props.onSelect}
116
76
  now={props.now}
117
- toolbar={props.toolbar}
118
- prompt={props.prompt}
119
- draft={props.draft}
120
- send={props.send}
77
+ onOpen={props.onOpen}
121
78
  />
122
79
  ) : null}
123
80
  {id === "graph" ? (
@@ -138,37 +95,12 @@ function Column({ id, props }: { id: ColumnId; props: ColumnsProps }) {
138
95
  );
139
96
  }
140
97
 
141
- /** A column and, where one follows it, its divider. */
142
- function Slot({ slot, props }: { slot: ColumnSlot; props: ColumnsProps }) {
143
- const divider = slot.divider;
144
- return (
145
- <Fragment>
146
- <Column id={slot.column.id} props={props} />
147
- {divider === null ? null : (
148
- <Divider
149
- id={divider}
150
- label={`Resize ${slot.column.id} column`}
151
- onDown={(x, pointerId) => props.onDown(divider, x, pointerId)}
152
- onMove={props.onMove}
153
- onUp={props.onUp}
154
- onKey={(key) => props.onKey(divider, key)}
155
- />
156
- )}
157
- </Fragment>
158
- );
159
- }
160
-
161
98
  export function Columns(props: ColumnsProps) {
162
- const grid = useRef<HTMLDivElement | null>(null);
163
- useLayoutEffect(() => {
164
- applyVars(grid.current, columnVars(props.resolved));
165
- }, [props.resolved]);
166
-
167
99
  return (
168
- <div class="weave-grid" ref={grid} data-columns={props.resolved.length}>
169
- {columnSlots(props.resolved).map((slot) => (
170
- <Slot key={slot.column.id} slot={slot} props={props} />
171
- ))}
100
+ <div class="weave-grid">
101
+ <Column id="tree" props={props} />
102
+ <Column id="note" props={props} />
103
+ <Column id="graph" props={props} />
172
104
  </div>
173
105
  );
174
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,62 +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";
22
+ import { openNote } from "../api";
28
23
  import type { ColorScheme } from "../graph/graph.model";
29
24
  import { schemeOf, watchScheme } from "../graph/scheme";
30
25
  import { createSigmaRenderer } from "../graph/renderer.dom";
31
- import { domEventSource } from "../live";
32
- import { createEditor, watchUnload, type EditorHandle } from "../note/editor.controller";
33
- import { editorPrompt, editorToolbar, initialEditorState, shouldBlockUnload } from "../note/editor.model";
34
26
  import { SearchPalette } from "../search/SearchPalette";
35
27
  import { restoreSelection, saveSelection } from "../selection.storage";
36
- import { connection, graph, graphFailed, noteBody, selectedId } from "../state";
28
+ import { initialWorkspaceState } from "../state";
29
+ import type { WorkspaceState } from "../state";
37
30
  import type { WorkspaceHandle } from "../workspace";
38
- import { observeNotes, select, startWorkspace } from "../workspace";
31
+ import { startWorkspace } from "../workspace";
39
32
  import { Columns } from "./Columns";
40
33
  import { deeplinkSelection, formatHash } from "./deeplink.model";
41
- import { dividerHandlers } from "./drag.model";
42
34
  import { Header } from "./Header";
43
35
  import { HelpOverlay } from "./HelpOverlay";
44
36
  import { watchKeys } from "./keys";
45
37
  import { focusSelector, runShellAction } from "./keys.model";
46
- import type { LayoutState } from "./layout.model";
47
- import { breakpointFor, loadLayout, resolveColumns, saveLayout } from "./layout.model";
48
38
  import { StatusBar } from "./StatusBar";
49
39
  import type { OverlayId } from "./shell.model";
50
- import { TICK_MS, connectionView, looksApple, searchShortcut, statusBarModel, summarize } from "./shell.model";
40
+ import { TICK_MS, looksApple, searchShortcut, statusBarModel, summarize } from "./shell.model";
51
41
  import { cycleTheme, effectiveScheme, loadTheme, saveTheme, themeAttr, themeButton } from "./theme.model";
52
42
  import type { ThemeChoice } from "./theme.model";
53
- import { watchViewport } from "./viewport";
54
43
 
55
44
  /**
56
45
  * Write the choice onto `<html>`, so the sheet's attribute branch and the
57
- * media query agree on who is in charge (see `theme.model.ts`'s header). The
58
- * one DOM write the theme needs, and it is here rather than in the model for
59
- * 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).
60
47
  */
61
48
  function applyThemeAttr(choice: ReturnType<typeof themeAttr>): void {
62
49
  if (choice === null) delete document.documentElement.dataset.weaveTheme;
@@ -66,17 +53,13 @@ function applyThemeAttr(choice: ReturnType<typeof themeAttr>): void {
66
53
  export interface ShellProps {
67
54
  /** From the page bootstrap. Shown in the status bar. */
68
55
  cwd: string;
69
- /** `window.innerWidth` at mount. Injected so the first render is testable. */
70
- initialWidth: number;
71
56
  /** `navigator.platform`, for the `⌘K` vs `Ctrl K` hint. */
72
57
  platform: string;
73
58
  }
74
59
 
75
60
  export function Shell(props: ShellProps) {
76
- const [width, setWidth] = useState(props.initialWidth);
61
+ const [workspaceState, setWorkspaceState] = useState<WorkspaceState>(initialWorkspaceState);
77
62
  const [overlay, setOverlay] = useState<OverlayId>(null);
78
- const [layout, setLayout] = useState<LayoutState>(() => loadLayout(localStorage, props.initialWidth));
79
- const [editorState, setEditorState] = useState(initialEditorState);
80
63
  // The theme: what the user picked, and what the OS is currently saying. Two
81
64
  // states because they answer different questions — `theme` changes on a
82
65
  // button press or the `t` key, `systemScheme` on an OS flip while the user
@@ -87,21 +70,13 @@ export function Shell(props: ShellProps) {
87
70
  // Filled by the graph column at mount, cleared on unmount. The global `g`
88
71
  // key's only route to the renderer — see `Graph.tsx`'s `fit` prop.
89
72
  const fit = useRef<(() => void) | null>(null);
90
- // The gesture and the global key listener read through these so a handler
91
- // built on an early render still sees current state — see `DragHost`.
92
- const live = useRef({ layout, width, overlay });
93
- live.current = { layout, width, overlay };
94
-
95
- // Built once and owned by the shell, not by the note column. The column is
96
- // unmounted by a resize below 800 px, and an editor whose lifetime was the
97
- // column's would lose an unsaved draft to a window drag.
98
- const editor: EditorHandle = useMemo(
99
- () => createEditor({ fetch: fetchJson, select: (id) => void select(fetchJson, id), onChange: setEditorState }),
100
- [],
101
- );
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 };
102
77
 
103
78
  useEffect(() => {
104
- const handle = startWorkspace({ fetch: fetchJson, open: domEventSource });
79
+ const handle = startWorkspace({ fetch: fetchJson, state: workspaceState, setState: setWorkspaceState });
105
80
  workspace.current = handle;
106
81
  return () => handle.stop();
107
82
  }, []);
@@ -116,43 +91,31 @@ export function Shell(props: ShellProps) {
116
91
  const bootHash = useRef(location.hash);
117
92
  useEffect(() => {
118
93
  if (!selectionRestored.current) return;
119
- saveSelection(localStorage, selectedId.value);
94
+ saveSelection(localStorage, workspaceState.selectedId);
120
95
  // replaceState, not pushState: the address bar mirrors the note on
121
96
  // screen, and reading three notes is not three history entries. The
122
97
  // *string* is the model's (`formatHash`); this is the write itself.
123
- history.replaceState(null, "", formatHash(selectedId.value));
124
- }, [selectedId.value]);
98
+ history.replaceState(null, "", formatHash(workspaceState.selectedId));
99
+ }, [workspaceState.selectedId]);
125
100
  useEffect(() => {
126
- if (selectionRestored.current || graph.value === null) return;
101
+ if (selectionRestored.current || workspaceState.graph === null) return;
127
102
  selectionRestored.current = true;
128
- if (selectedId.value !== null) return;
103
+ if (workspaceState.selectedId !== null) return;
129
104
  // A link wins over storage: the address bar is an explicit instruction,
130
105
  // the saved note is a habit. A link to a note the graph does not hold is
131
106
  // refused (see `deeplink.model.ts`) and continuity falls through.
132
- const linked = deeplinkSelection(bootHash.current, graph.value);
133
- if (linked !== null) void select(fetchJson, linked);
107
+ const linked = deeplinkSelection(bootHash.current, workspaceState.graph);
108
+ if (linked !== null) void workspace.current?.select(linked);
134
109
  else {
135
- const saved = restoreSelection(graph.value, localStorage);
136
- if (saved !== null) void select(fetchJson, saved);
110
+ const saved = restoreSelection(workspaceState.graph, localStorage);
111
+ if (saved !== null) void workspace.current?.select(saved);
137
112
  }
138
- }, [graph.value]);
139
-
140
- // Every note that arrives, from any of the three directions it can arrive
141
- // from (mount, selection, SSE refetch), so the editor can decide whether it
142
- // is news or an interruption — see `editor.model.ts`'s header.
143
- useEffect(() => observeNotes((payload) => editor.send({ type: "loaded", payload })), []);
144
-
145
- // Read through a thunk, not captured: a listener registered at mount
146
- // outlives every render, so a captured state would always look clean and
147
- // the guard would be installed but inert.
148
- useEffect(() => watchUnload(window, () => shouldBlockUnload(editor.state())), []);
149
-
150
- useEffect(() => watchViewport(window, setWidth), []);
113
+ }, [workspaceState.graph]);
151
114
 
152
115
  // The relative-time clock. Every "8h ago" in the tree and the note meta is
153
116
  // computed from a `now` stamped per render, and a resting workspace never
154
117
  // re-renders on its own — so the minutes used to go stale until the next
155
- // 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`);
156
119
  // `setNow` with a new value re-renders, and a re-render is all the copy
157
120
  // needs to catch up.
158
121
  const [now, setNow] = useState(() => Date.now());
@@ -176,45 +139,23 @@ export function Shell(props: ShellProps) {
176
139
  useEffect(
177
140
  () =>
178
141
  watchKeys(document, {
179
- context: () => ({ overlay: live.current.overlay, hasSelection: selectedId.value !== null }),
142
+ context: () => ({ overlay: live.current.overlay, hasSelection: live.current.selectedId !== null }),
180
143
  run: (action) =>
181
144
  runShellAction(action, {
182
145
  setOverlay,
183
146
  focusSelector: (selector) => focusSelector(document, selector),
184
147
  fitGraph: () => fit.current?.(),
185
- // Through the editor, not straight to `select`: clearing the
186
- // selection while a draft is dirty must be refused like any
187
- // other navigation, and `Esc` is the easiest way to do it by
188
- // accident.
189
- clearSelection: () => editor.send({ type: "navigate", id: null }),
190
- toggleEdit: () => editor.send({ type: "toggle" }),
191
- saveNote: () => editor.send({ type: "save" }),
148
+ clearSelection: () => { void workspace.current?.select(null); },
192
149
  cycleTheme: () => setTheme((current) => cycleTheme(current)),
193
150
  }),
194
151
  }),
195
152
  [],
196
153
  );
197
154
 
198
- const resolved = useMemo(() => resolveColumns(layout, width, breakpointFor(width)), [layout, width]);
199
-
200
- // Built once: the handlers read state through `live`, so they never go
201
- // stale and never need to be rebuilt.
202
- const drag = useMemo(
203
- () =>
204
- dividerHandlers({
205
- layout: () => live.current.layout,
206
- width: () => live.current.width,
207
- setLayout,
208
- persist: (next) => void saveLayout(localStorage, next),
209
- }),
210
- [],
211
- );
212
-
213
155
  return (
214
156
  <>
215
157
  <Header
216
- summary={summarize(graph.value)}
217
- connection={connectionView(connection.value)}
158
+ summary={summarize(workspaceState.graph)}
218
159
  shortcut={searchShortcut(looksApple(props.platform))}
219
160
  onRefresh={() => workspace.current?.refresh()}
220
161
  onSearch={() => setOverlay("search")}
@@ -222,26 +163,15 @@ export function Shell(props: ShellProps) {
222
163
  onTheme={() => setTheme((current) => cycleTheme(current))}
223
164
  />
224
165
  <Columns
225
- resolved={resolved}
226
- onDown={drag.onDown}
227
- onMove={drag.onMove}
228
- onUp={drag.onUp}
229
- onKey={drag.onKey}
230
166
  scheme={effectiveScheme(theme, systemScheme)}
231
- bootFailed={graphFailed.value}
232
- graph={graph.value}
233
- note={noteBody.value}
234
- selectedId={selectedId.value}
235
- // Through the editor: a selection made while a draft is dirty is
236
- // parked rather than performed, and the column then asks. Every
237
- // column's `onSelect` routes here, so there is one guarded door
238
- // rather than three that each had to remember.
239
- 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)}
240
174
  now={now}
241
- toolbar={editorToolbar(editorState)}
242
- prompt={editorPrompt(editorState)}
243
- draft={editorState.draft}
244
- send={editor.send}
245
175
  // The graph column's three ports (§7.5, §10). Supplied here, at the
246
176
  // one place that is already allowed to name browser globals, so
247
177
  // `Graph.tsx` and everything under it takes its world as parameters.
@@ -252,23 +182,20 @@ export function Shell(props: ShellProps) {
252
182
  />
253
183
  {/*
254
184
  `model.generatedAt`, not `stamp`. The two used to be the same string;
255
- since §15.6 `stamp` is a content digest (a hex validator for the ETag
256
- 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
257
187
  says "data as of". `generatedAt` kept the data-as-of job, which is
258
188
  exactly what this bar wants.
259
189
  */}
260
190
  <StatusBar
261
- 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)}
262
192
  />
263
193
  {overlay === "search" ? (
264
194
  <SearchPalette
265
- graph={graph.value}
266
- // Through the editor, like every column's `onSelect`: choosing a hit
267
- // while a draft is dirty must park behind the UNSAVED prompt, not
268
- // wipe it. This was the one navigation path that skipped the guard.
269
- onSelect={(id) => editor.send({ type: "navigate", id })}
195
+ graph={workspaceState.graph}
196
+ onSelect={(id) => void workspace.current?.select(id)}
270
197
  onClose={() => setOverlay(null)}
271
- ports={{ fetch: fetchJson, now: Date.now, delay: (run, ms) => void setTimeout(run, ms) }}
198
+ ports={{ fetch: fetchJson }}
272
199
  />
273
200
  ) : null}
274
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
+ };
@@ -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" }],