pi-weave 0.1.9 → 0.1.11
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/package.json +1 -1
- package/src/web/client/context/context.model.ts +155 -8
- package/src/web/client/dist/app.js +447 -127
- package/src/web/client/graph/Graph.tsx +43 -4
- package/src/web/client/graph/column.model.ts +42 -25
- package/src/web/client/graph/dynamics.ts +16 -5
- package/src/web/client/graph/graph.model.ts +326 -111
- package/src/web/client/graph/positions.ts +17 -4
- package/src/web/client/graph/renderer.ts +30 -6
- package/src/web/client/graph/scheme.ts +51 -6
- package/src/web/client/main.tsx +12 -0
- package/src/web/client/note/Editor.tsx +20 -13
- package/src/web/client/note/Note.tsx +209 -17
- package/src/web/client/note/editor.model.ts +50 -0
- package/src/web/client/note/note.model.ts +312 -1
- package/src/web/client/search/SearchPalette.tsx +11 -2
- package/src/web/client/shell/Columns.tsx +13 -0
- package/src/web/client/shell/ContextRail.tsx +52 -16
- package/src/web/client/shell/Header.tsx +66 -3
- package/src/web/client/shell/Shell.tsx +75 -6
- package/src/web/client/shell/deeplink.model.ts +61 -0
- package/src/web/client/shell/focus.model.ts +14 -6
- package/src/web/client/shell/icons.model.ts +101 -0
- package/src/web/client/shell/keys.model.ts +27 -3
- package/src/web/client/shell/layout.model.ts +12 -38
- package/src/web/client/shell/shell.model.ts +36 -1
- package/src/web/client/shell/theme.model.ts +147 -0
- package/src/web/client/shell/theme.ts +473 -96
- package/src/web/client/state.ts +11 -0
- package/src/web/client/tree/Tree.tsx +48 -5
- package/src/web/client/tree/tree.model.ts +88 -28
- package/src/web/client/workspace.ts +10 -2
- package/src/web/server/page.ts +4 -4
- package/src/web/shared/layout.ts +409 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-weave",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.11",
|
|
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,
|
|
@@ -39,7 +39,8 @@
|
|
|
39
39
|
import type { DetailLinkRow, ViewGraphModel } from "../../shared/view";
|
|
40
40
|
import { deriveBacklinks, detailModel, listLabel } from "../../shared/view";
|
|
41
41
|
import type { GraphPayload, WireGraphNode, WireNodeKind, WireNoteSource } from "../../shared/wire";
|
|
42
|
-
import {
|
|
42
|
+
import { isSessionNote, kindIcon, provenanceGlyph, provenanceTitle, viewModel } from "../tree/tree.model";
|
|
43
|
+
import type { IconName } from "../shell/icons.model";
|
|
43
44
|
|
|
44
45
|
// --- rows and groups ---------------------------------------------------------------
|
|
45
46
|
|
|
@@ -51,7 +52,8 @@ export interface ContextRow {
|
|
|
51
52
|
readonly target: string;
|
|
52
53
|
readonly label: string;
|
|
53
54
|
readonly kind: WireNodeKind;
|
|
54
|
-
|
|
55
|
+
/** Which sprite glyph the row draws; the `.tsx` builds the `<svg>`. */
|
|
56
|
+
readonly kindIcon: IconName;
|
|
55
57
|
readonly provenance: WireNoteSource | null;
|
|
56
58
|
readonly provenanceGlyph: string;
|
|
57
59
|
readonly provenanceTitle: string;
|
|
@@ -63,6 +65,8 @@ export interface ContextRow {
|
|
|
63
65
|
export interface ContextGroup {
|
|
64
66
|
/** `LINKS`, `BACKLINKS`, `TAGS`, `MENTIONS`. */
|
|
65
67
|
readonly heading: string;
|
|
68
|
+
/** How many rows the section holds — the heading's badge and the collapse default's input. */
|
|
69
|
+
readonly count: number;
|
|
66
70
|
/** Shown when the group is present but empty. Never rendered when hidden. */
|
|
67
71
|
readonly rows: readonly ContextRow[];
|
|
68
72
|
}
|
|
@@ -83,6 +87,15 @@ export interface ContextModel {
|
|
|
83
87
|
* shape.
|
|
84
88
|
*/
|
|
85
89
|
readonly tags: readonly TagGroupRow[];
|
|
90
|
+
/**
|
|
91
|
+
* How many tags TAGS holds — the heading's count.
|
|
92
|
+
*
|
|
93
|
+
* Carried next to `tags` rather than derived in the component for the same
|
|
94
|
+
* reason {@link ContextGroup.count} exists: the number is a *decision* about
|
|
95
|
+
* what the heading says, and an empty rail reports `0` where a group-less
|
|
96
|
+
* `tags.length` would report nothing to talk about.
|
|
97
|
+
*/
|
|
98
|
+
readonly tagsCount: number;
|
|
86
99
|
/** Present instead of groups when there is nothing to show. */
|
|
87
100
|
readonly empty: string | null;
|
|
88
101
|
}
|
|
@@ -92,14 +105,21 @@ export const HEADINGS = { links: "LINKS", backlinks: "BACKLINKS", tags: "TAGS",
|
|
|
92
105
|
|
|
93
106
|
// --- building rows ------------------------------------------------------------------
|
|
94
107
|
|
|
95
|
-
/**
|
|
108
|
+
/**
|
|
109
|
+
* A row from a node. The rail's one place that reads a node's presentation.
|
|
110
|
+
*
|
|
111
|
+
* The icon follows the tree's rule, not the bare kind: a session-memory note
|
|
112
|
+
* appears in LINKS just as often as in the tree, and the rail repeating the
|
|
113
|
+
* tree's icon is how "this is the same thing" is communicated across the two
|
|
114
|
+
* columns.
|
|
115
|
+
*/
|
|
96
116
|
export function rowFor(id: string, node: WireGraphNode, selectedId: string | null): ContextRow {
|
|
97
117
|
return {
|
|
98
118
|
id,
|
|
99
119
|
target: node.id,
|
|
100
120
|
label: listLabel(node),
|
|
101
121
|
kind: node.kind,
|
|
102
|
-
|
|
122
|
+
kindIcon: isSessionNote(node.id) ? "session" : kindIcon(node.kind),
|
|
103
123
|
provenance: node.provenance,
|
|
104
124
|
provenanceGlyph: provenanceGlyph(node.provenance),
|
|
105
125
|
provenanceTitle: provenanceTitle(node.provenance),
|
|
@@ -250,7 +270,7 @@ export const RAIL_EMPTY = {
|
|
|
250
270
|
|
|
251
271
|
/** The rail with nothing to show, and a reason. */
|
|
252
272
|
function emptyRail(reason: string): ContextModel {
|
|
253
|
-
return { subject: null, groups: [], tags: [], empty: reason };
|
|
273
|
+
return { subject: null, groups: [], tags: [], tagsCount: 0, empty: reason };
|
|
254
274
|
}
|
|
255
275
|
|
|
256
276
|
/**
|
|
@@ -278,7 +298,7 @@ export function contextModel(payload: GraphPayload | null, selectedId: string |
|
|
|
278
298
|
const byId = nodesById(model);
|
|
279
299
|
const groups: ContextGroup[] = [];
|
|
280
300
|
const push = (heading: string, rows: readonly ContextRow[]): void => {
|
|
281
|
-
if (rows.length > 0) groups.push({ heading, rows });
|
|
301
|
+
if (rows.length > 0) groups.push({ heading, count: rows.length, rows });
|
|
282
302
|
};
|
|
283
303
|
|
|
284
304
|
const resolve = (rows: readonly DetailLinkRow[]): ContextRow[] =>
|
|
@@ -307,7 +327,134 @@ export function contextModel(payload: GraphPayload | null, selectedId: string |
|
|
|
307
327
|
const tags = slug === null ? [] : tagsFor(payload, slug, byId, selectedId);
|
|
308
328
|
|
|
309
329
|
if (groups.length === 0 && tags.length === 0) {
|
|
310
|
-
return { subject: detail.label, groups: [], tags: [], empty: RAIL_EMPTY.isolated };
|
|
330
|
+
return { subject: detail.label, groups: [], tags: [], tagsCount: 0, empty: RAIL_EMPTY.isolated };
|
|
311
331
|
}
|
|
312
|
-
return { subject: detail.label, groups, tags, empty: null };
|
|
332
|
+
return { subject: detail.label, groups, tags, tagsCount: tags.length, empty: null };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// --- counts and collapse (Tier 6, §8 P6.4) ---------------------------------------------
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* A rail section as the component renders it: the heading's count, whether it
|
|
339
|
+
* is open, and the rows to show under it.
|
|
340
|
+
*
|
|
341
|
+
* Collapsed is a rendering state, not a data state, so `rows` is emptied here
|
|
342
|
+
* rather than the component filtering — otherwise the collapsed decision
|
|
343
|
+
* would be an untestable branch in a `.tsx`.
|
|
344
|
+
*/
|
|
345
|
+
export interface RailSectionView {
|
|
346
|
+
readonly heading: string;
|
|
347
|
+
readonly count: number;
|
|
348
|
+
readonly collapsed: boolean;
|
|
349
|
+
/** The rows, `[]` when collapsed. */
|
|
350
|
+
readonly rows: readonly ContextRow[];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Which sections the user has explicitly opened or closed.
|
|
355
|
+
*
|
|
356
|
+
* Two sets rather than one because "collapsed" has three origins — the user,
|
|
357
|
+
* the >8 default, and the selection's force-expand — and a single Set cannot
|
|
358
|
+
* say which of them a heading is in. `open` and `closed` are the user's word;
|
|
359
|
+
* the other two are computed by {@link railCollapsed}.
|
|
360
|
+
*/
|
|
361
|
+
export interface RailToggles {
|
|
362
|
+
/** Sections the user explicitly opened. */
|
|
363
|
+
readonly open: ReadonlySet<string>;
|
|
364
|
+
/** Sections the user explicitly closed. */
|
|
365
|
+
readonly closed: ReadonlySet<string>;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/** No user opinion yet — the rail's state on mount. */
|
|
369
|
+
export function emptyRailToggles(): RailToggles {
|
|
370
|
+
return { open: new Set(), closed: new Set() };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* A section longer than this opens collapsed.
|
|
375
|
+
*
|
|
376
|
+
* Eight is where a list stops being scannable without scrolling: the rail has
|
|
377
|
+
* a fixed fraction of a fixed column (§1.2), so a MENTIONS section with forty
|
|
378
|
+
* entries would push the sections below it out of the viewport entirely —
|
|
379
|
+
* which is how a rail meant to show *everything at once* ends up showing one
|
|
380
|
+
* group. Under eight the rhythm would cost more than it saved.
|
|
381
|
+
*/
|
|
382
|
+
export const RAIL_COLLAPSE_THRESHOLD = 8;
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* Whether a section is collapsed.
|
|
386
|
+
*
|
|
387
|
+
* Order of precedence, most specific wins:
|
|
388
|
+
*
|
|
389
|
+
* 1. **The selection is never hidden.** If the selected row lives in this
|
|
390
|
+
* section, force-expand — `selectedId` is the §1.3 bus and can change a
|
|
391
|
+
* hundred times a minute while the user is reading something else; a
|
|
392
|
+
* selection that vanished into a fold would be a bug the user experiences
|
|
393
|
+
* as "clicking did nothing" and never connects to a toggle 40px away.
|
|
394
|
+
* 2. The user's explicit *close* beats the default but not the selection.
|
|
395
|
+
* 3. The user's explicit *open* beats the default.
|
|
396
|
+
* 4. Otherwise, the {@link RAIL_COLLAPSE_THRESHOLD} default.
|
|
397
|
+
*/
|
|
398
|
+
export function railCollapsed(toggles: RailToggles, heading: string, count: number, holdsSelection: boolean): boolean {
|
|
399
|
+
if (holdsSelection) return false;
|
|
400
|
+
if (toggles.closed.has(heading)) return true;
|
|
401
|
+
if (toggles.open.has(heading)) return false;
|
|
402
|
+
return count > RAIL_COLLAPSE_THRESHOLD;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/** Whether the selection itself is one of this section's rows. */
|
|
406
|
+
function holdsSelection(rows: readonly ContextRow[]): boolean {
|
|
407
|
+
return rows.some((row) => row.selected);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Resolve one section for rendering.
|
|
412
|
+
*
|
|
413
|
+
* The heading identifies the toggle, and headings are a fixed vocabulary
|
|
414
|
+
* ({@link HEADINGS}), so a per-render id is stable across re-renders and
|
|
415
|
+
* remounts — which is what `aria-controls` needs to stay a true promise about
|
|
416
|
+
* the DOM.
|
|
417
|
+
*/
|
|
418
|
+
export function railSectionView(group: ContextGroup, toggles: RailToggles): RailSectionView {
|
|
419
|
+
const collapsed = railCollapsed(toggles, group.heading, group.count, holdsSelection(group.rows));
|
|
420
|
+
return { heading: group.heading, count: group.count, collapsed, rows: collapsed ? [] : group.rows };
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* The TAGS section through the same vocabulary as the link sections.
|
|
425
|
+
*
|
|
426
|
+
* `TAGS` is not a {@link ContextGroup} — its rows are tag groups — but it
|
|
427
|
+
* toggles and counts exactly the same way, so it goes through the same
|
|
428
|
+
* predicate rather than growing a second collapse rule. The count is the
|
|
429
|
+
* number of *tags*, not sibling rows: the heading names the category, and
|
|
430
|
+
* "3 tags" is what a scanner wants before deciding whether to open it.
|
|
431
|
+
*/
|
|
432
|
+
export function railTagsView(tags: readonly TagGroupRow[], toggles: RailToggles): RailSectionView {
|
|
433
|
+
const collapsed = railCollapsed(toggles, HEADINGS.tags, tags.length, tags.some((tag) => tag.siblings.some((row) => row.selected)));
|
|
434
|
+
return { heading: HEADINGS.tags, count: tags.length, collapsed, rows: [] };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Move one section to the other state.
|
|
439
|
+
*
|
|
440
|
+
* Both sets are updated per toggle so a heading can never sit in both: the
|
|
441
|
+
* user asking twice (click, selection moves it, click) always lands on a
|
|
442
|
+
* consistent single-word answer.
|
|
443
|
+
*/
|
|
444
|
+
export function railToggled(toggles: RailToggles, heading: string, collapsed: boolean): RailToggles {
|
|
445
|
+
const open = new Set(toggles.open);
|
|
446
|
+
const closed = new Set(toggles.closed);
|
|
447
|
+
if (collapsed) {
|
|
448
|
+
open.add(heading);
|
|
449
|
+
closed.delete(heading);
|
|
450
|
+
} else {
|
|
451
|
+
closed.add(heading);
|
|
452
|
+
open.delete(heading);
|
|
453
|
+
}
|
|
454
|
+
return { open, closed };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** The `id` of a section's row list, for `aria-controls` to point at. */
|
|
458
|
+
export function railPanelId(heading: string): string {
|
|
459
|
+
return `weave-ctx-panel-${heading.toLowerCase()}`;
|
|
313
460
|
}
|