pi-weave 0.3.8 → 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.
Files changed (69) hide show
  1. package/README.md +17 -16
  2. package/package.json +4 -3
  3. package/skills/weave-explore/SKILL.md +3 -5
  4. package/skills/weave-notepad/SKILL.md +22 -2
  5. package/skills/weave-notepad/references/second-thoughts.md +57 -0
  6. package/src/core/graph/build.ts +1 -1
  7. package/src/core/graph/current.ts +10 -8
  8. package/src/core/history.ts +417 -0
  9. package/src/core/index.ts +19 -0
  10. package/src/core/links/repair.ts +5 -68
  11. package/src/core/noteAction.ts +72 -2
  12. package/src/core/openInEditor.ts +3 -8
  13. package/src/core/repoAction.ts +20 -61
  14. package/src/core/repoIndex.ts +38 -0
  15. package/src/core/trail.ts +124 -0
  16. package/src/core/types.ts +1 -3
  17. package/src/core/vault.ts +116 -29
  18. package/src/core/view/focus.ts +2 -2
  19. package/src/core/view/index.ts +2 -3
  20. package/src/core/view/rawTail.ts +153 -0
  21. package/src/core/workspace.ts +20 -26
  22. package/src/core/workspaceCommands.ts +14 -14
  23. package/src/opencode/tools.ts +3 -3
  24. package/src/opencode/tui.ts +1 -7
  25. package/src/pi/index.ts +68 -80
  26. package/src/pi/tools/noteTool.ts +1 -1
  27. package/src/pi/tools/repoTool.ts +3 -6
  28. package/src/pi/viewer/web/run.ts +6 -8
  29. package/src/web/client/api.ts +103 -5
  30. package/src/web/client/dist/app.js +183 -52
  31. package/src/web/client/graph/Graph.tsx +23 -2
  32. package/src/web/client/graph/column.model.ts +1 -1
  33. package/src/web/client/graph/graph.model.ts +69 -7
  34. package/src/web/client/graph/renderer.ts +19 -3
  35. package/src/web/client/note/Note.tsx +171 -20
  36. package/src/web/client/note/Replay.tsx +128 -0
  37. package/src/web/client/note/drafts.ts +50 -9
  38. package/src/web/client/note/note.model.ts +17 -0
  39. package/src/web/client/note/replay.model.ts +173 -0
  40. package/src/web/client/shell/ContextRail.tsx +7 -0
  41. package/src/web/client/shell/Settings.tsx +21 -1
  42. package/src/web/client/shell/Shell.tsx +34 -10
  43. package/src/web/client/shell/keys.model.ts +20 -7
  44. package/src/web/client/shell/theme.ts +118 -1
  45. package/src/web/client/shell/workspace.css.ts +4 -0
  46. package/src/web/client/trail/TrailStrip.tsx +55 -0
  47. package/src/web/client/trail/trail.model.ts +195 -0
  48. package/src/web/client/trail/trail.ts +45 -0
  49. package/src/web/client/tree/Tree.tsx +7 -3
  50. package/src/web/client/tree/tree.model.ts +2 -5
  51. package/src/web/server/page.ts +1 -1
  52. package/src/web/server/routes.ts +83 -3
  53. package/src/web/shared/logo.ts +1 -1
  54. package/src/web/shared/view.ts +10 -4
  55. package/src/web/shared/wire.ts +74 -1
  56. package/src/core/view/health.ts +0 -160
  57. package/src/pi/viewer/tui/bodyStore.ts +0 -95
  58. package/src/pi/viewer/tui/branding.ts +0 -14
  59. package/src/pi/viewer/tui/model.ts +0 -390
  60. package/src/pi/viewer/tui/openNote.ts +0 -11
  61. package/src/pi/viewer/tui/run.ts +0 -71
  62. package/src/pi/viewer/tui/surface/base.ts +0 -203
  63. package/src/pi/viewer/tui/surface/detail.ts +0 -315
  64. package/src/pi/viewer/tui/surface/explore.ts +0 -241
  65. package/src/pi/viewer/tui/surface/focus.ts +0 -139
  66. package/src/pi/viewer/tui/surface/health.ts +0 -102
  67. package/src/pi/viewer/tui/theme.ts +0 -90
  68. package/src/pi/viewer/tui/workspace.ts +0 -53
  69. package/src/pi/viewer/tui/workspaceRoot.ts +0 -389
@@ -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 the caller should fall back to the in-terminal explorer:
78
- * a browser was wanted, was attempted, and did not launch. The server
79
- * stays up regardless — the URL is still the user's way back in.
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
- fallbackToTui: boolean;
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, fallbackToTui: wantsBrowser && !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 (falling back to the in-terminal explorer).`;
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
  }
@@ -36,7 +36,19 @@
36
36
  * fortnight.
37
37
  */
38
38
 
39
- import type { GraphPayload, MutationResult, NotePayload, OpenResult, SearchPayload, ViewNote } from "../shared/wire";
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 async function fetchGraph(fetchImpl: FetchLike, previous: GraphPayload | null = null): Promise<ApiResult<GraphPayload>> {
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("/api/graph", headers === undefined ? {} : { headers });
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 (!isGraphPayload(body)) return failure("malformed", response.status, "response did not match the expected shape");
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) => mutation(fetchImpl, noteUrl(slug), "POST", { body });
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
+ }