pi-weave 0.3.12 → 0.3.14
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 +26 -8
- package/package.json +6 -3
- package/src/claude/cli.ts +13 -0
- package/src/claude/control.ts +84 -0
- package/src/claude/generate.ts +62 -0
- package/src/claude/index.ts +87 -0
- package/src/claude/mcp.ts +105 -0
- package/src/claude/server.ts +154 -0
- package/src/claude/sessions.ts +158 -0
- package/src/core/index.ts +3 -0
- package/src/core/sessionDigest.ts +103 -0
- package/src/core/tools.ts +63 -0
- package/src/core/vault.ts +79 -5
- package/src/core/view/tree.ts +2 -2
- package/src/opencode/commands.ts +4 -71
- package/src/opencode/index.ts +1 -1
- package/src/opencode/v1.ts +1 -1
- package/src/pi/tools/noteTool.ts +10 -22
- package/src/pi/tools/parameters.ts +10 -0
- package/src/pi/tools/repoTool.ts +8 -9
- package/src/web/client/api.ts +4 -0
- package/src/web/client/dist/app.js +48 -48
- package/src/web/client/shell/shell.model.ts +3 -2
- package/src/web/client/tree/Tree.tsx +8 -6
- package/src/web/client/tree/tree.model.ts +22 -7
- package/src/web/server/routes.ts +25 -3
- package/src/opencode/tools.ts +0 -43
|
@@ -21,9 +21,10 @@ import { kindIcon } from "../tree/tree.model";
|
|
|
21
21
|
import type { IconName } from "./icons.model";
|
|
22
22
|
export const NOTE_DRAG_TYPE = "application/x-weave-note";
|
|
23
23
|
|
|
24
|
-
/** Pane drops open only note IDs that still exist in this workspace. */
|
|
24
|
+
/** Pane drops open only note and HTML artifact IDs that still exist in this workspace. */
|
|
25
25
|
export function noteDropId(graph: GraphPayload | null, id: string): string | null {
|
|
26
|
-
|
|
26
|
+
const kind = id.startsWith("note:") ? "note" : id.startsWith("artifact:") ? "file" : null;
|
|
27
|
+
return kind !== null && graph?.model.nodes.some((node) => node.id === id && node.kind === kind) ? id : null;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
export type ColumnId = "tree" | "note" | "graph";
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import { NOTE_DRAG_TYPE } from "../shell/shell.model";
|
|
4
4
|
import { useLayoutEffect, useRef, useState } from "preact/hooks";
|
|
5
|
-
import { createFolder, deleteFolder, deleteNote, moveNote, renameFolder, renameNote } from "../api";
|
|
5
|
+
import { createFolder, deleteArtifact, deleteFolder, deleteNote, moveArtifact, moveNote, renameArtifact, renameFolder, renameNote } from "../api";
|
|
6
6
|
import { fetchJson } from "../api.dom";
|
|
7
7
|
import { isTextEntry, type KeyTarget } from "../shell/keys.model";
|
|
8
8
|
import { ICON_BOX, ICON_STROKE, ICONS } from "../shell/icons.model";
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
deleteNeedsConfirmation,
|
|
22
22
|
depthVar,
|
|
23
23
|
dropFolder,
|
|
24
|
+
movableTreeRow,
|
|
24
25
|
initialTreeView,
|
|
25
26
|
newFolderParent,
|
|
26
27
|
newFolderPath,
|
|
@@ -41,6 +42,7 @@ import {
|
|
|
41
42
|
treeEmptyMessage,
|
|
42
43
|
treeKey,
|
|
43
44
|
} from "./tree.model";
|
|
45
|
+
import type { TreeTarget } from "./tree.model";
|
|
44
46
|
|
|
45
47
|
export interface TreeProps {
|
|
46
48
|
graph: GraphPayload | null;
|
|
@@ -91,7 +93,7 @@ function Row({ view, recentIds, edit, onEdit, onRename, onSelect, onToggle, onMe
|
|
|
91
93
|
aria-expanded={view.hasKids ? view.expanded : undefined}
|
|
92
94
|
style={depthVar(view.depth)}
|
|
93
95
|
onClick={(event) => onSelect(event.metaKey || event.ctrlKey)}
|
|
94
|
-
draggable={view.id
|
|
96
|
+
draggable={movableTreeRow(view.id) !== null}
|
|
95
97
|
aria-label={view.id === DRAFT_FOLDER_ID ? "New folder" : undefined}
|
|
96
98
|
onDragStart={(event) => { event.dataTransfer?.setData("text/plain", view.id); event.dataTransfer?.setData(NOTE_DRAG_TYPE, view.id); if (event.dataTransfer) event.dataTransfer.effectAllowed = "copyMove"; }}
|
|
97
99
|
onDragOver={folder === undefined ? undefined : (event) => event.preventDefault()}
|
|
@@ -210,7 +212,7 @@ export function Tree(props: TreeProps) {
|
|
|
210
212
|
// Before the request: asking after a delete is asking about a file that
|
|
211
213
|
// is already gone, and "keep editing" would strand the draft on a 404.
|
|
212
214
|
if (!props.onMutate(affectedNoteSlugs(props.graph, target))) return;
|
|
213
|
-
await run(target.type === "note" ? deleteNote(fetchJson, target.path) : deleteFolder(fetchJson, target.path), deletesSelection(props.selectedId, target) ? "vault" : undefined);
|
|
215
|
+
await run(target.type === "note" ? deleteNote(fetchJson, target.path) : target.type === "artifact" ? deleteArtifact(fetchJson, target.path) : deleteFolder(fetchJson, target.path), deletesSelection(props.selectedId, target) ? "vault" : undefined);
|
|
214
216
|
}
|
|
215
217
|
};
|
|
216
218
|
|
|
@@ -222,7 +224,7 @@ export function Tree(props: TreeProps) {
|
|
|
222
224
|
* the row shows no count for, so it takes two — and the second click's label
|
|
223
225
|
* names the folder, which is the chance to notice the wrong row was hit.
|
|
224
226
|
*/
|
|
225
|
-
const clickDelete = (target:
|
|
227
|
+
const clickDelete = (target: Pick<TreeTarget, "type">): void => {
|
|
226
228
|
if (menu === null) return;
|
|
227
229
|
if (deleteNeedsConfirmation(target) && menu.armed !== true) {
|
|
228
230
|
setMenu({ ...menu, armed: true });
|
|
@@ -259,7 +261,7 @@ export function Tree(props: TreeProps) {
|
|
|
259
261
|
// An open draft is a different question; `onMutate` asks that one.
|
|
260
262
|
if (target !== null && name && name !== current.label) {
|
|
261
263
|
if (!props.onMutate(affectedNoteSlugs(props.graph, target))) return;
|
|
262
|
-
void run(target.type === "note" ? renameNote(fetchJson, target.path, name) : renameFolder(fetchJson, target.path, name));
|
|
264
|
+
void run(target.type === "note" ? renameNote(fetchJson, target.path, name) : target.type === "artifact" ? renameArtifact(fetchJson, target.path, name) : renameFolder(fetchJson, target.path, name));
|
|
263
265
|
}
|
|
264
266
|
};
|
|
265
267
|
|
|
@@ -297,7 +299,7 @@ export function Tree(props: TreeProps) {
|
|
|
297
299
|
setMenu({ id: "vault", label: "Vault", x: event.clientX, y: event.clientY });
|
|
298
300
|
}}
|
|
299
301
|
>
|
|
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);
|
|
302
|
+
{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); const moving = movableTreeRow(dragged); if (moving === null || folder === undefined) return; if (moving.type === "artifact") { void run(moveArtifact(fetchJson, moving.slug, folder)); return; } const target = mutableTreeRow(dragged); if (target !== null && !props.onMutate(affectedNoteSlugs(props.graph, target))) return; void run(moveNote(fetchJson, moving.slug, folder)); }} />)}
|
|
301
303
|
</ul>
|
|
302
304
|
) : <p class="weave-tree-empty">{empty}</p>}
|
|
303
305
|
<p class="weave-tree-count">{rowCountLabel(rows)}</p>
|
|
@@ -131,7 +131,7 @@ export const DRAFT_FOLDER_ID = "vfolder:\u0000draft";
|
|
|
131
131
|
* it; on a *note*, a sibling of that note — which is what "new folder here"
|
|
132
132
|
* means when "here" is a file. Returns the parent path, `""` for the root.
|
|
133
133
|
*/
|
|
134
|
-
export function newFolderParent(target:
|
|
134
|
+
export function newFolderParent(target: TreeTarget): string {
|
|
135
135
|
if (target.type === "vault") return "";
|
|
136
136
|
if (target.type === "folder") return target.path;
|
|
137
137
|
return target.path.split("/").slice(0, -1).join("/");
|
|
@@ -225,25 +225,40 @@ export function dropFolder(id: string): string | null | undefined {
|
|
|
225
225
|
return id.startsWith("vfolder:") ? id.slice("vfolder:".length) : undefined;
|
|
226
226
|
}
|
|
227
227
|
|
|
228
|
-
|
|
228
|
+
/** A row the tree can drag into a folder: a vault note or a vault HTML artifact. */
|
|
229
|
+
export function movableTreeRow(id: string): { type: "note" | "artifact"; slug: string } | null {
|
|
230
|
+
if (id.startsWith("note:")) return { type: "note", slug: id.slice("note:".length) };
|
|
231
|
+
if (id.startsWith("artifact:")) return { type: "artifact", slug: id.slice("artifact:".length) };
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** A tree row the file actions (rename, delete, new folder) can act on. */
|
|
236
|
+
export interface TreeTarget {
|
|
237
|
+
type: "note" | "artifact" | "folder" | "vault";
|
|
238
|
+
path: string;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function mutableTreeRow(id: string): TreeTarget | null {
|
|
229
242
|
if (id === "vault") return { type: "vault", path: "" };
|
|
230
243
|
if (id.startsWith("note:")) return { type: "note", path: id.slice("note:".length) };
|
|
244
|
+
if (id.startsWith("artifact:")) return { type: "artifact", path: id.slice("artifact:".length) };
|
|
231
245
|
if (id.startsWith("vfolder:")) return { type: "folder", path: id.slice("vfolder:".length) };
|
|
232
246
|
return null;
|
|
233
247
|
}
|
|
234
248
|
|
|
235
249
|
/** Note slugs a note or folder mutation can invalidate. */
|
|
236
|
-
export function affectedNoteSlugs(graph: GraphPayload | null, target:
|
|
250
|
+
export function affectedNoteSlugs(graph: GraphPayload | null, target: TreeTarget): string[] {
|
|
237
251
|
if (target.type === "note") return [target.path];
|
|
238
252
|
if (target.type !== "folder" || graph === null) return [];
|
|
239
253
|
const prefix = `note:${target.path}/`;
|
|
240
254
|
return graph.model.nodes.filter((node) => node.kind === "note" && node.id.startsWith(prefix)).map((node) => node.id.slice(5));
|
|
241
255
|
}
|
|
242
256
|
|
|
243
|
-
export function deletesSelection(selectedId: string | null, target:
|
|
257
|
+
export function deletesSelection(selectedId: string | null, target: TreeTarget): boolean {
|
|
244
258
|
if (selectedId === null) return false;
|
|
245
259
|
if (target.type === "note") return selectedId === `note:${target.path}`;
|
|
246
|
-
if (target.type === "
|
|
260
|
+
if (target.type === "artifact") return selectedId === `artifact:${target.path}`;
|
|
261
|
+
if (target.type === "folder") return selectedId === `vfolder:${target.path}` || ["vfolder:", "note:", "artifact:"].some((kind) => selectedId.startsWith(`${kind}${target.path}/`));
|
|
247
262
|
return false;
|
|
248
263
|
}
|
|
249
264
|
|
|
@@ -260,7 +275,7 @@ export function deletesSelection(selectedId: string | null, target: { type: "not
|
|
|
260
275
|
* The confirmation is {@link deleteItemLabel}: the same menu item, clicked
|
|
261
276
|
* twice, with the second click's label saying exactly what it will destroy.
|
|
262
277
|
*/
|
|
263
|
-
export function deleteNeedsConfirmation(target:
|
|
278
|
+
export function deleteNeedsConfirmation(target: Pick<TreeTarget, "type">): boolean {
|
|
264
279
|
return target.type === "folder";
|
|
265
280
|
}
|
|
266
281
|
|
|
@@ -274,7 +289,7 @@ export function deleteNeedsConfirmation(target: { type: "note" | "folder" | "vau
|
|
|
274
289
|
* safety mechanism — it is the user's chance to notice they right-clicked the
|
|
275
290
|
* wrong row.
|
|
276
291
|
*/
|
|
277
|
-
export function deleteItemLabel(target:
|
|
292
|
+
export function deleteItemLabel(target: Pick<TreeTarget, "type">, label: string, armed: boolean): string {
|
|
278
293
|
if (!deleteNeedsConfirmation(target)) return "Delete";
|
|
279
294
|
return armed ? `Delete “${label}” and everything in it` : "Delete…";
|
|
280
295
|
}
|
package/src/web/server/routes.ts
CHANGED
|
@@ -21,6 +21,9 @@
|
|
|
21
21
|
* | POST | `/api/history-settings` | turn note history on or off |
|
|
22
22
|
* | GET | `/api/okf/:rel` | {@link OkfFilePayload} |
|
|
23
23
|
* | GET | `/api/artifact/:rel` | sandboxed HTML artifact |
|
|
24
|
+
* | POST | `/api/artifact/:rel/move` | move a vault HTML artifact |
|
|
25
|
+
* | POST | `/api/artifact/:rel/rename` | rename a vault HTML artifact |
|
|
26
|
+
* | DELETE | `/api/artifact/:rel` | delete a vault HTML artifact |
|
|
24
27
|
* | GET | `/api/search?q=` | {@link SearchPayload} |
|
|
25
28
|
* | POST | `/api/open` | {@link OpenResult} — hand the note to `$EDITOR` |
|
|
26
29
|
*
|
|
@@ -64,7 +67,7 @@ import { openNoteInEditor } from "../../core/openInEditor";
|
|
|
64
67
|
import { recallTrail, type RecallTrail } from "../../core/trail";
|
|
65
68
|
import type { Note } from "../../core/types";
|
|
66
69
|
import type { VaultMutationResult } from "../../core/vault";
|
|
67
|
-
import { addNote, createFolder, deleteFolder, deleteNote, getNote, moveNote, renameFolder, readNoteHistory, renameNote, resolveHtmlPath, searchNotes, setNoteBody } from "../../core/vault";
|
|
70
|
+
import { addNote, createFolder, deleteFolder, deleteNote, deleteHtmlArtifact, getNote, moveHtmlArtifact, moveNote, renameHtmlArtifact, renameFolder, readNoteHistory, renameNote, resolveHtmlPath, searchNotes, setNoteBody } from "../../core/vault";
|
|
68
71
|
import { deriveTagIndex, type TaggedNote } from "../../core/view/links";
|
|
69
72
|
import type {
|
|
70
73
|
GraphPayload,
|
|
@@ -434,6 +437,12 @@ async function route(
|
|
|
434
437
|
if (method === "GET" && path.startsWith("/api/artifact/")) {
|
|
435
438
|
return sendArtifact(deps, path.slice("/api/artifact/".length), res);
|
|
436
439
|
}
|
|
440
|
+
if (method === "POST" && path.startsWith("/api/artifact/") && /\/(move|rename)$/.test(path)) {
|
|
441
|
+
return changeArtifact(deps, path.slice("/api/artifact/".length), req, res);
|
|
442
|
+
}
|
|
443
|
+
if (method === "DELETE" && path.startsWith("/api/artifact/")) {
|
|
444
|
+
return sendMutation(res, await deleteHtmlArtifact(deps.vaultRoot, path.slice("/api/artifact/".length)), "artifact");
|
|
445
|
+
}
|
|
437
446
|
if (method === "GET" && path === "/api/search") return sendSearch(deps, query, res);
|
|
438
447
|
if (method === "POST" && path === "/api/open") return openNote(deps, req, res);
|
|
439
448
|
sendText(res, 404, "not found\n");
|
|
@@ -705,9 +714,22 @@ async function routeFolder(deps: RouteDeps, method: string, target: string, req:
|
|
|
705
714
|
return false;
|
|
706
715
|
}
|
|
707
716
|
|
|
708
|
-
|
|
717
|
+
/** `POST /api/artifact/:rel/move` and `/rename`; `rel` still carries the suffix. */
|
|
718
|
+
async function changeArtifact(deps: RouteDeps, target: string, req: IncomingMessage, res: ServerResponse): Promise<void> {
|
|
719
|
+
const rename = target.endsWith("/rename");
|
|
720
|
+
const rel = target.slice(0, target.lastIndexOf("/"));
|
|
721
|
+
const body = await readJsonBody(req);
|
|
722
|
+
const fields = typeof body === "object" && body !== null ? body as { folder?: unknown; name?: unknown } : {};
|
|
723
|
+
if (rename) {
|
|
724
|
+
if (typeof fields.name !== "string") sendJson(res, 400, { error: "expected { name: string }" });
|
|
725
|
+
else sendMutation(res, await renameHtmlArtifact(deps.vaultRoot, rel, fields.name), "artifact");
|
|
726
|
+
} else if (fields.folder !== null && typeof fields.folder !== "string") sendJson(res, 400, { error: "expected { folder: string | null }" });
|
|
727
|
+
else sendMutation(res, await moveHtmlArtifact(deps.vaultRoot, rel, fields.folder), "artifact");
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function sendMutation(res: ServerResponse, result: VaultMutationResult, kind: "note" | "artifact" = "note"): void {
|
|
709
731
|
if (result.ok) {
|
|
710
|
-
const id = result.slug === undefined ? result.path === undefined ? undefined : `vfolder:${result.path}` :
|
|
732
|
+
const id = result.slug === undefined ? result.path === undefined ? undefined : `vfolder:${result.path}` : `${kind}:${result.slug}`;
|
|
711
733
|
sendJson(res, 200, { ok: true, ...(id === undefined ? {} : { id }) });
|
|
712
734
|
return;
|
|
713
735
|
}
|
package/src/opencode/tools.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { executeNoteAction, executeRepoAction, WEAVE_NOTE_DESCRIPTION, WEAVE_REPO_DESCRIPTION, type NoteActionInput, type RepoActionInput } from "../core";
|
|
2
|
-
|
|
3
|
-
const noteInput = {
|
|
4
|
-
type: "object",
|
|
5
|
-
properties: {
|
|
6
|
-
action: { type: "string", enum: ["list", "get", "add", "append", "finalize", "search", "links", "suggest"] },
|
|
7
|
-
title: { type: "string", description: "Note title (add)" },
|
|
8
|
-
text: { type: "string", description: "Markdown body (add), addition (append), or restructured body (finalize)" },
|
|
9
|
-
tags: { type: "array", items: { type: "string" }, description: "Tags (add)" },
|
|
10
|
-
slug: { type: "string", description: "Note slug (get, append, finalize)" },
|
|
11
|
-
raw: { type: "boolean", description: "add/append: preserve text verbatim in the ## Raw tail (use for the user's own words)" },
|
|
12
|
-
source: { type: "string", enum: ["human", "agent"], description: "Provenance (add; defaults to agent)" },
|
|
13
|
-
query: { type: "string", description: "Search query (search)" },
|
|
14
|
-
fix: { type: "boolean", description: "links: apply unambiguous repairs" },
|
|
15
|
-
limit: { type: "number", description: "suggest: maximum suggestions (default 20)" },
|
|
16
|
-
},
|
|
17
|
-
required: ["action"],
|
|
18
|
-
additionalProperties: false,
|
|
19
|
-
} as const;
|
|
20
|
-
|
|
21
|
-
const repoInput = {
|
|
22
|
-
type: "object",
|
|
23
|
-
properties: {
|
|
24
|
-
action: { type: "string", enum: ["overview"] },
|
|
25
|
-
},
|
|
26
|
-
required: ["action"],
|
|
27
|
-
additionalProperties: false,
|
|
28
|
-
} as const;
|
|
29
|
-
|
|
30
|
-
export const weaveTools = [
|
|
31
|
-
{
|
|
32
|
-
name: "weave_note",
|
|
33
|
-
description: WEAVE_NOTE_DESCRIPTION,
|
|
34
|
-
input: noteInput,
|
|
35
|
-
execute: (input: unknown, _cwd: string, _progress?: (text: string) => Promise<void>) => executeNoteAction(input as NoteActionInput),
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
name: "weave_repo",
|
|
39
|
-
description: WEAVE_REPO_DESCRIPTION,
|
|
40
|
-
input: repoInput,
|
|
41
|
-
execute: (input: unknown, cwd: string) => executeRepoAction(input as RepoActionInput, cwd),
|
|
42
|
-
},
|
|
43
|
-
];
|