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.
- package/README.md +9 -3
- package/package.json +2 -2
- package/skills/weave-notepad/SKILL.md +13 -4
- package/src/core/concurrency.ts +36 -0
- package/src/core/frontmatter.ts +53 -0
- package/src/core/graph/build.ts +60 -1
- package/src/core/graph/model.ts +15 -0
- package/src/core/graph/wikilinks.ts +5 -1
- package/src/core/index.ts +2 -0
- package/src/core/paths.ts +7 -0
- package/src/core/sessions.ts +929 -0
- package/src/core/summaries.ts +1 -19
- package/src/core/vault.ts +264 -16
- package/src/core/workspace.ts +3 -3
- package/src/pi/index.ts +144 -37
- package/src/pi/sessionScan.ts +104 -0
- package/src/pi/summarize.ts +24 -4
- package/src/pi/tools/noteTool.ts +13 -4
- package/src/pi/viewer/tui/branding.ts +8 -7
- package/src/pi/viewer/tui/explorer.ts +1 -1
- package/src/web/client/dist/app.js +52 -39
- package/src/web/client/graph/Graph.tsx +70 -19
- package/src/web/client/graph/column.model.ts +11 -70
- package/src/web/client/graph/dynamics.ts +176 -0
- package/src/web/client/graph/graph.model.ts +10 -2
- package/src/web/client/graph/positions.ts +51 -10
- package/src/web/client/graph/renderer.ts +61 -1
- package/src/web/client/selection.storage.ts +69 -0
- package/src/web/client/shell/Header.tsx +11 -1
- package/src/web/client/shell/Shell.tsx +17 -0
- package/src/web/client/shell/theme.ts +15 -2
- package/src/web/client/state.ts +11 -0
- package/src/web/client/tree/Tree.tsx +4 -1
- package/src/web/client/workspace.ts +69 -5
- package/src/web/server/page.ts +2 -0
- package/src/web/server/routes.ts +19 -7
- package/src/web/shared/graph.ts +7 -0
- package/src/web/shared/layout.ts +158 -341
- 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
|
+
}
|
package/src/pi/summarize.ts
CHANGED
|
@@ -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
|
-
/**
|
|
48
|
-
|
|
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
|
|
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:
|
|
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" }
|
package/src/pi/tools/noteTool.ts
CHANGED
|
@@ -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
|
|
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, () =>
|
|
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
|
|
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" —
|
|
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
|
|
33
|
-
export const LOGO_MIME = "image/
|
|
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.
|
|
37
|
-
* Decision 1 (§15): ship a small raster copy
|
|
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
|
-
"/
|
|
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(
|
|
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");
|