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.
- package/README.md +17 -16
- package/package.json +4 -3
- package/skills/weave-explore/SKILL.md +3 -5
- package/skills/weave-notepad/SKILL.md +22 -2
- 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
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a note's append-only `## Raw` tail begins (weave-workspace §1.2).
|
|
3
|
+
*
|
|
4
|
+
* Pure and node-free on purpose: the vault uses it to keep the on-disk tail
|
|
5
|
+
* whatever a save says, and the browser editor uses the same function,
|
|
6
|
+
* through `src/web/shared/view.ts`, to keep the tail out of the textarea.
|
|
7
|
+
* Two implementations would disagree about a fenced `## Raw` sample, and the
|
|
8
|
+
* editor would then hide text the server is willing to overwrite.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The append-only tail heading.
|
|
13
|
+
*
|
|
14
|
+
* `../links/repair` and `../vault` re-export it, so their public API is
|
|
15
|
+
* unchanged.
|
|
16
|
+
*/
|
|
17
|
+
export const RAW_NOTES_HEADING = "## Raw";
|
|
18
|
+
|
|
19
|
+
const FENCE_RE = /^[ \t]*(`{3,}|~{3,})(.*)$/gm;
|
|
20
|
+
/** A `## Raw` heading on a line of its own — not `## Rawhide`. */
|
|
21
|
+
const RAW_HEADING_RE = new RegExp(`^${RAW_NOTES_HEADING}[ \\t]*\\r?$`, "m");
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Half-open `[start, end)` ranges covering fenced code blocks.
|
|
25
|
+
*
|
|
26
|
+
* CommonMark, not "every third-backtick-line toggles": a fence is closed only
|
|
27
|
+
* by the **same marker character**, at **at least the opening length**, and
|
|
28
|
+
* with no info string. Treating fences as a blind alternation gets two cases
|
|
29
|
+
* backwards — a `~~~` nested inside a ``` block would close it, and a ```
|
|
30
|
+
* inside a ```` block would too — and each mistake ends the protected region
|
|
31
|
+
* early, exposing example links in documentation to rewriting.
|
|
32
|
+
*/
|
|
33
|
+
export function fenceRanges(body: string): [number, number][] {
|
|
34
|
+
const ranges: [number, number][] = [];
|
|
35
|
+
let open: { at: number; marker: string; length: number } | null = null;
|
|
36
|
+
for (const match of body.matchAll(FENCE_RE)) {
|
|
37
|
+
const run = match[1]!;
|
|
38
|
+
// Group 2 is `(.*)`, which always participates: the info string.
|
|
39
|
+
const info = match[2]!;
|
|
40
|
+
if (open === null) {
|
|
41
|
+
open = { at: match.index, marker: run[0]!, length: run.length };
|
|
42
|
+
} else if (run[0] === open.marker && run.length >= open.length && info.trim() === "") {
|
|
43
|
+
ranges.push([open.at, match.index + match[0].length]);
|
|
44
|
+
open = null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// An unterminated fence runs to the end of the body, the way a renderer
|
|
48
|
+
// treats it.
|
|
49
|
+
if (open !== null) ranges.push([open.at, body.length]);
|
|
50
|
+
return ranges;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The byte offset where the `## Raw` tail begins, or the body length when
|
|
55
|
+
* there is none.
|
|
56
|
+
*
|
|
57
|
+
* The tail is append-only and verbatim — "NEVER edit below this line" is
|
|
58
|
+
* written into every note that has one. A link inside a user's dictation is
|
|
59
|
+
* *their* text, quoted; repairing it would edit words the vault promises not
|
|
60
|
+
* to touch.
|
|
61
|
+
*
|
|
62
|
+
* Fence-aware, and for the opposite reason to everything else here: a note
|
|
63
|
+
* *documenting* the raw-tail convention contains `## Raw` inside a code
|
|
64
|
+
* sample, and treating that as the real tail would silently protect — and so
|
|
65
|
+
* refuse to repair — every link below it. Matching a whole heading line also
|
|
66
|
+
* keeps `## Rawhide` from starting a tail.
|
|
67
|
+
*/
|
|
68
|
+
export function rawTailStart(body: string, fences: readonly [number, number][]): number {
|
|
69
|
+
let from = 0;
|
|
70
|
+
for (;;) {
|
|
71
|
+
const rest = body.slice(from);
|
|
72
|
+
const hit = RAW_HEADING_RE.exec(rest);
|
|
73
|
+
if (hit === null) return body.length;
|
|
74
|
+
const at = from + hit.index;
|
|
75
|
+
const fence = fences.find(([a, b]) => at >= a && at < b);
|
|
76
|
+
if (fence === undefined) return at;
|
|
77
|
+
from = fence[1]; // skip the whole fenced block and keep looking
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A `---` rule, alone on the line, with only blank space after it. */
|
|
82
|
+
const TAIL_RULE_RE = /(?:^|\n)-{3,}[ \t]*\r?\n\s*$/;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Where the tail's text begins — the `---` rule, not the heading
|
|
86
|
+
* `rawTailStart` returns. Splitting at the heading orphans the rule.
|
|
87
|
+
* The body length when there is no tail.
|
|
88
|
+
*/
|
|
89
|
+
export function rawTailBoundary(body: string): number {
|
|
90
|
+
const heading = rawTailStart(body, fenceRanges(body));
|
|
91
|
+
if (heading === body.length) return heading;
|
|
92
|
+
const rule = TAIL_RULE_RE.exec(body.slice(0, heading));
|
|
93
|
+
if (rule === null) return heading;
|
|
94
|
+
// `index` is the newline before the rule when there is one; step over it.
|
|
95
|
+
return rule.index === 0 && !body.startsWith("\n") ? 0 : rule.index + 1;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** A body split at its tail: the editable `head`, and the verbatim `tail` ("" when none). */
|
|
99
|
+
export interface RawTailSplit {
|
|
100
|
+
head: string;
|
|
101
|
+
tail: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Split a body into the part a person may edit and the tail nobody may.
|
|
106
|
+
*
|
|
107
|
+
* The head loses only trailing whitespace, so the editor's cursor lands
|
|
108
|
+
* right after the last word rather than on the blank lines before the rule.
|
|
109
|
+
*/
|
|
110
|
+
export function splitRawTail(body: string): RawTailSplit {
|
|
111
|
+
const at = rawTailBoundary(body);
|
|
112
|
+
return { head: body.slice(0, at).trimEnd(), tail: body.slice(at).trimEnd() };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Put a head back above its tail, the inverse of {@link splitRawTail}. */
|
|
116
|
+
export function joinRawTail(head: string, tail: string): string {
|
|
117
|
+
if (tail === "") return head;
|
|
118
|
+
// An emptied head leaves the tail alone, not a leading blank line.
|
|
119
|
+
return head === "" ? tail : `${head}\n\n${tail}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** A tail split below its heading: the fixed `opening`, and the `entries` a person may edit. */
|
|
123
|
+
export interface RawOpeningSplit {
|
|
124
|
+
opening: string;
|
|
125
|
+
entries: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/** The notice comment written under the heading; `<!-- appended … -->` starts an entry instead. */
|
|
129
|
+
const NOTICE_RE = /^<!--(?! appended\b).*-->[ \t]*\r?$/;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Split a tail (as {@link splitRawTail} returns it) after its `## Raw`
|
|
133
|
+
* heading and the notice line under it. The editor shows only `entries`, so
|
|
134
|
+
* clearing the box cannot leave an orphaned rule or heading in the body.
|
|
135
|
+
*/
|
|
136
|
+
export function splitRawOpening(tail: string): RawOpeningSplit {
|
|
137
|
+
const heading = rawTailStart(tail, fenceRanges(tail));
|
|
138
|
+
if (heading === tail.length) return { opening: tail, entries: "" };
|
|
139
|
+
let end = tail.indexOf("\n", heading);
|
|
140
|
+
if (end !== -1) {
|
|
141
|
+
const next = tail.indexOf("\n", end + 1);
|
|
142
|
+
const line = tail.slice(end + 1, next === -1 ? tail.length : next);
|
|
143
|
+
if (NOTICE_RE.test(line)) end = next;
|
|
144
|
+
}
|
|
145
|
+
if (end === -1) return { opening: tail.trimEnd(), entries: "" };
|
|
146
|
+
return { opening: tail.slice(0, end).trimEnd(), entries: tail.slice(end).replace(/^\s*\n/, "").trimEnd() };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Put edited entries back under their opening; no entries means no tail at all. */
|
|
150
|
+
export function joinRawOpening(opening: string, entries: string): string {
|
|
151
|
+
const text = entries.trim();
|
|
152
|
+
return text === "" ? "" : `${opening}\n\n${entries.replace(/^\s*\n/, "").trimEnd()}`;
|
|
153
|
+
}
|
package/src/core/workspace.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { findGitRoot } from "./git";
|
|
2
|
-
import {
|
|
2
|
+
import { readRepoIndex } from "./repoIndex";
|
|
3
3
|
import { readSummaries } from "./summaries";
|
|
4
4
|
import { basename } from "node:path";
|
|
5
5
|
import { resolveVaultRoot } from "./paths";
|
|
@@ -35,24 +35,27 @@ export async function getWorkspaceStatus(cwd: string, options: WorkspaceOptions
|
|
|
35
35
|
if (!repoRoot) return status;
|
|
36
36
|
|
|
37
37
|
const index = await readRepoIndex(repoRoot);
|
|
38
|
-
const staleness = await assessStaleness(repoRoot);
|
|
39
38
|
status.repository = {
|
|
40
39
|
root: repoRoot,
|
|
41
40
|
name: index?.identity.name ?? basename(repoRoot),
|
|
42
|
-
|
|
43
|
-
staleness,
|
|
44
|
-
summaryCount: index === null ? 0 : (await readSummaries(repoRoot)).length,
|
|
41
|
+
summaryCount: (await readSummaries(repoRoot)).length,
|
|
45
42
|
};
|
|
46
43
|
return status;
|
|
47
44
|
}
|
|
48
45
|
|
|
49
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* One-line status string for footers/status bars. It counts notes; the
|
|
48
|
+
* repository appears only once a deep scan has written summaries, because
|
|
49
|
+
* the structure index is plumbing the user never has to think about.
|
|
50
|
+
*/
|
|
50
51
|
export function formatStatusLine(status: WorkspaceStatus): string {
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
const notes = plural(status.vault.noteCount, "note", "notes");
|
|
53
|
+
const summaries = status.repository?.summaryCount ?? 0;
|
|
54
|
+
return summaries > 0 ? `🕸️ ${notes} · ${plural(summaries, "summary", "summaries")}` : `🕸️ ${notes}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function plural(count: number, one: string, many: string): string {
|
|
58
|
+
return `${count} ${count === 1 ? one : many}`;
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
/** Multi-line dashboard used by the /weave command and notifications. */
|
|
@@ -64,22 +67,13 @@ export function formatDashboard(status: WorkspaceStatus): string {
|
|
|
64
67
|
? ` ${status.vault.noteCount} note(s)`
|
|
65
68
|
: " not initialized — add a note to create it",
|
|
66
69
|
);
|
|
67
|
-
if (!status.repository) {
|
|
68
|
-
lines.push("Repository: none (not inside a git repository)");
|
|
69
|
-
return lines.join("\n");
|
|
70
|
-
}
|
|
71
70
|
const repo = status.repository;
|
|
71
|
+
if (!repo) return lines.join("\n");
|
|
72
72
|
lines.push(`Repository (${repo.name} @ ${repo.root}):`);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
for (const reason of repo.staleness.reasons) {
|
|
79
|
-
lines.push(` - ${reason}`);
|
|
80
|
-
}
|
|
81
|
-
if (repo.summaryCount > 0) {
|
|
82
|
-
lines.push(` summaries: ${repo.summaryCount} file(s) — run /weave-scan deep to refresh`);
|
|
83
|
-
}
|
|
73
|
+
lines.push(
|
|
74
|
+
repo.summaryCount > 0
|
|
75
|
+
? ` summaries: ${repo.summaryCount} file(s) — run /weave-scan to refresh`
|
|
76
|
+
: " no file summaries yet — run /weave-scan to write them with your session model",
|
|
77
|
+
);
|
|
84
78
|
return lines.join("\n");
|
|
85
79
|
}
|
|
@@ -2,7 +2,7 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import { join, resolve } from "node:path";
|
|
3
3
|
import { findGitRoot } from "./git";
|
|
4
4
|
import { resolveVaultRoot } from "./paths";
|
|
5
|
-
import {
|
|
5
|
+
import { ensureRepoIndex } from "./repoIndex";
|
|
6
6
|
import { formatDashboard, getWorkspaceStatus } from "./workspace";
|
|
7
7
|
import { DEEP_SCAN_SYSTEM_PROMPT, formatDeepScanResult, runDeepScan, type SummarizeFn } from "./summaries";
|
|
8
8
|
import { formatSessionScanResult, renderSessionDigest, runSessionDigestScan, runSessionScan, SESSION_SCAN_SYSTEM_PROMPT, type SessionDigest } from "./sessions";
|
|
@@ -91,7 +91,7 @@ export function createWorkspaceCommands(
|
|
|
91
91
|
|
|
92
92
|
commands.push({
|
|
93
93
|
name: "weave-scan",
|
|
94
|
-
description: "
|
|
94
|
+
description: "Summarize this repository's files with the session model; 'sessions [path]' summarizes session history",
|
|
95
95
|
async execute(sessionID, args) {
|
|
96
96
|
const [mode = "", ...rest] = args.trim().split(/\s+/).filter(Boolean);
|
|
97
97
|
const key = String(sessionID);
|
|
@@ -141,24 +141,24 @@ export function createWorkspaceCommands(
|
|
|
141
141
|
return;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
|
+
if (mode !== "" && mode.toLowerCase() !== "deep") {
|
|
145
|
+
await output(sessionID, "usage: /weave-scan [sessions [path]]");
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
144
148
|
const root = await findGitRoot(cwd);
|
|
145
149
|
if (!root) {
|
|
146
150
|
await output(sessionID, "pi-weave: not inside a git repository.");
|
|
147
151
|
return;
|
|
148
152
|
}
|
|
149
|
-
const index = await buildRepoIndex(root);
|
|
150
|
-
if (!index) {
|
|
151
|
-
await output(sessionID, "pi-weave: cannot index — the repository has no commits yet.");
|
|
152
|
-
return;
|
|
153
|
-
}
|
|
154
|
-
await writeRepoIndex(root, index);
|
|
155
|
-
await output(sessionID, `pi-weave: index refreshed\n${summarizeIndex(index).join("\n")}`);
|
|
156
|
-
onStatus({ sessionID: key, text: "pi-weave: index refreshed", active: false });
|
|
157
|
-
if (mode.toLowerCase() !== "deep") return;
|
|
158
|
-
|
|
159
153
|
const session = await host.getSession(sessionID);
|
|
160
154
|
if (!session.model) {
|
|
161
|
-
await output(sessionID, "pi-weave: deep scan needs an active session model — none configured.
|
|
155
|
+
await output(sessionID, "pi-weave: deep scan needs an active session model — none configured.");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// The structure index is plumbing the deep scan and graph rely on;
|
|
159
|
+
// refresh it silently rather than reporting it as a result.
|
|
160
|
+
if (!(await ensureRepoIndex(root))) {
|
|
161
|
+
await output(sessionID, "pi-weave: nothing to scan — the repository has no commits yet.");
|
|
162
162
|
return;
|
|
163
163
|
}
|
|
164
164
|
start(sessionID, async (signal) => {
|
|
@@ -182,7 +182,7 @@ export function createWorkspaceCommands(
|
|
|
182
182
|
|
|
183
183
|
commands.push({
|
|
184
184
|
name: "weave-scan-cancel",
|
|
185
|
-
description: "Cancel the active pi-weave
|
|
185
|
+
description: "Cancel the active pi-weave scan",
|
|
186
186
|
async execute(sessionID) {
|
|
187
187
|
const scan = running.get(String(sessionID));
|
|
188
188
|
if (!scan) {
|
package/src/opencode/tools.ts
CHANGED
|
@@ -8,7 +8,7 @@ const noteInput = {
|
|
|
8
8
|
text: { type: "string", description: "Markdown body (add), addition (append), or restructured body (finalize)" },
|
|
9
9
|
tags: { type: "array", items: { type: "string" }, description: "Tags (add)" },
|
|
10
10
|
slug: { type: "string", description: "Note slug (get, append, finalize)" },
|
|
11
|
-
raw: { type: "boolean", description: "append: preserve text verbatim in the ## Raw tail" },
|
|
11
|
+
raw: { type: "boolean", description: "add/append: preserve text verbatim in the ## Raw tail (use for the user's own words)" },
|
|
12
12
|
source: { type: "string", enum: ["human", "agent"], description: "Provenance (add; defaults to agent)" },
|
|
13
13
|
query: { type: "string", description: "Search query (search)" },
|
|
14
14
|
fix: { type: "boolean", description: "links: apply unambiguous repairs" },
|
|
@@ -21,7 +21,7 @@ const noteInput = {
|
|
|
21
21
|
const repoInput = {
|
|
22
22
|
type: "object",
|
|
23
23
|
properties: {
|
|
24
|
-
action: { type: "string", enum: ["
|
|
24
|
+
action: { type: "string", enum: ["overview"] },
|
|
25
25
|
},
|
|
26
26
|
required: ["action"],
|
|
27
27
|
additionalProperties: false,
|
|
@@ -38,6 +38,6 @@ export const weaveTools = [
|
|
|
38
38
|
name: "weave_repo",
|
|
39
39
|
description: WEAVE_REPO_DESCRIPTION,
|
|
40
40
|
input: repoInput,
|
|
41
|
-
execute: (input: unknown, cwd: string
|
|
41
|
+
execute: (input: unknown, cwd: string) => executeRepoAction(input as RepoActionInput, cwd),
|
|
42
42
|
},
|
|
43
43
|
];
|
package/src/opencode/tui.ts
CHANGED
|
@@ -55,13 +55,7 @@ const weaveTui = {
|
|
|
55
55
|
async setup(context) {
|
|
56
56
|
const rpc = context.client.rpc(WEAVE_RPC);
|
|
57
57
|
try {
|
|
58
|
-
|
|
59
|
-
const text = String(record(initial).text ?? "");
|
|
60
|
-
if (text.includes("repo:unindexed")) {
|
|
61
|
-
context.ui.toast.show({ message: "This repository is not indexed yet. Run /weave-scan.", variant: "info" });
|
|
62
|
-
} else if (text.includes(":stale")) {
|
|
63
|
-
context.ui.toast.show({ message: "The pi-weave repository index is stale. Run /weave-scan.", variant: "warning" });
|
|
64
|
-
}
|
|
58
|
+
await rpc.status({});
|
|
65
59
|
} catch {
|
|
66
60
|
context.ui.toast.show({ message: "pi-weave unavailable", variant: "warning" });
|
|
67
61
|
}
|
package/src/pi/index.ts
CHANGED
|
@@ -2,20 +2,18 @@ import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@e
|
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
4
|
import {
|
|
5
|
-
|
|
5
|
+
ensureRepoIndex,
|
|
6
6
|
findGitRoot,
|
|
7
7
|
formatDashboard,
|
|
8
8
|
formatStatusLine,
|
|
9
9
|
getWorkspaceStatus,
|
|
10
|
-
|
|
11
|
-
writeRepoIndex,
|
|
10
|
+
recallTrail,
|
|
12
11
|
type WorkspaceStatus,
|
|
13
12
|
} from "../core";
|
|
14
13
|
import { registerNoteTool } from "./tools/noteTool";
|
|
15
14
|
import { registerRepoTool } from "./tools/repoTool";
|
|
16
15
|
import { formatSessionScanResult, scanPiSessions } from "./sessionScan";
|
|
17
16
|
import { deepScanRepository, formatDeepScanResult } from "./summarize";
|
|
18
|
-
import { runWeaveViewTui } from "./viewer/tui/run";
|
|
19
17
|
import { WebWorkspaceController } from "./viewer/web/run";
|
|
20
18
|
|
|
21
19
|
/**
|
|
@@ -31,10 +29,9 @@ export default function piWeave(pi: ExtensionAPI): void {
|
|
|
31
29
|
registerNoteTool(pi);
|
|
32
30
|
registerRepoTool(pi);
|
|
33
31
|
|
|
34
|
-
// /weave-view opens the browser workspace
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
// so unlike the TUI it has a real teardown — see `session_shutdown` below.
|
|
32
|
+
// /weave-view opens the browser workspace. It owns a server, a file watcher
|
|
33
|
+
// and live SSE streams for the rest of the session, so it has a real
|
|
34
|
+
// teardown — see `session_shutdown` below.
|
|
38
35
|
let lastCtx: ExtensionContext | ExtensionCommandContext | null = null;
|
|
39
36
|
let lastStatusText: string | undefined = undefined;
|
|
40
37
|
let isActive = false;
|
|
@@ -66,7 +63,11 @@ export default function piWeave(pi: ExtensionAPI): void {
|
|
|
66
63
|
// server was listening. The browser-launch-failure path is exactly that
|
|
67
64
|
// shape, which is why it was the case that caught it.
|
|
68
65
|
const port = web.port();
|
|
69
|
-
|
|
66
|
+
// The recall trail's one terminal echo: how many notes this turn
|
|
67
|
+
// actually read or wrote. Resets when the next turn begins.
|
|
68
|
+
const used = recallTrail.usedThisTurn();
|
|
69
|
+
const recall = used === 0 ? "" : ` · used ${used} note${used === 1 ? "" : "s"}`;
|
|
70
|
+
const suffix = `${recall}${port === null ? "" : ` · web:${port}`}`;
|
|
70
71
|
|
|
71
72
|
// Clear only when there is genuinely nothing to say: no base text *and*
|
|
72
73
|
// no server.
|
|
@@ -91,8 +92,7 @@ export default function piWeave(pi: ExtensionAPI): void {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
pi.on("session_shutdown", async () => {
|
|
94
|
-
// The
|
|
95
|
-
// owns an HTTP server, a recursive file watcher and any number of open SSE
|
|
95
|
+
// The browser workspace owns an HTTP server, a recursive file watcher and any number of open SSE
|
|
96
96
|
// streams. `close()` releases all three (server.ts closes the hub and
|
|
97
97
|
// awaits the watcher); a leaked listener outliving the pi session is a
|
|
98
98
|
// real bug, not a tidiness concern.
|
|
@@ -100,30 +100,27 @@ export default function piWeave(pi: ExtensionAPI): void {
|
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
pi.on("agent_start", async (_event, ctx) => {
|
|
103
|
+
recallTrail.beginTurn();
|
|
103
104
|
isActive = true;
|
|
104
105
|
updateStatus(ctx);
|
|
105
106
|
});
|
|
106
107
|
|
|
107
108
|
pi.on("agent_end", async (_event, ctx) => {
|
|
108
109
|
isActive = false;
|
|
109
|
-
|
|
110
|
+
// The agent may have written notes this turn, so recount them — unless a
|
|
111
|
+
// background scan owns the status line with its progress, or no session
|
|
112
|
+
// ever seeded a status line to recount.
|
|
113
|
+
const cwd = ctx?.cwd ?? lastCtx?.cwd;
|
|
114
|
+
if (cwd && lastStatusText !== undefined && inFlightDeepScans.size === 0 && inFlightSessionScans.size === 0) {
|
|
115
|
+
updateStatus(ctx, formatStatusLine(await getWorkspaceStatus(cwd)));
|
|
116
|
+
} else {
|
|
117
|
+
updateStatus(ctx);
|
|
118
|
+
}
|
|
110
119
|
});
|
|
111
120
|
|
|
112
121
|
pi.on("session_start", async (_event, ctx) => {
|
|
113
122
|
const status = await getWorkspaceStatus(ctx.cwd);
|
|
114
123
|
updateStatus(ctx, formatStatusLine(status));
|
|
115
|
-
|
|
116
|
-
if (!ctx.hasUI) return;
|
|
117
|
-
const repo = status.repository;
|
|
118
|
-
if (repo && !repo.indexed) {
|
|
119
|
-
ctx.ui.notify(
|
|
120
|
-
"pi-weave: this repository has no knowledge index yet — ask pi to explore it, or run /weave-scan.",
|
|
121
|
-
"info",
|
|
122
|
-
);
|
|
123
|
-
} else if (repo && repo.staleness.state === "stale") {
|
|
124
|
-
const reason = repo.staleness.reasons[0] ?? "index is stale";
|
|
125
|
-
ctx.ui.notify(`pi-weave: repository index is stale (${reason}). Run /weave-scan to refresh.`, "warning");
|
|
126
|
-
}
|
|
127
124
|
});
|
|
128
125
|
|
|
129
126
|
pi.registerCommand("weave", {
|
|
@@ -135,32 +132,28 @@ export default function piWeave(pi: ExtensionAPI): void {
|
|
|
135
132
|
});
|
|
136
133
|
|
|
137
134
|
pi.registerCommand("weave-view", {
|
|
138
|
-
description: "Open the knowledge workspace in your browser ('
|
|
135
|
+
description: "Open the knowledge workspace in your browser ('--no-open' to just print the URL)",
|
|
139
136
|
handler: async (args, ctx) => {
|
|
140
137
|
const parsed = parseWeaveViewArgs(args);
|
|
141
|
-
if (parsed ===
|
|
142
|
-
ctx.ui.notify(
|
|
138
|
+
if (parsed === "retired-tui") {
|
|
139
|
+
ctx.ui.notify(WEAVE_VIEW_TUI_RETIRED, "warning");
|
|
143
140
|
return;
|
|
144
141
|
}
|
|
145
|
-
if (parsed
|
|
146
|
-
|
|
142
|
+
if (parsed === null) {
|
|
143
|
+
ctx.ui.notify(WEAVE_VIEW_USAGE, "warning");
|
|
147
144
|
return;
|
|
148
145
|
}
|
|
149
|
-
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
//
|
|
153
|
-
if (outcome.fallbackToTui && ctx.mode === "tui") await runWeaveViewTui(ctx);
|
|
154
|
-
// Unconditionally, and last: the TUI writes its own status line on
|
|
155
|
-
// close, which would otherwise drop the `· web:PORT` marker for a
|
|
156
|
-
// server that is still running.
|
|
146
|
+
// A browser that will not launch still leaves the URL in the
|
|
147
|
+
// notification, and the server stays up behind it.
|
|
148
|
+
await web.run(ctx, { open: parsed.open });
|
|
149
|
+
// Last, so the status line gains the `· web:PORT` marker.
|
|
157
150
|
updateStatus(ctx);
|
|
158
151
|
},
|
|
159
152
|
});
|
|
160
153
|
|
|
161
154
|
pi.registerCommand("weave-scan", {
|
|
162
155
|
description:
|
|
163
|
-
"
|
|
156
|
+
"Summarize this repository's files with the session model; 'sessions [dir]' summarizes session history",
|
|
164
157
|
handler: async (args, ctx) => {
|
|
165
158
|
const input = args.trim();
|
|
166
159
|
const [mode = "", ...rest] = input.split(/\s+/);
|
|
@@ -176,38 +169,33 @@ export default function piWeave(pi: ExtensionAPI): void {
|
|
|
176
169
|
return; // the background scan owns the status line until it settles
|
|
177
170
|
}
|
|
178
171
|
|
|
172
|
+
if (mode !== "" && mode.toLowerCase() !== "deep") {
|
|
173
|
+
ctx.ui.notify(WEAVE_SCAN_USAGE, "warning");
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
179
177
|
const root = await findGitRoot(ctx.cwd);
|
|
180
178
|
if (!root) {
|
|
181
179
|
ctx.ui.notify("pi-weave: not inside a git repository.", "warning");
|
|
182
180
|
return;
|
|
183
181
|
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
ctx.ui.notify("pi-weave: cannot index — the repository has no commits yet.", "warning");
|
|
182
|
+
if (inFlightDeepScans.has(root)) {
|
|
183
|
+
ctx.ui.notify("pi-weave: a deep scan is already running for this repository — run /weave-scan-cancel to stop it.", "warning");
|
|
187
184
|
return;
|
|
188
185
|
}
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
ctx.ui.notify("pi-weave: a deep scan is already running for this repository — run /weave-scan-cancel to stop it.", "warning");
|
|
195
|
-
} else {
|
|
196
|
-
// Compute the settled status here (no git-lock contention) and let
|
|
197
|
-
// the background scan restore it when it finishes.
|
|
198
|
-
const status = await getWorkspaceStatus(ctx.cwd);
|
|
199
|
-
startDeepScan(root, ctx, status, updateStatus);
|
|
200
|
-
}
|
|
201
|
-
return; // the background scan owns the status line until it settles
|
|
186
|
+
// The structure index is plumbing the deep scan and graph rely on;
|
|
187
|
+
// refresh it silently rather than reporting it as a result.
|
|
188
|
+
if (!(await ensureRepoIndex(root))) {
|
|
189
|
+
ctx.ui.notify("pi-weave: nothing to scan — the repository has no commits yet.", "warning");
|
|
190
|
+
return;
|
|
202
191
|
}
|
|
203
|
-
|
|
204
192
|
const status = await getWorkspaceStatus(ctx.cwd);
|
|
205
|
-
|
|
193
|
+
startDeepScan(root, ctx, status, updateStatus);
|
|
206
194
|
},
|
|
207
195
|
});
|
|
208
196
|
|
|
209
197
|
pi.registerCommand("weave-scan-cancel", {
|
|
210
|
-
description: "Cancel an in-flight /weave-scan
|
|
198
|
+
description: "Cancel an in-flight /weave-scan run",
|
|
211
199
|
handler: async (_args, ctx) => {
|
|
212
200
|
const root = await findGitRoot(ctx.cwd);
|
|
213
201
|
const deep = root ? inFlightDeepScans.get(root) : undefined;
|
|
@@ -241,30 +229,33 @@ export function resolveHistoryPath(cwd: string, input: string): string {
|
|
|
241
229
|
return resolve(cwd, trimmed);
|
|
242
230
|
}
|
|
243
231
|
|
|
244
|
-
export const
|
|
232
|
+
export const WEAVE_SCAN_USAGE = "usage: /weave-scan [sessions [dir]]";
|
|
233
|
+
|
|
234
|
+
export const WEAVE_VIEW_USAGE = "usage: /weave-view [--no-open]";
|
|
245
235
|
|
|
246
|
-
/**
|
|
236
|
+
/** Shown for the retired `/weave-view tui` argument (issue #61). */
|
|
237
|
+
export const WEAVE_VIEW_TUI_RETIRED =
|
|
238
|
+
"pi-weave: the terminal explorer was retired. /weave-view opens the browser workspace; use /weave-view --no-open to print its URL (e.g. over SSH).";
|
|
239
|
+
|
|
240
|
+
/** What `/weave-view` was asked to do. */
|
|
247
241
|
export interface WeaveViewArgs {
|
|
248
|
-
|
|
249
|
-
/** `false` for `--no-open`. Meaningless for `tui`, which never opens one. */
|
|
242
|
+
/** `false` for `--no-open`. */
|
|
250
243
|
open: boolean;
|
|
251
244
|
}
|
|
252
245
|
|
|
253
246
|
/**
|
|
254
|
-
* Parse `/weave-view`'s argument string,
|
|
247
|
+
* Parse `/weave-view`'s argument string: the options, `"retired-tui"` for the
|
|
248
|
+
* removed terminal explorer, or `null` for "print the usage".
|
|
255
249
|
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
* one word away for the SSH case. `--no-open` is accepted on the web
|
|
259
|
-
* surface only — silently ignoring it after `tui` would be a lie about what
|
|
260
|
-
* happened.
|
|
250
|
+
* `web` is still accepted as a no-op so muscle memory from the two-surface
|
|
251
|
+
* days keeps working.
|
|
261
252
|
*
|
|
262
253
|
* Exported because a parser is exactly the kind of thing that should be
|
|
263
254
|
* tested as a table rather than through eight command invocations.
|
|
264
255
|
*/
|
|
265
|
-
export function parseWeaveViewArgs(args: string): WeaveViewArgs | null {
|
|
256
|
+
export function parseWeaveViewArgs(args: string): WeaveViewArgs | "retired-tui" | null {
|
|
266
257
|
const tokens = args.trim().toLowerCase().split(/\s+/).filter(Boolean);
|
|
267
|
-
|
|
258
|
+
if (tokens.includes("tui")) return "retired-tui";
|
|
268
259
|
let open = true;
|
|
269
260
|
let sawSurface = false;
|
|
270
261
|
for (const token of tokens) {
|
|
@@ -274,13 +265,10 @@ export function parseWeaveViewArgs(args: string): WeaveViewArgs | null {
|
|
|
274
265
|
open = false;
|
|
275
266
|
continue;
|
|
276
267
|
}
|
|
277
|
-
if (sawSurface) return null;
|
|
278
|
-
if (token !== "tui" && token !== "web") return null;
|
|
279
|
-
surface = token;
|
|
268
|
+
if (sawSurface || token !== "web") return null;
|
|
280
269
|
sawSurface = true;
|
|
281
270
|
}
|
|
282
|
-
|
|
283
|
-
return { surface, open };
|
|
271
|
+
return { open };
|
|
284
272
|
}
|
|
285
273
|
|
|
286
274
|
/* ------------------------------------------------------------------ */
|
|
@@ -365,7 +353,7 @@ function startBackgroundScan(
|
|
|
365
353
|
|
|
366
354
|
/**
|
|
367
355
|
* Kick off a deep scan in the background. `baseStatus` is the workspace
|
|
368
|
-
* status captured before the scan
|
|
356
|
+
* status captured before the scan, the fallback if recounting fails.
|
|
369
357
|
*/
|
|
370
358
|
function startDeepScan(
|
|
371
359
|
root: string,
|
|
@@ -387,7 +375,7 @@ function startDeepScan(
|
|
|
387
375
|
}
|
|
388
376
|
if (outcome.kind === "no-model") {
|
|
389
377
|
return {
|
|
390
|
-
text: "pi-weave: deep scan needs an active session model — none configured.
|
|
378
|
+
text: "pi-weave: deep scan needs an active session model — none configured.",
|
|
391
379
|
level: "warning",
|
|
392
380
|
};
|
|
393
381
|
}
|
|
@@ -395,7 +383,9 @@ function startDeepScan(
|
|
|
395
383
|
return { text: `pi-weave: deep scan complete — ${formatDeepScanResult(outcome.result)}`, level: "info" };
|
|
396
384
|
}
|
|
397
385
|
return null;
|
|
398
|
-
}
|
|
386
|
+
},
|
|
387
|
+
// The status line counts summaries, so recount them once the scan settles.
|
|
388
|
+
() => getWorkspaceStatus(ctx.cwd));
|
|
399
389
|
}
|
|
400
390
|
|
|
401
391
|
/**
|
|
@@ -439,9 +429,7 @@ function startSessionScan(
|
|
|
439
429
|
}
|
|
440
430
|
return { text: `pi-weave: session scan complete — ${formatSessionScanResult(result)}`, level: "info" };
|
|
441
431
|
},
|
|
442
|
-
//
|
|
443
|
-
// contention), the session scan writes vault notes and takes no git lock:
|
|
444
|
-
// recompute the workspace status so the settled line counts the notes it
|
|
432
|
+
// Recompute the workspace status so the settled line counts the notes it
|
|
445
433
|
// just wrote.
|
|
446
434
|
() => getWorkspaceStatus(ctx.cwd),
|
|
447
435
|
);
|
package/src/pi/tools/noteTool.ts
CHANGED
|
@@ -26,7 +26,7 @@ export function registerNoteTool(pi: ExtensionAPI): void {
|
|
|
26
26
|
text: Type.Optional(Type.String({ description: "Markdown body (add), addition (append), or restructured body above the raw tail (finalize)" })),
|
|
27
27
|
tags: Type.Optional(Type.Array(Type.String(), { description: "Tags (add)" })),
|
|
28
28
|
slug: Type.Optional(Type.String({ description: "Note slug (get, append, finalize)" })),
|
|
29
|
-
raw: Type.Optional(Type.Boolean({ description: "append:
|
|
29
|
+
raw: Type.Optional(Type.Boolean({ description: "add/append: keep text as verbatim dictation in the ## Raw tail (timestamped fenced block; tail created if missing). Use for the user's own words, including the first words of a dictated note; omit for structured Markdown" })),
|
|
30
30
|
source: Type.Optional(StringEnum(["human", "agent"] as const, { description: "Provenance (add): human for user-scribbled notes, agent for Pi-drafted (default agent)" })),
|
|
31
31
|
query: Type.Optional(Type.String({ description: "Search query (search)" })),
|
|
32
32
|
fix: Type.Optional(Type.Boolean({ description: "links: apply the unambiguous repairs. Omit for a read-only report" })),
|
package/src/pi/tools/repoTool.ts
CHANGED
|
@@ -12,15 +12,12 @@ export function registerRepoTool(pi: ExtensionAPI): void {
|
|
|
12
12
|
promptSnippet: "Explore the repository's structure via its .okf knowledge index",
|
|
13
13
|
promptGuidelines: [
|
|
14
14
|
"Use weave_repo action=overview to learn repository structure before broad code exploration instead of scanning files one by one.",
|
|
15
|
-
"Use weave_repo action=scan when the user asks to explore or index this repository.",
|
|
16
15
|
],
|
|
17
16
|
parameters: Type.Object({
|
|
18
|
-
action: StringEnum(["
|
|
17
|
+
action: StringEnum(["overview"] as const),
|
|
19
18
|
}),
|
|
20
|
-
async execute(_toolCallId, params, _signal,
|
|
21
|
-
const result = await executeRepoAction(params, ctx.cwd
|
|
22
|
-
onUpdate?.({ content: [{ type: "text", text }], details: {} });
|
|
23
|
-
});
|
|
19
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
20
|
+
const result = await executeRepoAction(params, ctx.cwd);
|
|
24
21
|
return { content: [{ type: "text", text: result.text }], details: result.details };
|
|
25
22
|
},
|
|
26
23
|
});
|