pi-weave 0.3.7 → 0.3.9
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 +17 -16
- package/package.json +4 -3
- package/skills/weave-explore/SKILL.md +3 -5
- package/skills/weave-notepad/SKILL.md +30 -4
- package/skills/weave-notepad/references/second-thoughts.md +57 -0
- package/src/core/graph/build.ts +1 -1
- package/src/core/graph/current.ts +10 -8
- package/src/core/history.ts +417 -0
- package/src/core/index.ts +19 -0
- package/src/core/links/repair.ts +5 -68
- package/src/core/noteAction.ts +72 -2
- package/src/core/openInEditor.ts +3 -8
- package/src/core/repoAction.ts +20 -61
- package/src/core/repoIndex.ts +38 -0
- package/src/core/trail.ts +124 -0
- package/src/core/types.ts +1 -3
- package/src/core/vault.ts +116 -29
- package/src/core/view/focus.ts +2 -2
- package/src/core/view/index.ts +2 -3
- package/src/core/view/rawTail.ts +153 -0
- package/src/core/workspace.ts +20 -26
- package/src/core/workspaceCommands.ts +14 -14
- package/src/opencode/tools.ts +3 -3
- package/src/opencode/tui.ts +1 -7
- package/src/pi/index.ts +68 -80
- package/src/pi/tools/noteTool.ts +1 -1
- package/src/pi/tools/repoTool.ts +3 -6
- package/src/pi/viewer/web/run.ts +6 -8
- package/src/web/client/api.ts +103 -5
- package/src/web/client/dist/app.js +183 -52
- package/src/web/client/graph/Graph.tsx +23 -2
- package/src/web/client/graph/column.model.ts +1 -1
- package/src/web/client/graph/graph.model.ts +69 -7
- package/src/web/client/graph/renderer.ts +19 -3
- package/src/web/client/note/Note.tsx +171 -20
- package/src/web/client/note/Replay.tsx +128 -0
- package/src/web/client/note/drafts.ts +50 -9
- package/src/web/client/note/note.model.ts +17 -0
- package/src/web/client/note/replay.model.ts +173 -0
- package/src/web/client/shell/ContextRail.tsx +7 -0
- package/src/web/client/shell/Settings.tsx +21 -1
- package/src/web/client/shell/Shell.tsx +34 -10
- package/src/web/client/shell/keys.model.ts +20 -7
- package/src/web/client/shell/theme.ts +118 -1
- package/src/web/client/shell/workspace.css.ts +4 -0
- package/src/web/client/trail/TrailStrip.tsx +55 -0
- package/src/web/client/trail/trail.model.ts +195 -0
- package/src/web/client/trail/trail.ts +45 -0
- package/src/web/client/tree/Tree.tsx +7 -3
- package/src/web/client/tree/tree.model.ts +2 -5
- package/src/web/server/page.ts +1 -1
- package/src/web/server/routes.ts +83 -3
- package/src/web/shared/logo.ts +1 -1
- package/src/web/shared/view.ts +10 -4
- package/src/web/shared/wire.ts +74 -1
- package/src/core/view/health.ts +0 -160
- package/src/pi/viewer/tui/bodyStore.ts +0 -95
- package/src/pi/viewer/tui/branding.ts +0 -14
- package/src/pi/viewer/tui/model.ts +0 -390
- package/src/pi/viewer/tui/openNote.ts +0 -11
- package/src/pi/viewer/tui/run.ts +0 -71
- package/src/pi/viewer/tui/surface/base.ts +0 -203
- package/src/pi/viewer/tui/surface/detail.ts +0 -315
- package/src/pi/viewer/tui/surface/explore.ts +0 -241
- package/src/pi/viewer/tui/surface/focus.ts +0 -139
- package/src/pi/viewer/tui/surface/health.ts +0 -102
- package/src/pi/viewer/tui/theme.ts +0 -90
- package/src/pi/viewer/tui/workspace.ts +0 -53
- package/src/pi/viewer/tui/workspaceRoot.ts +0 -389
package/src/pi/viewer/web/run.ts
CHANGED
|
@@ -71,14 +71,12 @@ export interface RunWebOutcome {
|
|
|
71
71
|
session: WebWorkspaceSession;
|
|
72
72
|
/** True when this call booted the server rather than reusing one. */
|
|
73
73
|
started: boolean;
|
|
74
|
-
/** True when a browser was actually launched successfully. */
|
|
75
|
-
opened: boolean;
|
|
76
74
|
/**
|
|
77
|
-
* True when
|
|
78
|
-
*
|
|
79
|
-
*
|
|
75
|
+
* True when a browser was actually launched successfully. When one was
|
|
76
|
+
* wanted and did not launch, the server stays up regardless — the URL in
|
|
77
|
+
* the notification is still the user's way back in.
|
|
80
78
|
*/
|
|
81
|
-
|
|
79
|
+
opened: boolean;
|
|
82
80
|
}
|
|
83
81
|
|
|
84
82
|
/**
|
|
@@ -126,7 +124,7 @@ export class WebWorkspaceController {
|
|
|
126
124
|
|
|
127
125
|
ctx.ui.notify(describe({ url: session.server.entryUrl, started, opened, wantsBrowser }), "info");
|
|
128
126
|
|
|
129
|
-
return { session, started, opened
|
|
127
|
+
return { session, started, opened };
|
|
130
128
|
}
|
|
131
129
|
|
|
132
130
|
/** Stop the workspace server. Idempotent. */
|
|
@@ -154,7 +152,7 @@ function describe(facts: { url: string; started: boolean; opened: boolean; wants
|
|
|
154
152
|
const where = facts.started ? "workspace running at" : "workspace already running at";
|
|
155
153
|
if (facts.opened) return `pi-weave: ${where} ${facts.url} — opened in your browser.`;
|
|
156
154
|
if (facts.wantsBrowser) {
|
|
157
|
-
return `pi-weave: ${where} ${facts.url} — could not launch a browser; open the URL yourself
|
|
155
|
+
return `pi-weave: ${where} ${facts.url} — could not launch a browser; open the URL yourself.`;
|
|
158
156
|
}
|
|
159
157
|
return `pi-weave: ${where} ${facts.url} — open it in a browser.`;
|
|
160
158
|
}
|
package/src/web/client/api.ts
CHANGED
|
@@ -36,7 +36,19 @@
|
|
|
36
36
|
* fortnight.
|
|
37
37
|
*/
|
|
38
38
|
|
|
39
|
-
import type {
|
|
39
|
+
import type {
|
|
40
|
+
GraphPayload,
|
|
41
|
+
HistoryPayload,
|
|
42
|
+
HistoryRevision,
|
|
43
|
+
HistorySettingsPayload,
|
|
44
|
+
HistoryWirePayload,
|
|
45
|
+
MutationResult,
|
|
46
|
+
NotePayload,
|
|
47
|
+
OpenResult,
|
|
48
|
+
SearchPayload,
|
|
49
|
+
TrailPayload,
|
|
50
|
+
ViewNote,
|
|
51
|
+
} from "../shared/wire";
|
|
40
52
|
|
|
41
53
|
// --- the injected HTTP port ------------------------------------------------------
|
|
42
54
|
|
|
@@ -205,6 +217,21 @@ export function isGraphPayload(value: unknown): value is GraphPayload {
|
|
|
205
217
|
return isObject(value["tags"]) && isObject(value["dangling"]);
|
|
206
218
|
}
|
|
207
219
|
|
|
220
|
+
/** `TrailPayload`. Events are rendered as links, so each one's shape is checked. */
|
|
221
|
+
export function isTrailPayload(value: unknown): value is TrailPayload {
|
|
222
|
+
if (!isObject(value) || typeof value["stamp"] !== "string" || typeof value["turn"] !== "number") return false;
|
|
223
|
+
const events = value["events"];
|
|
224
|
+
return Array.isArray(events) && events.every((event: unknown) =>
|
|
225
|
+
isObject(event)
|
|
226
|
+
&& typeof event["seq"] === "number"
|
|
227
|
+
&& typeof event["turn"] === "number"
|
|
228
|
+
&& typeof event["at"] === "string"
|
|
229
|
+
&& (event["kind"] === "searched" || event["kind"] === "read" || event["kind"] === "wrote")
|
|
230
|
+
&& typeof event["action"] === "string"
|
|
231
|
+
&& isStringArray(event["slugs"])
|
|
232
|
+
&& (event["query"] === undefined || typeof event["query"] === "string"));
|
|
233
|
+
}
|
|
234
|
+
|
|
208
235
|
/** `ViewNote`. Every field is rendered, so every field is checked. */
|
|
209
236
|
export function isViewNote(value: unknown): value is ViewNote {
|
|
210
237
|
if (!isObject(value)) return false;
|
|
@@ -229,6 +256,47 @@ export function isOpenResult(value: unknown): value is OpenResult {
|
|
|
229
256
|
return isObject(value) && typeof value["opened"] === "boolean";
|
|
230
257
|
}
|
|
231
258
|
|
|
259
|
+
const HISTORY_AUTHORS: ReadonlySet<unknown> = new Set(["human", "agent", "outside", "system"]);
|
|
260
|
+
|
|
261
|
+
const isCount = (value: unknown): boolean => Number.isInteger(value) && (value as number) >= 0;
|
|
262
|
+
|
|
263
|
+
/** `HistoryWirePayload`. Every revision is rebuilt and rendered, so every revision is checked. */
|
|
264
|
+
export function isHistoryPayload(value: unknown): value is HistoryWirePayload {
|
|
265
|
+
if (!isObject(value) || typeof value["slug"] !== "string" || typeof value["enabled"] !== "boolean") return false;
|
|
266
|
+
if (typeof value["trimmed"] !== "boolean") return false;
|
|
267
|
+
const revisions = value["revisions"];
|
|
268
|
+
return Array.isArray(revisions) && revisions.every((revision) => {
|
|
269
|
+
if (!isObject(revision) || typeof revision["at"] !== "string" || !HISTORY_AUTHORS.has(revision["by"]) || typeof revision["op"] !== "string") return false;
|
|
270
|
+
if (typeof revision["full"] === "string") return true;
|
|
271
|
+
const keep = revision["keep"];
|
|
272
|
+
return Array.isArray(keep) && keep.length === 2 && keep.every(isCount) && typeof revision["text"] === "string";
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Rebuild every body from the wire's full copies and deltas, or null when a
|
|
278
|
+
* delta does not fit the body before it: showing a guess would show the user
|
|
279
|
+
* text nobody wrote.
|
|
280
|
+
*/
|
|
281
|
+
export function expandHistory(payload: HistoryWirePayload): HistoryPayload | null {
|
|
282
|
+
const revisions: HistoryRevision[] = [];
|
|
283
|
+
let body: string | null = null;
|
|
284
|
+
for (const { at, by, op, full, keep, text } of payload.revisions) {
|
|
285
|
+
if (full !== undefined) body = full;
|
|
286
|
+
else {
|
|
287
|
+
const [head, tail] = keep!;
|
|
288
|
+
if (body === null || head + tail > body.length) return null;
|
|
289
|
+
body = body.slice(0, head) + text! + body.slice(body.length - tail);
|
|
290
|
+
}
|
|
291
|
+
revisions.push({ at, by, op, body });
|
|
292
|
+
}
|
|
293
|
+
return { slug: payload.slug, enabled: payload.enabled, trimmed: payload.trimmed, revisions };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function isHistorySettings(value: unknown): value is HistorySettingsPayload {
|
|
297
|
+
return isObject(value) && typeof value["enabled"] === "boolean";
|
|
298
|
+
}
|
|
299
|
+
|
|
232
300
|
export function isMutationResult(value: unknown): value is MutationResult {
|
|
233
301
|
return isObject(value) && value["ok"] === true && (value["id"] === undefined || typeof value["id"] === "string");
|
|
234
302
|
}
|
|
@@ -242,12 +310,26 @@ export function isMutationResult(value: unknown): value is MutationResult {
|
|
|
242
310
|
* present its `stamp` is sent as `If-None-Match` and a `304` returns it
|
|
243
311
|
* back with `cached: true`.
|
|
244
312
|
*/
|
|
245
|
-
export
|
|
313
|
+
export function fetchGraph(fetchImpl: FetchLike, previous: GraphPayload | null = null): Promise<ApiResult<GraphPayload>> {
|
|
314
|
+
return fetchConditional(fetchImpl, "/api/graph", previous, isGraphPayload);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** `GET /api/trail` — the recall trail, revalidated on its stamp exactly like the graph. */
|
|
318
|
+
export function fetchTrail(fetchImpl: FetchLike, previous: TrailPayload | null = null): Promise<ApiResult<TrailPayload>> {
|
|
319
|
+
return fetchConditional(fetchImpl, "/api/trail", previous, isTrailPayload);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
async function fetchConditional<T extends { stamp: string }>(
|
|
323
|
+
fetchImpl: FetchLike,
|
|
324
|
+
url: string,
|
|
325
|
+
previous: T | null,
|
|
326
|
+
guard: (value: unknown) => value is T,
|
|
327
|
+
): Promise<ApiResult<T>> {
|
|
246
328
|
const headers = previous === null ? undefined : { "if-none-match": `"${previous.stamp}"` };
|
|
247
329
|
|
|
248
330
|
let response: HttpResponse;
|
|
249
331
|
try {
|
|
250
|
-
response = await fetchImpl(
|
|
332
|
+
response = await fetchImpl(url, headers === undefined ? {} : { headers });
|
|
251
333
|
} catch (error) {
|
|
252
334
|
return failure("network", 0, error instanceof Error ? error.message : "network request failed");
|
|
253
335
|
}
|
|
@@ -271,7 +353,7 @@ export async function fetchGraph(fetchImpl: FetchLike, previous: GraphPayload |
|
|
|
271
353
|
} catch {
|
|
272
354
|
return failure("malformed", response.status, "response was not valid JSON");
|
|
273
355
|
}
|
|
274
|
-
if (!
|
|
356
|
+
if (!guard(body)) return failure("malformed", response.status, "response did not match the expected shape");
|
|
275
357
|
return success(body);
|
|
276
358
|
}
|
|
277
359
|
|
|
@@ -335,10 +417,26 @@ const folderUrl = (path: string, suffix = ""): string => `/api/folder/${path.spl
|
|
|
335
417
|
* (last-write-wins, see core's `setNoteBody`), and a `MutationResult` rather
|
|
336
418
|
* than the note: the poll is what puts the new text back on screen.
|
|
337
419
|
*/
|
|
338
|
-
export const saveNote = (fetchImpl: FetchLike, slug: string, body: string
|
|
420
|
+
export const saveNote = (fetchImpl: FetchLike, slug: string, body: string, raw = false) =>
|
|
421
|
+
mutation(fetchImpl, noteUrl(slug), "POST", raw ? { body, raw: true } : { body });
|
|
339
422
|
export const renameNote = (fetchImpl: FetchLike, slug: string, name: string) => mutation(fetchImpl, noteUrl(slug, "/rename"), "POST", { name });
|
|
340
423
|
export const moveNote = (fetchImpl: FetchLike, slug: string, folder: string | null) => mutation(fetchImpl, noteUrl(slug, "/move"), "POST", { folder });
|
|
341
424
|
export const deleteNote = (fetchImpl: FetchLike, slug: string) => mutation(fetchImpl, noteUrl(slug), "DELETE");
|
|
342
425
|
export const createFolder = (fetchImpl: FetchLike, path: string) => mutation(fetchImpl, folderUrl(path), "POST");
|
|
343
426
|
export const renameFolder = (fetchImpl: FetchLike, path: string, name: string) => mutation(fetchImpl, folderUrl(path, "/rename"), "POST", { name });
|
|
344
427
|
export const deleteFolder = (fetchImpl: FetchLike, path: string) => mutation(fetchImpl, folderUrl(path), "DELETE");
|
|
428
|
+
|
|
429
|
+
/** `GET /api/history/:slug` — every saved revision of a note, oldest first. */
|
|
430
|
+
export async function fetchHistory(fetchImpl: FetchLike, slug: string): Promise<ApiResult<HistoryPayload>> {
|
|
431
|
+
const result = await request(fetchImpl, `/api/history/${encodeURIComponent(slug)}`, isHistoryPayload);
|
|
432
|
+
if (!result.ok) return result;
|
|
433
|
+
const expanded = expandHistory(result.data);
|
|
434
|
+
return expanded === null ? failure("malformed", 200, "history did not fit together") : success(expanded, result.cached);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/** `GET /api/history-settings`, or `POST` it with `enabled` to turn recording on or off. */
|
|
438
|
+
export function historySettings(fetchImpl: FetchLike, enabled?: boolean): Promise<ApiResult<HistorySettingsPayload>> {
|
|
439
|
+
return request(fetchImpl, "/api/history-settings", isHistorySettings, enabled === undefined ? undefined : {
|
|
440
|
+
method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ enabled }),
|
|
441
|
+
});
|
|
442
|
+
}
|