pi-weave 0.1.8 → 0.1.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 (39) hide show
  1. package/README.md +9 -3
  2. package/package.json +2 -2
  3. package/skills/weave-notepad/SKILL.md +13 -4
  4. package/src/core/concurrency.ts +36 -0
  5. package/src/core/frontmatter.ts +53 -0
  6. package/src/core/graph/build.ts +60 -1
  7. package/src/core/graph/model.ts +15 -0
  8. package/src/core/graph/wikilinks.ts +5 -1
  9. package/src/core/index.ts +2 -0
  10. package/src/core/paths.ts +7 -0
  11. package/src/core/sessions.ts +929 -0
  12. package/src/core/summaries.ts +1 -19
  13. package/src/core/vault.ts +264 -16
  14. package/src/core/workspace.ts +3 -3
  15. package/src/pi/index.ts +144 -37
  16. package/src/pi/sessionScan.ts +104 -0
  17. package/src/pi/summarize.ts +24 -4
  18. package/src/pi/tools/noteTool.ts +13 -4
  19. package/src/pi/viewer/tui/branding.ts +8 -7
  20. package/src/pi/viewer/tui/explorer.ts +1 -1
  21. package/src/web/client/dist/app.js +52 -39
  22. package/src/web/client/graph/Graph.tsx +70 -19
  23. package/src/web/client/graph/column.model.ts +11 -70
  24. package/src/web/client/graph/dynamics.ts +176 -0
  25. package/src/web/client/graph/graph.model.ts +10 -2
  26. package/src/web/client/graph/positions.ts +51 -10
  27. package/src/web/client/graph/renderer.ts +61 -1
  28. package/src/web/client/selection.storage.ts +69 -0
  29. package/src/web/client/shell/Header.tsx +11 -1
  30. package/src/web/client/shell/Shell.tsx +17 -0
  31. package/src/web/client/shell/theme.ts +15 -2
  32. package/src/web/client/state.ts +11 -0
  33. package/src/web/client/tree/Tree.tsx +4 -1
  34. package/src/web/client/workspace.ts +69 -5
  35. package/src/web/server/page.ts +2 -0
  36. package/src/web/server/routes.ts +19 -7
  37. package/src/web/shared/graph.ts +7 -0
  38. package/src/web/shared/layout.ts +158 -341
  39. package/src/web/shared/logo.ts +11 -0
@@ -0,0 +1,104 @@
1
+ /**
2
+ * pi adapter for the session scan (docs/session-scan.md): summarize pi
3
+ * session transcripts into the vault as incremental memory notes.
4
+ *
5
+ * Model wiring is shared with the deep scan (`createModelSummarizer` — the
6
+ * session's already-configured model, auth via `modelRegistry`); only the
7
+ * prompt differs, because a session transcript wants different sentences
8
+ * than a source file. `deps.complete` is the test seam — no network in unit
9
+ * tests.
10
+ */
11
+
12
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
13
+ import {
14
+ resolveSessionsRoot,
15
+ resolveVaultRoot,
16
+ runSessionScan,
17
+ type SessionScanOptions,
18
+ type SessionScanResult,
19
+ type SummarizeFn,
20
+ } from "../core";
21
+ import { createModelSummarizer, type SummarizerDeps } from "./summarize";
22
+
23
+ const SESSION_SYSTEM_PROMPT = [
24
+ "You write durable memory notes that compact coding-agent sessions into their bottom line.",
25
+ "Answer, in order: what happened, what we did, what shipped (features, files, commands,",
26
+ "decisions), and what went less well (dead ends, breakage, anything left unfinished).",
27
+ "Technical sessions get specifics — real file paths, commands, outcomes — not generalities.",
28
+ "3–6 sentences, past tense. No preamble, no headings, no code fences.",
29
+ ].join("\n");
30
+
31
+ const SESSION_MAX_OUTPUT_TOKENS = 400;
32
+
33
+ /** Session summarizer over the session model, or null when none is active. */
34
+ export function createSessionSummarizer(
35
+ ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
36
+ deps: SummarizerDeps = {},
37
+ ): { summarize: SummarizeFn; label: string } | null {
38
+ return createModelSummarizer(ctx, SESSION_SYSTEM_PROMPT, SESSION_MAX_OUTPUT_TOKENS, deps);
39
+ }
40
+
41
+ export type SessionScanOutcome =
42
+ | { kind: "ok"; result: SessionScanResult }
43
+ | { kind: "no-model" };
44
+
45
+ export interface SessionScanAdapterOptions {
46
+ complete?: SummarizerDeps["complete"];
47
+ maxSessions?: SessionScanOptions["maxSessions"];
48
+ maxFileBytes?: SessionScanOptions["maxFileBytes"];
49
+ concurrency?: SessionScanOptions["concurrency"];
50
+ sessionsRoot?: SessionScanOptions["sessionsRoot"];
51
+ vaultRoot?: SessionScanOptions["vaultRoot"];
52
+ now?: SessionScanOptions["now"];
53
+ onProgress?: SessionScanOptions["onProgress"];
54
+ signal?: SessionScanOptions["signal"];
55
+ }
56
+
57
+ /**
58
+ * Scan pi sessions into the vault using the session model. Roots default to
59
+ * the real locations (PI_WEAVE_SESSIONS / PI_WEAVE_VAULT overrides apply);
60
+ * tests inject both.
61
+ */
62
+ export async function scanPiSessions(
63
+ ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
64
+ deps: SessionScanAdapterOptions = {},
65
+ ): Promise<SessionScanOutcome> {
66
+ const llm = createSessionSummarizer(
67
+ ctx,
68
+ deps.complete !== undefined ? { complete: deps.complete } : {},
69
+ );
70
+ if (!llm) return { kind: "no-model" };
71
+ // exactOptionalPropertyTypes: only present keys may be spread in.
72
+ const result = await runSessionScan({
73
+ sessionsRoot: deps.sessionsRoot ?? resolveSessionsRoot(),
74
+ vaultRoot: deps.vaultRoot ?? resolveVaultRoot(),
75
+ summarize: llm.summarize,
76
+ model: llm.label,
77
+ ...(deps.maxSessions !== undefined ? { maxSessions: deps.maxSessions } : {}),
78
+ ...(deps.maxFileBytes !== undefined ? { maxFileBytes: deps.maxFileBytes } : {}),
79
+ ...(deps.concurrency !== undefined ? { concurrency: deps.concurrency } : {}),
80
+ ...(deps.now !== undefined ? { now: deps.now } : {}),
81
+ ...(deps.onProgress !== undefined ? { onProgress: deps.onProgress } : {}),
82
+ ...(deps.signal !== undefined ? { signal: deps.signal } : {}),
83
+ });
84
+ return { kind: "ok", result };
85
+ }
86
+
87
+ /** One-line human summary of a session-scan result (for notify output). */
88
+ export function formatSessionScanResult(result: SessionScanResult): string {
89
+ const parts = [
90
+ `${result.written} summarized (${result.created} new, ${result.updated} updated)`,
91
+ `${result.skippedFresh} unchanged`,
92
+ ];
93
+ if (result.skippedEmpty > 0) parts.push(`${result.skippedEmpty} empty`);
94
+ if (result.skippedTooBig > 0) parts.push(`${result.skippedTooBig} skipped (size)`);
95
+ if (result.skippedUnreadable > 0) parts.push(`${result.skippedUnreadable} unreadable`);
96
+ let text = `${parts.join(", ")} — ${result.considered} sessions considered`;
97
+ if (result.failed.length > 0) {
98
+ const [failed0] = result.failed;
99
+ if (failed0) {
100
+ text += `; ${result.failed.length} failed, first: ${failed0.path}: ${failed0.error}`;
101
+ }
102
+ }
103
+ return text;
104
+ }
@@ -44,9 +44,21 @@ const SYSTEM_PROMPT = [
44
44
  const MAX_OUTPUT_TOKENS = 220;
45
45
  const REQUEST_TIMEOUT_MS = 30_000;
46
46
 
47
- /** Create the model-backed summarizer, or null when the session has no active model. */
48
- export function createLlmSummarizer(
47
+ /**
48
+ * The shared session-model wiring behind every pi-weave summarizer (deep
49
+ * scan, session scan): resolve the session's already-configured model — no
50
+ * extra keys or providers (docs/scan-modes.md) — drive completion through
51
+ * `ctx.modelRegistry`, which owns auth, and reject empty outputs so a
52
+ * degraded model cannot silently blank a note.
53
+ *
54
+ * Returns null when the session has no active model (headless runs without a
55
+ * provider, etc.) — callers fall back or notify. `deps.complete` is the test
56
+ * seam: unit tests inject a fake and never touch a network.
57
+ */
58
+ export function createModelSummarizer(
49
59
  ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
60
+ systemPrompt: string,
61
+ maxOutputTokens: number,
50
62
  deps: SummarizerDeps = {},
51
63
  ): LlmSummarizer | null {
52
64
  const model = ctx.model;
@@ -58,7 +70,7 @@ export function createLlmSummarizer(
58
70
  const message = await complete(
59
71
  model,
60
72
  {
61
- systemPrompt: SYSTEM_PROMPT,
73
+ systemPrompt,
62
74
  messages: [
63
75
  {
64
76
  role: "user",
@@ -67,7 +79,7 @@ export function createLlmSummarizer(
67
79
  },
68
80
  ],
69
81
  },
70
- { maxTokens: MAX_OUTPUT_TOKENS, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) },
82
+ { maxTokens: maxOutputTokens, signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) },
71
83
  );
72
84
  const text = contentText(message.content).trim();
73
85
  if (text.length === 0) {
@@ -78,6 +90,14 @@ export function createLlmSummarizer(
78
90
  return { summarize, label };
79
91
  }
80
92
 
93
+ /** Create the file summarizer for deep scans, or null when no model is active. */
94
+ export function createLlmSummarizer(
95
+ ctx: Pick<ExtensionContext, "model" | "modelRegistry">,
96
+ deps: SummarizerDeps = {},
97
+ ): LlmSummarizer | null {
98
+ return createModelSummarizer(ctx, SYSTEM_PROMPT, MAX_OUTPUT_TOKENS, deps);
99
+ }
100
+
81
101
  export type DeepScanOutcome =
82
102
  | { kind: "ok"; result: DeepScanResult }
83
103
  | { kind: "no-model" }
@@ -5,6 +5,7 @@ import { Type } from "typebox";
5
5
  import {
6
6
  addNote,
7
7
  appendToNote,
8
+ extractRawTail,
8
9
  finalizeNote,
9
10
  formatNote,
10
11
  formatRawAppend,
@@ -30,7 +31,8 @@ export function registerNoteTool(pi: ExtensionAPI): void {
30
31
  description:
31
32
  "Read and write notes in the pi-weave vault — a persistent, human-readable knowledge base " +
32
33
  "of Markdown notes. Actions: list (all notes), get (one note by slug), add (new note), " +
33
- "append (extend a note), finalize (restructure a note above its raw tail), search (title/tags/body). " +
34
+ "append (extend a note; raw=true appends verbatim dictation into the ## Raw tail), " +
35
+ "finalize (restructure a note above its raw tail), search (title/tags/body). " +
34
36
  "Use it to remember decisions, facts, and user preferences across sessions.",
35
37
  promptSnippet: "Remember and retrieve durable knowledge in the pi-weave vault",
36
38
  promptGuidelines: [
@@ -43,6 +45,7 @@ export function registerNoteTool(pi: ExtensionAPI): void {
43
45
  text: Type.Optional(Type.String({ description: "Markdown body (add), addition (append), or restructured body above the raw tail (finalize)" })),
44
46
  tags: Type.Optional(Type.Array(Type.String(), { description: "Tags (add)" })),
45
47
  slug: Type.Optional(Type.String({ description: "Note slug (get, append, finalize)" })),
48
+ raw: Type.Optional(Type.Boolean({ description: "append: add text as verbatim dictation to the ## Raw tail (timestamped fenced block; tail created if missing). Use for dictation/scribbles; omit for structured Markdown additions" })),
46
49
  source: Type.Optional(StringEnum(["human", "agent"] as const, { description: "Provenance (add): human for user-scribbled notes, agent for Pi-drafted (default agent)" })),
47
50
  query: Type.Optional(Type.String({ description: "Search query (search)" })),
48
51
  }),
@@ -117,12 +120,14 @@ export function registerNoteTool(pi: ExtensionAPI): void {
117
120
  // Serialized read-modify-write: parallel weave_note appends (and
118
121
  // pi's own file tools) targeting the same note would otherwise
119
122
  // lose each other's additions.
120
- const note = await withMutationQueue(path, () => appendToNote(vault, slug, text));
123
+ const note = await withMutationQueue(path, () =>
124
+ appendToNote(vault, slug, text, new Date(), params.raw ? { raw: true } : {}),
125
+ );
121
126
  if (!note) {
122
127
  return { content: [{ type: "text", text: `No note found with slug '${params.slug}'.` }], details: { action: "append", found: false } };
123
128
  }
124
129
  return {
125
- content: [{ type: "text", text: `Appended to ${note.slug} (updated ${note.updated}).` }],
130
+ content: [{ type: "text", text: params.raw ? `Appended verbatim to the ## Raw tail of ${note.slug} (updated ${note.updated}).` : `Appended to ${note.slug} (updated ${note.updated}).` }],
126
131
  details: { action: "append", found: true, note },
127
132
  };
128
133
  }
@@ -150,8 +155,12 @@ export function registerNoteTool(pi: ExtensionAPI): void {
150
155
  if (!note) {
151
156
  return { content: [{ type: "text", text: `No note found with slug '${params.slug}'.` }], details: { action: "finalize", found: false } };
152
157
  }
158
+ // Be truthful about what was preserved: a note with no `## Raw`
159
+ // marker yet gets its whole pre-finalize body preserved as a new
160
+ // raw tail (never silently dropped).
161
+ const preserved = extractRawTail(note.body) !== "";
153
162
  return {
154
- content: [{ type: "text", text: `Finalized ${note.slug} (updated ${note.updated}). Raw notes tail preserved.` }],
163
+ content: [{ type: "text", text: `Finalized ${note.slug} (updated ${note.updated}). ${preserved ? "Raw tail preserved beneath the structured body." : "Note body was empty — nothing to preserve."}` }],
155
164
  details: { action: "finalize", found: true, note },
156
165
  };
157
166
  }
@@ -7,7 +7,7 @@
7
7
  * 1. "kitty" — the real raster logo via pi-tui's `Image` component (a small
8
8
  * ~2×2-cell favicon). The raw JPG is bundled and base64'd once.
9
9
  * 2. "glyph" — a tiny curated Unicode glyph derived from the logo silhouette.
10
- * 3. "plain" — `🧵` + wordmark, forced by PI_WEAVE_TUI_PLAIN.
10
+ * 3. "plain" — `🕸️` + wordmark, forced by PI_WEAVE_TUI_PLAIN.
11
11
  *
12
12
  * The line-art/unicode constants contain no ESC by construction (decision 1:
13
13
  * the representation fallback is a curated constant, never generated at runtime).
@@ -22,25 +22,26 @@ export type LogoTier = "kitty" | "glyph" | "plain";
22
22
  /** A tiny Unicode mark derived from the logo silhouette (decision 1 fallback). */
23
23
  export const MARK_GLYPH = "◈";
24
24
  /** Absolute last-resort mark (also the forced plain tier). */
25
- export const PLAIN_MARK = "🧵";
25
+ export const PLAIN_MARK = "🕸️";
26
26
  /** The wordmark shown after the mark. */
27
27
  export const WORDMARK = "weave view";
28
28
 
29
29
  /** Env var that forces the plain tier. */
30
30
  export const PLAIN_ENV = "PI_WEAVE_TUI_PLAIN";
31
31
 
32
- /** MIME of the bundled raster logo asset (downscaled JPEG of the logo). */
33
- export const LOGO_MIME = "image/jpeg";
32
+ /** MIME of the bundled raster logo asset (downscaled transparent PNG of the logo). */
33
+ export const LOGO_MIME = "image/png";
34
34
 
35
35
  /**
36
- * Bundled downscaled raster copy of `docs/pi-weave-logo.jpg` (32×32, ~1.3 KB).
37
- * Decision 1 (§15): ship a small raster copy and embed it via pi-tui's `Image`
36
+ * Bundled downscaled raster copy of `docs/pi-weave-logo.png` (the transparent-
37
+ * background spider, 32×32, ~1.7 KB). Decision 1 (§15): ship a small raster copy
38
+ * and embed it via pi-tui's `Image`
38
39
  * (Kitty graphics) as a small ~2×2-cell favicon in the header strip — a casual
39
40
  * in-bar mark, not a hero splash. Stored base64 so the render path needs no
40
41
  * fs/path resolution and the asset is trivially unit-testable.
41
42
  */
42
43
  export const LOGO_B64 =
43
- "/9j/4AAQSkZJRgABAQAASABIAAD/4QDORXhpZgAATU0AKgAAAAgABgESAAMAAAABAAEAAAEaAAUAAAABAAAAVgEbAAUAAAABAAAAXgEoAAMAAAABAAIAAAExAAIAAAAVAAAAZodpAAQAAAABAAAAfAAAAAAAAABIAAAAAQAAAEgAAAABUGl4ZWxtYXRvciBQcm8gMi4xLjMAAAAEkAQAAgAAABQAAACyoAEAAwAAAAEAAQAAoAIABAAAAAEAAAAgoAMABAAAAAEAAAAgAAAAADIwMjY6MDg6MjIgMTk6NTY6MjIA/+0AZFBob3Rvc2hvcCAzLjAAOEJJTQQEAAAAAAAsHAFaAAMbJUccAgAAAgACHAI+AAgyMDI2MDgyMhwCPwALMTk1NjIyKzAwMDA4QklNBCUAAAAAABBB7D4+CxrdyaRylkhoEmIW/8AAEQgAIAAgAwEiAAIRAQMRAf/EAB8AAAEFAQEBAQEBAAAAAAAAAAABAgMEBQYHCAkKC//EALUQAAIBAwMCBAMFBQQEAAABfQECAwAEEQUSITFBBhNRYQcicRQygZGhCCNCscEVUtHwJDNicoIJChYXGBkaJSYnKCkqNDU2Nzg5OkNERUZHSElKU1RVVldYWVpjZGVmZ2hpanN0dXZ3eHl6g4SFhoeIiYqSk5SVlpeYmZqio6Slpqeoqaqys7S1tre4ubrCw8TFxsfIycrS09TV1tfY2drh4uPk5ebn6Onq8fLz9PX29/j5+v/EAB8BAAMBAQEBAQEBAQEAAAAAAAABAgMEBQYHCAkKC//EALURAAIBAgQEAwQHBQQEAAECdwABAgMRBAUhMQYSQVEHYXETIjKBCBRCkaGxwQkjM1LwFWJy0QoWJDThJfEXGBkaJicoKSo1Njc4OTpDREVGR0hJSlNUVVZXWFlaY2RlZmdoaWpzdHV2d3h5eoKDhIWGh4iJipKTlJWWl5iZmqKjpKWmp6ipqrKztLW2t7i5usLDxMXGx8jJytLT1NXW19jZ2uLj5OXm5+jp6vLz9PX29/j5+v/bAEMAAgICAgICAwICAwUDAwMFBgUFBQUGCAYGBgYGCAoICAgICAgKCgoKCgoKCgwMDAwMDA4ODg4ODw8PDw8PDw8PD//bAEMBAgICBAQEBwQEBxALCQsQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEP/dAAQAAv/aAAwDAQACEQMRAD8A/E8Lk4rQsocXcD46SIf/AB4VDbpvYDua95+HfhtZtHn8QpBbXEdvKyM00iKW2RtKY4Vcjc+xGY45wAByQD62FwzqO17Luc0pWPC7yHNzM2Orsf1NUGXBxXu/xH8O/Y7Cy114baCG9m8oNBIrbCYlmCTKmdr7JEbHJwcH5gQPEZ02sR6UsThnTdr3XcIyuf/Q/GHT/JkcQzbQpO7Lccjtn3r0Dwv/AG1fMdH0e+e1e+kRTaEM4uZwTtBXhSRk7c8YOO/PlwJByK14dZvI7dbXcDEpJAKjv79ccetezQxfLHlZw1acn8L1PTPE+nanZpb2Wr6i99JBkw2gV4xbzSk8KuGA3BckgYKjGQMVzPjG70+ygh8LaXFZSQWshuGurdd8kkkqDKGY5YqgABUfKH3ECsW48U6vcWs1k02IbjbvUKBnb0AOMgdsA4xxXOE55oxGJ5lZDjRVk5atH//Z";
44
+ "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";
44
45
 
45
46
  /** Terminal capability surface branding probes. */
46
47
  export interface BrandCapabilities {
@@ -263,7 +263,7 @@ export class WeaveExplorer implements Component {
263
263
 
264
264
  private renderHeader(width: number): string[] {
265
265
  const t = this.theme;
266
- const line1 = t.bold(`🧵 weave view — data as of ${this.model.generatedAt || "now"}`) + ` ${this.surfaceName()}`;
266
+ const line1 = t.bold(`🕸️ weave view — data as of ${this.model.generatedAt || "now"}`) + ` ${this.surfaceName()}`;
267
267
  const counts = this.countNotes();
268
268
  const repo = this.model.nodes.find((n) => n.kind === "repository");
269
269
  const repoState = this.model.staleness?.state ?? (repo ? "fresh" : "missing");