pi-weave 0.2.4 → 0.3.1
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.
- package/README.md +67 -37
- package/package.json +12 -3
- package/skills/weave-explore/SKILL.md +17 -33
- package/skills/weave-notepad/SKILL.md +57 -120
- package/skills/weave-notepad/references/link-repair.md +25 -73
- package/src/core/frontmatter.ts +3 -3
- package/src/core/graph/model.ts +2 -2
- package/src/core/graph/wikilinks.ts +1 -1
- package/src/core/index.ts +25 -1
- package/src/core/noteAction.ts +343 -0
- package/src/core/repoAction.ts +84 -0
- package/src/core/sessions.ts +80 -1
- package/src/core/summaries.ts +16 -0
- package/src/core/types.ts +1 -1
- package/src/core/workspace.ts +1 -1
- package/src/core/workspaceCommands.ts +227 -0
- package/src/opencode/commands.ts +138 -0
- package/src/opencode/index.ts +88 -0
- package/src/opencode/rpc.ts +37 -0
- package/src/opencode/tools.ts +43 -0
- package/src/opencode/tui.ts +96 -0
- package/src/opencode/v1.ts +161 -0
- package/src/pi/sessionScan.ts +4 -28
- package/src/pi/summarize.ts +10 -25
- package/src/pi/tools/noteTool.ts +7 -406
- package/src/pi/tools/repoTool.ts +8 -75
- package/src/pi/viewer/tui/surface/detail.ts +1 -1
- package/src/pi/viewer/web/run.ts +16 -77
- package/src/web/client/dist/app.js +210 -178
- package/src/web/client/graph/Graph.tsx +18 -0
- package/src/web/client/graph/column.model.ts +19 -2
- package/src/web/client/graph/graph.model.ts +7 -5
- package/src/web/client/graph/positions.ts +1 -1
- package/src/web/client/graph/renderer.ts +9 -1
- package/src/web/client/note/Note.tsx +66 -91
- package/src/web/client/note/drafts.ts +172 -0
- package/src/web/client/note/note.model.ts +2 -34
- package/src/web/client/search/SearchPalette.tsx +9 -8
- package/src/web/client/shell/ContextRail.tsx +8 -9
- package/src/web/client/shell/Shell.tsx +371 -254
- package/src/web/client/shell/StatusBar.tsx +1 -1
- package/src/web/client/shell/focus.model.ts +2 -2
- package/src/web/client/shell/keys.model.ts +26 -31
- package/src/web/client/shell/keys.ts +1 -0
- package/src/web/client/shell/shell.model.ts +31 -107
- package/src/web/client/shell/theme.model.ts +4 -4
- package/src/web/client/shell/theme.ts +12 -93
- package/src/web/client/shell/workspace.css.ts +117 -0
- package/src/web/client/tree/Tree.tsx +43 -14
- package/src/web/client/tree/tree.model.ts +19 -0
- package/src/web/client/workspace.ts +19 -1
- package/src/web/server/controller.ts +79 -0
- package/src/web/server/routes.ts +34 -3
- package/src/web/server/server.ts +5 -0
- package/src/web/server/workspace-state.ts +121 -0
- package/src/web/shared/workspace.ts +270 -0
- package/src/web/client/selection.storage.ts +0 -69
- package/src/web/client/shell/Columns.tsx +0 -139
- package/src/web/client/shell/Divider.tsx +0 -15
- package/src/web/client/shell/Header.tsx +0 -119
- package/src/web/client/shell/drag.model.ts +0 -35
- package/src/web/client/shell/layout.model.ts +0 -132
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/** Tabbed workspace furniture; the graph keeps its existing palette and renderer. */
|
|
2
|
+
export const WORKSPACE_CSS = `
|
|
3
|
+
.weave-workbench{position:relative;display:flex;min-height:0;overflow:hidden;background:var(--weave-page)}
|
|
4
|
+
.weave-workbench button{font:inherit;color:inherit;cursor:pointer}
|
|
5
|
+
.weave-workbench button:focus-visible,.weave-workbench summary:focus-visible{outline:2px solid var(--weave-accent);outline-offset:-2px}
|
|
6
|
+
.weave-workbench button:disabled{opacity:.35;cursor:default}
|
|
7
|
+
.weave-ribbon{width:44px;flex:none;display:flex;align-items:center;flex-direction:column;gap:8px;padding:12px 0;background:var(--weave-panel);border-right:1px solid var(--weave-line)}
|
|
8
|
+
.weave-ribbon button{width:32px;height:32px;border:0;border-radius:var(--weave-radius);background:transparent;font-size:var(--weave-px-display);color:var(--weave-dim)}
|
|
9
|
+
.weave-ribbon button:hover,.weave-ribbon button[aria-pressed=true]{color:var(--weave-accent);background:var(--weave-new)}
|
|
10
|
+
.weave-ribbon-space{flex:1}
|
|
11
|
+
.weave-sidebar{flex:none;min-width:0;display:flex;flex-direction:column;background:var(--weave-panel);overflow:hidden}
|
|
12
|
+
.weave-sidebar-dock{display:flex;flex:none;min-width:0;width:calc(var(--weave-sidebar-width) + 4px);overflow:hidden;opacity:1;visibility:visible;transition:width 200ms ease,opacity 160ms ease,visibility 0s}
|
|
13
|
+
.weave-sidebar-dock[data-open=false]{width:0;opacity:0;visibility:hidden;transition:width 200ms ease,opacity 160ms ease,visibility 0s 200ms}
|
|
14
|
+
.weave-sidebar-dock>.weave-sidebar{width:var(--weave-sidebar-width);transition:transform 200ms ease}
|
|
15
|
+
.weave-sidebar-dock[data-open=false]>.weave-sidebar{transform:translateX(-16px)}
|
|
16
|
+
.weave-context-dock[data-open=false]>.weave-sidebar{transform:translateX(16px)}
|
|
17
|
+
.weave-sidebar-heading{display:flex;align-items:center;gap:8px;height:46px;flex:none;padding:0 12px;color:var(--weave-dim);font-size:var(--weave-px-row)}
|
|
18
|
+
.weave-sidebar-heading button{border:0;background:none;padding:7px 6px;border-radius:var(--weave-radius);color:var(--weave-dim)}
|
|
19
|
+
.weave-sidebar-heading button[aria-pressed=true]{color:var(--weave-fg);background:var(--weave-raise)}
|
|
20
|
+
.weave-sidebar-heading button:last-child{margin-left:auto}
|
|
21
|
+
.weave-sidebar .weave-tree-controls{padding:4px 12px 10px;gap:5px;flex-wrap:wrap;border:0}
|
|
22
|
+
.weave-sidebar .weave-filter{height:29px;flex-basis:100%;background:var(--weave-bg);border-radius:var(--weave-radius)}
|
|
23
|
+
.weave-sidebar .weave-row{min-height:32px;border-radius:var(--weave-radius);font-size:var(--weave-px-base)}
|
|
24
|
+
.weave-sidebar [role=tree]{padding:0 8px}
|
|
25
|
+
.weave-sidebar .weave-meta{font-size:var(--weave-px-caption);opacity:.65}
|
|
26
|
+
.weave-sidebar .weave-tree-count{border:0;padding:8px 14px}
|
|
27
|
+
.weave-vault-label{margin-top:auto;padding:14px 16px;border-top:1px solid var(--weave-line);display:flex;flex-direction:column;gap:4px;overflow:hidden}
|
|
28
|
+
.weave-vault-label span{font-size:var(--weave-px-caption);color:var(--weave-faint);text-transform:uppercase;letter-spacing:.08em}
|
|
29
|
+
.weave-vault-label strong{font-size:var(--weave-px-row);font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
30
|
+
.weave-recents{display:flex;flex-direction:column;gap:3px;padding:6px 10px;overflow:auto}
|
|
31
|
+
.weave-recents button{display:flex;align-items:center;gap:8px;width:100%;text-align:left;background:none;border:0;padding:9px;border-radius:var(--weave-radius);overflow:hidden}
|
|
32
|
+
.weave-recents button:hover{background:var(--weave-new)}
|
|
33
|
+
.weave-recents button[aria-current=page]{background:var(--weave-raise);color:var(--weave-fg)}
|
|
34
|
+
.weave-recents .weave-recent-label{min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
35
|
+
.weave-workspace-divider{flex:none;width:4px;background:var(--weave-panel);cursor:col-resize;touch-action:none;z-index:3}
|
|
36
|
+
.weave-workspace-divider:hover,.weave-workspace-divider:focus-visible{background:var(--weave-accent)}
|
|
37
|
+
.weave-workspace-divider[aria-orientation=horizontal]{width:auto;height:4px;cursor:row-resize}
|
|
38
|
+
.weave-panes{display:grid;flex:1;min-width:0;min-height:0;overflow:hidden}
|
|
39
|
+
.weave-pane{min-width:0;min-height:0;display:grid;grid-template-rows:42px 36px minmax(0,1fr);overflow:hidden;background:var(--weave-page)}
|
|
40
|
+
.weave-pane[hidden]{display:none}
|
|
41
|
+
.weave-pane-drop{position:relative}
|
|
42
|
+
.weave-pane-drop::after{content:attr(data-drop-label);position:absolute;inset:4px;z-index:20;display:grid;place-items:center;pointer-events:none;border:2px dashed var(--weave-accent);border-radius:var(--weave-radius);color:var(--weave-accent);background:var(--weave-new)}
|
|
43
|
+
.weave-tabs{display:flex;align-items:stretch;min-width:0;overflow-x:auto;background:var(--weave-panel);gap:3px;padding:5px 6px 0;border-bottom:1px solid var(--weave-line);scrollbar-width:thin}
|
|
44
|
+
.weave-tab{display:flex;align-items:center;min-width:90px;max-width:210px;border-radius:var(--weave-radius) var(--weave-radius) 0 0;color:var(--weave-dim);flex-shrink:0;border-bottom:2px solid transparent}
|
|
45
|
+
.weave-tab-active{background:var(--weave-page);color:var(--weave-fg)}
|
|
46
|
+
.weave-pane-active .weave-tab-active{border-bottom-color:var(--weave-accent)}
|
|
47
|
+
.weave-tab button{border:0;background:none}
|
|
48
|
+
.weave-tab [role=tab]{display:flex;align-items:center;gap:8px;text-align:left;padding:8px 9px;min-width:0;height:100%;font-size:var(--weave-px-row)}
|
|
49
|
+
.weave-tab [role=tab]>span:nth-child(2){white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
50
|
+
.weave-tab [role=tab]>span:first-child{color:var(--weave-faint)}
|
|
51
|
+
.weave-tab .weave-tab-close{margin-right:4px;padding:2px 5px;font-size:var(--weave-px-subhead);color:var(--weave-faint)}
|
|
52
|
+
.weave-tab-close:hover{color:var(--weave-fg);background:var(--weave-raise)}
|
|
53
|
+
.weave-dirty{font-size:var(--weave-px-prov);color:var(--weave-accent)}
|
|
54
|
+
.weave-new-tab{border:0;background:none;padding:4px 10px;font-size:var(--weave-px-display)!important;color:var(--weave-dim)!important}
|
|
55
|
+
.weave-pane-toolbar{display:flex;align-items:center;gap:5px;padding:0 10px;border-bottom:1px solid var(--weave-line);color:var(--weave-dim);font-size:var(--weave-px-row)}
|
|
56
|
+
.weave-pane-toolbar>button{width:26px;height:26px;border:0;background:none;border-radius:var(--weave-radius)}
|
|
57
|
+
.weave-pane-toolbar>button:hover{background:var(--weave-new)}
|
|
58
|
+
.weave-breadcrumb{flex:1;text-align:center;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;padding:0 8px;font-size:var(--weave-px-ui)}
|
|
59
|
+
.weave-pane-menu{position:relative}
|
|
60
|
+
.weave-pane-menu>summary{list-style:none;cursor:pointer;padding:5px 10px;font-size:var(--weave-px-subhead)}
|
|
61
|
+
.weave-pane-menu>summary::-webkit-details-marker{display:none}
|
|
62
|
+
.weave-pane-menu>div{position:absolute;right:0;top:100%;z-index:12;width:185px;padding:5px;display:flex;flex-direction:column;background:var(--weave-panel);border:1px solid var(--weave-line-strong);border-radius:var(--weave-radius)}
|
|
63
|
+
.weave-pane-menu button{text-align:left;border:0;background:none;padding:9px 10px;font-size:var(--weave-px-row)}
|
|
64
|
+
.weave-pane-menu button:hover{background:var(--weave-new)}
|
|
65
|
+
.weave-pane-content{min-width:0;min-height:0;display:flex;overflow:hidden;position:relative}
|
|
66
|
+
.weave-document{display:flex;min-width:0;min-height:0;flex:1}
|
|
67
|
+
.weave-document .weave-note{width:100%;border-left:0}
|
|
68
|
+
.weave-document .weave-note-head{position:static;padding:28px max(24px,calc((100% - 760px)/2)) 18px;border:0}
|
|
69
|
+
.weave-document .weave-note-title{font-size:var(--weave-px-display);letter-spacing:-.03em;margin-bottom:12px}
|
|
70
|
+
.weave-document .weave-note-body{padding:10px max(24px,calc((100% - 760px)/2)) 64px;font-size:var(--weave-px-subhead);line-height:1.8}
|
|
71
|
+
.weave-document .weave-note-body h1{font-size:var(--weave-px-display);line-height:1.4;margin-bottom:18px}
|
|
72
|
+
.weave-document .weave-note-body h2{font-size:var(--weave-px-title);margin-top:28px}
|
|
73
|
+
.weave-document .weave-note-editor{padding:14px 28px;min-height:220px;flex:1}
|
|
74
|
+
.weave-document .weave-note-tags{margin-top:12px}
|
|
75
|
+
.weave-document .weave-note-edit,.weave-document .weave-note-save{padding:5px 10px;border-radius:var(--weave-radius)}
|
|
76
|
+
.weave-welcome{align-self:center;margin:auto;padding:35px;max-width:560px;color:var(--weave-dim);line-height:1.7}
|
|
77
|
+
.weave-welcome-mark{font-size:var(--weave-px-display);color:var(--weave-accent)}
|
|
78
|
+
.weave-welcome h1{font-size:var(--weave-px-display);line-height:1.4;letter-spacing:-.025em;color:var(--weave-fg);font-weight:550}
|
|
79
|
+
.weave-welcome p{font-size:var(--weave-px-base);margin:14px 0 24px}
|
|
80
|
+
.weave-welcome>div{display:flex;gap:12px;flex-wrap:wrap}
|
|
81
|
+
.weave-welcome button{border:1px solid var(--weave-line-strong);background:var(--weave-panel);padding:9px 13px;border-radius:var(--weave-radius);font-size:var(--weave-px-row)}
|
|
82
|
+
.weave-welcome button:hover{border-color:var(--weave-accent)}
|
|
83
|
+
.weave-welcome kbd{color:var(--weave-faint);padding-left:16px}
|
|
84
|
+
.weave-sidebar-context .weave-rail{flex:1;border:0;overflow:auto;padding:6px 12px}
|
|
85
|
+
.weave-sidebar-context .weave-ctx-link{padding-top:7px;padding-bottom:7px}
|
|
86
|
+
.weave-graph-host{position:absolute;z-index:2;display:flex;background:var(--weave-bg)}
|
|
87
|
+
.weave-graph-host>.weave-graph{width:100%;height:100%;flex:1}
|
|
88
|
+
.weave-graph-preview{pointer-events:none;position:absolute;z-index:2;right:8px;bottom:64px;width:min(280px,calc(100% - 16px));box-sizing:border-box;display:flex;flex-direction:column;gap:8px;padding:10px 34px 10px 10px;background:var(--weave-panel);border:1px solid var(--weave-line-strong);border-radius:var(--weave-radius);box-shadow:0 6px 22px #0003}
|
|
89
|
+
.weave-graph-preview-info{display:flex;align-items:center;gap:8px;min-width:0}
|
|
90
|
+
.weave-graph-preview-copy{display:flex;flex-direction:column;min-width:0;gap:2px}
|
|
91
|
+
.weave-graph-preview-kind{font-size:var(--weave-px-caption);color:var(--weave-faint);text-transform:capitalize}
|
|
92
|
+
.weave-graph-preview-title{font-size:var(--weave-px-row);font-weight:550;line-height:1.35;overflow:hidden;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;overflow-wrap:anywhere}
|
|
93
|
+
.weave-graph-preview-close{pointer-events:auto;position:absolute;right:5px;top:5px;width:24px;height:24px;border:0;border-radius:var(--weave-radius);background:transparent;font-size:18px;color:var(--weave-dim)}
|
|
94
|
+
.weave-graph-preview-close:hover{background:var(--weave-new);color:var(--weave-fg)}
|
|
95
|
+
.weave-graph-preview>.weave-chip{pointer-events:auto;align-self:flex-start;max-width:100%;white-space:normal;text-align:left}
|
|
96
|
+
.weave-footer{display:flex;align-items:center;min-width:0;background:var(--weave-panel);border-top:1px solid var(--weave-line);font-size:var(--weave-px-ui);color:var(--weave-faint)}
|
|
97
|
+
.weave-footer>.weave-status{flex:1;min-width:0;border:0}
|
|
98
|
+
.weave-footer>span{padding:4px 10px;white-space:nowrap}
|
|
99
|
+
.weave-footer>button{font:inherit;color:var(--weave-fg);background:none;border:0;padding:7px 12px;cursor:pointer}
|
|
100
|
+
@media(max-width:1049px){
|
|
101
|
+
.weave-context-dock{position:absolute;right:0;top:0;bottom:0;z-index:10;width:var(--weave-sidebar-width)}
|
|
102
|
+
.weave-sidebar-context{border-left:1px solid var(--weave-line-strong)}
|
|
103
|
+
.weave-context-dock>.weave-workspace-divider{display:none}
|
|
104
|
+
}
|
|
105
|
+
@media(max-width:849px){
|
|
106
|
+
.weave-notes-dock{position:absolute;left:44px;top:0;bottom:0;z-index:10;width:var(--weave-sidebar-width);box-shadow:8px 0 24px #0002}
|
|
107
|
+
.weave-sidebar-notes{border-right:1px solid var(--weave-line-strong)}
|
|
108
|
+
.weave-notes-dock>.weave-workspace-divider{display:none}
|
|
109
|
+
.weave-workbench>.weave-workspace-divider{display:none}
|
|
110
|
+
.weave-panes{grid-template-columns:minmax(0,1fr)!important;grid-template-rows:minmax(0,1fr)!important}
|
|
111
|
+
.weave-document .weave-note-head{padding:20px}
|
|
112
|
+
.weave-document .weave-note-body{padding:10px 20px 40px}
|
|
113
|
+
}
|
|
114
|
+
@media(prefers-reduced-motion:reduce){
|
|
115
|
+
.weave-sidebar-dock,.weave-sidebar-dock[data-open=false],.weave-sidebar-dock>.weave-sidebar{transition:none}
|
|
116
|
+
}
|
|
117
|
+
`;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
/** The vault and repository tree column. */
|
|
2
2
|
|
|
3
|
+
import { NOTE_DRAG_TYPE } from "../shell/shell.model";
|
|
3
4
|
import { useLayoutEffect, useRef, useState } from "preact/hooks";
|
|
4
5
|
import { createFolder, deleteFolder, deleteNote, moveNote, renameFolder, renameNote } from "../api";
|
|
5
6
|
import { fetchJson } from "../api.dom";
|
|
@@ -20,7 +21,6 @@ import {
|
|
|
20
21
|
deleteNeedsConfirmation,
|
|
21
22
|
depthVar,
|
|
22
23
|
dropFolder,
|
|
23
|
-
expand,
|
|
24
24
|
initialTreeView,
|
|
25
25
|
newFolderParent,
|
|
26
26
|
newFolderPath,
|
|
@@ -33,6 +33,8 @@ import {
|
|
|
33
33
|
rowCountLabel,
|
|
34
34
|
rowViews,
|
|
35
35
|
rowsFor,
|
|
36
|
+
revealFolder,
|
|
37
|
+
affectedNoteSlugs,
|
|
36
38
|
setQuery,
|
|
37
39
|
toggleExpanded,
|
|
38
40
|
toggleInternals,
|
|
@@ -44,12 +46,12 @@ export interface TreeProps {
|
|
|
44
46
|
graph: GraphPayload | null;
|
|
45
47
|
selectedId: string | null;
|
|
46
48
|
recentIds: ReadonlySet<string>;
|
|
47
|
-
onSelect: (id: string) => void;
|
|
49
|
+
onSelect: (id: string, newTab?: boolean, keepSidebar?: boolean) => void;
|
|
48
50
|
/**
|
|
49
51
|
* Ask before a mutation that would invalidate an open draft. `false` means
|
|
50
52
|
* keep editing: the mutation must not run.
|
|
51
53
|
*/
|
|
52
|
-
onMutate: () => boolean;
|
|
54
|
+
onMutate: (slugs: readonly string[]) => boolean;
|
|
53
55
|
onRefresh: () => void;
|
|
54
56
|
now: number;
|
|
55
57
|
}
|
|
@@ -74,7 +76,7 @@ export function Icon({ name, class: className }: { name: IconName; class?: strin
|
|
|
74
76
|
);
|
|
75
77
|
}
|
|
76
78
|
|
|
77
|
-
function Row({ view, recentIds, edit, onEdit, onRename, onSelect, onToggle, onMenu, onDrop }: { view: ReturnType<typeof rowViews>[number]; recentIds: ReadonlySet<string>; edit: string | null; onEdit: (value: string) => void; onRename: (value: string | null) => void; onSelect: () => void; onToggle: () => void; onMenu: (x: number, y: number) => void; onDrop: (id: string) => void }) {
|
|
79
|
+
function Row({ view, recentIds, edit, onEdit, onRename, onSelect, onToggle, onMenu, onDrop }: { view: ReturnType<typeof rowViews>[number]; recentIds: ReadonlySet<string>; edit: string | null; onEdit: (value: string) => void; onRename: (value: string | null) => void; onSelect: (newTab: boolean) => void; onToggle: () => void; onMenu: (x: number, y: number) => void; onDrop: (id: string) => void }) {
|
|
78
80
|
const folder = dropFolder(view.id);
|
|
79
81
|
return (
|
|
80
82
|
<li
|
|
@@ -88,10 +90,10 @@ function Row({ view, recentIds, edit, onEdit, onRename, onSelect, onToggle, onMe
|
|
|
88
90
|
aria-selected={view.selected}
|
|
89
91
|
aria-expanded={view.hasKids ? view.expanded : undefined}
|
|
90
92
|
style={depthVar(view.depth)}
|
|
91
|
-
onClick={onSelect}
|
|
93
|
+
onClick={(event) => onSelect(event.metaKey || event.ctrlKey)}
|
|
92
94
|
draggable={view.id.startsWith("note:")}
|
|
93
95
|
aria-label={view.id === DRAFT_FOLDER_ID ? "New folder" : undefined}
|
|
94
|
-
onDragStart={(event) => event.dataTransfer?.setData("text/plain", view.id)}
|
|
96
|
+
onDragStart={(event) => { event.dataTransfer?.setData("text/plain", view.id); event.dataTransfer?.setData(NOTE_DRAG_TYPE, view.id); if (event.dataTransfer) event.dataTransfer.effectAllowed = "copyMove"; }}
|
|
95
97
|
onDragOver={folder === undefined ? undefined : (event) => event.preventDefault()}
|
|
96
98
|
onDrop={folder === undefined ? undefined : (event) => { event.preventDefault(); onDrop(event.dataTransfer?.getData("text/plain") ?? ""); }}
|
|
97
99
|
onContextMenu={(event) => { event.preventDefault(); event.stopPropagation(); onMenu(event.clientX, event.clientY); }}
|
|
@@ -161,6 +163,14 @@ export function Tree(props: TreeProps) {
|
|
|
161
163
|
* what makes a mis-click cheap.
|
|
162
164
|
*/
|
|
163
165
|
const [menu, setMenu] = useState<{ id: string; label: string; x: number; y: number; armed?: boolean } | null>(null);
|
|
166
|
+
const root = useRef<HTMLDivElement | null>(null);
|
|
167
|
+
useLayoutEffect(() => {
|
|
168
|
+
if (menu !== null) root.current?.querySelector<HTMLButtonElement>(".weave-menu button")?.focus();
|
|
169
|
+
}, [menu?.id]);
|
|
170
|
+
const closeMenu = (): void => {
|
|
171
|
+
setMenu(null);
|
|
172
|
+
root.current?.querySelector<HTMLButtonElement>(".weave-tree-actions")?.focus();
|
|
173
|
+
};
|
|
164
174
|
const [editing, setEditing] = useState<{ id: string; label: string; value: string } | null>(null);
|
|
165
175
|
/**
|
|
166
176
|
* The parent path a pending "New folder" will be created under, or `null`.
|
|
@@ -190,15 +200,16 @@ export function Tree(props: TreeProps) {
|
|
|
190
200
|
// gesture `rename` already uses, and it shows *where* the folder will
|
|
191
201
|
// land, which a prompt cannot.
|
|
192
202
|
const parent = newFolderParent(target);
|
|
193
|
-
|
|
203
|
+
setState((current) => revealFolder(current, parent));
|
|
194
204
|
setDraft(parent);
|
|
195
205
|
setEditing({ id: DRAFT_FOLDER_ID, label: "", value: "" });
|
|
196
206
|
} else if (action === "rename") {
|
|
207
|
+
setState((current) => revealFolder(current, target.path.split("/").slice(0, -1).join("/")));
|
|
197
208
|
setEditing({ id: menu.id, label: menu.label, value: menu.label });
|
|
198
209
|
} else if (target.type !== "vault") {
|
|
199
210
|
// Before the request: asking after a delete is asking about a file that
|
|
200
211
|
// is already gone, and "keep editing" would strand the draft on a 404.
|
|
201
|
-
if (
|
|
212
|
+
if (!props.onMutate(affectedNoteSlugs(props.graph, target))) return;
|
|
202
213
|
await run(target.type === "note" ? deleteNote(fetchJson, target.path) : deleteFolder(fetchJson, target.path), deletesSelection(props.selectedId, target) ? "vault" : undefined);
|
|
203
214
|
}
|
|
204
215
|
};
|
|
@@ -247,24 +258,32 @@ export function Tree(props: TreeProps) {
|
|
|
247
258
|
// is the least useful moment to ask. Renaming back is one more rename.
|
|
248
259
|
// An open draft is a different question; `onMutate` asks that one.
|
|
249
260
|
if (target !== null && name && name !== current.label) {
|
|
250
|
-
if (
|
|
261
|
+
if (!props.onMutate(affectedNoteSlugs(props.graph, target))) return;
|
|
251
262
|
void run(target.type === "note" ? renameNote(fetchJson, target.path, name) : renameFolder(fetchJson, target.path, name));
|
|
252
263
|
}
|
|
253
264
|
};
|
|
254
265
|
|
|
255
266
|
return (
|
|
256
|
-
<div class="weave-tree" onKeyDown={(event) => {
|
|
267
|
+
<div class="weave-tree" ref={root} onKeyDown={(event) => {
|
|
268
|
+
if (menu !== null && event.key === "Escape") {
|
|
269
|
+
event.preventDefault(); event.stopPropagation(); closeMenu(); return;
|
|
270
|
+
}
|
|
257
271
|
const target = event.target as KeyTarget;
|
|
258
272
|
const next = treeKey(rows, state, props.selectedId, event.key, isTextEntry(target?.tagName ?? null, target?.isContentEditable === true));
|
|
259
273
|
if (!next.handled) return;
|
|
260
274
|
event.preventDefault();
|
|
261
275
|
setState(next.state);
|
|
262
|
-
if (next.selectedId !== null) props.onSelect(next.selectedId);
|
|
276
|
+
if (next.selectedId !== null) props.onSelect(next.selectedId, false, true);
|
|
263
277
|
}}>
|
|
264
278
|
<div class="weave-tree-controls">
|
|
265
279
|
<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))} />
|
|
266
280
|
<button type="button" class="weave-chip" title={provenanceHint(state.provFilter)} onClick={() => setState(cycleProvenance(state))}>◧ {provenanceLabel(state.provFilter)}</button>
|
|
267
281
|
<button type="button" class="weave-chip" title={internalsHint(state.showInternals)} onClick={() => setState(toggleInternals(state))}>◧ {internalsLabel(state.showInternals)}</button>
|
|
282
|
+
<button type="button" class="weave-chip weave-tree-actions" aria-label="File actions" title="File actions for the selected item" onClick={(event) => {
|
|
283
|
+
const id = props.selectedId !== null && mutableTreeRow(props.selectedId) !== null ? props.selectedId : "vault";
|
|
284
|
+
const at = event.currentTarget.getBoundingClientRect();
|
|
285
|
+
setMenu({ id, label: props.graph?.model.nodes.find((node) => node.id === id)?.label ?? "Vault", x: at.left, y: at.bottom });
|
|
286
|
+
}}>···</button>
|
|
268
287
|
</div>
|
|
269
288
|
{empty === null ? (
|
|
270
289
|
<ul
|
|
@@ -278,7 +297,7 @@ export function Tree(props: TreeProps) {
|
|
|
278
297
|
setMenu({ id: "vault", label: "Vault", x: event.clientX, y: event.clientY });
|
|
279
298
|
}}
|
|
280
299
|
>
|
|
281
|
-
{rowViews(rows, props.selectedId, props.now).map((view) => <Row key={view.id} view={view} recentIds={props.recentIds} edit={editing?.id === view.id ? editing.value : null} onEdit={(value) => setEditing((current) => current === null ? null : { ...current, value })} onRename={commitEdit} onSelect={() => { props.onSelect(view.id); if (view.hasKids) setState((current) => toggleExpanded(current, view.id)); }} onToggle={() => setState(toggleExpanded(state, view.id))} onMenu={(x, y) => setMenu({ id: view.id, label: view.label, x, y })} onDrop={(dragged) => { const folder = dropFolder(view.id); if (dragged.startsWith("note:") && folder !== undefined) {
|
|
300
|
+
{rowViews(rows, props.selectedId, props.now).map((view) => <Row key={view.id} view={view} recentIds={props.recentIds} edit={editing?.id === view.id ? editing.value : null} onEdit={(value) => setEditing((current) => current === null ? null : { ...current, value })} onRename={commitEdit} onSelect={(newTab) => { props.onSelect(view.id, newTab); if (view.hasKids) setState((current) => toggleExpanded(current, view.id)); }} onToggle={() => setState(toggleExpanded(state, view.id))} onMenu={(x, y) => setMenu({ id: view.id, label: view.label, x, y })} onDrop={(dragged) => { const folder = dropFolder(view.id); if (dragged.startsWith("note:") && folder !== undefined) { const target = mutableTreeRow(dragged); if (target !== null && !props.onMutate(affectedNoteSlugs(props.graph, target))) return; void run(moveNote(fetchJson, dragged.slice("note:".length), folder)); } }} />)}
|
|
282
301
|
</ul>
|
|
283
302
|
) : <p class="weave-tree-empty">{empty}</p>}
|
|
284
303
|
<p class="weave-tree-count">{rowCountLabel(rows)}</p>
|
|
@@ -286,8 +305,18 @@ export function Tree(props: TreeProps) {
|
|
|
286
305
|
const target = mutableTreeRow(menu.id)!;
|
|
287
306
|
return (
|
|
288
307
|
<>
|
|
289
|
-
<button type="button" class="weave-menu-backdrop" aria-label="Close context menu" onClick={
|
|
290
|
-
<div class="weave-menu" role="menu" style={{ left: `${menu.x}px`, top: `${menu.y}px` }}
|
|
308
|
+
<button type="button" class="weave-menu-backdrop" aria-label="Close context menu" onClick={closeMenu} />
|
|
309
|
+
<div class="weave-menu" role="menu" style={{ left: `${menu.x}px`, top: `${menu.y}px` }} onKeyDown={(event) => {
|
|
310
|
+
if (event.key === "Escape") { event.preventDefault(); closeMenu(); }
|
|
311
|
+
else if (["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
|
|
312
|
+
event.preventDefault();
|
|
313
|
+
const items = [...event.currentTarget.querySelectorAll<HTMLButtonElement>("button")];
|
|
314
|
+
const index = items.indexOf(document.activeElement as HTMLButtonElement);
|
|
315
|
+
const next = event.key === "Home" ? 0 : event.key === "End" ? items.length - 1 : (index + (event.key === "ArrowDown" ? 1 : -1) + items.length) % items.length;
|
|
316
|
+
items[next]?.focus();
|
|
317
|
+
}
|
|
318
|
+
event.stopPropagation();
|
|
319
|
+
}}>
|
|
291
320
|
<button type="button" role="menuitem" onClick={() => void act("newFolder")}>New folder…</button>
|
|
292
321
|
{target.type !== "vault" ? (
|
|
293
322
|
<>
|
|
@@ -74,6 +74,17 @@ export function initialTreeView(roots: readonly string[] = ["vault", "repository
|
|
|
74
74
|
return { expanded: new Set(roots), showInternals: false, provFilter: null, query: "" };
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/** Make an inline file operation visible even when its destination was filtered or collapsed. */
|
|
78
|
+
export function revealFolder(state: TreeViewState, path: string): TreeViewState {
|
|
79
|
+
const expanded = new Set(state.expanded).add("vault");
|
|
80
|
+
let parent = "";
|
|
81
|
+
for (const part of path ? path.split("/") : []) {
|
|
82
|
+
parent = parent ? `${parent}/${part}` : part;
|
|
83
|
+
expanded.add(`vfolder:${parent}`);
|
|
84
|
+
}
|
|
85
|
+
return { ...state, expanded, query: "", provFilter: null };
|
|
86
|
+
}
|
|
87
|
+
|
|
77
88
|
// --- reassembling the wire payload -------------------------------------------------
|
|
78
89
|
|
|
79
90
|
/**
|
|
@@ -224,6 +235,14 @@ export function mutableTreeRow(id: string): { type: "note" | "folder" | "vault";
|
|
|
224
235
|
return null;
|
|
225
236
|
}
|
|
226
237
|
|
|
238
|
+
/** Note slugs a note or folder mutation can invalidate. */
|
|
239
|
+
export function affectedNoteSlugs(graph: GraphPayload | null, target: { type: "note" | "folder" | "vault"; path: string }): string[] {
|
|
240
|
+
if (target.type === "note") return [target.path];
|
|
241
|
+
if (target.type !== "folder" || graph === null) return [];
|
|
242
|
+
const prefix = `note:${target.path}/`;
|
|
243
|
+
return graph.model.nodes.filter((node) => node.kind === "note" && node.id.startsWith(prefix)).map((node) => node.id.slice(5));
|
|
244
|
+
}
|
|
245
|
+
|
|
227
246
|
export function deletesSelection(selectedId: string | null, target: { type: "note" | "folder" | "vault"; path: string }): boolean {
|
|
228
247
|
if (selectedId === null) return false;
|
|
229
248
|
if (target.type === "note") return selectedId === `note:${target.path}`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Fetching and polling for the browser workspace. */
|
|
2
2
|
|
|
3
|
-
import type { GraphPayload } from "../shared/wire";
|
|
3
|
+
import type { GraphPayload, NotePayload } from "../shared/wire";
|
|
4
4
|
import type { ApiResult, FetchLike } from "./api";
|
|
5
5
|
import { fetchGraph, fetchNote } from "./api";
|
|
6
6
|
import type { WorkspaceState } from "./state";
|
|
@@ -115,3 +115,21 @@ export function startWorkspace(opts: WorkspaceOptions): WorkspaceHandle {
|
|
|
115
115
|
},
|
|
116
116
|
};
|
|
117
117
|
}
|
|
118
|
+
|
|
119
|
+
/** Per-document loading: retry failures, and drop completions after a tab leaves. */
|
|
120
|
+
export function watchNote(fetch: FetchLike, slug: string, notify: (result: ApiResult<NotePayload>) => void,
|
|
121
|
+
defer: (fn: () => void, ms: number) => () => void = (fn, ms) => {
|
|
122
|
+
const timer = setTimeout(fn, ms); return () => clearTimeout(timer);
|
|
123
|
+
}): () => void {
|
|
124
|
+
let stopped = false;
|
|
125
|
+
let cancel: (() => void) | null = null;
|
|
126
|
+
const load = async (): Promise<void> => {
|
|
127
|
+
if (stopped) return;
|
|
128
|
+
const result = await fetchNote(fetch, slug);
|
|
129
|
+
if (stopped) return;
|
|
130
|
+
notify(result);
|
|
131
|
+
if (!result.ok) cancel = defer(() => void load(), POLL_MS);
|
|
132
|
+
};
|
|
133
|
+
void load();
|
|
134
|
+
return () => { stopped = true; cancel?.(); };
|
|
135
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { platform as osPlatform } from "node:os";
|
|
2
|
+
import { resolveVaultRoot, WorkspaceCache } from "../../core";
|
|
3
|
+
import {
|
|
4
|
+
startWorkspaceServer,
|
|
5
|
+
type StartWorkspaceServerOptions,
|
|
6
|
+
type WorkspaceServer,
|
|
7
|
+
} from "./server";
|
|
8
|
+
|
|
9
|
+
export type StartServerFn = (opts: StartWorkspaceServerOptions) => Promise<WorkspaceServer>;
|
|
10
|
+
|
|
11
|
+
export interface WorkspaceServerSession {
|
|
12
|
+
server: WorkspaceServer;
|
|
13
|
+
cache: WorkspaceCache;
|
|
14
|
+
cwd: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface WorkspaceServerControllerDeps {
|
|
18
|
+
startServer?: StartServerFn;
|
|
19
|
+
vaultRoot?: () => string;
|
|
20
|
+
onStateChange?: () => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Owns one browser-workspace server for one plugin instance. */
|
|
24
|
+
export class WorkspaceServerController {
|
|
25
|
+
private session: WorkspaceServerSession | null = null;
|
|
26
|
+
private pending: Promise<void> = Promise.resolve();
|
|
27
|
+
|
|
28
|
+
constructor(private readonly deps: WorkspaceServerControllerDeps = {}) {}
|
|
29
|
+
|
|
30
|
+
port(): number | null {
|
|
31
|
+
return this.session?.server.port ?? null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
run(cwd: string): Promise<{ session: WorkspaceServerSession; started: boolean }> {
|
|
35
|
+
return this.enqueue(async () => {
|
|
36
|
+
const existing = this.session;
|
|
37
|
+
if (existing?.cwd === cwd) return { session: existing, started: false };
|
|
38
|
+
if (existing) await this.stop();
|
|
39
|
+
const session = await this.bootOnce(cwd);
|
|
40
|
+
this.session = session;
|
|
41
|
+
this.deps.onStateChange?.();
|
|
42
|
+
return { session, started: true };
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
close(): Promise<void> {
|
|
47
|
+
return this.enqueue(() => this.stop());
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private enqueue<T>(task: () => Promise<T>): Promise<T> {
|
|
51
|
+
const result = this.pending.then(task);
|
|
52
|
+
this.pending = result.then(() => undefined, () => undefined);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private async stop(): Promise<void> {
|
|
57
|
+
const session = this.session;
|
|
58
|
+
if (!session) return;
|
|
59
|
+
this.session = null;
|
|
60
|
+
await session.server.close();
|
|
61
|
+
this.deps.onStateChange?.();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
private async bootOnce(cwd: string): Promise<WorkspaceServerSession> {
|
|
65
|
+
const vaultRoot = (this.deps.vaultRoot ?? resolveVaultRoot)();
|
|
66
|
+
const cache = new WorkspaceCache({ cwd, vaultRoot });
|
|
67
|
+
const server = await (this.deps.startServer ?? startWorkspaceServer)({ cwd, vaultRoot, cache });
|
|
68
|
+
return { server, cache, cwd };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function browserOpenCommand(
|
|
73
|
+
url: string,
|
|
74
|
+
os: NodeJS.Platform = osPlatform(),
|
|
75
|
+
): { command: string; args: string[] } {
|
|
76
|
+
if (os === "darwin") return { command: "open", args: [url] };
|
|
77
|
+
if (os === "win32") return { command: "cmd", args: ["/c", "start", "", url] };
|
|
78
|
+
return { command: "xdg-open", args: [url] };
|
|
79
|
+
}
|
package/src/web/server/routes.ts
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
* `resolveNotePath` (via `getNote`) rejects unsafe slugs.
|
|
44
44
|
*/
|
|
45
45
|
|
|
46
|
-
import { createHash } from "node:crypto";
|
|
46
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
47
47
|
import { readFile } from "node:fs/promises";
|
|
48
48
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
49
49
|
import type { WorkspaceSnapshot } from "../../core/cache/workspace";
|
|
@@ -65,6 +65,8 @@ import type {
|
|
|
65
65
|
} from "../shared/wire";
|
|
66
66
|
import { WIRE_MODEL_OMITTED_KEYS } from "../shared/wire";
|
|
67
67
|
import { renderPage } from "./page";
|
|
68
|
+
import { isWorkspaceViewId, type WorkspaceStateStore } from "./workspace-state";
|
|
69
|
+
import { parseWorkspaceLayout } from "../shared/workspace";
|
|
68
70
|
import type { RequestFacts, SecurityPolicy } from "./security";
|
|
69
71
|
import { requestFacts } from "./security";
|
|
70
72
|
|
|
@@ -78,6 +80,7 @@ export interface RouteDeps {
|
|
|
78
80
|
bundlePath: string;
|
|
79
81
|
/** Test seam for `POST /api/open`; defaults to the real editor shell-out. */
|
|
80
82
|
openNote?: ((slug: string) => Promise<boolean>) | undefined;
|
|
83
|
+
workspaceState: WorkspaceStateStore;
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
const JSON_TYPE = "application/json; charset=utf-8";
|
|
@@ -160,8 +163,9 @@ export function parseTarget(url: string): Target {
|
|
|
160
163
|
* the caller has exactly one failure branch.
|
|
161
164
|
*/
|
|
162
165
|
export const MAX_BODY_BYTES = 64 * 1024;
|
|
166
|
+
const MAX_WORKSPACE_STATE_BODY_BYTES = 256 * 1024;
|
|
163
167
|
|
|
164
|
-
export async function readJsonBody(req: IncomingMessage): Promise<unknown | null> {
|
|
168
|
+
export async function readJsonBody(req: IncomingMessage, maxBytes = MAX_BODY_BYTES): Promise<unknown | null> {
|
|
165
169
|
const chunks: Buffer[] = [];
|
|
166
170
|
let size = 0;
|
|
167
171
|
// `IncomingMessage` yields Buffers unless `setEncoding` was called, and we
|
|
@@ -169,7 +173,7 @@ export async function readJsonBody(req: IncomingMessage): Promise<unknown | null
|
|
|
169
173
|
// pretending to be defensive.
|
|
170
174
|
for await (const chunk of req as AsyncIterable<Buffer>) {
|
|
171
175
|
size += chunk.length;
|
|
172
|
-
if (size >
|
|
176
|
+
if (size > maxBytes) return null;
|
|
173
177
|
chunks.push(chunk);
|
|
174
178
|
}
|
|
175
179
|
if (chunks.length === 0) return null;
|
|
@@ -375,6 +379,10 @@ async function route(
|
|
|
375
379
|
if (method === "GET" && path === "/") return sendShell(deps, res);
|
|
376
380
|
if (method === "GET" && path === "/app.js") return sendBundle(deps, res);
|
|
377
381
|
if (method === "GET" && path === "/api/graph") return sendGraph(deps, req, res);
|
|
382
|
+
if (method === "GET" && path === "/api/workspace-state") {
|
|
383
|
+
return sendJson(res, 200, { viewId: randomUUID(), layout: await deps.workspaceState.readLatest() }, { "cache-control": "no-store" });
|
|
384
|
+
}
|
|
385
|
+
if (method === "POST" && path === "/api/workspace-state") return saveWorkspaceState(deps, req, res);
|
|
378
386
|
if (path.startsWith("/api/folder/")) {
|
|
379
387
|
const handled = await routeFolder(deps, method, path.slice("/api/folder/".length), req, res);
|
|
380
388
|
if (handled) return;
|
|
@@ -396,6 +404,29 @@ async function route(
|
|
|
396
404
|
|
|
397
405
|
// --- handlers ----------------------------------------------------------------
|
|
398
406
|
|
|
407
|
+
async function saveWorkspaceState(deps: RouteDeps, req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
408
|
+
const body = await readJsonBody(req, MAX_WORKSPACE_STATE_BODY_BYTES);
|
|
409
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
410
|
+
sendJson(res, 400, { error: "Expected a workspace state object" });
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const record = body as Record<string, unknown>;
|
|
414
|
+
if (
|
|
415
|
+
Object.keys(record).length !== 2 || !Object.hasOwn(record, "viewId") || !Object.hasOwn(record, "layout") ||
|
|
416
|
+
!isWorkspaceViewId(record.viewId)
|
|
417
|
+
) {
|
|
418
|
+
sendJson(res, 400, { error: "Expected a UUID viewId and a valid layout" });
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
const layout = parseWorkspaceLayout(record.layout);
|
|
422
|
+
if (layout === null) {
|
|
423
|
+
sendJson(res, 400, { error: "Invalid workspace layout" });
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
await deps.workspaceState.write(record.viewId, layout);
|
|
427
|
+
sendJson(res, 200, { ok: true }, { "cache-control": "no-store" });
|
|
428
|
+
}
|
|
429
|
+
|
|
399
430
|
function sendShell(deps: RouteDeps, res: ServerResponse): void {
|
|
400
431
|
const page = renderPage({
|
|
401
432
|
bootstrap: { cwd: deps.cwd },
|
package/src/web/server/server.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { WorkspaceCache } from "../../core/cache/workspace";
|
|
|
19
19
|
import { resolveVaultRoot } from "../../core/paths";
|
|
20
20
|
import { handleRequest, type RouteDeps } from "./routes";
|
|
21
21
|
import { createSecurityPolicy, type SecurityPolicy } from "./security";
|
|
22
|
+
import { createWorkspaceStateStore, defaultWorkspaceStateDir } from "./workspace-state";
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* The committed bundle, resolved relative to this module rather than to
|
|
@@ -41,6 +42,8 @@ export interface StartWorkspaceServerOptions {
|
|
|
41
42
|
cookieName?: string | undefined;
|
|
42
43
|
/** Absolute path to the client bundle. Defaults to the committed one. */
|
|
43
44
|
bundlePath?: string | undefined;
|
|
45
|
+
/** Presentation snapshot directory. Defaults to the user's pi-weave config directory. */
|
|
46
|
+
presentationStateDir?: string | undefined;
|
|
44
47
|
/** Test seam for `POST /api/open`. */
|
|
45
48
|
openNote?: ((slug: string) => Promise<boolean>) | undefined;
|
|
46
49
|
}
|
|
@@ -61,6 +64,7 @@ export interface WorkspaceServer {
|
|
|
61
64
|
export async function startWorkspaceServer(opts: StartWorkspaceServerOptions): Promise<WorkspaceServer> {
|
|
62
65
|
const vaultRoot = opts.vaultRoot ?? resolveVaultRoot();
|
|
63
66
|
const cache = opts.cache ?? new WorkspaceCache({ cwd: opts.cwd, vaultRoot });
|
|
67
|
+
const workspaceState = await createWorkspaceStateStore(opts.cwd, vaultRoot, opts.presentationStateDir ?? defaultWorkspaceStateDir());
|
|
64
68
|
// `deps` is assembled before `listen` because the request handler closes
|
|
65
69
|
// over it, but the security policy needs the bound port — so the policy
|
|
66
70
|
// slot is filled after binding and the handler reads it through the
|
|
@@ -104,6 +108,7 @@ export async function startWorkspaceServer(opts: StartWorkspaceServerOptions): P
|
|
|
104
108
|
security,
|
|
105
109
|
bundlePath: opts.bundlePath ?? defaultBundlePath(),
|
|
106
110
|
...(opts.openNote !== undefined ? { openNote: opts.openNote } : {}),
|
|
111
|
+
workspaceState,
|
|
107
112
|
};
|
|
108
113
|
|
|
109
114
|
return {
|