pi-weave 0.3.3 → 0.3.5
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 +10 -42
- package/package.json +1 -1
- package/src/web/client/api.ts +7 -0
- package/src/web/client/dist/app.js +60 -46
- package/src/web/client/shell/Settings.tsx +3 -2
- package/src/web/client/shell/Shell.tsx +31 -5
- package/src/web/client/shell/workspace.css.ts +16 -2
- package/src/web/server/routes.ts +12 -1
|
@@ -18,6 +18,7 @@ export function Settings(props: {
|
|
|
18
18
|
onChange: (layout: WorkspaceLayout) => void; onRefresh: () => void; onClose: () => void;
|
|
19
19
|
}) {
|
|
20
20
|
const [category, setCategory] = useState<typeof CATEGORIES[number]>("General");
|
|
21
|
+
const [refreshCount, setRefreshCount] = useState(0);
|
|
21
22
|
const trap = useFocusTrap();
|
|
22
23
|
const p = props.layout.preferences;
|
|
23
24
|
const update = (values: Partial<Preferences>): void => props.onChange({ ...props.layout, preferences: { ...p, ...values } });
|
|
@@ -30,8 +31,8 @@ export function Settings(props: {
|
|
|
30
31
|
<section aria-label={`${category} settings`}><h3>{category}</h3>
|
|
31
32
|
{category === "General" ? <>
|
|
32
33
|
<p>Pi Weave {props.info.version}</p>
|
|
33
|
-
<dl class="weave-settings-locations"><dt>Vault</dt><dd>{props.info.vaultRoot || "Loading…"}</dd><dt>Repository</dt><dd>{props.cwd}</dd></dl>
|
|
34
|
-
<div class="weave-settings-actions"><button type="button" onClick={props.onRefresh}>Refresh workspace</button><a href="https://github.com/EranYonai/pi-weave#readme" target="_blank" rel="noreferrer">Documentation ↗</a></div>
|
|
34
|
+
<dl class="weave-settings-locations"><dt>Vault</dt><dd><code>{props.info.vaultRoot || "Loading…"}</code></dd><dt>Repository</dt><dd><code>{props.cwd}</code></dd></dl>
|
|
35
|
+
<div class="weave-settings-actions"><button type="button" onClick={() => { setRefreshCount((value) => value + 1); props.onRefresh(); }}><span key={refreshCount} class={refreshCount ? "weave-refresh-spin" : ""} aria-hidden="true">↻</span> Refresh workspace</button><a href="https://github.com/EranYonai/pi-weave#readme" target="_blank" rel="noreferrer">Documentation ↗</a></div>
|
|
35
36
|
<details><summary tabIndex={0}>Keyboard shortcuts</summary>{keyHelp(props.shortcut.slice(0, -1)).map((group) => <div key={group.title}><h4>{group.title}</h4>{group.entries.map((entry) => <div class="weave-key-row" key={entry.combo}><kbd>{entry.combo}</kbd><span>{entry.what}</span></div>)}</div>)}</details>
|
|
36
37
|
</> : null}
|
|
37
38
|
{category === "Appearance" ? <>
|
|
@@ -4,7 +4,7 @@ import type { GraphPayload, NotePayload } from "../../shared/wire";
|
|
|
4
4
|
import { activePane, activeTab, activateTab, closePane, closeTab, focusDocument, initialLayout, reorderTab, moveTab, navigateTab, openDocument, openWithPreferences, restoreWorkspace, openGraph, parseWorkspaceLayout, splitPane, tabDropSource, tabSelection } from "../../shared/workspace";
|
|
5
5
|
import type { Pane, WorkspaceLayout, WorkspaceTab } from "../../shared/workspace";
|
|
6
6
|
import { fetchJson } from "../api.dom";
|
|
7
|
-
import { openNote, saveNote } from "../api";
|
|
7
|
+
import { createNote, fetchGraph, openNote, saveNote } from "../api";
|
|
8
8
|
import { Graph } from "../graph/Graph";
|
|
9
9
|
import { schemeOf, watchScheme } from "../graph/scheme";
|
|
10
10
|
import { createSigmaRenderer } from "../graph/renderer.dom";
|
|
@@ -51,13 +51,18 @@ function RecentList(props: { visits: readonly string[]; graph: GraphPayload | nu
|
|
|
51
51
|
function DocumentView(props: {
|
|
52
52
|
preferences: Preferences; tab: WorkspaceTab; graph: GraphPayload | null; drafts: DraftStore; revision: number;
|
|
53
53
|
onSelect: (id: string, newTab?: boolean) => void; onSave: (slug: string, body: string) => Promise<boolean>;
|
|
54
|
-
now: number; onScroll: (value: number) => void; onSearch: () => void; onGraph: () => void;
|
|
54
|
+
now: number; onScroll: (value: number) => void; onSearch: () => void; onGraph: () => void; onCreate: (title: string) => Promise<boolean>;
|
|
55
55
|
}) {
|
|
56
56
|
const id = tabSelection(props.tab);
|
|
57
57
|
const slug = id?.startsWith("note:") ? id.slice(5) : null;
|
|
58
58
|
const [loaded, setLoaded] = useState<{ slug: string; note: NotePayload | null; failed: boolean; version: number } | null>(null);
|
|
59
59
|
const element = useRef<HTMLDivElement | null>(null);
|
|
60
60
|
const savedScroll = useRef(props.tab.scroll);
|
|
61
|
+
const [newNote, setNewNote] = useState(false);
|
|
62
|
+
const [title, setTitle] = useState("");
|
|
63
|
+
const [creating, setCreating] = useState(false);
|
|
64
|
+
const titleInput = useRef<HTMLInputElement | null>(null);
|
|
65
|
+
useLayoutEffect(() => { if (newNote) titleInput.current?.focus(); }, [newNote]);
|
|
61
66
|
useEffect(() => {
|
|
62
67
|
if (slug === null) return;
|
|
63
68
|
const version = props.drafts.nextLoadVersion();
|
|
@@ -69,11 +74,22 @@ function DocumentView(props: {
|
|
|
69
74
|
useLayoutEffect(() => {
|
|
70
75
|
const note = element.current?.querySelector<HTMLElement>(".weave-note");
|
|
71
76
|
if (note !== null && note !== undefined) note.scrollTop = savedScroll.current;
|
|
77
|
+
if (slug !== null && props.drafts.get(slug) !== null) element.current?.querySelector<HTMLTextAreaElement>(".weave-note-editor")?.focus();
|
|
72
78
|
}, [id, payload?.note?.note.slug]);
|
|
73
79
|
if (id === null) return <div class="weave-welcome">
|
|
74
80
|
<span class="weave-welcome-mark" aria-hidden="true">✳</span><h1>Your knowledge, connected.</h1>
|
|
75
|
-
<p>
|
|
76
|
-
<div><button type="button" onClick={props.onSearch}>Search workspace <kbd>⌘K</kbd></button><button type="button" onClick={props.onGraph}>Explore graph</button></div>
|
|
81
|
+
<p>Ask your agent to write, organize, and connect your notes. Browse that knowledge here, or start a note yourself.</p>
|
|
82
|
+
<div><button type="button" onClick={props.onSearch}>Search workspace <kbd>⌘K</kbd></button><button type="button" onClick={props.onGraph}>Explore graph</button><button type="button" onClick={() => setNewNote(true)}>New note</button></div>
|
|
83
|
+
{newNote ? <form class="weave-new-note" onSubmit={(event) => {
|
|
84
|
+
event.preventDefault();
|
|
85
|
+
if (creating || !title.trim()) return;
|
|
86
|
+
setCreating(true);
|
|
87
|
+
void props.onCreate(title.trim()).finally(() => setCreating(false));
|
|
88
|
+
}} onKeyDown={(event) => { if (event.key === "Escape") { event.stopPropagation(); if (!creating) setNewNote(false); } }}>
|
|
89
|
+
<label for={`new-note-${props.tab.id}`}>Note title</label>
|
|
90
|
+
<input id={`new-note-${props.tab.id}`} ref={titleInput} required value={title} disabled={creating} onInput={(event) => setTitle(event.currentTarget.value)} />
|
|
91
|
+
<div><button type="submit" disabled={creating || !title.trim()}>{creating ? "Creating…" : "Create note"}</button><button type="button" disabled={creating} onClick={() => setNewNote(false)}>Cancel</button></div>
|
|
92
|
+
</form> : null}
|
|
77
93
|
</div>;
|
|
78
94
|
const missing = props.graph !== null && !props.graph.model.nodes.some((node) => node.id === id);
|
|
79
95
|
if (missing || (payload?.failed && props.drafts.isDirty(slug ?? ""))) {
|
|
@@ -186,6 +202,16 @@ export function Shell(props: ShellProps) {
|
|
|
186
202
|
if (!result.ok) { window.alert(result.message); return false; }
|
|
187
203
|
setRevision((value) => value + 1); workspace.current?.refresh(); return true;
|
|
188
204
|
};
|
|
205
|
+
const create = async (title: string, paneId: string): Promise<boolean> => {
|
|
206
|
+
const result = await createNote(fetchJson, title);
|
|
207
|
+
if (!result.ok) { window.alert(result.message); return false; }
|
|
208
|
+
const graph = await fetchGraph(fetchJson);
|
|
209
|
+
setData((current) => ({ ...current, graph: graph.ok ? graph.data : null, graphFailed: !graph.ok }));
|
|
210
|
+
drafts.open(result.data.note.slug, result.data.note.body);
|
|
211
|
+
change(openDocument(live.current.layout, `note:${result.data.note.slug}`, { paneId }));
|
|
212
|
+
workspace.current?.refresh();
|
|
213
|
+
return true;
|
|
214
|
+
};
|
|
189
215
|
useEffect(() => drafts.subscribe(() => setDraftRevision((value) => value + 1)), [drafts]);
|
|
190
216
|
useEffect(() => {
|
|
191
217
|
workspace.current = startWorkspace({ fetch: fetchJson, state: initialWorkspaceState(), setState: setData });
|
|
@@ -399,7 +425,7 @@ export function Shell(props: ShellProps) {
|
|
|
399
425
|
<div class="weave-pane-content" role="tabpanel" tabIndex={-1} id={`panel-${group.id}`} aria-labelledby={`tab-${current.id}`} {...(current.kind === "graph" ? { ref: graphSlot } : {})}>
|
|
400
426
|
{current.kind === "document" ? <DocumentView key={`${current.id}:${current.cursor}:${tabSelection(current)}`} preferences={layout.preferences} tab={current} graph={data.graph} drafts={drafts} revision={revision} now={now}
|
|
401
427
|
onSelect={(id, newTab) => select(id, newTab, group.id)} onSave={save} onScroll={(value) => rememberScroll(current.id, value)}
|
|
402
|
-
onSearch={() => setOverlay("search")} onGraph={showGraph} /> : null}
|
|
428
|
+
onSearch={() => setOverlay("search")} onGraph={showGraph} onCreate={(title) => create(title, group.id)} /> : null}
|
|
403
429
|
</div>
|
|
404
430
|
</section>;
|
|
405
431
|
};
|
|
@@ -74,7 +74,9 @@ export const WORKSPACE_CSS = `
|
|
|
74
74
|
.weave-document .weave-note-body h2{font-size:var(--weave-px-title);margin-top:28px}
|
|
75
75
|
.weave-document .weave-note-editor{padding:14px max(24px,calc((100% - var(--weave-reading-width,760px))/2));min-height:220px;flex:1;font-size:var(--weave-note-size,var(--weave-px-subhead))}
|
|
76
76
|
.weave-document .weave-note-tags{margin-top:12px}
|
|
77
|
-
.weave-document .weave-note-edit,.weave-document .weave-note-save{padding:5px 10px;border-radius:var(--weave-radius)}
|
|
77
|
+
.weave-document .weave-note-edit,.weave-document .weave-note-save{padding:5px 10px;border-radius:var(--weave-radius);font-size:var(--weave-px-caption)}
|
|
78
|
+
.weave-document .weave-note-edit{color:var(--weave-dim);background:transparent;border-color:transparent}
|
|
79
|
+
.weave-document .weave-note-edit:hover{color:var(--weave-accent);background:var(--weave-new);border-color:transparent}
|
|
78
80
|
.weave-welcome{align-self:center;margin:auto;padding:35px;max-width:560px;color:var(--weave-dim);line-height:1.7}
|
|
79
81
|
.weave-welcome-mark{font-size:var(--weave-px-display);color:var(--weave-accent)}
|
|
80
82
|
.weave-welcome h1{font-size:var(--weave-px-display);line-height:1.4;letter-spacing:-.025em;color:var(--weave-fg);font-weight:550}
|
|
@@ -83,6 +85,10 @@ export const WORKSPACE_CSS = `
|
|
|
83
85
|
.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)}
|
|
84
86
|
.weave-welcome button:hover{border-color:var(--weave-accent)}
|
|
85
87
|
.weave-welcome kbd{color:var(--weave-faint);padding-left:16px}
|
|
88
|
+
.weave-new-note{display:flex;flex-direction:column;gap:10px;margin-top:24px}
|
|
89
|
+
.weave-new-note input{font:inherit;color:var(--weave-fg);background:var(--weave-panel);border:1px solid var(--weave-line-strong);border-radius:var(--weave-radius);padding:9px 13px;min-width:0}
|
|
90
|
+
.weave-new-note input:focus-visible{outline:2px solid var(--weave-accent);outline-offset:2px}
|
|
91
|
+
.weave-new-note>div{display:flex;gap:12px}
|
|
86
92
|
.weave-sidebar-context .weave-rail{flex:1;border:0;overflow:auto;padding:6px 12px}
|
|
87
93
|
.weave-sidebar-context .weave-ctx-link{padding-top:7px;padding-bottom:7px}
|
|
88
94
|
.weave-graph-host{position:absolute;z-index:2;display:flex;background:var(--weave-bg)}
|
|
@@ -134,7 +140,15 @@ export const WORKSPACE_CSS = `
|
|
|
134
140
|
.weave-setting-range{display:flex;align-items:center;gap:12px;width:190px}.weave-setting-range input{width:130px;min-width:0}.weave-setting-range output{min-width:42px;text-align:right;color:var(--weave-accent);font-variant-numeric:tabular-nums}
|
|
135
141
|
.weave-settings button:focus-visible,.weave-settings a:focus-visible,.weave-settings summary:focus-visible{outline:2px solid var(--weave-accent);outline-offset:2px}
|
|
136
142
|
.weave-settings-actions{display:flex;gap:16px;align-items:center;flex-wrap:wrap;margin:24px 0}.weave-settings-actions button,.weave-settings-download,.weave-settings-forces>button{display:inline-block;padding:9px 14px;border:1px solid var(--weave-line-strong);border-radius:6px;color:var(--weave-accent);background:var(--weave-raise);text-decoration:none}
|
|
137
|
-
.weave-settings-
|
|
143
|
+
.weave-settings-actions button{transition:transform 100ms ease,background 100ms ease}
|
|
144
|
+
.weave-settings-actions button:hover{background:var(--weave-new);border-color:var(--weave-accent)}
|
|
145
|
+
.weave-settings-actions button:active{transform:scale(.97)}
|
|
146
|
+
.weave-settings-actions button>span{display:inline-block}
|
|
147
|
+
.weave-refresh-spin{animation:weave-refresh-spin 500ms ease}
|
|
148
|
+
@keyframes weave-refresh-spin{to{transform:rotate(360deg)}}
|
|
149
|
+
.weave-settings-forces>button{margin-top:20px}.weave-settings-forces>button:disabled{opacity:.5}.weave-settings-locations dd{margin:6px 0 18px;overflow-wrap:anywhere;color:var(--weave-dim);font-family:var(--weave-mono)}
|
|
150
|
+
.weave-settings-locations code{display:block;padding:10px 12px;background:var(--weave-bg);border:1px solid var(--weave-line);border-radius:var(--weave-radius);font-family:var(--weave-mono);white-space:pre-wrap}
|
|
151
|
+
@media(prefers-reduced-motion:reduce){.weave-settings-actions button{transition:none}.weave-settings-actions button:active{transform:none}.weave-refresh-spin{animation:none}}
|
|
138
152
|
.weave-settings details summary{cursor:pointer;padding:12px 0}.weave-settings .weave-key-row{gap:16px;align-items:baseline}.weave-settings .weave-key-row kbd{min-width:150px;color:var(--weave-accent)}
|
|
139
153
|
@media(max-width:650px){.weave-settings{grid-template-columns:1fr;grid-template-rows:auto auto minmax(0,1fr)}.weave-settings nav{border-right:0;border-bottom:1px solid var(--weave-line);padding:8px 16px}.weave-settings nav select{width:100%;max-width:none}.weave-settings section{padding:20px}.weave-setting-row{align-items:flex-start;gap:12px}.weave-settings select{max-width:170px}.weave-setting-range{width:140px}.weave-setting-range input{width:90px}}
|
|
140
154
|
`;
|
package/src/web/server/routes.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* | GET | `/api/graph` | {@link GraphPayload}, ETag'd on `stamp` |
|
|
9
9
|
* | GET | `/api/note/:slug` | {@link NotePayload} |
|
|
10
10
|
* | POST | `/api/note/:slug` | save a note's Markdown body |
|
|
11
|
+
* | POST | `/api/notes` | create an empty human-authored vault note |
|
|
11
12
|
* | POST | `/api/note/:slug/rename` | rename a vault note |
|
|
12
13
|
* | POST | `/api/note/:slug/move` | move a vault note |
|
|
13
14
|
* | DELETE | `/api/note/:slug` | delete a vault note |
|
|
@@ -56,7 +57,7 @@ import type { GraphModel as CoreGraphModel } from "../../core/graph/model";
|
|
|
56
57
|
import { openNoteInEditor } from "../../core/openInEditor";
|
|
57
58
|
import type { Note } from "../../core/types";
|
|
58
59
|
import type { VaultMutationResult } from "../../core/vault";
|
|
59
|
-
import { createFolder, deleteFolder, deleteNote, getNote, moveNote, renameFolder, renameNote, resolveHtmlPath, searchNotes, setNoteBody } from "../../core/vault";
|
|
60
|
+
import { addNote, createFolder, deleteFolder, deleteNote, getNote, moveNote, renameFolder, renameNote, resolveHtmlPath, searchNotes, setNoteBody } from "../../core/vault";
|
|
60
61
|
import { deriveTagIndex, type TaggedNote } from "../../core/view/links";
|
|
61
62
|
import type {
|
|
62
63
|
GraphPayload,
|
|
@@ -394,6 +395,16 @@ async function route(
|
|
|
394
395
|
return sendJson(res, 200, { viewId: randomUUID(), layout: await deps.workspaceState.readLatest(), info: { vaultRoot: deps.vaultRoot, version: (JSON.parse(await readFile(new URL("../../../package.json", import.meta.url), "utf8")) as { version: string }).version } }, { "cache-control": "no-store" });
|
|
395
396
|
}
|
|
396
397
|
if (method === "POST" && path === "/api/workspace-state") return saveWorkspaceState(deps, req, res);
|
|
398
|
+
if (method === "POST" && path === "/api/notes") {
|
|
399
|
+
const body = await readJsonBody(req);
|
|
400
|
+
const title = body !== null && typeof body === "object" && !Array.isArray(body) ? (body as { title?: unknown }).title : null;
|
|
401
|
+
if (typeof title !== "string" || !title.trim() || /[\r\n]/.test(title)) {
|
|
402
|
+
sendJson(res, 400, { error: "expected a non-empty, single-line title" });
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
const note = await addNote(deps.vaultRoot, { title: title.trim(), body: "", source: "human" });
|
|
406
|
+
return sendJson(res, 201, notePayload(note), { "cache-control": "no-store" });
|
|
407
|
+
}
|
|
397
408
|
if (path.startsWith("/api/folder/")) {
|
|
398
409
|
const handled = await routeFolder(deps, method, path.slice("/api/folder/".length), req, res);
|
|
399
410
|
if (handled) return;
|