pi-better-goal 0.10.3 → 0.11.0
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 +1 -1
- package/package.json +1 -1
- package/src/index.ts +6 -45
- package/src/shared-navigator-panels.ts +250 -0
package/README.md
CHANGED
|
@@ -14,7 +14,7 @@ Use `pi-better-goal` when a Pi session should keep an explicit objective visible
|
|
|
14
14
|
|
|
15
15
|
- `/goal` runtime for starting, pausing, resuming, completing, and clearing the current objective.
|
|
16
16
|
- `/goal settings` opens an interactive settings page in the TUI. Select a row with Up/Down and toggle with Space/Enter; changes save immediately, and Escape closes the page. All three controls default to `on`. Automatic continuation controls idle and background-drain wakes; conversational resume controls the agent's Escape-pause resume tool; **Pause on Esc** controls whether editor Escape pauses the active goal. Goal kickoff, `/goal resume`, and `alt+g` remain available with these controls off. For scripts or non-interactive sessions, use `/goal settings auto-continue on|off`, `/goal settings conversational-resume on|off`, and `/goal settings pause-on-escape on|off`; bare `/goal settings` prints the saved values outside the TUI.
|
|
17
|
-
- `escape` pauses the active goal by default, and it stays paused while you talk to the agent. With **Pause on Esc** off, observed editor Escape leaves the goal active while preserving native streaming interruption and menu/dialog cancellation. Automatic continuation remains independent. Hosts without terminal/focus inspection and unknown custom editors retain generic abort-to-pause behavior because they cannot identify Escape reliably. With conversational resume enabled, say "go" (the agent then calls `goal_resume`), or use `/goal resume` or `alt+g`. `/goal pause` resumes only through `/goal resume` or `alt+g`.
|
|
17
|
+
- `escape` pauses the active goal by default, and it stays paused while you talk to the agent. With **Pause on Esc** off, observed editor Escape leaves the goal active while preserving native streaming interruption and menu/dialog cancellation. Automatic continuation remains independent. Hosts without terminal/focus inspection and unknown custom editors retain generic abort-to-pause behavior because they cannot identify Escape reliably. With conversational resume enabled, say "go" (the agent then calls `goal_resume`), or use `/goal resume` or `alt+g`. `/goal pause` resumes only through `/goal resume` or `alt+g`. Paused goals are never poked. On a macOS terminal without Option-as-Meta, `alt+g` types `©`; use `/goal resume` there.
|
|
18
18
|
- Background work that finishes while an `ask_user_question` is pending is handed to the agent right after the answer.
|
|
19
19
|
- A compact goal widget that does not replace Pi's footer.
|
|
20
20
|
- Background activity tracking for subagents and other registered providers.
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
renderGoalClockLine,
|
|
39
39
|
} from "./goal-clock.js";
|
|
40
40
|
import { createRenderScheduler } from "./shared-render-scheduler.js";
|
|
41
|
+
import { initializeNavigatorPanels, setNavigatorPanel } from "./shared-navigator-panels.js";
|
|
41
42
|
import { collectSubagentActivity } from "./subagents.js";
|
|
42
43
|
import { currentWorkflowOwner, skillCommandName, workflowEntry, workflowOwnerFromSkill, WORKFLOW_ENTRY_TYPE } from "./workflow.js";
|
|
43
44
|
import { completionGate, pauseGate, schedulerWaitUntil } from "./workflow-gates.js";
|
|
@@ -301,6 +302,7 @@ function formatSnapshot(snapshot: ActivitySnapshot): string {
|
|
|
301
302
|
}
|
|
302
303
|
|
|
303
304
|
export default function (pi: ExtensionAPI): void {
|
|
305
|
+
initializeNavigatorPanels(pi);
|
|
304
306
|
const providers = new Map<string, BackgroundActivityProvider>();
|
|
305
307
|
const providerUnsubscribers = new Map<string, () => void>();
|
|
306
308
|
providers.set("subagents", {
|
|
@@ -374,7 +376,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
374
376
|
* Chat `notify` appends lines above the Working/bash status region. While the
|
|
375
377
|
* agent is streaming, that height change desyncs Pi main-screen differential
|
|
376
378
|
* rendering and stacks "Working..." / "Elapsed Xs" into scrollback. Keep
|
|
377
|
-
* mid-stream feedback out of the transcript
|
|
379
|
+
* mid-stream non-error feedback out of the transcript.
|
|
378
380
|
*/
|
|
379
381
|
const notifyGoal = (
|
|
380
382
|
ctx: ExtensionContext,
|
|
@@ -385,11 +387,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
385
387
|
return;
|
|
386
388
|
}
|
|
387
389
|
if (isForegroundBusy(ctx) && type !== "error") {
|
|
388
|
-
try {
|
|
389
|
-
ctx.ui.setStatus(EXTENSION_NAME, message);
|
|
390
|
-
} catch {
|
|
391
|
-
// Footer status is best-effort only.
|
|
392
|
-
}
|
|
393
390
|
return;
|
|
394
391
|
}
|
|
395
392
|
ctx.ui.notify(message, type);
|
|
@@ -424,7 +421,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
424
421
|
clearIdleContinuation();
|
|
425
422
|
syncPollingState();
|
|
426
423
|
syncResumeTool(goal);
|
|
427
|
-
applyStatus(ctx);
|
|
428
424
|
// Force a full redraw when the dock height changes (absent ↔ visible clock).
|
|
429
425
|
refreshGoalWidget?.(!wasVisible || !isGoalClockVisible(goal));
|
|
430
426
|
};
|
|
@@ -443,7 +439,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
443
439
|
clearIdleContinuation();
|
|
444
440
|
syncPollingState();
|
|
445
441
|
syncResumeTool(null);
|
|
446
|
-
applyStatus(ctx);
|
|
447
442
|
refreshGoalWidget?.(wasVisible);
|
|
448
443
|
};
|
|
449
444
|
|
|
@@ -726,41 +721,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
726
721
|
pi.events.emit(EVENT_TERMINAL_ATTENTION, snapshot);
|
|
727
722
|
if (execution !== executionGeneration) return null;
|
|
728
723
|
}
|
|
729
|
-
applyStatus(ctx);
|
|
730
|
-
|
|
731
724
|
return snapshot;
|
|
732
725
|
};
|
|
733
726
|
|
|
734
|
-
/** Footer status from the goal state and the latest activity snapshot. */
|
|
735
|
-
const statusText = (ctx: ExtensionContext, snapshot: ActivitySnapshot | null): string | undefined => {
|
|
736
|
-
const goal = getGoal(ctx);
|
|
737
|
-
let waitingOnAnswer = 0;
|
|
738
|
-
if (snapshot) {
|
|
739
|
-
for (const activeAtStart of pendingQuestions.values()) {
|
|
740
|
-
waitingOnAnswer += finishedSinceQuestion(activeAtStart, snapshot).length;
|
|
741
|
-
}
|
|
742
|
-
}
|
|
743
|
-
if (waitingOnAnswer > 0) return `${waitingOnAnswer} background done; waiting on your answer`;
|
|
744
|
-
if (goal?.status === "paused") return pausedGoalStatus(goal, preferences.conversationalResume);
|
|
745
|
-
if (snapshot?.backgroundRunning) return undefined;
|
|
746
|
-
const continuation = goal ? currentContinuationState(ctx, goal.goalId) : null;
|
|
747
|
-
if (continuation?.blocked) return "waiting: no progress";
|
|
748
|
-
const goalStall = observeGoalStall(goal, continuation, {
|
|
749
|
-
foregroundRunning,
|
|
750
|
-
backgroundRunning: snapshot?.backgroundRunning ?? false,
|
|
751
|
-
});
|
|
752
|
-
return goalStall?.state === "stalled" ? "goal stalled" : undefined;
|
|
753
|
-
};
|
|
754
|
-
|
|
755
|
-
function applyStatus(ctx: ExtensionContext): void {
|
|
756
|
-
if (!ctx.hasUI) return;
|
|
757
|
-
try {
|
|
758
|
-
ctx.ui.setStatus(EXTENSION_NAME, statusText(ctx, latestSnapshot));
|
|
759
|
-
} catch {
|
|
760
|
-
// UI status is best-effort only.
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
|
|
764
727
|
/**
|
|
765
728
|
* `goal_resume` is in the model's tool list only while an escape-paused goal
|
|
766
729
|
* waits. Pi activates newly registered tools by default, so this also removes
|
|
@@ -832,7 +795,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
832
795
|
if (!ctx.hasUI) {
|
|
833
796
|
return;
|
|
834
797
|
}
|
|
835
|
-
|
|
798
|
+
setNavigatorPanel(
|
|
799
|
+
ctx,
|
|
836
800
|
EXTENSION_NAME,
|
|
837
801
|
(tui, theme) => {
|
|
838
802
|
const inputIsEditor = (): boolean => {
|
|
@@ -918,7 +882,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
918
882
|
preferences = writeGoalPreference(key, enabled);
|
|
919
883
|
if (previous.autoContinue !== preferences.autoContinue) clearIdleContinuation();
|
|
920
884
|
syncResumeTool(getGoal(ctx));
|
|
921
|
-
applyStatus(ctx);
|
|
922
885
|
if (!previous.autoContinue && preferences.autoContinue) {
|
|
923
886
|
const snapshot = await publishSnapshot(ctx);
|
|
924
887
|
const goal = getGoal(ctx);
|
|
@@ -1578,7 +1541,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1578
1541
|
if (blocked) {
|
|
1579
1542
|
refreshGoalWidget?.();
|
|
1580
1543
|
if (ctx.hasUI) {
|
|
1581
|
-
ctx.ui.setStatus(EXTENSION_NAME, "waiting: no progress");
|
|
1582
1544
|
ctx.ui.notify(
|
|
1583
1545
|
`Goal automatic continuation is waiting after ${noProgressRetries} identical retries (${evidence.summary}). Use /goal resume or alt+g to resume it.`,
|
|
1584
1546
|
"warning",
|
|
@@ -1630,8 +1592,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
1630
1592
|
latestSnapshot = null;
|
|
1631
1593
|
collectionPending = false;
|
|
1632
1594
|
try {
|
|
1633
|
-
ctx
|
|
1634
|
-
ctx.ui.setWidget(EXTENSION_NAME, undefined);
|
|
1595
|
+
setNavigatorPanel(ctx, EXTENSION_NAME, undefined);
|
|
1635
1596
|
} catch {
|
|
1636
1597
|
// Best-effort cleanup.
|
|
1637
1598
|
}
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// Generated from packages/navigator/panels.ts. Do not edit directly.
|
|
2
|
+
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { readHarnessSetting, updateHarnessSetting } from "./shared-harness-settings.js";
|
|
6
|
+
|
|
7
|
+
export const NAVIGATOR_PANEL_WIDGET_KEY = "background-work-list";
|
|
8
|
+
export const NAVIGATOR_ENTRY_TYPE = "pi-better-harness-navigator-panels";
|
|
9
|
+
export const NAVIGATOR_STICKY_SETTING_ID = "navigator-sticky";
|
|
10
|
+
type MouseEvent = { type: string; button: string; x: number; y: number; width: number; shift?: boolean; alt?: boolean; ctrl?: boolean };
|
|
11
|
+
type PanelComponent = Component & { dispose?(): void; handleMouse?(event: MouseEvent): { handled: true; render: boolean } | undefined };
|
|
12
|
+
export type NavigatorPanelFactory = (tui: TUI, theme: Theme) => PanelComponent;
|
|
13
|
+
type Panel = { factory: NavigatorPanelFactory; component?: PanelComponent | undefined };
|
|
14
|
+
type Preference = { version: 1; sticky: boolean };
|
|
15
|
+
type Owner = { pi: ExtensionAPI; stop?: (() => void) | undefined };
|
|
16
|
+
type State = {
|
|
17
|
+
panels: Map<string, Panel>;
|
|
18
|
+
owners: Map<ExtensionAPI, Owner>;
|
|
19
|
+
ctx?: ExtensionContext | undefined;
|
|
20
|
+
ui?: ExtensionContext["ui"] | undefined;
|
|
21
|
+
pi?: ExtensionAPI | undefined;
|
|
22
|
+
sticky: boolean;
|
|
23
|
+
mounted: boolean;
|
|
24
|
+
tui?: TUI | undefined;
|
|
25
|
+
theme?: Theme | undefined;
|
|
26
|
+
anchor?: string | undefined;
|
|
27
|
+
pendingAnchor: boolean;
|
|
28
|
+
generation: number;
|
|
29
|
+
ranges: Array<{ start: number; end: number; component: PanelComponent }>;
|
|
30
|
+
control?: { id: string; label: string; values: string[]; get(): string; change(value: string, ctx: ExtensionContext): void } | undefined;
|
|
31
|
+
};
|
|
32
|
+
const KEY = Symbol.for("pi-better-harness.navigator-panels.state");
|
|
33
|
+
function state(): State {
|
|
34
|
+
const global = globalThis as typeof globalThis & { [KEY]?: State };
|
|
35
|
+
return global[KEY] ??= { panels: new Map(), owners: new Map(), sticky: true, mounted: false, pendingAnchor: false, generation: 0, ranges: [] };
|
|
36
|
+
}
|
|
37
|
+
function parsePreference(value: unknown): Preference {
|
|
38
|
+
if (value === undefined) return { version: 1, sticky: true };
|
|
39
|
+
const preference = value as Partial<Preference> | null;
|
|
40
|
+
if (!preference || preference.version !== 1 || typeof preference.sticky !== "boolean") {
|
|
41
|
+
throw new Error("Navigator preference must contain version 1 and a boolean sticky value.");
|
|
42
|
+
}
|
|
43
|
+
return { version: 1, sticky: preference.sticky };
|
|
44
|
+
}
|
|
45
|
+
function canScroll(pi: ExtensionAPI | undefined): boolean {
|
|
46
|
+
return typeof pi?.registerEntryRenderer === "function" && typeof pi?.appendEntry === "function";
|
|
47
|
+
}
|
|
48
|
+
function isTui(ctx: ExtensionContext | undefined): ctx is ExtensionContext {
|
|
49
|
+
return ctx?.mode === "tui" && ctx.hasUI && typeof ctx.ui?.setWidget === "function";
|
|
50
|
+
}
|
|
51
|
+
function disposePanels(s: State): void {
|
|
52
|
+
for (const panel of s.panels.values()) {
|
|
53
|
+
try { panel.component?.dispose?.(); } catch { /* stale UI cleanup is best effort */ }
|
|
54
|
+
panel.component = undefined;
|
|
55
|
+
}
|
|
56
|
+
s.ranges = [];
|
|
57
|
+
}
|
|
58
|
+
function orderedPanels(s: State): Panel[] {
|
|
59
|
+
const rank = (key: string) => key === "pi-better-goal" ? 0 : key === "pi-better-plan" ? 1 : 2;
|
|
60
|
+
return [...s.panels.entries()].sort(([a], [b]) => rank(a) - rank(b)).map(([, panel]) => panel);
|
|
61
|
+
}
|
|
62
|
+
function buildPanels(s: State): void {
|
|
63
|
+
if (!s.tui || !s.theme) return;
|
|
64
|
+
for (const panel of orderedPanels(s)) panel.component ??= panel.factory(s.tui, s.theme);
|
|
65
|
+
}
|
|
66
|
+
function renderPanels(s: State, width: number): string[] {
|
|
67
|
+
buildPanels(s);
|
|
68
|
+
const lines: string[] = [];
|
|
69
|
+
s.ranges = [];
|
|
70
|
+
for (const panel of orderedPanels(s)) {
|
|
71
|
+
if (!panel.component) continue;
|
|
72
|
+
const rendered = panel.component.render(width);
|
|
73
|
+
s.ranges.push({ start: lines.length, end: lines.length + rendered.length, component: panel.component });
|
|
74
|
+
lines.push(...rendered);
|
|
75
|
+
}
|
|
76
|
+
return lines;
|
|
77
|
+
}
|
|
78
|
+
function mouse(s: State, event: MouseEvent) {
|
|
79
|
+
const range = s.ranges.find(range => event.y >= range.start && event.y < range.end);
|
|
80
|
+
return range?.component.handleMouse?.({ ...event, y: event.y - range.start });
|
|
81
|
+
}
|
|
82
|
+
function restoreAnchor(ctx: ExtensionContext): string | undefined {
|
|
83
|
+
let anchor: string | undefined;
|
|
84
|
+
for (const entry of ctx.sessionManager?.getBranch?.() ?? []) {
|
|
85
|
+
if (entry.type !== "custom" || entry.customType !== NAVIGATOR_ENTRY_TYPE) continue;
|
|
86
|
+
const data = entry.data as { version?: unknown; anchor?: unknown } | undefined;
|
|
87
|
+
if (data?.version === 1 && typeof data.anchor === "string") anchor = data.anchor;
|
|
88
|
+
}
|
|
89
|
+
return anchor;
|
|
90
|
+
}
|
|
91
|
+
function appendAnchor(s: State): void {
|
|
92
|
+
if (s.sticky || s.anchor || !canScroll(s.pi)) return;
|
|
93
|
+
const anchor = randomUUID();
|
|
94
|
+
s.anchor = anchor;
|
|
95
|
+
try { s.pi!.appendEntry(NAVIGATOR_ENTRY_TYPE, { version: 1, anchor }); }
|
|
96
|
+
catch (error) { s.anchor = undefined; throw error; }
|
|
97
|
+
}
|
|
98
|
+
function scheduleAnchor(s: State): void {
|
|
99
|
+
if (s.anchor || s.pendingAnchor || s.sticky) return;
|
|
100
|
+
s.pendingAnchor = true;
|
|
101
|
+
const generation = s.generation;
|
|
102
|
+
// Session writes cannot run inside a component render. Empty sessions stay untouched.
|
|
103
|
+
queueMicrotask(() => {
|
|
104
|
+
if (generation !== s.generation) return;
|
|
105
|
+
s.pendingAnchor = false;
|
|
106
|
+
if (!s.sticky && s.panels.size) appendAnchor(s);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
function clearHost(s: State): void {
|
|
110
|
+
s.generation++;
|
|
111
|
+
s.pendingAnchor = false;
|
|
112
|
+
try {
|
|
113
|
+
if (s.mounted && isTui(s.ctx)) s.ctx.ui.setWidget(NAVIGATOR_PANEL_WIDGET_KEY, undefined);
|
|
114
|
+
} catch { /* an old session context may already be detached */ }
|
|
115
|
+
disposePanels(s);
|
|
116
|
+
s.mounted = false;
|
|
117
|
+
s.tui = undefined;
|
|
118
|
+
s.theme = undefined;
|
|
119
|
+
}
|
|
120
|
+
function adoptContext(s: State, ctx: ExtensionContext): void {
|
|
121
|
+
if (s.ui && s.ui !== ctx.ui) {
|
|
122
|
+
clearHost(s);
|
|
123
|
+
s.panels.clear();
|
|
124
|
+
s.anchor = undefined;
|
|
125
|
+
}
|
|
126
|
+
s.ctx = ctx;
|
|
127
|
+
s.ui = ctx.ui;
|
|
128
|
+
}
|
|
129
|
+
function mountHost(s: State): void {
|
|
130
|
+
if (!isTui(s.ctx) || s.mounted || !s.panels.size) return;
|
|
131
|
+
s.mounted = true;
|
|
132
|
+
s.ctx.ui.setWidget(NAVIGATOR_PANEL_WIDGET_KEY, (tui, theme) => {
|
|
133
|
+
if (s.tui !== tui || s.theme !== theme) disposePanels(s);
|
|
134
|
+
s.tui = tui;
|
|
135
|
+
s.theme = theme;
|
|
136
|
+
buildPanels(s);
|
|
137
|
+
return {
|
|
138
|
+
render(width: number) {
|
|
139
|
+
const lines = renderPanels(s, width);
|
|
140
|
+
if (s.sticky) return lines;
|
|
141
|
+
if (lines.length) scheduleAnchor(s);
|
|
142
|
+
return [];
|
|
143
|
+
},
|
|
144
|
+
handleMouse: (event: MouseEvent) => s.sticky ? mouse(s, event) : undefined,
|
|
145
|
+
invalidate() {
|
|
146
|
+
for (const panel of s.panels.values()) panel.component?.invalidate();
|
|
147
|
+
},
|
|
148
|
+
dispose() { disposePanels(s); s.mounted = false; },
|
|
149
|
+
};
|
|
150
|
+
}, { placement: "aboveEditor" });
|
|
151
|
+
}
|
|
152
|
+
function changeSticky(value: string, ctx: ExtensionContext): void {
|
|
153
|
+
if (value !== "On" && value !== "Off") throw new Error("Sticky navigator expects On or Off.");
|
|
154
|
+
const s = state();
|
|
155
|
+
if (value === "Off" && !canScroll(s.pi)) {
|
|
156
|
+
throw new Error("Scrolling navigator requires a Pi version with registerEntryRenderer support.");
|
|
157
|
+
}
|
|
158
|
+
updateHarnessSetting("navigator", current => {
|
|
159
|
+
parsePreference(current);
|
|
160
|
+
return { version: 1, sticky: value === "On" };
|
|
161
|
+
});
|
|
162
|
+
adoptContext(s, ctx);
|
|
163
|
+
if (s.sticky && value === "Off") s.anchor = undefined;
|
|
164
|
+
s.sticky = value === "On";
|
|
165
|
+
refreshNavigatorPanels(ctx);
|
|
166
|
+
}
|
|
167
|
+
export function initializeNavigatorPanels(pi: ExtensionAPI): void {
|
|
168
|
+
const s = state();
|
|
169
|
+
if (s.owners.has(pi)) return;
|
|
170
|
+
s.pi = pi;
|
|
171
|
+
try { s.sticky = parsePreference(readHarnessSetting("navigator")).sticky; }
|
|
172
|
+
catch { s.sticky = true; }
|
|
173
|
+
if (!canScroll(pi)) s.sticky = true;
|
|
174
|
+
if (canScroll(pi)) pi.registerEntryRenderer(NAVIGATOR_ENTRY_TYPE, (entry) => {
|
|
175
|
+
const data = entry.data as { version?: number; anchor?: string } | undefined;
|
|
176
|
+
if (data?.version !== 1 || !data.anchor) return undefined;
|
|
177
|
+
// Pi reconstructs chat before session_start restores the visible branch.
|
|
178
|
+
// Keep the component mounted and resolve its active anchor at render time.
|
|
179
|
+
return {
|
|
180
|
+
render: width => !s.sticky && data.anchor === s.anchor ? renderPanels(s, width) : [],
|
|
181
|
+
handleMouse: event => !s.sticky && data.anchor === s.anchor ? mouse(s, event) : undefined,
|
|
182
|
+
invalidate() { for (const panel of s.panels.values()) panel.component?.invalidate(); },
|
|
183
|
+
} as PanelComponent;
|
|
184
|
+
});
|
|
185
|
+
s.control ??= {
|
|
186
|
+
id: NAVIGATOR_STICKY_SETTING_ID, label: "Sticky navigator", values: ["On", "Off"],
|
|
187
|
+
get: () => state().sticky ? "On" : "Off", change: changeSticky,
|
|
188
|
+
};
|
|
189
|
+
const owner: Owner = { pi };
|
|
190
|
+
s.owners.set(pi, owner);
|
|
191
|
+
// Use one stable contribution callback across vendored copies, but each owner
|
|
192
|
+
// cleans up its own event subscription when its extension is unloaded.
|
|
193
|
+
const register = () => pi.events?.emit?.("harness-settings:register", s.control);
|
|
194
|
+
owner.stop = pi.events?.on?.("harness-settings:request", register);
|
|
195
|
+
register();
|
|
196
|
+
pi.on("session_start", (_event, ctx) => {
|
|
197
|
+
adoptContext(s, ctx);
|
|
198
|
+
s.anchor = restoreAnchor(ctx);
|
|
199
|
+
});
|
|
200
|
+
pi.on("session_tree", (_event, ctx) => {
|
|
201
|
+
adoptContext(s, ctx);
|
|
202
|
+
s.anchor = restoreAnchor(ctx);
|
|
203
|
+
refreshNavigatorPanels(ctx);
|
|
204
|
+
});
|
|
205
|
+
pi.on("session_shutdown", () => {
|
|
206
|
+
owner.stop?.();
|
|
207
|
+
s.owners.delete(pi);
|
|
208
|
+
if (s.owners.size) return;
|
|
209
|
+
clearHost(s);
|
|
210
|
+
s.panels.clear();
|
|
211
|
+
s.anchor = undefined;
|
|
212
|
+
s.ctx = undefined;
|
|
213
|
+
s.ui = undefined;
|
|
214
|
+
s.pi = undefined;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
export function setNavigatorPanel(
|
|
218
|
+
ctx: ExtensionContext, key: string, content: NavigatorPanelFactory | undefined,
|
|
219
|
+
_options?: { placement?: "aboveEditor" | "belowEditor" },
|
|
220
|
+
): void {
|
|
221
|
+
const s = state();
|
|
222
|
+
if (!isTui(ctx)) return;
|
|
223
|
+
adoptContext(s, ctx);
|
|
224
|
+
s.panels.get(key)?.component?.dispose?.();
|
|
225
|
+
if (content) s.panels.set(key, { factory: content });
|
|
226
|
+
else s.panels.delete(key);
|
|
227
|
+
if (!s.panels.size) { clearHost(s); return; }
|
|
228
|
+
mountHost(s);
|
|
229
|
+
buildPanels(s);
|
|
230
|
+
s.tui?.requestRender?.();
|
|
231
|
+
}
|
|
232
|
+
export function refreshNavigatorPanels(ctx?: ExtensionContext): void {
|
|
233
|
+
const s = state();
|
|
234
|
+
if (ctx) adoptContext(s, ctx);
|
|
235
|
+
mountHost(s);
|
|
236
|
+
s.tui?.requestRender?.(true);
|
|
237
|
+
}
|
|
238
|
+
export function __resetNavigatorPanelsForTests(): void {
|
|
239
|
+
const s = state();
|
|
240
|
+
clearHost(s);
|
|
241
|
+
for (const owner of s.owners.values()) owner.stop?.();
|
|
242
|
+
s.owners.clear();
|
|
243
|
+
s.panels.clear();
|
|
244
|
+
s.pi = undefined;
|
|
245
|
+
s.ctx = undefined;
|
|
246
|
+
s.ui = undefined;
|
|
247
|
+
s.anchor = undefined;
|
|
248
|
+
s.sticky = true;
|
|
249
|
+
s.control = undefined;
|
|
250
|
+
}
|