pi-weave 0.3.8 → 0.3.10
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 +21 -17
- package/package.json +4 -3
- package/skills/weave-explore/SKILL.md +3 -5
- package/skills/weave-notepad/SKILL.md +22 -2
- package/skills/weave-notepad/references/second-thoughts.md +57 -0
- package/src/core/graph/build.ts +1 -1
- package/src/core/graph/current.ts +10 -8
- package/src/core/history.ts +417 -0
- package/src/core/index.ts +19 -0
- package/src/core/links/repair.ts +5 -68
- package/src/core/noteAction.ts +72 -2
- package/src/core/openInEditor.ts +3 -8
- package/src/core/repoAction.ts +20 -61
- package/src/core/repoIndex.ts +38 -0
- package/src/core/trail.ts +124 -0
- package/src/core/types.ts +1 -3
- package/src/core/vault.ts +116 -29
- package/src/core/view/focus.ts +2 -2
- package/src/core/view/index.ts +2 -3
- package/src/core/view/rawTail.ts +153 -0
- package/src/core/workspace.ts +20 -26
- package/src/core/workspaceCommands.ts +14 -14
- package/src/opencode/tools.ts +3 -3
- package/src/opencode/tui.ts +1 -7
- package/src/pi/index.ts +68 -80
- package/src/pi/tools/noteTool.ts +1 -1
- package/src/pi/tools/repoTool.ts +3 -6
- package/src/pi/viewer/web/run.ts +6 -8
- package/src/web/client/api.ts +103 -5
- package/src/web/client/context/context.model.ts +5 -0
- package/src/web/client/dist/app.js +499 -129
- package/src/web/client/graph/Graph.tsx +166 -14
- package/src/web/client/graph/column.model.ts +59 -2
- package/src/web/client/graph/graph.model.ts +183 -7
- package/src/web/client/graph/renderer.ts +39 -9
- package/src/web/client/graph/spotlight.model.ts +472 -0
- package/src/web/client/note/Note.tsx +370 -66
- package/src/web/client/note/Replay.tsx +139 -0
- package/src/web/client/note/drafts.ts +115 -11
- package/src/web/client/note/editbar.model.ts +153 -0
- package/src/web/client/note/note.model.ts +82 -1
- package/src/web/client/note/replay.model.ts +248 -0
- package/src/web/client/search/SearchPalette.tsx +103 -34
- package/src/web/client/search/search.model.ts +533 -26
- package/src/web/client/shell/ContextRail.tsx +7 -0
- package/src/web/client/shell/Settings.tsx +21 -1
- package/src/web/client/shell/Shell.tsx +93 -27
- package/src/web/client/shell/StatusBar.tsx +1 -0
- package/src/web/client/shell/icons.model.ts +68 -1
- package/src/web/client/shell/keys.model.ts +31 -8
- package/src/web/client/shell/shell.model.ts +48 -1
- package/src/web/client/shell/theme.model.ts +3 -0
- package/src/web/client/shell/theme.ts +354 -32
- package/src/web/client/shell/workspace.css.ts +40 -9
- package/src/web/client/trail/TrailStrip.tsx +55 -0
- package/src/web/client/trail/trail.model.ts +195 -0
- package/src/web/client/trail/trail.ts +45 -0
- package/src/web/client/tree/Tree.tsx +7 -3
- package/src/web/client/tree/tree.model.ts +2 -5
- package/src/web/server/page.ts +1 -1
- package/src/web/server/routes.ts +83 -3
- package/src/web/shared/logo.ts +1 -1
- package/src/web/shared/themes.ts +1 -1
- package/src/web/shared/view.ts +10 -4
- package/src/web/shared/wire.ts +74 -1
- package/src/web/shared/workspace.ts +10 -2
- package/src/core/view/health.ts +0 -160
- package/src/pi/viewer/tui/bodyStore.ts +0 -95
- package/src/pi/viewer/tui/branding.ts +0 -14
- package/src/pi/viewer/tui/model.ts +0 -390
- package/src/pi/viewer/tui/openNote.ts +0 -11
- package/src/pi/viewer/tui/run.ts +0 -71
- package/src/pi/viewer/tui/surface/base.ts +0 -203
- package/src/pi/viewer/tui/surface/detail.ts +0 -315
- package/src/pi/viewer/tui/surface/explore.ts +0 -241
- package/src/pi/viewer/tui/surface/focus.ts +0 -139
- package/src/pi/viewer/tui/surface/health.ts +0 -102
- package/src/pi/viewer/tui/theme.ts +0 -90
- package/src/pi/viewer/tui/workspace.ts +0 -53
- package/src/pi/viewer/tui/workspaceRoot.ts +0 -389
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Replay: the pure half of the note history slider.
|
|
3
|
+
*
|
|
4
|
+
* The server sends every saved revision of a note, oldest first (see
|
|
5
|
+
* `src/core/history.ts`). This module decides what one step of the slider
|
|
6
|
+
* says ("Agent rewrote the summary · 10:05 · +120 −40") and which text that
|
|
7
|
+
* step added, so the column can highlight it.
|
|
8
|
+
*
|
|
9
|
+
* ## Highlighting through Markdown
|
|
10
|
+
*
|
|
11
|
+
* The added text is found in the Markdown source, but it has to be shown in
|
|
12
|
+
* the rendered HTML. So the source is marked with two private-use characters,
|
|
13
|
+
* {@link MARK_OPEN} and {@link MARK_CLOSE}, before it is rendered, and the
|
|
14
|
+
* column turns the marked text nodes into `<mark>` elements afterwards
|
|
15
|
+
* ({@link splitMarked}). The markers are characters, not tags, so they pass
|
|
16
|
+
* through the renderer and the sanitiser as plain text and cannot introduce
|
|
17
|
+
* markup of their own.
|
|
18
|
+
*
|
|
19
|
+
* A marker must not change what the Markdown means, so {@link markChange}
|
|
20
|
+
* puts one pair on each changed line, after any block syntax (`## `, `- `,
|
|
21
|
+
* `> `), skips fence and rule lines, and widens a range to cover a whole link
|
|
22
|
+
* rather than splitting `[[target]]` or `(url)`.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { splitRawTail } from "../../shared/view";
|
|
26
|
+
import type { HistoryRevision } from "../../shared/wire";
|
|
27
|
+
|
|
28
|
+
export const MARK_OPEN = "";
|
|
29
|
+
export const MARK_CLOSE = "";
|
|
30
|
+
|
|
31
|
+
/** Milliseconds between steps while playing. */
|
|
32
|
+
export const PLAY_INTERVAL_MS = 900;
|
|
33
|
+
|
|
34
|
+
/** The smallest prefix/suffix-preserving edit from `from` to `to` (same rule as core's log). */
|
|
35
|
+
export function changedRange(from: string, to: string): { start: number; end: number; removed: number } {
|
|
36
|
+
const max = Math.min(from.length, to.length);
|
|
37
|
+
let head = 0;
|
|
38
|
+
while (head < max && from.charCodeAt(head) === to.charCodeAt(head)) head++;
|
|
39
|
+
let tail = 0;
|
|
40
|
+
while (tail < max - head && from.charCodeAt(from.length - 1 - tail) === to.charCodeAt(to.length - 1 - tail)) tail++;
|
|
41
|
+
return { start: head, end: to.length - tail, removed: from.length - tail - head };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const BLOCK_PREFIX_RE = /^[ \t]*(?:#{1,6}[ \t]+|>[ \t]?|(?:[*+-]|\d{1,9}[.)])[ \t]+(?:\[[ xX]\][ \t]+)?)*/;
|
|
45
|
+
const SKIP_LINE_RE = /^[ \t]*(?:`{3,}|~{3,}|<!--|(?:[-*_][ \t]*){3,}$)/;
|
|
46
|
+
const LINK_RE = /!?\[\[[^\]\n]*\]\]|!?\[[^\]\n]*\]\([^)\n]*\)|<[a-z][a-z0-9+.-]*:[^>\s]*>/gi;
|
|
47
|
+
|
|
48
|
+
/** Mark the text `body` gained over `previous` (the whole body for the first revision). */
|
|
49
|
+
export function markChange(previous: string | null, body: string): string {
|
|
50
|
+
const { start, end } = changedRange(previous ?? "", body);
|
|
51
|
+
if (start >= end) return body;
|
|
52
|
+
const ranges: Array<[number, number]> = [];
|
|
53
|
+
let lineStart = body.lastIndexOf("\n", start - 1) + 1;
|
|
54
|
+
while (lineStart < end) {
|
|
55
|
+
const newline = body.indexOf("\n", lineStart);
|
|
56
|
+
const lineEnd = newline === -1 ? body.length : newline;
|
|
57
|
+
const line = body.slice(lineStart, lineEnd);
|
|
58
|
+
if (!SKIP_LINE_RE.test(line)) {
|
|
59
|
+
const prefix = (BLOCK_PREFIX_RE.exec(line) as RegExpExecArray)[0].length;
|
|
60
|
+
let from = Math.max(start - lineStart, prefix);
|
|
61
|
+
let to = Math.min(end, lineEnd) - lineStart;
|
|
62
|
+
for (const link of line.matchAll(LINK_RE)) {
|
|
63
|
+
const linkEnd = (link.index as number) + link[0].length;
|
|
64
|
+
if (from > (link.index as number) && from < linkEnd) from = link.index as number;
|
|
65
|
+
if (to > (link.index as number) && to < linkEnd) to = linkEnd;
|
|
66
|
+
}
|
|
67
|
+
if (line.slice(from, to).trim() !== "") ranges.push([lineStart + from, lineStart + to]);
|
|
68
|
+
}
|
|
69
|
+
lineStart = lineEnd + 1;
|
|
70
|
+
}
|
|
71
|
+
let marked = body;
|
|
72
|
+
for (const [from, to] of ranges.reverse()) {
|
|
73
|
+
marked = marked.slice(0, from) + MARK_OPEN + marked.slice(from, to) + MARK_CLOSE + marked.slice(to);
|
|
74
|
+
}
|
|
75
|
+
return marked;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Split one text node's text at the markers. `inside` carries over from the
|
|
80
|
+
* previous text node, because a marked range can span elements
|
|
81
|
+
* (`plain **bold** plain`).
|
|
82
|
+
*/
|
|
83
|
+
export function splitMarked(text: string, inside: boolean): { parts: Array<{ text: string; marked: boolean }>; inside: boolean } {
|
|
84
|
+
const parts: Array<{ text: string; marked: boolean }> = [];
|
|
85
|
+
let current = "";
|
|
86
|
+
let state = inside;
|
|
87
|
+
for (const char of text) {
|
|
88
|
+
if (char === MARK_OPEN || char === MARK_CLOSE) {
|
|
89
|
+
if (current !== "") parts.push({ text: current, marked: state });
|
|
90
|
+
current = "";
|
|
91
|
+
state = char === MARK_OPEN;
|
|
92
|
+
} else {
|
|
93
|
+
current += char;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
if (current !== "") parts.push({ text: current, marked: state });
|
|
97
|
+
return { parts, inside: state };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const WHO: Record<HistoryRevision["by"], string> = {
|
|
101
|
+
human: "You",
|
|
102
|
+
agent: "Agent",
|
|
103
|
+
outside: "Outside edit",
|
|
104
|
+
system: "pi-weave",
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const WHAT: Record<string, string> = {
|
|
108
|
+
create: "created the note",
|
|
109
|
+
edit: "edited",
|
|
110
|
+
append: "added to the note",
|
|
111
|
+
raw: "added words to Raw",
|
|
112
|
+
finalize: "rewrote the summary",
|
|
113
|
+
generate: "generated the note",
|
|
114
|
+
repair: "fixed links",
|
|
115
|
+
outside: "changed outside pi-weave",
|
|
116
|
+
baseline: "earliest saved version",
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/** One step of the slider, as the column shows it. */
|
|
120
|
+
export interface ReplayStep {
|
|
121
|
+
/** "3 of 12". */
|
|
122
|
+
position: string;
|
|
123
|
+
/** "Agent rewrote the summary". */
|
|
124
|
+
action: string;
|
|
125
|
+
/** Short local time; the full ISO string goes in `atIso`. */
|
|
126
|
+
at: string;
|
|
127
|
+
atIso: string;
|
|
128
|
+
/** "+120 −40", or "" when nothing changed in the text. */
|
|
129
|
+
change: string;
|
|
130
|
+
/** The body to render, with the added text marked. */
|
|
131
|
+
marked: string;
|
|
132
|
+
by: HistoryRevision["by"];
|
|
133
|
+
/**
|
|
134
|
+
* Whether the column scrolls to the change. Not when it starts in the
|
|
135
|
+
* `## Raw` tail: dictation lands at the very end of the note, and following
|
|
136
|
+
* it dragged the reader to the bottom on every Raw step, then back up on the
|
|
137
|
+
* next edit. The added words are still marked where they are.
|
|
138
|
+
*/
|
|
139
|
+
follow: boolean;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function shortTime(iso: string, previous: string | null): string {
|
|
143
|
+
const date = new Date(iso);
|
|
144
|
+
if (Number.isNaN(date.getTime())) return iso;
|
|
145
|
+
const time = date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" });
|
|
146
|
+
const sameDay = previous !== null && new Date(previous).toDateString() === date.toDateString();
|
|
147
|
+
return sameDay ? time : `${date.toLocaleDateString(undefined, { month: "short", day: "numeric" })}, ${time}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Whether offset `at` of `body` falls in its `## Raw` tail (or the gap just above it). */
|
|
151
|
+
export function inRawTail(body: string, at: number): boolean {
|
|
152
|
+
const { head, tail } = splitRawTail(body);
|
|
153
|
+
return tail !== "" && at >= head.length;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Describe revision `index`. Clamped, so a stale index after a refetch still shows something. */
|
|
157
|
+
export function replayStep(revisions: readonly HistoryRevision[], index: number): ReplayStep | null {
|
|
158
|
+
if (revisions.length === 0) return null;
|
|
159
|
+
const i = Math.max(0, Math.min(revisions.length - 1, index));
|
|
160
|
+
const revision = revisions[i] as HistoryRevision;
|
|
161
|
+
const previous = i === 0 ? null : (revisions[i - 1] as HistoryRevision);
|
|
162
|
+
const range = changedRange(previous?.body ?? "", revision.body);
|
|
163
|
+
const added = range.end - range.start;
|
|
164
|
+
const change = [added > 0 ? `+${added}` : "", range.removed > 0 ? `−${range.removed}` : ""].filter(Boolean).join(" ");
|
|
165
|
+
const what = WHAT[revision.op] ?? revision.op;
|
|
166
|
+
return {
|
|
167
|
+
position: `${i + 1} of ${revisions.length}`,
|
|
168
|
+
action: revision.op === "baseline" ? what[0]!.toUpperCase() + what.slice(1) : `${WHO[revision.by]} ${what}`,
|
|
169
|
+
at: shortTime(revision.at, previous?.at ?? null),
|
|
170
|
+
atIso: revision.at,
|
|
171
|
+
change,
|
|
172
|
+
marked: markChange(previous?.body ?? null, revision.body),
|
|
173
|
+
by: revision.by,
|
|
174
|
+
follow: !inRawTail(revision.body, range.start),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** One bar of the history band: how much a version changed, and who changed it. */
|
|
179
|
+
export interface ChangeBar {
|
|
180
|
+
/** 0–1 of the band's height; never so small that a version disappears. */
|
|
181
|
+
height: number;
|
|
182
|
+
by: HistoryRevision["by"];
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** The shortest bar, so a one-character fix still reads as a version. */
|
|
186
|
+
export const MIN_BAR = 0.14;
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Bar heights for the history band (the refresh's "change bars"). Height
|
|
190
|
+
* follows how many characters a version added and removed, on a log scale so
|
|
191
|
+
* one big rewrite does not flatten every other bar into the floor. The first
|
|
192
|
+
* version counts its whole body.
|
|
193
|
+
*/
|
|
194
|
+
export function changeBars(revisions: readonly HistoryRevision[]): ChangeBar[] {
|
|
195
|
+
const sizes = revisions.map((revision, i) => {
|
|
196
|
+
const range = changedRange(i === 0 ? "" : (revisions[i - 1] as HistoryRevision).body, revision.body);
|
|
197
|
+
return range.end - range.start + range.removed;
|
|
198
|
+
});
|
|
199
|
+
const top = Math.log1p(Math.max(0, ...sizes));
|
|
200
|
+
return revisions.map((revision, i) => ({
|
|
201
|
+
height: top === 0 ? MIN_BAR : MIN_BAR + (1 - MIN_BAR) * (Math.log1p(sizes[i] as number) / top),
|
|
202
|
+
by: revision.by,
|
|
203
|
+
}));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** A day label under the band, at a fraction of its width. */
|
|
207
|
+
export interface DayMark {
|
|
208
|
+
/** 0–1 along the band: version index over the last index. */
|
|
209
|
+
at: number;
|
|
210
|
+
label: string;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Where each day starts along the band, labelled "Today", "Yesterday" or a
|
|
215
|
+
* short date. The first version always gets a label, so the band says when
|
|
216
|
+
* the history begins.
|
|
217
|
+
*/
|
|
218
|
+
export function dayMarks(revisions: readonly HistoryRevision[], now: number): DayMark[] {
|
|
219
|
+
const today = new Date(now);
|
|
220
|
+
const yesterday = new Date(now - 86_400_000);
|
|
221
|
+
const last = Math.max(1, revisions.length - 1);
|
|
222
|
+
const marks: DayMark[] = [];
|
|
223
|
+
let previous = "";
|
|
224
|
+
revisions.forEach((revision, i) => {
|
|
225
|
+
const date = new Date(revision.at);
|
|
226
|
+
if (Number.isNaN(date.getTime())) return;
|
|
227
|
+
const day = date.toDateString();
|
|
228
|
+
if (day === previous) return;
|
|
229
|
+
previous = day;
|
|
230
|
+
const label = day === today.toDateString() ? "Today"
|
|
231
|
+
: day === yesterday.toDateString() ? "Yesterday"
|
|
232
|
+
: date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
|
233
|
+
marks.push({ at: revisions.length === 1 ? 0 : i / last, label });
|
|
234
|
+
});
|
|
235
|
+
return marks;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** The next index while playing, or null at the end. */
|
|
239
|
+
export function nextStep(index: number, count: number): number | null {
|
|
240
|
+
return index + 1 < count ? index + 1 : null;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export const HISTORY_LABEL = "History";
|
|
244
|
+
export const HISTORY_CLOSE_LABEL = "Close history";
|
|
245
|
+
export const HISTORY_OFF_HINT = "History is off, so new changes are not being saved. Turn it on in Settings → Editor.";
|
|
246
|
+
/** Shown on the oldest version kept, once older ones were dropped (the file holds at most 500). */
|
|
247
|
+
export const HISTORY_TRIMMED = "Older versions were trimmed";
|
|
248
|
+
export const HISTORY_EMPTY = "No saved versions yet. Changes are saved from now on.";
|
|
@@ -1,32 +1,72 @@
|
|
|
1
1
|
import { useEffect, useRef, useState } from "preact/hooks";
|
|
2
2
|
import type { GraphPayload } from "../../shared/wire";
|
|
3
3
|
import { useFocusTrap } from "../shell/FocusTrap";
|
|
4
|
+
import { ICON_BOX, ICON_STROKE } from "../shell/icons.model";
|
|
5
|
+
import type { IconDef } from "../shell/icons.model";
|
|
4
6
|
import { fetchSearch } from "../api";
|
|
5
7
|
import type { FetchLike } from "../api";
|
|
6
|
-
import type { SearchRowView } from "./search.model";
|
|
7
|
-
import {
|
|
8
|
-
|
|
8
|
+
import type { PaletteIntent, SearchRowView, SearchState } from "./search.model";
|
|
9
|
+
import {
|
|
10
|
+
DEBOUNCE_MS,
|
|
11
|
+
LIGHT_UP_KEY,
|
|
12
|
+
PALETTE_HINT,
|
|
13
|
+
PALETTE_PLACEHOLDER,
|
|
14
|
+
PALETTE_TITLE,
|
|
15
|
+
SCOPE_GROUP_LABEL,
|
|
16
|
+
SEARCH_GLYPH,
|
|
17
|
+
closesPalette,
|
|
18
|
+
initialSearchState,
|
|
19
|
+
lightUpIntent,
|
|
20
|
+
lightUpLabel,
|
|
21
|
+
nextState,
|
|
22
|
+
paletteKey,
|
|
23
|
+
paletteModel,
|
|
24
|
+
rowIntent,
|
|
25
|
+
serverQuery,
|
|
26
|
+
} from "./search.model";
|
|
9
27
|
|
|
10
28
|
export interface SearchPaletteProps {
|
|
11
29
|
graph: GraphPayload | null;
|
|
12
30
|
onSelect: (id: string, newTab?: boolean) => void;
|
|
13
31
|
onClose: () => void;
|
|
32
|
+
/** Light the query's matches (`ids`, the results) up in the graph. The footer offers it only when this is given. */
|
|
33
|
+
onLightUp?: (query: string, ids: readonly string[]) => void;
|
|
14
34
|
ports: { fetch: FetchLike };
|
|
15
35
|
}
|
|
16
36
|
|
|
37
|
+
function Glyph({ def, class: className }: { def: IconDef; class: string }) {
|
|
38
|
+
return (
|
|
39
|
+
<svg
|
|
40
|
+
class={className}
|
|
41
|
+
width={ICON_BOX}
|
|
42
|
+
height={ICON_BOX}
|
|
43
|
+
viewBox={`0 0 ${ICON_BOX} ${ICON_BOX}`}
|
|
44
|
+
fill={def.filled ? "currentColor" : "none"}
|
|
45
|
+
stroke={def.filled ? undefined : "currentColor"}
|
|
46
|
+
stroke-width={def.filled ? undefined : def.width ?? ICON_STROKE}
|
|
47
|
+
stroke-linecap="round"
|
|
48
|
+
stroke-linejoin="round"
|
|
49
|
+
aria-hidden="true"
|
|
50
|
+
>
|
|
51
|
+
{def.d.map((d) => <path d={d} />)}
|
|
52
|
+
</svg>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
17
56
|
function Row({ row, onPick, onHover }: { row: SearchRowView; onPick: (newTab: boolean) => void; onHover: () => void }) {
|
|
18
57
|
return (
|
|
19
58
|
<li
|
|
20
59
|
id={row.domId}
|
|
21
60
|
role="option"
|
|
22
61
|
aria-selected={row.active}
|
|
23
|
-
class={`weave-hit weave-hit-${row.
|
|
62
|
+
class={`weave-hit weave-hit-${row.group}${row.active ? " weave-hit-on" : ""}`}
|
|
24
63
|
onMouseMove={onHover}
|
|
25
64
|
onClick={(event) => onPick(event.metaKey || event.ctrlKey)}
|
|
26
65
|
>
|
|
27
|
-
<
|
|
28
|
-
<span class="weave-hit-label">{row.label}</span>
|
|
29
|
-
<span class="weave-hit-detail">{row.
|
|
66
|
+
<Glyph def={row.icon} class="weave-icon weave-hit-icon" />
|
|
67
|
+
<span class={row.mono ? "weave-hit-label weave-hit-mono" : "weave-hit-label"}>{row.label}</span>
|
|
68
|
+
<span class="weave-hit-detail">{row.parts.map((part) => part.mark ? <mark class="weave-hit-mark">{part.text}</mark> : part.text)}</span>
|
|
69
|
+
{row.meta ? <span class="weave-hit-meta">{row.meta}</span> : null}
|
|
30
70
|
</li>
|
|
31
71
|
);
|
|
32
72
|
}
|
|
@@ -41,7 +81,7 @@ export function SearchPalette(props: SearchPaletteProps) {
|
|
|
41
81
|
useEffect(() => void input.current?.focus(), []);
|
|
42
82
|
|
|
43
83
|
useEffect(() => {
|
|
44
|
-
const query = state.query
|
|
84
|
+
const query = serverQuery(state.query);
|
|
45
85
|
let active = true;
|
|
46
86
|
if (!query) {
|
|
47
87
|
abort.current?.abort();
|
|
@@ -69,15 +109,17 @@ export function SearchPalette(props: SearchPaletteProps) {
|
|
|
69
109
|
};
|
|
70
110
|
}, [state.query, props.ports.fetch]);
|
|
71
111
|
|
|
72
|
-
const model = paletteModel(state, props.graph);
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
if (
|
|
76
|
-
props.onClose();
|
|
112
|
+
const model = paletteModel(state, props.graph, Date.now());
|
|
113
|
+
const run = (intent: PaletteIntent): void => {
|
|
114
|
+
if (intent.type === "open") props.onSelect(intent.id, intent.newTab);
|
|
115
|
+
if (intent.type === "lightUp") props.onLightUp?.(intent.query, intent.ids);
|
|
116
|
+
if (closesPalette(intent)) props.onClose();
|
|
117
|
+
else setState((current) => nextState(current, intent));
|
|
118
|
+
if (intent.type === "scope" || intent.type === "tag") input.current?.focus();
|
|
77
119
|
};
|
|
78
120
|
|
|
79
121
|
return (
|
|
80
|
-
<div class="weave-scrim" onClick={() =>
|
|
122
|
+
<div class="weave-scrim" onClick={() => run({ type: "close" })}>
|
|
81
123
|
<div
|
|
82
124
|
class="weave-palette"
|
|
83
125
|
role="dialog"
|
|
@@ -87,36 +129,63 @@ export function SearchPalette(props: SearchPaletteProps) {
|
|
|
87
129
|
ref={trap.ref as { current: HTMLDivElement | null }}
|
|
88
130
|
onClick={(event) => event.stopPropagation()}
|
|
89
131
|
onKeyDown={(event) => {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
132
|
+
const intent = paletteKey(model, { key: event.key, shift: event.shiftKey, mod: event.metaKey || event.ctrlKey, inInput: event.target === input.current }, props.onLightUp !== undefined);
|
|
133
|
+
if (intent.type === "none") {
|
|
134
|
+
trap.onKeyDown(event as unknown as KeyboardEvent);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
93
137
|
event.preventDefault();
|
|
94
|
-
|
|
95
|
-
if (next.dismiss) pick(next.activate, event.metaKey || event.ctrlKey);
|
|
138
|
+
run(intent);
|
|
96
139
|
}}
|
|
97
140
|
>
|
|
98
|
-
<
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
141
|
+
<div class="weave-palette-bar">
|
|
142
|
+
<Glyph def={SEARCH_GLYPH} class="weave-icon weave-palette-glyph" />
|
|
143
|
+
<input
|
|
144
|
+
type="search"
|
|
145
|
+
class="weave-palette-input"
|
|
146
|
+
ref={input}
|
|
147
|
+
value={state.query}
|
|
148
|
+
placeholder={PALETTE_PLACEHOLDER}
|
|
149
|
+
role="combobox"
|
|
150
|
+
aria-label={PALETTE_TITLE}
|
|
151
|
+
aria-expanded={model.count > 0}
|
|
152
|
+
aria-autocomplete="list"
|
|
153
|
+
aria-controls="weave-search-results"
|
|
154
|
+
aria-activedescendant={model.activeDomId ?? undefined}
|
|
155
|
+
onInput={(event) => setState((current) => ({ ...current, query: event.currentTarget.value, cursor: 0 }))}
|
|
156
|
+
/>
|
|
157
|
+
<kbd class="weave-palette-kbd" aria-hidden="true">Esc</kbd>
|
|
158
|
+
</div>
|
|
159
|
+
{model.scopes.length ? (
|
|
160
|
+
<div class="weave-palette-scopes" role="group" aria-label={SCOPE_GROUP_LABEL}>
|
|
161
|
+
{model.scopes.map((scope) => (
|
|
162
|
+
<button key={scope.scope} type="button" class="weave-palette-scope" aria-pressed={scope.active} onClick={() => run({ type: "scope", scope: scope.scope })}>
|
|
163
|
+
{scope.label} <span class="weave-palette-scope-count">{scope.count}</span>
|
|
164
|
+
</button>
|
|
165
|
+
))}
|
|
166
|
+
</div>
|
|
167
|
+
) : null}
|
|
109
168
|
{model.status === null ? (
|
|
110
169
|
<ul id="weave-search-results" class="weave-hits" role="listbox" aria-label={PALETTE_TITLE}>
|
|
111
|
-
{model.
|
|
170
|
+
{model.groups.map((group) => (
|
|
171
|
+
<li key={group.group} role="presentation" class="weave-hit-section">
|
|
172
|
+
<div id={group.domId} role="presentation" class="weave-hit-group">{group.heading}</div>
|
|
173
|
+
<ul role="group" aria-labelledby={group.domId} class="weave-hit-rows">
|
|
174
|
+
{group.rows.map((row) => <Row key={row.id} row={row} onPick={(newTab) => run(rowIntent(row, newTab))} onHover={() => run({ type: "move", cursor: row.index })} />)}
|
|
175
|
+
</ul>
|
|
176
|
+
</li>
|
|
177
|
+
))}
|
|
112
178
|
</ul>
|
|
113
179
|
) : (
|
|
114
180
|
<p class="weave-palette-status" role="status">{model.status}</p>
|
|
115
181
|
)}
|
|
116
182
|
<p class="weave-palette-foot">
|
|
117
|
-
<span>{
|
|
118
|
-
|
|
119
|
-
|
|
183
|
+
<span>{PALETTE_HINT}</span>
|
|
184
|
+
{props.onLightUp && model.lightUp.length > 0 ? (
|
|
185
|
+
<button type="button" class="weave-palette-light" onClick={() => run(lightUpIntent(model))}>
|
|
186
|
+
{lightUpLabel(model.lightUp.length)} <kbd class="weave-palette-kbd">{LIGHT_UP_KEY}</kbd>
|
|
187
|
+
</button>
|
|
188
|
+
) : null}
|
|
120
189
|
</p>
|
|
121
190
|
</div>
|
|
122
191
|
</div>
|