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
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* workspaceRoot.ts — the v2 workspace ROOT component (weave-view-tui-v2 §4, §7).
|
|
3
|
-
*
|
|
4
|
-
* Sits above the pure fixed `workspace.ts` model and the `surface/*`
|
|
5
|
-
* components. It composes panes from a `Workspace` value into a real pi-tui
|
|
6
|
-
* tree (HStack/Pane), owns the single input listener, routes keys and
|
|
7
|
-
* cross-pane SurfaceEvents, renders the
|
|
8
|
-
* branded header + footer + narrow-mode tab bar (§9.2, decision 5), and
|
|
9
|
-
* applies responsive collapse.
|
|
10
|
-
*
|
|
11
|
-
* It is drivable with a fake tui/theme/loaders and is the path wired in run.ts.
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { HStack, matchesKey, truncateToWidth, visibleWidth, type Component } from "@earendil-works/pi-tui";
|
|
15
|
-
import { BodyStore } from "./bodyStore";
|
|
16
|
-
import { renderMark } from "./branding";
|
|
17
|
-
import type { WeaveLoaders, WeaveTheme, WeaveTui } from "./surface/base";
|
|
18
|
-
import { countProvenance } from "./model";
|
|
19
|
-
import type { GraphModel, GraphNode } from "../../../core/graph/model";
|
|
20
|
-
import { Pane } from "./surface/base";
|
|
21
|
-
import type { Surface, SurfaceEvent, SurfaceContext } from "./surface/base";
|
|
22
|
-
import { ExploreSurface } from "./surface/explore";
|
|
23
|
-
import { bindDetail, DetailSurface } from "./surface/detail";
|
|
24
|
-
import { FocusSurface } from "./surface/focus";
|
|
25
|
-
import { HealthSurface } from "./surface/health";
|
|
26
|
-
import { collapseForWidth, defaultWorkspace, focusNext, workspacePanes, type PaneNode, type Workspace } from "./workspace";
|
|
27
|
-
|
|
28
|
-
export interface WeaveWorkspaceOptions {
|
|
29
|
-
model: GraphModel;
|
|
30
|
-
theme: WeaveTheme;
|
|
31
|
-
tui: WeaveTui;
|
|
32
|
-
loaders: WeaveLoaders;
|
|
33
|
-
done: (result: null) => void;
|
|
34
|
-
rows?: number;
|
|
35
|
-
now?: () => number;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/** Decode the few root-level keys; all other input belongs to the active surface. */
|
|
39
|
-
export function decodeWorkspaceKey(data: string): string | null {
|
|
40
|
-
if (data === "?") return "help";
|
|
41
|
-
if (data === "q") return "quit";
|
|
42
|
-
if (data === "r") return "refresh";
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
export class WeaveWorkspace implements Component {
|
|
47
|
-
model: GraphModel;
|
|
48
|
-
private readonly theme: WeaveTheme;
|
|
49
|
-
private readonly tui: WeaveTui;
|
|
50
|
-
private readonly loaders: WeaveLoaders;
|
|
51
|
-
private readonly done: (result: null) => void;
|
|
52
|
-
private readonly rows: number;
|
|
53
|
-
private readonly nowFn: () => number;
|
|
54
|
-
private readonly logo: string;
|
|
55
|
-
private readonly bodies: BodyStore;
|
|
56
|
-
private ctx: SurfaceContext;
|
|
57
|
-
/** paneId → surface instance. */
|
|
58
|
-
private panes = new Map<string, Surface>();
|
|
59
|
-
/** The fixed pane layout + focus. */
|
|
60
|
-
workspace: Workspace;
|
|
61
|
-
private helpOpen = false;
|
|
62
|
-
refreshing = false;
|
|
63
|
-
private quitted = false;
|
|
64
|
-
private renderCache = new Map<string, string[]>();
|
|
65
|
-
wantsKeyRelease = false;
|
|
66
|
-
|
|
67
|
-
constructor(opts: WeaveWorkspaceOptions) {
|
|
68
|
-
this.model = opts.model;
|
|
69
|
-
this.theme = opts.theme;
|
|
70
|
-
this.tui = opts.tui;
|
|
71
|
-
this.loaders = opts.loaders;
|
|
72
|
-
this.done = opts.done;
|
|
73
|
-
this.rows = opts.rows ?? 24;
|
|
74
|
-
this.nowFn = opts.now ?? Date.now;
|
|
75
|
-
this.logo = renderMark(opts.theme, 20);
|
|
76
|
-
this.bodies = new BodyStore({
|
|
77
|
-
loaders: opts.loaders,
|
|
78
|
-
onChange: () => this.invalidateAndRender(),
|
|
79
|
-
});
|
|
80
|
-
this.ctx = {
|
|
81
|
-
model: opts.model,
|
|
82
|
-
theme: opts.theme,
|
|
83
|
-
loaders: opts.loaders,
|
|
84
|
-
bodies: this.bodies,
|
|
85
|
-
now: this.nowFn,
|
|
86
|
-
};
|
|
87
|
-
this.workspace = defaultWorkspace(opts.model);
|
|
88
|
-
this.syncPanes();
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
invalidate(): void {
|
|
92
|
-
this.renderCache.clear();
|
|
93
|
-
for (const s of this.panes.values()) s.invalidate();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
private invalidateAndRender(): void {
|
|
97
|
-
this.invalidate();
|
|
98
|
-
this.tui.requestRender();
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
setModel(model: GraphModel): void {
|
|
102
|
-
this.model = model;
|
|
103
|
-
// Surfaces read the model through the shared context (this.ctx.model), not
|
|
104
|
-
// this.model — so a refresh that only swaps this.model leaves every pane
|
|
105
|
-
// showing the pre-refresh graph. Propagate to the context so `r` actually
|
|
106
|
-
// re-renders the body, then drop stale pane selections that no longer exist.
|
|
107
|
-
this.ctx.model = model;
|
|
108
|
-
for (const s of this.panes.values()) s.rebind?.(model);
|
|
109
|
-
this.bodies.clear();
|
|
110
|
-
this.refreshing = false;
|
|
111
|
-
this.invalidateAndRender();
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/** Rebuild surface instances so every pane node in the tree has one. */
|
|
115
|
-
private syncPanes(): void {
|
|
116
|
-
const wanted = workspacePanes(this.workspace);
|
|
117
|
-
const next = new Map<string, Surface>();
|
|
118
|
-
for (const pn of wanted) {
|
|
119
|
-
let s = this.panes.get(pn.id);
|
|
120
|
-
if (!s) s = this.createSurface(pn);
|
|
121
|
-
next.set(pn.id, s);
|
|
122
|
-
}
|
|
123
|
-
this.panes = next;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
private createSurface(pn: PaneNode): Surface {
|
|
127
|
-
const init = { context: this.ctx, onEvent: (e: SurfaceEvent) => this.onSurfaceEvent(e) };
|
|
128
|
-
switch (pn.surface) {
|
|
129
|
-
case "explore":
|
|
130
|
-
return new ExploreSurface(init);
|
|
131
|
-
case "detail":
|
|
132
|
-
return pn.nodeId ? bindDetail(init, pn.nodeId) : new DetailSurface(init);
|
|
133
|
-
case "focus": {
|
|
134
|
-
const f = new FocusSurface(init);
|
|
135
|
-
if (pn.nodeId) f.setFocus(pn.nodeId);
|
|
136
|
-
return f;
|
|
137
|
-
}
|
|
138
|
-
case "health":
|
|
139
|
-
return new HealthSurface(init);
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
handleInput(data: string): void {
|
|
144
|
-
if (matchesKey(data, "tab")) return this.cycleFocus(1);
|
|
145
|
-
if (matchesKey(data, "shift+tab")) return this.cycleFocus(-1);
|
|
146
|
-
if (matchesKey(data, "escape")) {
|
|
147
|
-
// Esc precedence (mirrors v1): if the active pane is in a sub-mode
|
|
148
|
-
// (Explore search OR Detail goto-line), Esc clears it; otherwise Esc
|
|
149
|
-
// quits the explorer.
|
|
150
|
-
const active = this.activePane();
|
|
151
|
-
if (active && inSubMode(active)) {
|
|
152
|
-
active.handleInput?.(data);
|
|
153
|
-
this.invalidate();
|
|
154
|
-
this.tui.requestRender();
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
this.quit();
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
const wsKey = decodeWorkspaceKey(data);
|
|
161
|
-
if (wsKey) {
|
|
162
|
-
this.applyWorkspaceKey(wsKey);
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
const active = this.activePane();
|
|
166
|
-
if (active?.handleInput) {
|
|
167
|
-
active.handleInput(data);
|
|
168
|
-
// CRITICAL: the surface updated its own selection/scroll state, but it
|
|
169
|
-
// has no `tui` and the workspace render cache is now stale. Invalidate
|
|
170
|
-
// (clears the workspace + per-surface caches) and request a fresh render
|
|
171
|
-
// — without this, arrow/enter/filter keys change state but the screen
|
|
172
|
-
// never updates (the "arrows don't navigate" bug).
|
|
173
|
-
this.invalidate();
|
|
174
|
-
this.tui.requestRender();
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
private quit(): void {
|
|
179
|
-
if (!this.quitted) {
|
|
180
|
-
this.quitted = true;
|
|
181
|
-
this.done(null);
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
private applyWorkspaceKey(key: string): void {
|
|
186
|
-
switch (key) {
|
|
187
|
-
case "quit":
|
|
188
|
-
this.quit();
|
|
189
|
-
return;
|
|
190
|
-
case "refresh":
|
|
191
|
-
if (this.refreshing) return;
|
|
192
|
-
this.refreshing = true;
|
|
193
|
-
this.invalidateAndRender();
|
|
194
|
-
void this.loaders
|
|
195
|
-
.rebuild()
|
|
196
|
-
.then((m) => this.setModel(m))
|
|
197
|
-
.catch(() => {
|
|
198
|
-
this.refreshing = false;
|
|
199
|
-
this.invalidateAndRender();
|
|
200
|
-
});
|
|
201
|
-
return;
|
|
202
|
-
case "help":
|
|
203
|
-
this.helpOpen = !this.helpOpen;
|
|
204
|
-
this.invalidateAndRender();
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
private cycleFocus(dir: 1 | -1): void {
|
|
210
|
-
this.workspace = focusNext(this.workspace, dir);
|
|
211
|
-
this.refreshFocusFlags();
|
|
212
|
-
this.invalidateAndRender();
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
private refreshFocusFlags(): void {
|
|
216
|
-
for (const pn of workspacePanes(this.workspace)) {
|
|
217
|
-
const s = this.panes.get(pn.id);
|
|
218
|
-
s?.setFocused(pn.id === this.workspace.activePaneId);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
private setActive(paneId: string): void {
|
|
223
|
-
this.workspace = { ...this.workspace, activePaneId: paneId };
|
|
224
|
-
this.refreshFocusFlags();
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
private activePane(): Surface | undefined {
|
|
228
|
-
return this.panes.get(this.workspace.activePaneId);
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
private onSurfaceEvent(e: SurfaceEvent): void {
|
|
232
|
-
if (e.type === "openEditor") {
|
|
233
|
-
this.openInEditor(e.id);
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
if (e.type === "openDetail") {
|
|
237
|
-
this.openDetail(e.id);
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
if (e.type === "focusNode") {
|
|
241
|
-
this.openFocus(e.id);
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
private openInEditor(id: string): void {
|
|
246
|
-
const node = this.model.nodes.find((n) => n.id === id);
|
|
247
|
-
if (!node || node.kind !== "note") return;
|
|
248
|
-
if (node.detail.slug) void this.loaders.openNote(node.detail.slug);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/** Open a selected node in the fixed Detail pane. */
|
|
252
|
-
private openDetail(id: string): void {
|
|
253
|
-
// (The active Detail surface already rebinds itself on enter, so no
|
|
254
|
-
// active-pane special case is needed here.)
|
|
255
|
-
const panes = workspacePanes(this.workspace);
|
|
256
|
-
const detail = panes.find((p) => p.surface === "detail");
|
|
257
|
-
if (!detail) return;
|
|
258
|
-
this.bindDetail(detail.id, id);
|
|
259
|
-
this.setActive(detail.id);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
private bindDetail(paneId: string, id: string): void {
|
|
263
|
-
const s = this.panes.get(paneId) as DetailSurface;
|
|
264
|
-
if (!s) return;
|
|
265
|
-
s.state = { nodeId: id, selectedId: id, scrollOffset: 0 };
|
|
266
|
-
this.requestBody(id);
|
|
267
|
-
this.invalidateAndRender();
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
private openFocus(id: string): void {
|
|
271
|
-
const active = this.activePane();
|
|
272
|
-
if (active && active.kind === "focus") {
|
|
273
|
-
(active as FocusSurface).setFocus(id);
|
|
274
|
-
this.invalidateAndRender();
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
const panes = workspacePanes(this.workspace);
|
|
278
|
-
const focus = panes.find((p) => p.surface === "focus");
|
|
279
|
-
if (focus) {
|
|
280
|
-
(this.panes.get(focus.id) as FocusSurface).setFocus(id);
|
|
281
|
-
this.setActive(focus.id);
|
|
282
|
-
this.invalidateAndRender();
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
private requestBody(id: string): void {
|
|
287
|
-
const node = this.model.nodes.find((n) => n.id === id);
|
|
288
|
-
if (!node) return;
|
|
289
|
-
if (node.kind === "note") this.bodies.load(id, "note", node.detail.slug);
|
|
290
|
-
else if (node.kind === "file") this.bodies.load(id, "file", node.detail.path);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
render(width: number): string[] {
|
|
294
|
-
const key = `${width}:${this.workspace.activePaneId}:${this.helpOpen}:${this.refreshing}`;
|
|
295
|
-
const cached = this.renderCache.get(key);
|
|
296
|
-
if (cached) return cached;
|
|
297
|
-
const out: string[] = [];
|
|
298
|
-
out.push(...this.renderHeader(width));
|
|
299
|
-
out.push(...this.renderBody(width));
|
|
300
|
-
if (width < 80) out.push(...this.renderTabBar(width));
|
|
301
|
-
out.push(...this.renderFooter(width));
|
|
302
|
-
const clamped = out.slice(0, Math.max(1, this.rows)).map((l) => (visibleWidth(l) > width ? truncateToWidth(l, width) : l));
|
|
303
|
-
this.renderCache.set(key, clamped);
|
|
304
|
-
return clamped;
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
private renderHeader(width: number): string[] {
|
|
308
|
-
const t = this.theme;
|
|
309
|
-
const counts = countProvenance(this.model.nodes);
|
|
310
|
-
const repo = this.model.nodes.find((n) => n.kind === "repository");
|
|
311
|
-
const repoState = repoStaleness(this.model, repo);
|
|
312
|
-
const repoPart = repo ? ` · repo ${repo.label}:${repoState}` : "";
|
|
313
|
-
const mark = this.logo;
|
|
314
|
-
const head = `${mark}${mark ? " " : ""}${t.bold("weave view")}`;
|
|
315
|
-
const fill = "─".repeat(Math.max(1, width - visibleWidth(head) - 2));
|
|
316
|
-
const line1 = `${head} ${fill} ${this.workspace.name} · ${workspacePanes(this.workspace).length} panes${repoPart}`;
|
|
317
|
-
const out: string[] = [truncateToWidth(line1, width)];
|
|
318
|
-
const banner = this.bannerText();
|
|
319
|
-
if (banner) out.push(t.fg("warning", truncateToWidth(banner, width)));
|
|
320
|
-
const countsLine = `notes ${counts.total} (● ${counts.human} / ◐ ${counts.agent} / ○ ${counts.generated})`;
|
|
321
|
-
out.push(truncateToWidth(t.fg("dim", countsLine), width));
|
|
322
|
-
return out;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
private bannerText(): string | null {
|
|
326
|
-
return this.refreshing ? "refreshing…" : null;
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
private renderBody(width: number): string[] {
|
|
330
|
-
const effective = collapseForWidth(this.workspace, width);
|
|
331
|
-
const bodyRows = Math.max(1, this.rows - 4);
|
|
332
|
-
const stack = new HStack();
|
|
333
|
-
for (const pane of effective.panes) stack.addChild(this.buildPane(pane, bodyRows), { grow: 1 });
|
|
334
|
-
return stack.render(width);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
private buildPane(node: PaneNode, rows: number): Component {
|
|
338
|
-
const s = this.panes.get(node.id);
|
|
339
|
-
if (!s) return { render: () => [], invalidate: () => {} };
|
|
340
|
-
(s as { paneRows?: number }).paneRows = Math.max(1, rows - 3);
|
|
341
|
-
const p = new Pane(s, this.theme);
|
|
342
|
-
p.rows = rows;
|
|
343
|
-
p.setFocused(node.id === this.workspace.activePaneId);
|
|
344
|
-
return p;
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
private renderTabBar(width: number): string[] {
|
|
348
|
-
const panes = workspacePanes(this.workspace);
|
|
349
|
-
const t = this.theme;
|
|
350
|
-
const parts = panes.map((p) => {
|
|
351
|
-
const active = p.id === this.workspace.activePaneId;
|
|
352
|
-
return active ? t.fg("accent", `[${p.surface}]`) : p.surface;
|
|
353
|
-
});
|
|
354
|
-
return [truncateToWidth(parts.join(" "), width)];
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
private renderFooter(width: number): string[] {
|
|
358
|
-
const t = this.theme;
|
|
359
|
-
if (this.helpOpen) {
|
|
360
|
-
const help = [
|
|
361
|
-
"Tab focus · ↑↓/jk move · ←→/hl expand · enter open · f focus · r refresh · ? help · q quit",
|
|
362
|
-
"↑↓/jk move · ←→/hl expand · enter open · / filter · p prov · f focus · r refresh · ? help · q quit",
|
|
363
|
-
];
|
|
364
|
-
return help.map((l) => t.fg("dim", truncateToWidth(l, width)));
|
|
365
|
-
}
|
|
366
|
-
const hint = "Tab focus · ↑↓/jk move · ←→/hl expand · enter open · f focus · r refresh · ? help · q quit";
|
|
367
|
-
return [t.fg("dim", truncateToWidth(hint, width))];
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
/** True when the active surface is in a sub-mode that Esc should clear instead
|
|
372
|
-
* of quitting — Explore's search (`state.searching`) or Detail's goto-line
|
|
373
|
-
* (`state.gotoBuf`). Surfaces own their own state; read defensively so a
|
|
374
|
-
* surface without either field simply returns false. */
|
|
375
|
-
function inSubMode(s: Surface | undefined): boolean {
|
|
376
|
-
const st = (s as { state?: { searching?: boolean; gotoBuf?: string | null } }).state;
|
|
377
|
-
return !!st && (st.searching === true || st.gotoBuf != null);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
function repoStaleness(model: GraphModel, repo: GraphNode | undefined): string {
|
|
381
|
-
if (repo) {
|
|
382
|
-
const st = model.staleness;
|
|
383
|
-
if (st && "state" in st) return st.state;
|
|
384
|
-
return "fresh";
|
|
385
|
-
}
|
|
386
|
-
return "missing";
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
export type { Component };
|