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.
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 +30 -4
  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
@@ -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
+ }
@@ -1,5 +1,5 @@
1
1
  import { findGitRoot } from "./git";
2
- import { assessStaleness, readRepoIndex } from "./repoIndex";
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
- indexed: index !== null,
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
- /** One-line status string for footers/status bars. */
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 vault = `vault:${status.vault.noteCount}`;
52
- if (!status.repository) return `🕸️ ${vault}`;
53
- if (!status.repository.indexed) return `🕸️ ${vault} · repo:unindexed`;
54
- const mark = status.repository.staleness.state === "fresh" ? "ok" : status.repository.staleness.state;
55
- return `🕸️ ${vault} · ${status.repository.name}:${mark}`;
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
- if (!repo.indexed) {
74
- lines.push(" not indexed — run /weave-scan or ask your agent to explore");
75
- return lines.join("\n");
76
- }
77
- lines.push(` index: ${repo.staleness.state}`);
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 { buildRepoIndex, summarizeIndex, writeRepoIndex } from "./repoIndex";
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: "Refresh the index; use 'deep' or 'sessions [path]' for model-backed scans",
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. Light index only.");
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 deep or session scan",
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) {
@@ -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: ["status", "scan", "overview"] },
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, progress?: (text: string) => Promise<void>) => executeRepoAction(input as RepoActionInput, cwd, progress),
41
+ execute: (input: unknown, cwd: string) => executeRepoAction(input as RepoActionInput, cwd),
42
42
  },
43
43
  ];
@@ -55,13 +55,7 @@ const weaveTui = {
55
55
  async setup(context) {
56
56
  const rpc = context.client.rpc(WEAVE_RPC);
57
57
  try {
58
- const initial = await rpc.status({});
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
- buildRepoIndex,
5
+ ensureRepoIndex,
6
6
  findGitRoot,
7
7
  formatDashboard,
8
8
  formatStatusLine,
9
9
  getWorkspaceStatus,
10
- summarizeIndex,
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 by default (weave-workspace §13);
35
- // `tui` selects the in-terminal explorer. The browser workspace owns a
36
- // server, a file watcher and live SSE streams for the rest of the session,
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
- const suffix = port === null ? "" : ` · web:${port}`;
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 TUI explorer owns nothing session-scoped, but the browser workspace
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
- updateStatus(ctx);
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 ('tui' for the in-terminal explorer, '--no-open' to just print the URL)",
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 === null) {
142
- ctx.ui.notify(WEAVE_VIEW_USAGE, "warning");
138
+ if (parsed === "retired-tui") {
139
+ ctx.ui.notify(WEAVE_VIEW_TUI_RETIRED, "warning");
143
140
  return;
144
141
  }
145
- if (parsed.surface === "tui") {
146
- await runWeaveViewTui(ctx);
142
+ if (parsed === null) {
143
+ ctx.ui.notify(WEAVE_VIEW_USAGE, "warning");
147
144
  return;
148
145
  }
149
- const outcome = await web.run(ctx, { open: parsed.open });
150
- // A browser that would not launch on a machine that has a terminal
151
- // still leaves the user somewhere to go. The server stays up — the URL
152
- // in the notification remains valid — and the TUI opens on top of it.
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
- "Build or refresh the repository index; 'deep' summarizes files; 'sessions [dir]' summarizes session history",
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
- const index = await buildRepoIndex(root);
185
- if (!index) {
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
- await writeRepoIndex(root, index);
190
- ctx.ui.notify(`pi-weave: index refreshed\n${summarizeIndex(index).join("\n")}`, "info");
191
-
192
- if (mode.toLowerCase() === "deep") {
193
- if (inFlightDeepScans.has(root)) {
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
- updateStatus(ctx, formatStatusLine(status));
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 deep or sessions run",
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 WEAVE_VIEW_USAGE = "usage: /weave-view [tui|web] [--no-open]";
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
- /** Which explorer `/weave-view` was asked for. */
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
- surface: "tui" | "web";
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, or `null` for "print the usage".
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
- * Bare `/weave-view` is the **browser** workspace (weave-workspace §13):
257
- * the default is the thing most people want most of the time, and `tui` is
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
- let surface: "tui" | "web" = "web";
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
- if (surface === "tui" && !open) return null;
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 and restored when it settles.
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. Light index only.",
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
- // Unlike the deep scan (whose settled status is precomputed to avoid git
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
  );
@@ -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: 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" })),
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" })),
@@ -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(["status", "scan", "overview"] as const),
17
+ action: StringEnum(["overview"] as const),
19
18
  }),
20
- async execute(_toolCallId, params, _signal, onUpdate, ctx) {
21
- const result = await executeRepoAction(params, ctx.cwd, (text) => {
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
  });