pi-weave 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -37
- package/package.json +17 -5
- package/skills/weave-notepad/SKILL.md +13 -4
- package/src/core/cache/workspace.ts +466 -0
- package/src/core/concurrency.ts +36 -0
- package/src/core/frontmatter.ts +270 -23
- package/src/core/git.ts +19 -0
- package/src/core/graph/build.ts +97 -5
- package/src/core/graph/current.ts +41 -28
- package/src/core/graph/mentions.ts +170 -0
- package/src/core/graph/model.ts +39 -0
- package/src/core/graph/wikilinks.ts +5 -1
- package/src/core/index.ts +14 -0
- package/src/core/openInEditor.ts +69 -0
- package/src/core/paths.ts +7 -0
- package/src/core/sessions.ts +929 -0
- package/src/core/summaries.ts +1 -19
- package/src/core/types.ts +40 -0
- package/src/core/vault.ts +739 -57
- package/src/core/view/cluster.ts +262 -0
- package/src/core/view/detail.ts +118 -0
- package/src/core/view/focus.ts +109 -0
- package/src/core/view/health.ts +156 -0
- package/src/core/view/index.ts +15 -0
- package/src/core/view/links.ts +105 -0
- package/src/core/view/time.ts +47 -0
- package/src/core/view/tree.ts +269 -0
- package/src/core/view/types.ts +39 -0
- package/src/core/workspace.ts +3 -3
- package/src/pi/index.ts +248 -48
- package/src/pi/sessionScan.ts +104 -0
- package/src/pi/summarize.ts +24 -4
- package/src/pi/tools/noteTool.ts +13 -4
- package/src/pi/viewer/tui/branding.ts +8 -7
- package/src/pi/viewer/tui/explorer.ts +5 -3
- package/src/pi/viewer/tui/model.ts +46 -667
- package/src/pi/viewer/tui/openNote.ts +7 -56
- package/src/pi/viewer/tui/surface/explore.ts +4 -2
- package/src/pi/viewer/web/run.ts +331 -0
- package/src/web/client/api.dom.ts +40 -0
- package/src/web/client/api.ts +472 -0
- package/src/web/client/bootstrap.ts +58 -0
- package/src/web/client/context/context.model.ts +313 -0
- package/src/web/client/dist/app.js +764 -0
- package/src/web/client/graph/Graph.tsx +209 -0
- package/src/web/client/graph/column.model.ts +372 -0
- package/src/web/client/graph/dynamics.ts +176 -0
- package/src/web/client/graph/graph.model.ts +546 -0
- package/src/web/client/graph/positions.ts +380 -0
- package/src/web/client/graph/project.ts +153 -0
- package/src/web/client/graph/renderer.dom.ts +52 -0
- package/src/web/client/graph/renderer.ts +339 -0
- package/src/web/client/graph/scheme.ts +44 -0
- package/src/web/client/live.model.ts +275 -0
- package/src/web/client/live.ts +151 -0
- package/src/web/client/main.tsx +27 -0
- package/src/web/client/note/Editor.tsx +102 -0
- package/src/web/client/note/Note.tsx +113 -0
- package/src/web/client/note/editor.controller.ts +151 -0
- package/src/web/client/note/editor.model.ts +636 -0
- package/src/web/client/note/note.model.ts +738 -0
- package/src/web/client/search/SearchPalette.tsx +105 -0
- package/src/web/client/search/search.model.ts +588 -0
- package/src/web/client/search/search.ts +107 -0
- package/src/web/client/selection.storage.ts +69 -0
- package/src/web/client/shell/Columns.tsx +161 -0
- package/src/web/client/shell/ContextRail.tsx +87 -0
- package/src/web/client/shell/Divider.tsx +44 -0
- package/src/web/client/shell/FocusTrap.tsx +56 -0
- package/src/web/client/shell/Header.tsx +64 -0
- package/src/web/client/shell/HelpOverlay.tsx +70 -0
- package/src/web/client/shell/Shell.tsx +210 -0
- package/src/web/client/shell/StatusBar.tsx +28 -0
- package/src/web/client/shell/cssvars.ts +70 -0
- package/src/web/client/shell/drag.model.ts +170 -0
- package/src/web/client/shell/focus.model.ts +100 -0
- package/src/web/client/shell/keys.model.ts +453 -0
- package/src/web/client/shell/keys.ts +59 -0
- package/src/web/client/shell/layout.model.ts +526 -0
- package/src/web/client/shell/shell.model.ts +333 -0
- package/src/web/client/shell/theme.ts +490 -0
- package/src/web/client/shell/viewport.ts +29 -0
- package/src/web/client/state.ts +89 -0
- package/src/web/client/tree/Tree.tsx +141 -0
- package/src/web/client/tree/tree.model.ts +674 -0
- package/src/web/client/workspace.ts +278 -0
- package/src/web/server/page.ts +258 -0
- package/src/web/server/routes.ts +987 -0
- package/src/web/server/security.ts +361 -0
- package/src/web/server/server.ts +275 -0
- package/src/web/server/sse.ts +321 -0
- package/src/web/server/watcher.ts +507 -0
- package/src/web/shared/graph.ts +213 -0
- package/src/web/shared/layout.ts +314 -0
- package/src/web/shared/logo.ts +11 -0
- package/src/web/shared/metrics.ts +136 -0
- package/src/web/shared/view.ts +200 -0
- package/src/web/shared/wire.ts +358 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Link and tag derivations over vault/graph data (weave-workspace §3, §4.3).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { GraphEdge } from "../graph/model";
|
|
6
|
+
import type { NoteMeta } from "../types";
|
|
7
|
+
|
|
8
|
+
/** Map each node id → its incoming `links-to` sources (backlinks). */
|
|
9
|
+
export function deriveBacklinks(edges: readonly GraphEdge[]): Map<string, string[]> {
|
|
10
|
+
const out = new Map<string, string[]>();
|
|
11
|
+
for (const e of edges) {
|
|
12
|
+
if (e.kind !== "links-to") continue;
|
|
13
|
+
const list = out.get(e.target);
|
|
14
|
+
if (list) list.push(e.source);
|
|
15
|
+
else out.set(e.target, [e.source]);
|
|
16
|
+
}
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* One tag and every note carrying it (weave-workspace §4.3).
|
|
22
|
+
*
|
|
23
|
+
* `slugs` rather than node ids: `note:` is a graph-internal prefix, and every
|
|
24
|
+
* consumer of this index — the note column's tag chips, `GraphPayload.tags` —
|
|
25
|
+
* addresses notes by slug.
|
|
26
|
+
*/
|
|
27
|
+
export interface TagIndex {
|
|
28
|
+
tag: string;
|
|
29
|
+
slugs: string[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The minimum a note has to expose to be tag-indexed: its identity and its
|
|
34
|
+
* tags.
|
|
35
|
+
*
|
|
36
|
+
* Declared structurally instead of taking `NoteSummary` outright so the
|
|
37
|
+
* function accepts a `Note` (which has a `body` and no `bodyLength`) just as
|
|
38
|
+
* happily as a `NoteSummary`. §4.3 specifies `NoteSummary[]`; this is that
|
|
39
|
+
* signature widened to its actual requirement rather than narrowed past it,
|
|
40
|
+
* and `NoteSummary` still satisfies it exactly.
|
|
41
|
+
*/
|
|
42
|
+
export type TaggedNote = Pick<NoteMeta, "tags"> & { slug: string };
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Invert `note → tags` into `tag → notes` (weave-workspace §4.3).
|
|
46
|
+
*
|
|
47
|
+
* ## Why this takes notes and not a `GraphModel`
|
|
48
|
+
*
|
|
49
|
+
* The obvious alternative is deriving from the graph, since every other
|
|
50
|
+
* function in this directory does. It is the wrong source here, and the
|
|
51
|
+
* reason is worth stating because "derive everything from the graph" is
|
|
52
|
+
* otherwise a good rule.
|
|
53
|
+
*
|
|
54
|
+
* `buildGraph` flattens tags into `detail.tags`, a comma-joined **display**
|
|
55
|
+
* string (`build.ts`: `note.tags.join(", ")`). Recovering an array from it
|
|
56
|
+
* means splitting on `", "` — which is re-parsing a display string into
|
|
57
|
+
* structure, precisely what §4.2/§4.3 forbid `detail` from being used for,
|
|
58
|
+
* and it is lossy besides: a tag containing a comma round-trips wrong, and
|
|
59
|
+
* the tag is simply absent when a note has none. The alternative is to make
|
|
60
|
+
* `GraphNode` carry a real `tags: string[]` alongside the display string,
|
|
61
|
+
* which puts the same fact on the node twice and grows the wire model for
|
|
62
|
+
* every note whether or not anything reads it.
|
|
63
|
+
*
|
|
64
|
+
* Notes are the upstream source of truth. Both callers already hold them —
|
|
65
|
+
* `buildCurrentGraph` and `WorkspaceCache` each read the vault before they
|
|
66
|
+
* build a graph — so deriving here costs no extra I/O and keeps one
|
|
67
|
+
* representation of a tag: the array on the note. The graph's display string
|
|
68
|
+
* stays a projection of that, never an input to it.
|
|
69
|
+
*
|
|
70
|
+
* ## Ordering
|
|
71
|
+
*
|
|
72
|
+
* Count descending, then tag ascending, per §4.3 — the popular tags surface
|
|
73
|
+
* first and ties are alphabetical rather than insertion-ordered. Each `slugs`
|
|
74
|
+
* list is sorted ascending too, so the whole structure is a pure function of
|
|
75
|
+
* the tag/slug *set*: two vaults with the same tags in a different note order
|
|
76
|
+
* produce byte-identical JSON, which is what lets this ride the ETag.
|
|
77
|
+
*
|
|
78
|
+
* The tiebreak is **codepoint** order, not `localeCompare`, which is the one
|
|
79
|
+
* place this file departs from the habit elsewhere in core. `localeCompare`
|
|
80
|
+
* reads the host's default locale and the runtime's ICU build — a Node
|
|
81
|
+
* compiled with small-icu can order `"Arch"` against `"arch"` differently
|
|
82
|
+
* from one with full-icu. Everywhere else that is a cosmetic difference in a
|
|
83
|
+
* list a human is about to read; here the output is hashed into a cache key
|
|
84
|
+
* (§5.3), so an ordering that varies by machine is a correctness bug rather
|
|
85
|
+
* than a preference. Codepoint order is total, locale-free and matches the
|
|
86
|
+
* bare `.sort()` used for `slugs` just below.
|
|
87
|
+
*
|
|
88
|
+
* Duplicate tags within one note collapse (front matter is hand-editable and
|
|
89
|
+
* `tags: [a, a]` is a typo, not two memberships).
|
|
90
|
+
*/
|
|
91
|
+
export function deriveTagIndex(notes: readonly TaggedNote[]): TagIndex[] {
|
|
92
|
+
const bySlug = new Map<string, Set<string>>();
|
|
93
|
+
for (const note of notes) {
|
|
94
|
+
for (const tag of note.tags) {
|
|
95
|
+
const slugs = bySlug.get(tag);
|
|
96
|
+
if (slugs) slugs.add(note.slug);
|
|
97
|
+
else bySlug.set(tag, new Set([note.slug]));
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return [...bySlug]
|
|
101
|
+
.map(([tag, slugs]): TagIndex => ({ tag, slugs: [...slugs].sort() }))
|
|
102
|
+
// Tags come from a Map's keys, so no two are equal and the comparator
|
|
103
|
+
// never needs to return 0 for the tiebreak — `< ? -1 : 1` is total here.
|
|
104
|
+
.sort((a, b) => b.slugs.length - a.slugs.length || (a.tag < b.tag ? -1 : 1));
|
|
105
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Time formatting for the portable view-models (weave-workspace §3).
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { TreeMeta } from "./types";
|
|
6
|
+
|
|
7
|
+
/** Relative human time, mirroring the page's `relTime`. `now` is epoch ms. */
|
|
8
|
+
export function relTime(iso: string | undefined, now: number): string {
|
|
9
|
+
if (!iso) return "";
|
|
10
|
+
const t = Date.parse(iso);
|
|
11
|
+
if (Number.isNaN(t)) return "";
|
|
12
|
+
const s = Math.max(0, Math.floor((now - t) / 1000));
|
|
13
|
+
if (s < 60) return "just now";
|
|
14
|
+
const m = Math.floor(s / 60);
|
|
15
|
+
if (m < 60) return `${m}m ago`;
|
|
16
|
+
const h = Math.floor(m / 60);
|
|
17
|
+
if (h < 24) return `${h}h ago`;
|
|
18
|
+
const d = Math.floor(h / 24);
|
|
19
|
+
if (d < 30) return `${d}d ago`;
|
|
20
|
+
const mo = Math.floor(d / 30);
|
|
21
|
+
if (mo < 12) return `${mo}mo ago`;
|
|
22
|
+
return `${Math.floor(mo / 12)}y ago`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** Short-sha length used when abbreviating a commit for display. */
|
|
26
|
+
const SHORT_SHA_LEN = 7;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Render a `TreeMeta` as a single plain-text annotation.
|
|
30
|
+
*
|
|
31
|
+
* This reproduces the strings the TUI printed when `TreeRow.meta` was itself a
|
|
32
|
+
* string, so terminal output is unchanged by the structuring. Richer renderers
|
|
33
|
+
* (the browser workspace) should switch on the union instead of calling this.
|
|
34
|
+
*/
|
|
35
|
+
export function formatTreeMeta(meta: TreeMeta, now: number): string {
|
|
36
|
+
if (meta === null) return "";
|
|
37
|
+
switch (meta.kind) {
|
|
38
|
+
case "relTime":
|
|
39
|
+
return relTime(meta.iso, now);
|
|
40
|
+
case "count":
|
|
41
|
+
return meta.phrasing === "attribute" ? `${meta.unit}=${meta.n}` : `${meta.n} ${meta.unit}`;
|
|
42
|
+
case "commit":
|
|
43
|
+
return meta.sha.slice(0, SHORT_SHA_LEN);
|
|
44
|
+
case "text":
|
|
45
|
+
return meta.text;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* treeRows — the portable Explore/tree view-model (weave-workspace §3).
|
|
3
|
+
*
|
|
4
|
+
* Ported 1:1 from the retired browser page's `listTree`. Pure: it takes a
|
|
5
|
+
* `GraphModel` plus a `TreeState` and returns rows. No renderer concepts —
|
|
6
|
+
* no widths, no truncation, no escape codes. Both the TUI and the browser
|
|
7
|
+
* workspace build their tree column from this.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { GraphModel, GraphNode, NodeKind } from "../graph/model";
|
|
11
|
+
import type { NoteSource } from "../types";
|
|
12
|
+
import type { SelectableRow, TreeMeta } from "./types";
|
|
13
|
+
|
|
14
|
+
/** One row in the Explore tree. */
|
|
15
|
+
export interface TreeRow extends SelectableRow {
|
|
16
|
+
depth: number;
|
|
17
|
+
hasKids: boolean;
|
|
18
|
+
expanded: boolean;
|
|
19
|
+
label: string;
|
|
20
|
+
kind: NodeKind;
|
|
21
|
+
provenance: NoteSource | null;
|
|
22
|
+
/** Structured trailing annotation; renderers format it (see formatTreeMeta). */
|
|
23
|
+
meta: TreeMeta;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface TreeState {
|
|
27
|
+
/** Expanded node ids (when no filter is active). */
|
|
28
|
+
expanded: ReadonlySet<string>;
|
|
29
|
+
/** Reveal repo plumbing (gitState/external/package/entryPoint). */
|
|
30
|
+
showInternals: boolean;
|
|
31
|
+
/** Provenance filter (null = all). */
|
|
32
|
+
provFilter: NoteSource | null;
|
|
33
|
+
/** Substring filter on node labels (case-insensitive). */
|
|
34
|
+
query: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Disambiguate labels that would otherwise read as the same entry twice
|
|
38
|
+
* (a remote URL whose tail matches the repo name, an npm package named
|
|
39
|
+
* after the repo). Mirrors the page's `listLabel`. */
|
|
40
|
+
export function listLabel(node: GraphNode): string {
|
|
41
|
+
if (node.kind === "external" && node.detail.url) {
|
|
42
|
+
const u = node.detail.url
|
|
43
|
+
.replace(/^[a-z]+:\/\//, "")
|
|
44
|
+
.replace(/^[^@\/]+@/, "")
|
|
45
|
+
.replace(/\.git$/, "")
|
|
46
|
+
.replace(/\/+$/, "");
|
|
47
|
+
if (u) return u;
|
|
48
|
+
}
|
|
49
|
+
if (node.kind === "package" && node.detail.manifest) {
|
|
50
|
+
return `${node.label} (${node.detail.manifest})`;
|
|
51
|
+
}
|
|
52
|
+
return node.label;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Build the contains/anchored-at nesting + entry-point-to-module map. */
|
|
56
|
+
interface TreeIndex {
|
|
57
|
+
byId: Map<string, GraphNode>;
|
|
58
|
+
/** strict contains (used for entry-point → module placement). */
|
|
59
|
+
contains: Map<string, string[]>;
|
|
60
|
+
/** contains + anchored-at (the nesting hierarchy). */
|
|
61
|
+
tree: Map<string, string[]>;
|
|
62
|
+
/** node id → true when it has an incoming contains/anchored-at edge. */
|
|
63
|
+
incoming: Set<string>;
|
|
64
|
+
/** module id → entry-point ids nested under it (by path prefix). */
|
|
65
|
+
moduleEntries: Map<string, string[]>;
|
|
66
|
+
/** roots (nodes with no incoming edge), in node-input order. */
|
|
67
|
+
roots: string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function indexTree(model: GraphModel): TreeIndex {
|
|
71
|
+
const byId = new Map<string, GraphNode>();
|
|
72
|
+
for (const n of model.nodes) byId.set(n.id, n);
|
|
73
|
+
|
|
74
|
+
const contains = new Map<string, string[]>();
|
|
75
|
+
const tree = new Map<string, string[]>();
|
|
76
|
+
const incoming = new Set<string>();
|
|
77
|
+
for (const e of model.edges) {
|
|
78
|
+
if (e.kind === "contains") {
|
|
79
|
+
const list = contains.get(e.source);
|
|
80
|
+
if (list) list.push(e.target);
|
|
81
|
+
else contains.set(e.source, [e.target]);
|
|
82
|
+
}
|
|
83
|
+
if (e.kind === "contains" || e.kind === "anchored-at") {
|
|
84
|
+
const list = tree.get(e.source);
|
|
85
|
+
if (list) list.push(e.target);
|
|
86
|
+
else tree.set(e.source, [e.target]);
|
|
87
|
+
incoming.add(e.target);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Entry points nest under the module whose path is the entry's directory prefix.
|
|
92
|
+
// The caller only invokes this for entryPoint nodes, so the path guard suffices.
|
|
93
|
+
const moduleFor = (entry: GraphNode): string | null => {
|
|
94
|
+
if (!entry.detail.path) return null;
|
|
95
|
+
const p = entry.detail.path;
|
|
96
|
+
let best: string | null = null;
|
|
97
|
+
let bestLen = 0;
|
|
98
|
+
const repoKids = contains.get("repository") ?? [];
|
|
99
|
+
for (const cid of repoKids) {
|
|
100
|
+
const c = byId.get(cid);
|
|
101
|
+
if (c && c.kind === "module" && c.detail.path) {
|
|
102
|
+
const mp = c.detail.path;
|
|
103
|
+
if (p.startsWith(mp + "/") && mp.length > bestLen) {
|
|
104
|
+
best = cid;
|
|
105
|
+
bestLen = mp.length;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return best;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const moduleEntries = new Map<string, string[]>();
|
|
113
|
+
for (const n of model.nodes) {
|
|
114
|
+
if (n.kind !== "entryPoint") continue;
|
|
115
|
+
const m = moduleFor(n);
|
|
116
|
+
if (m) {
|
|
117
|
+
const list = moduleEntries.get(m);
|
|
118
|
+
if (list) list.push(n.id);
|
|
119
|
+
else moduleEntries.set(m, [n.id]);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const roots = model.nodes.filter((n) => !incoming.has(n.id)).map((n) => n.id);
|
|
124
|
+
return { byId, contains, tree, incoming, moduleEntries, roots };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The roots of a graph (nodes with no incoming contains/anchored-at edge). */
|
|
128
|
+
export function graphRoots(model: GraphModel): string[] {
|
|
129
|
+
const incoming = new Set<string>();
|
|
130
|
+
for (const e of model.edges) {
|
|
131
|
+
if (e.kind === "contains" || e.kind === "anchored-at") incoming.add(e.target);
|
|
132
|
+
}
|
|
133
|
+
return model.nodes.filter((n) => !incoming.has(n.id)).map((n) => n.id);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Knowledge-first default: hide repo plumbing unless showInternals. */
|
|
137
|
+
function hiddenInternalKind(kind: NodeKind): boolean {
|
|
138
|
+
return kind === "gitState" || kind === "external" || kind === "package" || kind === "entryPoint";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function sortKids(ids: string[], byId: Map<string, GraphNode>): string[] {
|
|
142
|
+
return ids
|
|
143
|
+
.slice()
|
|
144
|
+
.sort((a, b) => {
|
|
145
|
+
const la = listLabel(byId.get(a)!);
|
|
146
|
+
const lb = listLabel(byId.get(b)!);
|
|
147
|
+
return la.localeCompare(lb);
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** A `count` meta when the detail value is numeric, else a verbatim `text`
|
|
152
|
+
* meta — so a hand-built or corrupt graph still displays what it holds. */
|
|
153
|
+
function countMeta(raw: string, unit: string, phrasing: "attribute" | "prose"): TreeMeta {
|
|
154
|
+
const n = Number(raw);
|
|
155
|
+
if (!Number.isFinite(n)) {
|
|
156
|
+
return { kind: "text", text: phrasing === "attribute" ? `${unit}=${raw}` : `${raw} ${unit}` };
|
|
157
|
+
}
|
|
158
|
+
return { kind: "count", n, unit, phrasing };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function metaFor(node: GraphNode): TreeMeta {
|
|
162
|
+
switch (node.kind) {
|
|
163
|
+
case "note":
|
|
164
|
+
return node.detail.updated ? { kind: "relTime", iso: node.detail.updated } : null;
|
|
165
|
+
case "module":
|
|
166
|
+
return node.detail.files ? countMeta(node.detail.files, "files", "attribute") : null;
|
|
167
|
+
case "gitState":
|
|
168
|
+
return node.detail.commit ? { kind: "commit", sha: node.detail.commit } : null;
|
|
169
|
+
case "repository":
|
|
170
|
+
return node.detail.files ? countMeta(node.detail.files, "files", "prose") : null;
|
|
171
|
+
case "package":
|
|
172
|
+
return node.detail.kind ? { kind: "text", text: node.detail.kind } : null;
|
|
173
|
+
case "entryPoint":
|
|
174
|
+
return node.detail.summary ? { kind: "text", text: "summary" } : null;
|
|
175
|
+
default:
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Build the Explore tree rows (port of the page's `listTree`).
|
|
182
|
+
*
|
|
183
|
+
* - Roots: `vault`, `repository` (nodes with no incoming containment edge).
|
|
184
|
+
* - Entry points nest under their prefix module.
|
|
185
|
+
* - Internals hidden by default; the caller toggles `showInternals`.
|
|
186
|
+
* - Filter (query and/or provenance): prune to matches + ancestors, auto-expanding ancestors.
|
|
187
|
+
* - Default expansion (no filter): controlled by `state.expanded`.
|
|
188
|
+
*/
|
|
189
|
+
export function treeRows(model: GraphModel, state: TreeState): TreeRow[] {
|
|
190
|
+
const idx = indexTree(model);
|
|
191
|
+
const { byId, tree, moduleEntries, roots } = idx;
|
|
192
|
+
|
|
193
|
+
const children = (id: string): string[] => {
|
|
194
|
+
const kids = tree.get(id) ?? [];
|
|
195
|
+
// Drop entry points that have a claiming module (they re-nest under it).
|
|
196
|
+
const filtered = kids.filter((kid) => {
|
|
197
|
+
const n = byId.get(kid);
|
|
198
|
+
if (!n || n.kind !== "entryPoint") return true;
|
|
199
|
+
for (const [, entries] of moduleEntries) {
|
|
200
|
+
if (entries.includes(kid)) return false;
|
|
201
|
+
}
|
|
202
|
+
return true;
|
|
203
|
+
});
|
|
204
|
+
let all = filtered.concat(moduleEntries.get(id) ?? []);
|
|
205
|
+
// Skip kids absent from the node set (corrupt/hand-built graphs): the page
|
|
206
|
+
// never produces these, but a defensive filter keeps the tree robust.
|
|
207
|
+
all = all.filter((k) => byId.has(k));
|
|
208
|
+
if (!state.showInternals) {
|
|
209
|
+
all = all.filter((k) => {
|
|
210
|
+
const n = byId.get(k);
|
|
211
|
+
return !(n && hiddenInternalKind(n.kind));
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
return sortKids(all, byId);
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const filtering = state.provFilter !== null || state.query.length > 0;
|
|
218
|
+
|
|
219
|
+
const matches = (node: GraphNode): boolean => {
|
|
220
|
+
if (state.provFilter !== null && node.provenance !== state.provFilter) return false;
|
|
221
|
+
if (state.query.length > 0 && !listLabel(node).toLowerCase().includes(state.query.toLowerCase())) return false;
|
|
222
|
+
return true;
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
// Pass 1: mark visible nodes (self-match or a visible descendant).
|
|
226
|
+
const visible = new Set<string>();
|
|
227
|
+
const mark = (id: string): boolean => {
|
|
228
|
+
const node = byId.get(id)!;
|
|
229
|
+
let any = false;
|
|
230
|
+
for (const k of children(id)) if (mark(k)) any = true;
|
|
231
|
+
const show = matches(node) || any;
|
|
232
|
+
if (show) visible.add(id);
|
|
233
|
+
return show;
|
|
234
|
+
};
|
|
235
|
+
for (const r of roots) mark(r);
|
|
236
|
+
|
|
237
|
+
const rows: TreeRow[] = [];
|
|
238
|
+
const walk = (id: string, depth: number): void => {
|
|
239
|
+
if (!visible.has(id)) return;
|
|
240
|
+
const node = byId.get(id)!;
|
|
241
|
+
const kids = children(id);
|
|
242
|
+
const visibleKids = kids.filter((k) => visible.has(k));
|
|
243
|
+
const expanded = filtering ? visibleKids.length > 0 : state.expanded.has(id);
|
|
244
|
+
rows.push({
|
|
245
|
+
id,
|
|
246
|
+
depth,
|
|
247
|
+
hasKids: kids.length > 0,
|
|
248
|
+
expanded,
|
|
249
|
+
label: listLabel(node),
|
|
250
|
+
kind: node.kind,
|
|
251
|
+
provenance: node.provenance,
|
|
252
|
+
meta: metaFor(node),
|
|
253
|
+
});
|
|
254
|
+
if (expanded) for (const k of visibleKids) walk(k, depth + 1);
|
|
255
|
+
};
|
|
256
|
+
for (const r of roots) walk(r, 0);
|
|
257
|
+
return rows;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Empty-state hint for the tree surface (weave-view-tui-design §5.1). */
|
|
261
|
+
export function treeEmptyHint(model: GraphModel): string | null {
|
|
262
|
+
const vault = model.nodes.find((n) => n.kind === "vault");
|
|
263
|
+
if (!vault) return null;
|
|
264
|
+
// no notes and no repository
|
|
265
|
+
const hasNotes = model.nodes.some((n) => n.kind === "note");
|
|
266
|
+
const hasRepo = model.nodes.some((n) => n.kind === "repository");
|
|
267
|
+
if (!hasNotes && !hasRepo) return "no notes yet — add one with the weave_note tool";
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared row/meta types for the portable view-models (weave-workspace §3).
|
|
3
|
+
*
|
|
4
|
+
* These are *renderer-agnostic*: no terminal widths, no pre-truncation, no
|
|
5
|
+
* DOM. The TUI and the browser workspace both consume them and each applies
|
|
6
|
+
* its own formatting.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** A selectable row in any surface. `target` (when set) is the node id a row
|
|
10
|
+
* jumps to on activation (a link / neighbor). */
|
|
11
|
+
export interface SelectableRow {
|
|
12
|
+
id: string;
|
|
13
|
+
/** Node id this row jumps to on activation (links/backlinks/neighbors). */
|
|
14
|
+
target?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The dim trailing annotation on a tree row, as structured data.
|
|
19
|
+
*
|
|
20
|
+
* Deliberately *not* a formatted string: a terminal column, an HTML `<span>`,
|
|
21
|
+
* and a `<time>` element each want something different, and a relative time
|
|
22
|
+
* has to be re-derived as the clock moves. `formatTreeMeta` (view/time.ts)
|
|
23
|
+
* reproduces the terminal phrasing; other renderers are free to ignore it.
|
|
24
|
+
*/
|
|
25
|
+
export type TreeMeta =
|
|
26
|
+
/** Relative timestamp, e.g. a note's `updated`. Rendered against a clock. */
|
|
27
|
+
| { kind: "relTime"; iso: string }
|
|
28
|
+
/**
|
|
29
|
+
* A counted quantity. `phrasing` is a display hint, not a hard rule:
|
|
30
|
+
* `"attribute"` reads as `unit=n` (a property of the row), `"prose"` reads
|
|
31
|
+
* as `n unit` (a description of it).
|
|
32
|
+
*/
|
|
33
|
+
| { kind: "count"; n: number; unit: string; phrasing: "attribute" | "prose" }
|
|
34
|
+
/** A full commit sha; renderers abbreviate to taste. */
|
|
35
|
+
| { kind: "commit"; sha: string }
|
|
36
|
+
/** An opaque, already-human string (a package kind, a marker word). */
|
|
37
|
+
| { kind: "text"; text: string }
|
|
38
|
+
/** No annotation. */
|
|
39
|
+
| null;
|
package/src/core/workspace.ts
CHANGED
|
@@ -49,10 +49,10 @@ export async function getWorkspaceStatus(cwd: string, options: WorkspaceOptions
|
|
|
49
49
|
/** One-line status string for footers/status bars. */
|
|
50
50
|
export function formatStatusLine(status: WorkspaceStatus): string {
|
|
51
51
|
const vault = `vault:${status.vault.noteCount}`;
|
|
52
|
-
if (!status.repository) return
|
|
53
|
-
if (!status.repository.indexed) return
|
|
52
|
+
if (!status.repository) return `🕸️ ${vault}`;
|
|
53
|
+
if (!status.repository.indexed) return `🕸️ ${vault} · repo:unindexed`;
|
|
54
54
|
const mark = status.repository.staleness.state === "fresh" ? "ok" : status.repository.staleness.state;
|
|
55
|
-
return
|
|
55
|
+
return `🕸️ ${vault} · ${status.repository.name}:${mark}`;
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
/** Multi-line dashboard used by the /weave command and notifications. */
|