pi-weave 0.1.11 → 0.1.13
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 +148 -132
- package/package.json +1 -2
- package/src/core/cache/workspace.ts +26 -9
- package/src/core/concurrency.ts +3 -6
- package/src/core/frontmatter.ts +0 -53
- package/src/core/graph/build.ts +12 -8
- package/src/core/graph/current.ts +4 -6
- package/src/core/graph/model.ts +1 -1
- package/src/core/graph/wikilinks.ts +3 -3
- package/src/core/index.ts +26 -27
- package/src/core/paths.ts +0 -7
- package/src/core/slug.ts +13 -0
- package/src/core/types.ts +1 -0
- package/src/core/vault.ts +45 -467
- package/src/core/view/detail.ts +1 -1
- package/src/core/view/health.ts +1 -1
- package/src/core/view/tree.ts +15 -3
- package/src/pi/index.ts +6 -85
- package/src/pi/summarize.ts +2 -2
- package/src/pi/viewer/tui/bodyStore.ts +4 -7
- package/src/pi/viewer/tui/branding.ts +7 -148
- package/src/pi/viewer/tui/run.ts +3 -17
- package/src/pi/viewer/tui/surface/base.ts +24 -3
- package/src/pi/viewer/tui/surface/explore.ts +41 -6
- package/src/pi/viewer/tui/workspace.ts +23 -351
- package/src/pi/viewer/tui/workspaceRoot.ts +31 -172
- package/src/pi/viewer/web/run.ts +7 -117
- package/src/web/client/api.dom.ts +2 -2
- package/src/web/client/api.ts +14 -176
- package/src/web/client/bootstrap.ts +5 -14
- package/src/web/client/context/context.model.ts +9 -11
- package/src/web/client/dist/app.js +77 -167
- package/src/web/client/graph/dynamics.ts +5 -65
- package/src/web/client/graph/renderer.dom.ts +7 -8
- package/src/web/client/graph/renderer.ts +9 -35
- package/src/web/client/main.tsx +1 -1
- package/src/web/client/note/Note.tsx +37 -60
- package/src/web/client/note/note.model.ts +23 -0
- package/src/web/client/search/SearchPalette.tsx +45 -36
- package/src/web/client/search/search.model.ts +33 -454
- package/src/web/client/shell/Columns.tsx +13 -81
- package/src/web/client/shell/Header.tsx +2 -10
- package/src/web/client/shell/Shell.tsx +50 -123
- package/src/web/client/shell/StatusBar.tsx +1 -4
- package/src/web/client/shell/icons.model.ts +4 -7
- package/src/web/client/shell/keys.model.ts +5 -42
- package/src/web/client/shell/keys.ts +2 -2
- package/src/web/client/shell/shell.model.ts +10 -133
- package/src/web/client/shell/theme.model.ts +2 -2
- package/src/web/client/shell/theme.ts +33 -121
- package/src/web/client/state.ts +9 -89
- package/src/web/client/tree/Tree.tsx +24 -97
- package/src/web/client/tree/tree.model.ts +8 -56
- package/src/web/client/workspace.ts +72 -242
- package/src/web/server/page.ts +8 -10
- package/src/web/server/routes.ts +30 -415
- package/src/web/server/server.ts +6 -145
- package/src/web/shared/layout.ts +72 -624
- package/src/web/shared/wire.ts +10 -178
- package/src/core/sessions.ts +0 -929
- package/src/pi/sessionScan.ts +0 -104
- package/src/pi/viewer/tui/explorer.ts +0 -586
- package/src/web/client/live.model.ts +0 -275
- package/src/web/client/live.ts +0 -151
- package/src/web/client/note/Editor.tsx +0 -109
- package/src/web/client/note/editor.controller.ts +0 -151
- package/src/web/client/note/editor.model.ts +0 -686
- package/src/web/client/search/search.ts +0 -107
- package/src/web/client/shell/Divider.tsx +0 -44
- package/src/web/client/shell/cssvars.ts +0 -70
- package/src/web/client/shell/drag.model.ts +0 -170
- package/src/web/client/shell/layout.model.ts +0 -500
- package/src/web/client/shell/viewport.ts +0 -29
- package/src/web/server/sse.ts +0 -321
- package/src/web/server/watcher.ts +0 -507
|
@@ -1,19 +1,10 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The tree column (weave-workspace §1.2, P2.3).
|
|
3
|
-
*
|
|
4
|
-
* Props in, JSX out. Every string, glyph, branch and key binding comes from
|
|
5
|
-
* `tree.model.ts`; what is left here is a `useState`, a `map` and four
|
|
6
|
-
* handlers that forward into it. §10's rule, and the reason the 95 % gate
|
|
7
|
-
* survives a UI phase with no DOM test environment.
|
|
8
|
-
*/
|
|
1
|
+
/** The read-only tree column. */
|
|
9
2
|
|
|
10
3
|
import { useState } from "preact/hooks";
|
|
11
|
-
import { recentIds } from "../state";
|
|
12
4
|
import { isTextEntry, type KeyTarget } from "../shell/keys.model";
|
|
13
5
|
import { ICON_BOX, ICON_STROKE, ICONS } from "../shell/icons.model";
|
|
14
6
|
import type { IconName } from "../shell/icons.model";
|
|
15
7
|
import type { GraphPayload } from "../../shared/wire";
|
|
16
|
-
import type { TreeRowView, TreeViewState } from "./tree.model";
|
|
17
8
|
import {
|
|
18
9
|
FILTER_HINT,
|
|
19
10
|
FILTER_LABEL,
|
|
@@ -40,22 +31,11 @@ import {
|
|
|
40
31
|
export interface TreeProps {
|
|
41
32
|
graph: GraphPayload | null;
|
|
42
33
|
selectedId: string | null;
|
|
34
|
+
recentIds: ReadonlySet<string>;
|
|
43
35
|
onSelect: (id: string) => void;
|
|
44
|
-
/** Epoch ms for relative times. Injected so the render is deterministic. */
|
|
45
36
|
now: number;
|
|
46
37
|
}
|
|
47
38
|
|
|
48
|
-
/**
|
|
49
|
-
* One sprite glyph, as a real `<svg>`.
|
|
50
|
-
*
|
|
51
|
-
* The element is built from {@link ICONS}' path data rather than injected as
|
|
52
|
-
* an HTML string — CSP-identical (neither path touches a `script-src` hook),
|
|
53
|
-
* but the string form would carry a whole `<svg>` per row and Preact can
|
|
54
|
-
* branch the two paint modes with a spread and no `if`. Every attribute here
|
|
55
|
-
* is a presentation *attribute*, not a `style` one: `style-src` never sees it.
|
|
56
|
-
* Sizing rides the width/height attributes rather than CSS for the same
|
|
57
|
-
* reason — the box is part of the icon, not of its context.
|
|
58
|
-
*/
|
|
59
39
|
export function Icon({ name, class: className }: { name: IconName; class?: string }) {
|
|
60
40
|
const def = ICONS[name];
|
|
61
41
|
return (
|
|
@@ -71,20 +51,17 @@ export function Icon({ name, class: className }: { name: IconName; class?: strin
|
|
|
71
51
|
stroke-linejoin="round"
|
|
72
52
|
aria-hidden="true"
|
|
73
53
|
>
|
|
74
|
-
{def.d.map((d) =>
|
|
75
|
-
<path d={d} />
|
|
76
|
-
))}
|
|
54
|
+
{def.d.map((d) => <path d={d} />)}
|
|
77
55
|
</svg>
|
|
78
56
|
);
|
|
79
57
|
}
|
|
80
58
|
|
|
81
|
-
function Row({ view, onSelect, onToggle }: { view:
|
|
59
|
+
function Row({ view, recentIds, onSelect, onToggle }: { view: ReturnType<typeof rowViews>[number]; recentIds: ReadonlySet<string>; onSelect: () => void; onToggle: () => void }) {
|
|
82
60
|
return (
|
|
83
61
|
<li
|
|
84
62
|
id={view.domId}
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}`}
|
|
63
|
+
data-row-id={view.id}
|
|
64
|
+
class={`weave-row weave-row-${view.kind}${view.selected ? " weave-row-on" : ""}${recentIds.has(view.id) ? " weave-row-new" : ""}`}
|
|
88
65
|
role="treeitem"
|
|
89
66
|
aria-level={view.level}
|
|
90
67
|
aria-posinset={view.posinset}
|
|
@@ -94,27 +71,11 @@ function Row({ view, onSelect, onToggle }: { view: TreeRowView; onSelect: () =>
|
|
|
94
71
|
style={depthVar(view.depth)}
|
|
95
72
|
onClick={onSelect}
|
|
96
73
|
>
|
|
97
|
-
|
|
98
|
-
the kind glyph duplicates nothing a screen reader needs, and the
|
|
99
|
-
provenance shape is announced through its `title` instead. The
|
|
100
|
-
chevron rotates through CSS, so "open" is a class, not a different
|
|
101
|
-
sprite. */}
|
|
102
|
-
<span
|
|
103
|
-
class="weave-twisty"
|
|
104
|
-
aria-hidden="true"
|
|
105
|
-
onClick={(event) => {
|
|
106
|
-
event.stopPropagation();
|
|
107
|
-
onToggle();
|
|
108
|
-
}}
|
|
109
|
-
>
|
|
74
|
+
<span class="weave-twisty" aria-hidden="true" onClick={(event) => { event.stopPropagation(); onToggle(); }}>
|
|
110
75
|
{view.hasKids ? <Icon name="chevron" class={view.expanded ? "weave-icon weave-icon-open" : "weave-icon"} /> : null}
|
|
111
76
|
</span>
|
|
112
|
-
<span class="weave-kind" aria-hidden="true">
|
|
113
|
-
|
|
114
|
-
</span>
|
|
115
|
-
<span class={`weave-prov weave-prov-${view.provenance ?? "none"}`} title={view.provenanceTitle}>
|
|
116
|
-
{view.provenanceGlyph}
|
|
117
|
-
</span>
|
|
77
|
+
<span class="weave-kind" aria-hidden="true"><Icon name={view.kindIcon} class="weave-icon" /></span>
|
|
78
|
+
<span class={`weave-prov weave-prov-${view.provenance ?? "none"}`} title={view.provenanceTitle}>{view.provenanceGlyph}</span>
|
|
118
79
|
<span class="weave-label">{view.label}</span>
|
|
119
80
|
<span class="weave-meta">{view.meta}</span>
|
|
120
81
|
</li>
|
|
@@ -122,62 +83,28 @@ function Row({ view, onSelect, onToggle }: { view: TreeRowView; onSelect: () =>
|
|
|
122
83
|
}
|
|
123
84
|
|
|
124
85
|
export function Tree(props: TreeProps) {
|
|
125
|
-
const [state, setState] = useState
|
|
86
|
+
const [state, setState] = useState(initialTreeView);
|
|
126
87
|
const rows = rowsFor(props.graph, state);
|
|
127
88
|
const empty = treeEmptyMessage(props.graph, rows, state);
|
|
128
|
-
|
|
129
89
|
return (
|
|
130
|
-
<div
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const next = treeKey(rows, state, props.selectedId, event.key, typing);
|
|
139
|
-
if (!next.handled) return;
|
|
140
|
-
event.preventDefault();
|
|
141
|
-
setState(next.state);
|
|
142
|
-
if (next.selectedId !== null) props.onSelect(next.selectedId);
|
|
143
|
-
}}
|
|
144
|
-
>
|
|
90
|
+
<div class="weave-tree" onKeyDown={(event) => {
|
|
91
|
+
const target = event.target as KeyTarget;
|
|
92
|
+
const next = treeKey(rows, state, props.selectedId, event.key, isTextEntry(target?.tagName ?? null, target?.isContentEditable === true));
|
|
93
|
+
if (!next.handled) return;
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
setState(next.state);
|
|
96
|
+
if (next.selectedId !== null) props.onSelect(next.selectedId);
|
|
97
|
+
}}>
|
|
145
98
|
<div class="weave-tree-controls">
|
|
146
|
-
<input
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
value={state.query}
|
|
150
|
-
placeholder={FILTER_PLACEHOLDER}
|
|
151
|
-
aria-label={FILTER_LABEL}
|
|
152
|
-
title={FILTER_HINT}
|
|
153
|
-
onInput={(event) => setState(setQuery(state, event.currentTarget.value))}
|
|
154
|
-
/>
|
|
155
|
-
<button type="button" class="weave-chip" title={provenanceHint(state.provFilter)} onClick={() => setState(cycleProvenance(state))}>
|
|
156
|
-
◧ {provenanceLabel(state.provFilter)}
|
|
157
|
-
</button>
|
|
158
|
-
<button type="button" class="weave-chip" title={internalsHint(state.showInternals)} onClick={() => setState(toggleInternals(state))}>
|
|
159
|
-
◧ {internalsLabel(state.showInternals)}
|
|
160
|
-
</button>
|
|
99
|
+
<input type="search" class="weave-filter" value={state.query} placeholder={FILTER_PLACEHOLDER} aria-label={FILTER_LABEL} title={FILTER_HINT} onInput={(event) => setState(setQuery(state, event.currentTarget.value))} />
|
|
100
|
+
<button type="button" class="weave-chip" title={provenanceHint(state.provFilter)} onClick={() => setState(cycleProvenance(state))}>◧ {provenanceLabel(state.provFilter)}</button>
|
|
101
|
+
<button type="button" class="weave-chip" title={internalsHint(state.showInternals)} onClick={() => setState(toggleInternals(state))}>◧ {internalsLabel(state.showInternals)}</button>
|
|
161
102
|
</div>
|
|
162
103
|
{empty === null ? (
|
|
163
|
-
<ul
|
|
164
|
-
|
|
165
|
-
role="tree"
|
|
166
|
-
tabIndex={0}
|
|
167
|
-
aria-label={TREE_LABEL}
|
|
168
|
-
// Focus stays on the `<ul>` and the *active* row is named by
|
|
169
|
-
// reference — the alternative, a roving `tabindex`, would put every
|
|
170
|
-
// row in the Tab order and make Tab a fourth way to walk the tree.
|
|
171
|
-
// `null` when the selection is not visible; see the model.
|
|
172
|
-
aria-activedescendant={treeActiveDescendant(rows, props.selectedId) ?? undefined}
|
|
173
|
-
>
|
|
174
|
-
{rowViews(rows, props.selectedId, props.now).map((view) => (
|
|
175
|
-
<Row key={view.id} view={view} onSelect={() => props.onSelect(view.id)} onToggle={() => setState(toggleExpanded(state, view.id))} />
|
|
176
|
-
))}
|
|
104
|
+
<ul class="weave-rows" role="tree" tabIndex={0} aria-label={TREE_LABEL} aria-activedescendant={treeActiveDescendant(rows, props.selectedId) ?? undefined}>
|
|
105
|
+
{rowViews(rows, props.selectedId, props.now).map((view) => <Row key={view.id} view={view} recentIds={props.recentIds} onSelect={() => { props.onSelect(view.id); if (view.hasKids) setState((current) => toggleExpanded(current, view.id)); }} onToggle={() => setState(toggleExpanded(state, view.id))} />)}
|
|
177
106
|
</ul>
|
|
178
|
-
) :
|
|
179
|
-
<p class="weave-tree-empty">{empty}</p>
|
|
180
|
-
)}
|
|
107
|
+
) : <p class="weave-tree-empty">{empty}</p>}
|
|
181
108
|
<p class="weave-tree-count">{rowCountLabel(rows)}</p>
|
|
182
109
|
</div>
|
|
183
110
|
);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Everything the tree column *decides
|
|
2
|
+
* Everything the tree column *decides*.
|
|
3
3
|
*
|
|
4
4
|
* The column itself is `treeRows` with a different renderer — that is §3's
|
|
5
5
|
* whole claim, and this module is what makes it true in the browser: it holds
|
|
@@ -40,7 +40,7 @@ import type { IconName } from "../shell/icons.model";
|
|
|
40
40
|
* It is **not** `src/web/client/state.ts`'s `TreeState`. That one is a P1
|
|
41
41
|
* placeholder carrying only an expanded-id list, and it stays where it is:
|
|
42
42
|
* this state is owned by the tree column, lives in the component that renders
|
|
43
|
-
* it, and never crosses the context bus.
|
|
43
|
+
* it, and never crosses the context bus. 's bus is `selectedId` — which
|
|
44
44
|
* rows happen to be open is not something the note column or the graph has any
|
|
45
45
|
* business reacting to.
|
|
46
46
|
*/
|
|
@@ -81,7 +81,7 @@ export function initialTreeView(roots: readonly string[] = ["vault", "repository
|
|
|
81
81
|
*
|
|
82
82
|
* `WireGraphModel` is `Omit<GraphModel, "danglingLinks">` — the payload hoists
|
|
83
83
|
* that map to its own top-level `dangling` rather than shipping it twice
|
|
84
|
-
* (§
|
|
84
|
+
* (§3) — so exactly one field has to be put back before `treeRows` or
|
|
85
85
|
* `detailModel` will accept it. The door (`shared/view.ts`) deliberately does
|
|
86
86
|
* not do this: it is a wire concern, and a door that carried a transformation
|
|
87
87
|
* would be a second implementation rather than a re-export.
|
|
@@ -248,7 +248,7 @@ export function parentOf(rows: readonly TreeRow[], id: string): string | null {
|
|
|
248
248
|
}
|
|
249
249
|
|
|
250
250
|
/**
|
|
251
|
-
* Vim-ish aliases: `j` is `ArrowDown`, `k` is `ArrowUp` (
|
|
251
|
+
* Vim-ish aliases: `j` is `ArrowDown`, `k` is `ArrowUp` ( P4).
|
|
252
252
|
*
|
|
253
253
|
* A *normalizer* rather than two more branches in {@link treeKey}, because
|
|
254
254
|
* the aliasing and the navigation are separate concerns and folding them
|
|
@@ -257,7 +257,7 @@ export function parentOf(rows: readonly TreeRow[], id: string): string | null {
|
|
|
257
257
|
*
|
|
258
258
|
* ## Why the aliases are tree-scoped and not global
|
|
259
259
|
*
|
|
260
|
-
*
|
|
260
|
+
* says "vim-ish `j/k` **in the tree**", and the qualifier is load-bearing
|
|
261
261
|
* on both sides. A global `j` would move the tree's cursor while the user is
|
|
262
262
|
* reading the note column — an invisible change to a surface they are not
|
|
263
263
|
* looking at — and it would make `j` untypeable in the graph's depth control.
|
|
@@ -368,45 +368,6 @@ export function kindIcon(kind: WireNodeKind): IconName {
|
|
|
368
368
|
return KIND_ICONS[kind];
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
-
// --- the session fold -----------------------------------------------------------------
|
|
372
|
-
|
|
373
|
-
/**
|
|
374
|
-
* The synthesized folder session memory lives under.
|
|
375
|
-
*
|
|
376
|
-
* Core's graph builder (`src/core/graph/build.ts`) nests notes whose slug has
|
|
377
|
-
* a directory under a synthesized `vfolder:<dir>` node — kind `module`,
|
|
378
|
-
* because reusing the tree's containment chain needs no client change. That
|
|
379
|
-
* reuse is why there is no `session` node *kind*: a session note is an
|
|
380
|
-
* ordinary `note` that happens to be filed there, and this client recognises
|
|
381
|
-
* it by path, not by kind.
|
|
382
|
-
*/
|
|
383
|
-
export const SESSION_DIR = "sessions";
|
|
384
|
-
|
|
385
|
-
/** True when a node id names a note under the {@link SESSION_DIR} fold. */
|
|
386
|
-
export function isSessionNote(id: string): boolean {
|
|
387
|
-
return id.startsWith(`note:${SESSION_DIR}/`);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Whether a row renders a notch quieter.
|
|
392
|
-
*
|
|
393
|
-
* `sessions/<n>.md` notes are machine-written memory that accrues by the
|
|
394
|
-
* dozens with near-duplicate titles, so at any real vault size they are most
|
|
395
|
-
* of the tree's rows — and rows that all look alike at full weight make the
|
|
396
|
-
* six notes a human actually wrote harder to find. So session rows sit in
|
|
397
|
-
* `--weave-dim` until hovered or selected, which is how Obsidian treats its
|
|
398
|
-
* own long tails.
|
|
399
|
-
*
|
|
400
|
-
* The *never* half is the load-bearing part: `selectedId` is the §1.3 bus, so
|
|
401
|
-
* the selection is decided somewhere the tree does not own, and a rule that
|
|
402
|
-
* could dim the selected row would be a rule the graph's click could silently
|
|
403
|
-
* break. The class-coverage gate does not see this — it is asserted here,
|
|
404
|
-
* which is where §10 says it belongs.
|
|
405
|
-
*/
|
|
406
|
-
export function isMuted(row: TreeRow, selectedId: string | null): boolean {
|
|
407
|
-
return row.id !== selectedId && isSessionNote(row.id);
|
|
408
|
-
}
|
|
409
|
-
|
|
410
371
|
/**
|
|
411
372
|
* Provenance → glyph.
|
|
412
373
|
*
|
|
@@ -557,14 +518,6 @@ export interface TreeRowView {
|
|
|
557
518
|
readonly hasKids: boolean;
|
|
558
519
|
readonly expanded: boolean;
|
|
559
520
|
readonly selected: boolean;
|
|
560
|
-
/**
|
|
561
|
-
* Render a notch quieter — the session rows of {@link isMuted}.
|
|
562
|
-
*
|
|
563
|
-
* A view-model flag rather than a `.tsx` branch for the same reason every
|
|
564
|
-
* other branch is here, and the stylesheet keys off it with
|
|
565
|
-
* `.weave-row-muted`.
|
|
566
|
-
*/
|
|
567
|
-
readonly muted: boolean;
|
|
568
521
|
/** The trailing annotation, already formatted against `now`. */
|
|
569
522
|
readonly meta: string;
|
|
570
523
|
/** ARIA `aria-level`, which is 1-based where `depth` is 0-based. */
|
|
@@ -600,7 +553,6 @@ export function rowView(row: TreeRow, selectedId: string | null, now: number, po
|
|
|
600
553
|
hasKids: row.hasKids,
|
|
601
554
|
expanded: row.expanded,
|
|
602
555
|
selected: row.id === selectedId,
|
|
603
|
-
muted: isMuted(row, selectedId),
|
|
604
556
|
meta: formatTreeMeta(row.meta, now),
|
|
605
557
|
level: row.depth + 1,
|
|
606
558
|
};
|
|
@@ -675,7 +627,7 @@ export const FILTER_PLACEHOLDER = "filter…";
|
|
|
675
627
|
* Names *what is in it* rather than what it is: "Tree" is already the
|
|
676
628
|
* column's heading and the role announces the widget type, so a second
|
|
677
629
|
* "tree" would be read three times. "Vault and repository" is the sentence
|
|
678
|
-
*
|
|
630
|
+
* uses for the same thing.
|
|
679
631
|
*/
|
|
680
632
|
export const TREE_LABEL = "Vault and repository";
|
|
681
633
|
|
|
@@ -699,7 +651,7 @@ export const FILTER_HINT = "Filter the tree (/)";
|
|
|
699
651
|
* The obvious order — "no rows? then work out why" — is wrong here, and it is
|
|
700
652
|
* wrong in the case that matters most. A brand-new vault is not a graph with
|
|
701
653
|
* no rows: `treeRows` still emits the `vault` root, so the column renders one
|
|
702
|
-
* word and nothing else, and a user's first ever
|
|
654
|
+
* word and nothing else, and a user's first ever use says nothing about
|
|
703
655
|
* how to add a note. `treeEmptyHint` is core's answer to exactly that
|
|
704
656
|
* question, and it is deliberately narrow — it returns a string only for a
|
|
705
657
|
* vault with no notes *and* no repository — so consulting it first cannot
|
|
@@ -708,7 +660,7 @@ export const FILTER_HINT = "Filter the tree (/)";
|
|
|
708
660
|
* It also outranks the filter message, for the same reason: when the vault is
|
|
709
661
|
* genuinely empty, "nothing matches this filter" is true and useless.
|
|
710
662
|
*
|
|
711
|
-
* Using core's sentence rather than writing one here is
|
|
663
|
+
* Using core's sentence rather than writing one here is : the TUI's empty
|
|
712
664
|
* tree and the browser's say the same thing about the same vault, because
|
|
713
665
|
* there is one sentence.
|
|
714
666
|
*/
|