pi-weave 0.1.5 → 0.1.7
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 +30 -49
- package/package.json +3 -1
- package/skills/weave-explore/SKILL.md +18 -30
- package/skills/weave-notepad/SKILL.md +39 -36
- package/src/pi/index.ts +7 -17
- package/src/pi/viewer/tui/bodyStore.ts +98 -0
- package/src/pi/viewer/tui/branding.ts +154 -0
- package/src/pi/viewer/tui/explorer.ts +16 -29
- package/src/pi/viewer/tui/openNote.ts +60 -0
- package/src/pi/viewer/tui/run.ts +13 -4
- package/src/pi/viewer/tui/surface/base.ts +182 -0
- package/src/pi/viewer/tui/surface/detail.ts +315 -0
- package/src/pi/viewer/tui/surface/explore.ts +204 -0
- package/src/pi/viewer/tui/surface/focus.ts +139 -0
- package/src/pi/viewer/tui/surface/health.ts +102 -0
- package/src/pi/viewer/tui/workspace.ts +381 -0
- package/src/pi/viewer/tui/workspaceRoot.ts +530 -0
- package/src/pi/viewer/browser.ts +0 -29
- package/src/pi/viewer/page.ts +0 -1316
- package/src/pi/viewer/server.ts +0 -229
package/README.md
CHANGED
|
@@ -13,21 +13,15 @@
|
|
|
13
13
|
|
|
14
14
|
**An agent-native knowledge workspace for your life and your code.**
|
|
15
15
|
|
|
16
|
-
pi-weave is a [pi](https://github.com/earendil-works/pi)
|
|
17
|
-
extension with two faces that are secretly one:
|
|
16
|
+
pi-weave is a [pi](https://github.com/earendil-works/pi) extension with two faces that are secretly one:
|
|
18
17
|
|
|
19
|
-
1. **A smart notepad with AI skills.** A persistent vault of knowledge —
|
|
20
|
-
|
|
21
|
-
front matter under `~/.okf/notes/`. Your agent reads and writes it *with*
|
|
22
|
-
you; everything is editable by hand in any editor.
|
|
18
|
+
1. **A smart notepad with AI skills.** A persistent vault of knowledge — decisions, ideas, people, meetings — stored as plain Markdown notes
|
|
19
|
+
with front matter under `~/.okf/notes/`. Your agent reads and writes it *with* you; everything is editable by hand in any editor.
|
|
23
20
|
|
|
24
|
-
2. **A repository exploration engine.** A derived, git-aware knowledge index
|
|
25
|
-
|
|
26
|
-
packages, modules, entry points, and staleness state. Rebuildable,
|
|
27
|
-
disposable, never the source of truth.
|
|
21
|
+
2. **A repository exploration engine.** A derived, git-aware knowledge index of the repo you're in, living at `<repo>/.okf/` — structure,
|
|
22
|
+
languages, packages, modules, entry points, and staleness state. Rebuildable, disposable, never the source of truth.
|
|
28
23
|
|
|
29
|
-
And one rule across both: **everything is equally usable by humans and
|
|
30
|
-
agents.** No opaque databases. No lock-in formats.
|
|
24
|
+
And one rule across both: **everything is equally usable by humans and agents.** No opaque databases. No lock-in formats.
|
|
31
25
|
|
|
32
26
|
```
|
|
33
27
|
🧵 vault:12 · my-project:ok ← pi's status line when weave is active
|
|
@@ -43,47 +37,37 @@ pi install /path/to/pi-weave # local path
|
|
|
43
37
|
|
|
44
38
|
Or for development: `pi -e ./src/pi/index.ts`.
|
|
45
39
|
|
|
46
|
-
The package is [published on npm](https://www.npmjs.com/package/pi-weave);
|
|
47
|
-
|
|
48
|
-
([Publish workflow](.github/workflows/publish.yml)).
|
|
40
|
+
The package is [published on npm](https://www.npmjs.com/package/pi-weave); releases are cut from `main` and auto-published with provenance
|
|
41
|
+
attestation ([Publish workflow](.github/workflows/publish.yml)).
|
|
49
42
|
|
|
50
43
|
## What you get
|
|
51
44
|
|
|
52
45
|
| Surface | Name | Purpose |
|
|
53
46
|
|---|---|---|
|
|
54
|
-
| Tool | `weave_note` | list / get / add / append / search vault notes |
|
|
47
|
+
| Tool | `weave_note` | list / get / add / append / finalize / search vault notes |
|
|
55
48
|
| Tool | `weave_repo` | status / scan / overview of the `.okf` repo index |
|
|
56
49
|
| Command | `/weave` | workspace dashboard (vault + repository) |
|
|
57
50
|
| Command | `/weave-scan` | build/refresh the repository index (light) |
|
|
58
51
|
| Command | `/weave-scan deep` | light index + model-summarized sidecars (opt-in, incremental) |
|
|
59
|
-
| Command | `/weave-
|
|
60
|
-
| Command | `/weave-view
|
|
52
|
+
| Command | `/weave-scan-cancel` | stop an in-flight `/weave-scan deep` run |
|
|
53
|
+
| Command | `/weave-view` | explore the local knowledge graph in-terminal (keyboard) |
|
|
61
54
|
| Skill | `weave-notepad` | how the agent should take good notes |
|
|
62
55
|
| Skill | `weave-explore` | how the agent should explore repositories |
|
|
63
56
|
|
|
64
|
-
**`/weave-view`**
|
|
65
|
-
and
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
`WeaveExplorer` component. See `docs/weave-view-tui-design.md`.
|
|
79
|
-
|
|
80
|
-
On session start, pi-weave detects the repository you're in, checks whether
|
|
81
|
-
`.okf` exists and is fresh, and says so in the status line.
|
|
82
|
-
|
|
83
|
-
**`/weave-scan deep`** is the opt-in, incremental deep pass: it refreshes the
|
|
84
|
-
light index and then writes a short model summary per file to
|
|
85
|
-
`.okf/repository/summaries/`, skipping files whose content hash is unchanged
|
|
86
|
-
since their last summary. It costs tokens, so it never runs implicitly.
|
|
57
|
+
**`/weave-view`** explores the knowledge graph in the terminal: an expandable containment tree (Explore), a 1-hop neighborhood
|
|
58
|
+
(Focus), a selected-node detail view with note/`.okf` bodies, and a staleness + link health surface — all keyboard-driven and read-only.
|
|
59
|
+
It reads disk live (never a stale cache) over the `GraphModel` assembled from the vault + repository index; a pure, harness-free view-model
|
|
60
|
+
(`src/pi/viewer/tui/model.ts`) backs the `WeaveExplorer` component. See `docs/weave-view-tui-design.md`.
|
|
61
|
+
|
|
62
|
+
> The earlier in-browser graph viewer (`/weave-view` in a browser) has been retired and is being rebuilt on pixi.js; until then
|
|
63
|
+
> `/weave-view` opens the in-terminal explorer.
|
|
64
|
+
|
|
65
|
+
On session start, pi-weave detects the repository you're in, checks whether `.okf` exists and is fresh, and says so in the status footer — a
|
|
66
|
+
filled `●` marks weave as active (a deep scan spins it while running).
|
|
67
|
+
|
|
68
|
+
**`/weave-scan deep`** is the opt-in, incremental deep pass: it refreshes the light index and then writes a short model summary per file to
|
|
69
|
+
`.okf/repository/summaries/`, skipping files whose content hash is unchanged since their last summary. It costs tokens, so it never runs
|
|
70
|
+
implicitly — and it runs in the background, so `/weave-scan-cancel` can stop it mid-flight.
|
|
87
71
|
|
|
88
72
|
## The formats (why everything is portable)
|
|
89
73
|
|
|
@@ -112,15 +96,13 @@ Repository index (`<repo>/.okf/`):
|
|
|
112
96
|
└── structure.json # languages, packages, modules, entry points
|
|
113
97
|
```
|
|
114
98
|
|
|
115
|
-
The `.okf` index is **derived**: delete it, rescan, lose nothing. By default
|
|
116
|
-
it
|
|
117
|
-
sharing is a deliberate opt-in.
|
|
99
|
+
The `.okf` index is **derived**: delete it, rescan, lose nothing. By default it's excluded from git locally (`.git/info/exclude`);
|
|
100
|
+
committing it for team sharing is a deliberate opt-in.
|
|
118
101
|
|
|
119
102
|
## For other agent harnesses
|
|
120
103
|
|
|
121
|
-
The on-disk artifacts and `src/core` are harness-agnostic by design — Claude
|
|
122
|
-
|
|
123
|
-
skills follow the [Agent Skills standard](https://agentskills.io/specification).
|
|
104
|
+
The on-disk artifacts and `src/core` are harness-agnostic by design — Claude Code and opencode adapters are on the roadmap (docs/design.md
|
|
105
|
+
§21), and the skills follow the [Agent Skills standard](https://agentskills.io/specification).
|
|
124
106
|
|
|
125
107
|
## Development
|
|
126
108
|
|
|
@@ -129,5 +111,4 @@ npm install
|
|
|
129
111
|
npm run check # typecheck + tests with coverage gate (≥95%)
|
|
130
112
|
```
|
|
131
113
|
|
|
132
|
-
See [AGENTS.md](AGENTS.md) for contributor/agent rules and
|
|
133
|
-
[docs/design.md](docs/design.md) for the full design.
|
|
114
|
+
See [AGENTS.md](AGENTS.md) for contributor/agent rules and [docs/design.md](docs/design.md) for the full design.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-weave",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"description": "An agent-native knowledge workspace for your life and your code. Smart notepad + repository exploration, readable by humans and agents alike.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -48,6 +48,8 @@
|
|
|
48
48
|
"coverage": "vitest run --coverage",
|
|
49
49
|
"typecheck": "tsc --noEmit",
|
|
50
50
|
"check": "npm run typecheck && npm run coverage",
|
|
51
|
+
"rewrap:md": "node scripts/rewrap-md.mjs",
|
|
52
|
+
"rewrap:md:check": "node scripts/rewrap-md.mjs --check",
|
|
51
53
|
"prepublishOnly": "npm run check"
|
|
52
54
|
},
|
|
53
55
|
"peerDependencies": {
|
|
@@ -5,45 +5,34 @@ description: Explore a git repository through its pi-weave knowledge index (.okf
|
|
|
5
5
|
|
|
6
6
|
# Weave Explore
|
|
7
7
|
|
|
8
|
-
pi-weave keeps a **derived** knowledge index of the repository at
|
|
9
|
-
|
|
10
|
-
Deleting `.okf` loses nothing — rescan to regenerate it.
|
|
8
|
+
pi-weave keeps a **derived** knowledge index of the repository at `<repo>/.okf/`. Source code is the truth; the index is a rebuildable
|
|
9
|
+
cache. Deleting `.okf` loses nothing — rescan to regenerate it.
|
|
11
10
|
|
|
12
11
|
## Tools
|
|
13
12
|
|
|
14
|
-
In pi, use the `weave_repo` tool (or the `/weave-scan` command). To see
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
In other harnesses, read the JSON documents under `.okf/` directly.
|
|
13
|
+
In pi, use the `weave_repo` tool (or the `/weave-scan` command). To see the assembled graph in the terminal, run `/weave-view tui` (Explore
|
|
14
|
+
tree, Focus neighborhood, Health surface); `/weave-view` opens the browser viewer. In other harnesses, read the JSON documents under `.okf/`
|
|
15
|
+
directly.
|
|
18
16
|
|
|
19
17
|
## Workflow
|
|
20
18
|
|
|
21
19
|
1. **Check for an index**: `weave_repo` action=status.
|
|
22
|
-
- `missing` → offer to scan (`weave_repo` action=scan), or scan directly
|
|
23
|
-
when the user asked to explore.
|
|
20
|
+
- `missing` → offer to scan (`weave_repo` action=scan), or scan directly when the user asked to explore.
|
|
24
21
|
- `stale` → scan again; the repository moved on.
|
|
25
22
|
- `fresh` → read it: action=overview.
|
|
26
|
-
2. **Start from the overview**: file counts, languages, packages, module
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
the
|
|
32
|
-
|
|
33
|
-
(`.okf/repository/summaries/` exists), read the relevant sidecars first —
|
|
34
|
-
they tell you what a file does and its outward surface in 1–3 sentences,
|
|
35
|
-
so you can decide whether to open the full file at all.
|
|
36
|
-
5. **Answer with structure**: name modules and packages by their indexed
|
|
37
|
-
paths so the user can jump straight to them.
|
|
23
|
+
2. **Start from the overview**: file counts, languages, packages, module groupings, and likely entry points. This replaces dozens of
|
|
24
|
+
`ls`/`find` calls.
|
|
25
|
+
3. **Descend progressively** (design §9): only open files in modules relevant to the user's question. The index gives you the map; the code
|
|
26
|
+
gives you the terrain.
|
|
27
|
+
4. **Read summaries before full files**: if the repo has been deep-scanned (`.okf/repository/summaries/` exists), read the relevant sidecars
|
|
28
|
+
first — they tell you what a file does and its outward surface in 1–3 sentences, so you can decide whether to open the full file at all.
|
|
29
|
+
5. **Answer with structure**: name modules and packages by their indexed paths so the user can jump straight to them.
|
|
38
30
|
|
|
39
31
|
## Deep summaries
|
|
40
32
|
|
|
41
|
-
`/weave-scan deep` creates or refreshes `.okf/repository/summaries/` — one
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
whose content hash changed since their last summary. If summaries are
|
|
45
|
-
missing or stale, offer `/weave-scan deep` to create or refresh them before
|
|
46
|
-
diving into full files.
|
|
33
|
+
`/weave-scan deep` creates or refreshes `.okf/repository/summaries/` — one sidecar per file, written by the session model. It is **opt-in
|
|
34
|
+
and incremental**: it never runs implicitly, and it only re-summarizes files whose content hash changed since their last summary. If
|
|
35
|
+
summaries are missing or stale, offer `/weave-scan deep` to create or refresh them before diving into full files.
|
|
47
36
|
|
|
48
37
|
## On-disk layout
|
|
49
38
|
|
|
@@ -59,6 +48,5 @@ diving into full files.
|
|
|
59
48
|
|
|
60
49
|
## Trust model
|
|
61
50
|
|
|
62
|
-
Everything in `.okf` is machine-generated (`source: generated`). If the user
|
|
63
|
-
|
|
64
|
-
`weave-notepad` skill) as human knowledge, not in the derived index.
|
|
51
|
+
Everything in `.okf` is machine-generated (`source: generated`). If the user corrects an interpretation, that correction belongs in the
|
|
52
|
+
vault (see the `weave-notepad` skill) as human knowledge, not in the derived index.
|
|
@@ -5,21 +5,17 @@ description: "Take and retrieve durable notes in the pi-weave vault. Use when th
|
|
|
5
5
|
|
|
6
6
|
# Weave Notepad
|
|
7
7
|
|
|
8
|
-
The pi-weave vault is the user's long-term memory: plain Markdown notes with
|
|
9
|
-
|
|
10
|
-
you write here, they can read and edit, and vice versa.
|
|
8
|
+
The pi-weave vault is the user's long-term memory: plain Markdown notes with front matter under `~/.okf/notes/`. It is shared with the human
|
|
9
|
+
— anything you write here, they can read and edit, and vice versa.
|
|
11
10
|
|
|
12
11
|
## Tools
|
|
13
12
|
|
|
14
|
-
In pi, use the `weave_note` tool. In other harnesses (or when the tool is not
|
|
15
|
-
available), operate on the files directly:
|
|
13
|
+
In pi, use the `weave_note` tool. In other harnesses (or when the tool is not available), operate on the files directly:
|
|
16
14
|
|
|
17
15
|
- **Notes** live at `~/.okf/notes/<slug>.md` (vault root overridable via `PI_WEAVE_VAULT`).
|
|
18
|
-
- Each note has YAML front matter: `title`, `created`, `updated` (ISO-8601),
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
`finalize` restructures the body *above* the `## Raw` tail and preserves
|
|
22
|
-
the tail verbatim.
|
|
16
|
+
- Each note has YAML front matter: `title`, `created`, `updated` (ISO-8601), `tags: [..]`, and `source: human | agent | generated`.
|
|
17
|
+
- `weave_note` actions: `list`, `get`, `add`, `append`, `finalize`, `search`. `finalize` restructures the body *above* the `## Raw` tail and
|
|
18
|
+
preserves the tail verbatim.
|
|
23
19
|
|
|
24
20
|
## Raw Tail Format
|
|
25
21
|
|
|
@@ -41,49 +37,56 @@ Every note maintains a verbatim, append-only raw section at the bottom separated
|
|
|
41
37
|
|
|
42
38
|
### How to capture and append raw input
|
|
43
39
|
|
|
44
|
-
1. **Divider and Heading**: The raw section starts with `---` followed by `## Raw` and the notice comment: `<!-- NEVER edit below this line.
|
|
40
|
+
1. **Divider and Heading**: The raw section starts with `---` followed by `## Raw` and the notice comment: `<!-- NEVER edit below this line.
|
|
41
|
+
Verbatim user input preserved here. -->`.
|
|
45
42
|
2. **Code Blocks for Verbatim Input**: Always wrap verbatim user lines inside code blocks (triple backticks).
|
|
46
43
|
3. **Date and Time on Appends**: When appending subsequent snippets, prepend each snippet with: `<!-- appended YYYY-MM-DD HH:MM -->`.
|
|
47
44
|
4. **Finalization (`finalize`)**:
|
|
48
45
|
- `finalize` replaces or structures content only above the `---` and `## Raw` section.
|
|
49
46
|
- The `## Raw` block and all verbatim code blocks are never modified or removed.
|
|
50
47
|
|
|
48
|
+
## Dictation mode (continuous compile)
|
|
49
|
+
|
|
50
|
+
During live dictation / interview note-taking (see the skill description), Pi does **not** wait until the end to organize the note. Every
|
|
51
|
+
interactive append is immediately compiled into the body:
|
|
52
|
+
|
|
53
|
+
1. **Append the raw words verbatim** to the `## Raw` tail as usual (a dated `<!-- appended YYYY-MM-DD HH:MM -->` code block).
|
|
54
|
+
2. **Then immediately finalize** (`weave_note` action=finalize): rewrite the body *above* the `## Raw` tail — front-loaded summary,
|
|
55
|
+
sections, decisions, questions, tasks, entities, links — so the compiled document reflects everything said so far.
|
|
56
|
+
3. **Never rewrite or remove the `## Raw` tail.** It stays append-only and verbatim; only the body above it changes.
|
|
57
|
+
|
|
58
|
+
The result is a continuously-updated compiled document that stays current throughout the session — not just a raw tail that gets organized
|
|
59
|
+
once at the end.
|
|
60
|
+
|
|
51
61
|
## When to take a note
|
|
52
62
|
|
|
53
|
-
Create a note **only when the user explicitly asks** for one to exist:
|
|
54
|
-
"
|
|
55
|
-
Never promote conversation into a note on your own initiative — capture is
|
|
56
|
-
explicit by design.
|
|
63
|
+
Create a note **only when the user explicitly asks** for one to exist: "start a note on X", "add to the X note", "remember this", "jot that
|
|
64
|
+
down". Never promote conversation into a note on your own initiative — capture is explicit by design.
|
|
57
65
|
|
|
58
66
|
## When NOT to take a note
|
|
59
67
|
|
|
60
|
-
- Anything derivable from the repository itself (that knowledge belongs to
|
|
61
|
-
the `.okf` index, not the vault).
|
|
68
|
+
- Anything derivable from the repository itself (that knowledge belongs to the `.okf` index, not the vault).
|
|
62
69
|
- Session-scratch information (in-progress task state).
|
|
63
70
|
- Secrets, credentials, or anything the user hasn't confirmed is safe to persist.
|
|
64
71
|
|
|
65
72
|
## How to write a good note
|
|
66
73
|
|
|
67
|
-
1. **Search first** (`weave_note` action=search): if a note exists, `append`
|
|
68
|
-
to it rather than creating a duplicate.
|
|
74
|
+
1. **Search first** (`weave_note` action=search): if a note exists, `append` to it rather than creating a duplicate.
|
|
69
75
|
2. Title: short noun phrase ("Auth boundary decision", not "Notes").
|
|
70
|
-
3. **Scribble in, verbatim.** When the user is dictating, append their words
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
file directly in other harnesses). Move nothing out of
|
|
77
|
-
is append-only and never rewritten.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
(the default). Never overwrite a `source: human` note's meaning; append
|
|
83
|
-
with a dated "Agent addendum" section instead.
|
|
76
|
+
3. **Scribble in, verbatim.** When the user is dictating, append their words to the note as rough, verbatim scribbles — no silent rewording.
|
|
77
|
+
Keep them under the `## Raw` tail format at the end of the note.
|
|
78
|
+
4. **Compile continuously during dictation.** After *every* interactive append in dictation mode, immediately finalize the body *above* the
|
|
79
|
+
raw tail so the compiled doc stays current (see [Dictation mode](#dictation-mode-continuous-compile)). Outside dictation mode,
|
|
80
|
+
compilation stays on request.
|
|
81
|
+
5. **Finalize on request.** When the user says "finalize this" / "clean this up", restructure the body *above* the raw tail: front-loaded
|
|
82
|
+
summary, sections, entities, links. Use `weave_note` action=finalize (or edit the file directly in other harnesses). Move nothing out of
|
|
83
|
+
`## Raw` — it is append-only and never rewritten.
|
|
84
|
+
6. Tags: 1–4 lowercase tags; reuse existing tags when possible.
|
|
85
|
+
7. Provenance: notes the user scribbled stay `source: human` (finalization is editorial, not authorship) — pass `source: "human"` to `add`
|
|
86
|
+
for user-scribbled notes. Notes you draft from scratch are `source: agent` (the default). Never overwrite a `source: human` note's
|
|
87
|
+
meaning; append with a dated "Agent addendum" section instead.
|
|
84
88
|
|
|
85
89
|
## Retrieving knowledge
|
|
86
90
|
|
|
87
|
-
Use `weave_note` action=search with the user's key terms, then `get` the best
|
|
88
|
-
|
|
89
|
-
facts about code and flag the discrepancy — the note may be stale intent.
|
|
91
|
+
Use `weave_note` action=search with the user's key terms, then `get` the best hits. When a note and the repository index disagree, trust the
|
|
92
|
+
repository for facts about code and flag the discrepancy — the note may be stale intent.
|
package/src/pi/index.ts
CHANGED
|
@@ -12,8 +12,6 @@ import {
|
|
|
12
12
|
import { registerNoteTool } from "./tools/noteTool";
|
|
13
13
|
import { registerRepoTool } from "./tools/repoTool";
|
|
14
14
|
import { deepScanRepository, formatDeepScanResult } from "./summarize";
|
|
15
|
-
import { openInBrowser } from "./viewer/browser";
|
|
16
|
-
import { startViewer, type ViewerServer } from "./viewer/server";
|
|
17
15
|
import { runWeaveViewTui } from "./viewer/tui/run";
|
|
18
16
|
|
|
19
17
|
/**
|
|
@@ -29,9 +27,9 @@ export default function piWeave(pi: ExtensionAPI): void {
|
|
|
29
27
|
registerNoteTool(pi);
|
|
30
28
|
registerRepoTool(pi);
|
|
31
29
|
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
|
|
30
|
+
// /weave-view is the in-terminal explorer (the old browser viewer was
|
|
31
|
+
// retired for a pixi.js rewrite); it holds no session resources, so there
|
|
32
|
+
// is no per-session lifecycle to tear down.
|
|
35
33
|
let lastCtx: ExtensionContext | ExtensionCommandContext | null = null;
|
|
36
34
|
let lastStatusText: string | undefined = undefined;
|
|
37
35
|
let isActive = false;
|
|
@@ -60,9 +58,7 @@ export default function piWeave(pi: ExtensionAPI): void {
|
|
|
60
58
|
}
|
|
61
59
|
|
|
62
60
|
pi.on("session_shutdown", async () => {
|
|
63
|
-
|
|
64
|
-
viewer = null;
|
|
65
|
-
await server?.stop();
|
|
61
|
+
// The TUI explorer owns no session-scoped resources; nothing to stop.
|
|
66
62
|
});
|
|
67
63
|
|
|
68
64
|
pi.on("agent_start", async (_event, ctx) => {
|
|
@@ -101,20 +97,14 @@ export default function piWeave(pi: ExtensionAPI): void {
|
|
|
101
97
|
});
|
|
102
98
|
|
|
103
99
|
pi.registerCommand("weave-view", {
|
|
104
|
-
description: "Open the
|
|
100
|
+
description: "Open the in-terminal knowledge-graph explorer (vault + repository); reads from disk live",
|
|
105
101
|
handler: async (args, ctx) => {
|
|
106
102
|
const arg = args.trim().toLowerCase();
|
|
107
|
-
if (arg
|
|
108
|
-
await runWeaveViewTui(ctx);
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
if (arg !== "") {
|
|
103
|
+
if (arg !== "" && arg !== "tui") {
|
|
112
104
|
ctx.ui.notify("usage: /weave-view [tui]", "warning");
|
|
113
105
|
return;
|
|
114
106
|
}
|
|
115
|
-
|
|
116
|
-
ctx.ui.notify(`pi-weave viewer: ${viewer.url} (reads from disk live; refresh the page any time)`, "info");
|
|
117
|
-
await openInBrowser(pi, ctx, viewer.url);
|
|
107
|
+
await runWeaveViewTui(ctx);
|
|
118
108
|
},
|
|
119
109
|
});
|
|
120
110
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BodyStore — shared note/.okf body cache (weave-view-tui-v2 §6, §9.1).
|
|
3
|
+
*
|
|
4
|
+
* The v1 WeaveExplorer cached bodies in private maps. v2 lifts that cache into
|
|
5
|
+
* a per-session store so every pane (e.g. two Detail panes reading the same
|
|
6
|
+
* note) shares one fetch per node id. Behavior is identical to v1's
|
|
7
|
+
* per-explorer cache: a body load is kicked off once per id, in-flight loads
|
|
8
|
+
* are deduped, and a refresh busts the cache so the next read re-fetches.
|
|
9
|
+
*
|
|
10
|
+
* The store is harness-free (takes injected loaders + an optional
|
|
11
|
+
* onChange callback), so it is unit-tested with fake loaders exactly like
|
|
12
|
+
* v1's body tests.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ViewNote } from "../../../core/graph/current";
|
|
16
|
+
|
|
17
|
+
/** The body loaders a BodyStore is bound to (bound to vault/cwd by run.ts). */
|
|
18
|
+
export interface BodyLoaders {
|
|
19
|
+
loadNote: (slug: string) => Promise<ViewNote | null>;
|
|
20
|
+
loadOkf: (rel: string) => Promise<{ path: string; body: string } | null>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface BodyStoreOptions {
|
|
24
|
+
loaders: BodyLoaders;
|
|
25
|
+
/** Invoked (no args) when an async load resolves so the owner can re-render. */
|
|
26
|
+
onChange?: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A cache+dedup keyed by node id. `get` never throws and never double-loads.
|
|
31
|
+
*
|
|
32
|
+
* - `null` body = the node exists but has no loadable body (or it loaded to
|
|
33
|
+
* null); the pane renders nothing for it.
|
|
34
|
+
* - `undefined` = not yet requested (the pane shows a placeholder and asks the
|
|
35
|
+
* store to load, mirroring v1's `bodyLinesFor`).
|
|
36
|
+
* - `isLoading(id)` = an in-flight request is outstanding (pane shows "loading").
|
|
37
|
+
*/
|
|
38
|
+
export class BodyStore {
|
|
39
|
+
private readonly loaders: BodyLoaders;
|
|
40
|
+
private readonly onChange: (() => void) | undefined;
|
|
41
|
+
private cache = new Map<string, string | null>();
|
|
42
|
+
private loading = new Set<string>();
|
|
43
|
+
|
|
44
|
+
constructor(opts: BodyStoreOptions) {
|
|
45
|
+
this.loaders = opts.loaders;
|
|
46
|
+
this.onChange = opts.onChange;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** True when the id is mid-load. */
|
|
50
|
+
isLoading(id: string): boolean {
|
|
51
|
+
return this.loading.has(id);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Cached body for the id, or undefined when not yet requested. */
|
|
55
|
+
get(id: string): string | null | undefined {
|
|
56
|
+
return this.cache.get(id);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** True once a body has been requested for the id (cached or failed). */
|
|
60
|
+
has(id: string): boolean {
|
|
61
|
+
return this.cache.has(id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Kick off a load for `id` if not already requested/in-flight. Returns true
|
|
66
|
+
* when a load was started (so the caller can show a placeholder). The load
|
|
67
|
+
* uses `kind` (note/file) to pick the loader and `ref` (slug/rel path).
|
|
68
|
+
*/
|
|
69
|
+
load(id: string, kind: "note" | "file", ref: string | undefined): boolean {
|
|
70
|
+
if (ref === undefined) return false;
|
|
71
|
+
if (this.cache.has(id) || this.loading.has(id)) return false;
|
|
72
|
+
this.loading.add(id);
|
|
73
|
+
if (kind === "note") {
|
|
74
|
+
void this.loaders
|
|
75
|
+
.loadNote(ref)
|
|
76
|
+
.then((note) => this.finish(id, note?.body ?? null))
|
|
77
|
+
.catch(() => this.finish(id, null));
|
|
78
|
+
} else {
|
|
79
|
+
void this.loaders
|
|
80
|
+
.loadOkf(ref)
|
|
81
|
+
.then((file) => this.finish(id, file?.body ?? null))
|
|
82
|
+
.catch(() => this.finish(id, null));
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Drop all cached bodies and in-flight markers (the `r` refresh). */
|
|
88
|
+
clear(): void {
|
|
89
|
+
this.cache.clear();
|
|
90
|
+
this.loading.clear();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private finish(id: string, body: string | null): void {
|
|
94
|
+
this.cache.set(id, body);
|
|
95
|
+
this.loading.delete(id);
|
|
96
|
+
this.onChange?.();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* branding.ts — weave-view TUI logo & wordmark tiers (weave-view-tui-v2 §5).
|
|
3
|
+
*
|
|
4
|
+
* Renders the brand as a small, casual in-bar mark. Three tiers, auto-selected
|
|
5
|
+
* once per session from terminal capability and cached:
|
|
6
|
+
*
|
|
7
|
+
* 1. "kitty" — the real raster logo via pi-tui's `Image` component (a small
|
|
8
|
+
* ~2×2-cell favicon). The raw JPG is bundled and base64'd once.
|
|
9
|
+
* 2. "glyph" — a tiny curated Unicode glyph derived from the logo silhouette.
|
|
10
|
+
* 3. "plain" — `🧵` + wordmark, forced by PI_WEAVE_TUI_PLAIN.
|
|
11
|
+
*
|
|
12
|
+
* The line-art/unicode constants contain no ESC by construction (decision 1:
|
|
13
|
+
* the representation fallback is a curated constant, never generated at runtime).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { detectCapabilities, getCapabilities, Image, type Component } from "@earendil-works/pi-tui";
|
|
17
|
+
import type { ThemeSlot } from "./theme";
|
|
18
|
+
|
|
19
|
+
/** Logo render tier. */
|
|
20
|
+
export type LogoTier = "kitty" | "glyph" | "plain";
|
|
21
|
+
|
|
22
|
+
/** A tiny Unicode mark derived from the logo silhouette (decision 1 fallback). */
|
|
23
|
+
export const MARK_GLYPH = "◈";
|
|
24
|
+
/** Absolute last-resort mark (also the forced plain tier). */
|
|
25
|
+
export const PLAIN_MARK = "🧵";
|
|
26
|
+
/** The wordmark shown after the mark. */
|
|
27
|
+
export const WORDMARK = "weave view";
|
|
28
|
+
|
|
29
|
+
/** Env var that forces the plain tier. */
|
|
30
|
+
export const PLAIN_ENV = "PI_WEAVE_TUI_PLAIN";
|
|
31
|
+
|
|
32
|
+
/** MIME of the bundled raster logo asset (downscaled JPEG of the logo). */
|
|
33
|
+
export const LOGO_MIME = "image/jpeg";
|
|
34
|
+
|
|
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`
|
|
38
|
+
* (Kitty graphics) as a small ~2×2-cell favicon in the header strip — a casual
|
|
39
|
+
* in-bar mark, not a hero splash. Stored base64 so the render path needs no
|
|
40
|
+
* fs/path resolution and the asset is trivially unit-testable.
|
|
41
|
+
*/
|
|
42
|
+
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
|
+
|
|
45
|
+
/** Terminal capability surface branding probes. */
|
|
46
|
+
export interface BrandCapabilities {
|
|
47
|
+
/** Kitty graphics protocol available. */
|
|
48
|
+
kitty: boolean;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Pure tier selection; exported for direct unit testing. */
|
|
52
|
+
export function logoTierFor(caps: BrandCapabilities, forcePlain: boolean): LogoTier {
|
|
53
|
+
if (forcePlain) return "plain";
|
|
54
|
+
if (caps.kitty) return "kitty";
|
|
55
|
+
return "glyph";
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve the environment (PI_WEAVE_TUI_PLAIN) to a plain flag. Exported as a
|
|
60
|
+
* small pure function so the env read is testable in isolation.
|
|
61
|
+
*/
|
|
62
|
+
export function plainEnv(env: Record<string, string | undefined> = process.env): boolean {
|
|
63
|
+
const v = env[PLAIN_ENV];
|
|
64
|
+
return v !== undefined && v !== "" && v !== "0";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Session-level probe cache (probed once, reused for the whole session §5.1).
|
|
68
|
+
let cachedTier: LogoTier | null = null;
|
|
69
|
+
let cachedCaps: BrandCapabilities | null = null;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Return the session-cached capabilities (probed once). The probe runs at most
|
|
73
|
+
* once per process; subsequent calls reuse the cache. `getCaps` is injectable
|
|
74
|
+
* for tests (defaults to a live kitty probe). Never throws.
|
|
75
|
+
*/
|
|
76
|
+
export function probeGraphics(getCaps: () => BrandCapabilities): BrandCapabilities {
|
|
77
|
+
if (cachedCaps) return cachedCaps;
|
|
78
|
+
try {
|
|
79
|
+
cachedCaps = getCaps();
|
|
80
|
+
} catch {
|
|
81
|
+
cachedCaps = { kitty: false };
|
|
82
|
+
}
|
|
83
|
+
return cachedCaps;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Resolve the session logo tier, probing terminal capability once and caching
|
|
88
|
+
* for the process. `env` is injectable for tests.
|
|
89
|
+
*/
|
|
90
|
+
export function logoTier(env: Record<string, string | undefined> = process.env): LogoTier {
|
|
91
|
+
if (cachedTier) return cachedTier;
|
|
92
|
+
const caps = probeGraphics(() => getBrandCapabilities());
|
|
93
|
+
cachedTier = logoTierFor(caps, plainEnv(env));
|
|
94
|
+
return cachedTier;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Reset the session probe cache (test seam). */
|
|
98
|
+
export function resetBrandCache(): void {
|
|
99
|
+
cachedTier = null;
|
|
100
|
+
cachedCaps = null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** A live kitty-capability probe backed by pi-tui's terminal capability query. */
|
|
104
|
+
export function getBrandCapabilities(): BrandCapabilities {
|
|
105
|
+
return { kitty: detectCapabilities().images === "kitty" };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The mark rendered as a single header line (≤ width). This is the string the
|
|
110
|
+
* header strip / empty state embed. For the kitty tier the mark glyph is used
|
|
111
|
+
* in the text strip; the real raster is emitted via `logoImage()` when a kitty
|
|
112
|
+
* surface is available.
|
|
113
|
+
*/
|
|
114
|
+
export function renderMark(tier: LogoTier, theme: { fg: (slot: ThemeSlot, text: string) => string }, width: number): string {
|
|
115
|
+
let mark: string;
|
|
116
|
+
if (tier === "plain") {
|
|
117
|
+
mark = theme.fg("muted", PLAIN_MARK);
|
|
118
|
+
} else {
|
|
119
|
+
// kitty and glyph tiers share the curated glyph in the text strip.
|
|
120
|
+
mark = theme.fg("accent", MARK_GLYPH);
|
|
121
|
+
}
|
|
122
|
+
return mark.slice(0, Math.max(1, width));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* A small pi-tui `Image` component for the kitty tier (decision 1 favicon).
|
|
127
|
+
* Takes the base64 JPG + mime; renders at ~2×2 cells. Returns a component that
|
|
128
|
+
* emits the Kitty sequence when the terminal supports it, else a text fallback.
|
|
129
|
+
*/
|
|
130
|
+
export function logoImage(
|
|
131
|
+
base64Data: string,
|
|
132
|
+
mimeType: string,
|
|
133
|
+
theme: { fg: (slot: ThemeSlot, text: string) => string },
|
|
134
|
+
): Component {
|
|
135
|
+
return new Image(base64Data, mimeType, { fallbackColor: (t) => theme.fg("text", t) }, {
|
|
136
|
+
maxWidthCells: 2,
|
|
137
|
+
maxHeightCells: 2,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Build the bundled kitty raster logo `Image` (decision 1 favicon). Returns a
|
|
143
|
+
* component only when the terminal supports the Kitty graphics protocol;
|
|
144
|
+
* `null` otherwise so the caller keeps the one-line glyph header. The render
|
|
145
|
+
* path splices this component's lines onto their own row(s) — never inlined
|
|
146
|
+
* into a styled text line. The base64 asset is a compile-time constant, so the
|
|
147
|
+
* only fallback needed is the Kitty gate (no fs/path reads at runtime).
|
|
148
|
+
*/
|
|
149
|
+
export function bundledLogoImage(
|
|
150
|
+
theme: { fg: (slot: ThemeSlot, text: string) => string },
|
|
151
|
+
): Component | null {
|
|
152
|
+
if (getCapabilities().images === "kitty") return logoImage(LOGO_B64, LOGO_MIME, theme);
|
|
153
|
+
return null;
|
|
154
|
+
}
|