pi-advisor-flow 0.9.0 → 0.9.2-dev.1
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/CHANGELOG.md +17 -0
- package/README.md +1 -1
- package/dist/index.js +54 -20
- package/package.json +14 -14
- package/src/commands/manual-command.ts +31 -11
- package/src/commands/manual-consultation.ts +7 -3
- package/src/commands/runtime.ts +7 -10
- package/src/tools/render-advisor-result.ts +9 -1
- package/src/tools/scout-status.ts +11 -8
- package/src/ui-guard.ts +31 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,23 @@ All notable changes to this project are documented here.
|
|
|
4
4
|
|
|
5
5
|
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0/).
|
|
6
6
|
|
|
7
|
+
## 0.9.1 - 2026-09-30
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Overrode transitive `brace-expansion` 5.0.12 and `undici` 8.11.2 to clear four high-severity audit advisories (stack-exhaustion DoS, WebSocket subprotocol DoS, TLS validation bypass).
|
|
12
|
+
|
|
13
|
+
- Fixed manual Advisor consultations crashing non-interactive (`--print`) sessions on Pi 0.99: UI status updates and scout lifecycle reads now no-op safely once the session context is retired instead of throwing, `/advisor-manual` completes within the command handler in print mode, and streaming tool-result repaint timers stop once their renderer is gone.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Added Pi 0.99.x support (peer range `^0.87.1 || ^0.99.1`); Pi 0.99's context-lifecycle enforcement is now honored. Manual consultation results no longer trigger a follow-up turn in print mode.
|
|
18
|
+
- Updated the dev toolchain: oxlint 1.86.0, @oxlint/plugins 1.86.0, oxfmt 0.71.0, ultracite 7.12.2, knip 6.39.0.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- Regression tests covering stale-context resilience of the guarded UI helpers.
|
|
23
|
+
|
|
7
24
|
## 0.9.0 - 2026-09-28
|
|
8
25
|
|
|
9
26
|
### Added
|
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ Regular consultations never block execution. Automatic loop gates are different:
|
|
|
41
41
|
|
|
42
42
|
## Install
|
|
43
43
|
|
|
44
|
-
Requires Pi 0.
|
|
44
|
+
Requires Pi 0.87.1+ (0.87.x) or 0.99.1+ (0.99.x).
|
|
45
45
|
|
|
46
46
|
```bash
|
|
47
47
|
pi install npm:pi-advisor-flow
|
package/dist/index.js
CHANGED
|
@@ -3884,6 +3884,30 @@ var runAdvisorGate = async (ctx, question, trigger = "repeated-tool-call", signa
|
|
|
3884
3884
|
// src/tools/scout-status.ts
|
|
3885
3885
|
import { Text as Text2 } from "@earendil-works/pi-tui";
|
|
3886
3886
|
|
|
3887
|
+
// src/ui-guard.ts
|
|
3888
|
+
var isStaleContext = (error) => error instanceof Error && /stale/iu.test(error.message);
|
|
3889
|
+
var uiAvailable = (ctx) => {
|
|
3890
|
+
try {
|
|
3891
|
+
return ctx.hasUI;
|
|
3892
|
+
} catch (error) {
|
|
3893
|
+
if (isStaleContext(error)) {
|
|
3894
|
+
return false;
|
|
3895
|
+
}
|
|
3896
|
+
throw error;
|
|
3897
|
+
}
|
|
3898
|
+
};
|
|
3899
|
+
var uiAction = (ctx, action) => {
|
|
3900
|
+
try {
|
|
3901
|
+
if (ctx.hasUI) {
|
|
3902
|
+
action(ctx.ui);
|
|
3903
|
+
}
|
|
3904
|
+
} catch (error) {
|
|
3905
|
+
if (!isStaleContext(error)) {
|
|
3906
|
+
throw error;
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
3909
|
+
};
|
|
3910
|
+
|
|
3887
3911
|
// src/tools/render-common.ts
|
|
3888
3912
|
import { getMarkdownTheme } from "@earendil-works/pi-coding-agent";
|
|
3889
3913
|
import {
|
|
@@ -4071,14 +4095,14 @@ class ScoutStatusManager {
|
|
|
4071
4095
|
}
|
|
4072
4096
|
}
|
|
4073
4097
|
update(ctx, token, event) {
|
|
4074
|
-
if (this.#retired.has(token) || !ctx
|
|
4098
|
+
if (this.#retired.has(token) || !uiAvailable(ctx)) {
|
|
4075
4099
|
return;
|
|
4076
4100
|
}
|
|
4077
4101
|
this.#known.add(token);
|
|
4078
4102
|
if (event.type === "call" || event.type === "chunk") {
|
|
4079
4103
|
this.#active.add(token);
|
|
4080
4104
|
if (this.showStatus) {
|
|
4081
|
-
ctx
|
|
4105
|
+
uiAction(ctx, (ui) => ui.setStatus("advisor-scout", "Scout curating…"));
|
|
4082
4106
|
}
|
|
4083
4107
|
return;
|
|
4084
4108
|
}
|
|
@@ -4088,10 +4112,10 @@ class ScoutStatusManager {
|
|
|
4088
4112
|
this.#active.delete(token);
|
|
4089
4113
|
this.#known.delete(token);
|
|
4090
4114
|
this.#retired.add(token);
|
|
4091
|
-
if (!(ctx
|
|
4115
|
+
if (!(uiAvailable(ctx) && this.showStatus)) {
|
|
4092
4116
|
return;
|
|
4093
4117
|
}
|
|
4094
|
-
ctx
|
|
4118
|
+
uiAction(ctx, (ui) => ui.setStatus("advisor-scout", this.#active.size > 0 ? "Scout curating…" : undefined));
|
|
4095
4119
|
}
|
|
4096
4120
|
clear(ctx) {
|
|
4097
4121
|
for (const token of this.#known) {
|
|
@@ -4099,8 +4123,8 @@ class ScoutStatusManager {
|
|
|
4099
4123
|
}
|
|
4100
4124
|
this.#known.clear();
|
|
4101
4125
|
this.#active.clear();
|
|
4102
|
-
if (
|
|
4103
|
-
ctx
|
|
4126
|
+
if (this.showStatus) {
|
|
4127
|
+
uiAction(ctx, (ui) => ui.setStatus("advisor-scout", undefined));
|
|
4104
4128
|
}
|
|
4105
4129
|
}
|
|
4106
4130
|
}
|
|
@@ -4638,9 +4662,7 @@ var advisorSessionState = new AdvisorSessionState;
|
|
|
4638
4662
|
|
|
4639
4663
|
// src/commands/runtime.ts
|
|
4640
4664
|
var notify = (ctx, message, level) => {
|
|
4641
|
-
|
|
4642
|
-
ctx.ui.notify(message, level);
|
|
4643
|
-
}
|
|
4665
|
+
uiAction(ctx, (ui) => ui.notify(message, level));
|
|
4644
4666
|
};
|
|
4645
4667
|
var reportManualBudgetExhausted = (ctx) => {
|
|
4646
4668
|
const message = "Advisor call budget exhausted for this session.";
|
|
@@ -4648,9 +4670,7 @@ var reportManualBudgetExhausted = (ctx) => {
|
|
|
4648
4670
|
notifyHerdrAdvisorFailure("Advisor budget exhausted", message);
|
|
4649
4671
|
};
|
|
4650
4672
|
var requestManualRender = (ctx) => {
|
|
4651
|
-
|
|
4652
|
-
ctx.ui.setStatus("advisor-manual", undefined);
|
|
4653
|
-
}
|
|
4673
|
+
uiAction(ctx, (ui) => ui.setStatus("advisor-manual", undefined));
|
|
4654
4674
|
};
|
|
4655
4675
|
|
|
4656
4676
|
class CommandRuntime {
|
|
@@ -4706,9 +4726,7 @@ class CommandRuntime {
|
|
|
4706
4726
|
}
|
|
4707
4727
|
}
|
|
4708
4728
|
updateAdvisorUsageStatus(ctx) {
|
|
4709
|
-
|
|
4710
|
-
ctx.ui.setStatus("advisor-usage", getAdvisorSettings().showUsageFooter ? this.advisorSessionState.usageStatus() : undefined);
|
|
4711
|
-
}
|
|
4729
|
+
uiAction(ctx, (ui) => ui.setStatus("advisor-usage", getAdvisorSettings().showUsageFooter ? this.advisorSessionState.usageStatus() : undefined));
|
|
4712
4730
|
}
|
|
4713
4731
|
}
|
|
4714
4732
|
var createCommandRuntime = (pi, dependencies = {}) => new CommandRuntime(pi, dependencies);
|
|
@@ -5849,9 +5867,10 @@ var screeningSkipText = (outcome) => outcome.kind === "repeat" && outcome.reatta
|
|
|
5849
5867
|
|
|
5850
5868
|
// src/commands/manual-consultation.ts
|
|
5851
5869
|
var startManualConsultation = async (runtime, ctx, question, controller, scoutStatusToken, progress, gitContext) => {
|
|
5870
|
+
const isPrintMode = ctx.mode === "print";
|
|
5852
5871
|
progress.phase = "preparing";
|
|
5853
5872
|
runtime.requestManualRender(ctx);
|
|
5854
|
-
if (ctx
|
|
5873
|
+
if (uiAvailable(ctx)) {
|
|
5855
5874
|
const timer = setInterval(() => {
|
|
5856
5875
|
if (controller.signal.aborted || runtime.manualConsultations.get(controller) !== scoutStatusToken) {
|
|
5857
5876
|
clearInterval(timer);
|
|
@@ -5916,7 +5935,7 @@ ${markdown}`,
|
|
|
5916
5935
|
display: true
|
|
5917
5936
|
}, {
|
|
5918
5937
|
deliverAs: "steer",
|
|
5919
|
-
triggerTurn:
|
|
5938
|
+
triggerTurn: !isPrintMode
|
|
5920
5939
|
});
|
|
5921
5940
|
} catch (error) {
|
|
5922
5941
|
if (controller.signal.aborted) {
|
|
@@ -5941,7 +5960,7 @@ ${markdown}`,
|
|
|
5941
5960
|
text: `**Advisor consultation failed:** ${message}`
|
|
5942
5961
|
},
|
|
5943
5962
|
display: true
|
|
5944
|
-
}, { deliverAs: "steer", triggerTurn:
|
|
5963
|
+
}, { deliverAs: "steer", triggerTurn: !isPrintMode });
|
|
5945
5964
|
notify(ctx, `Advisor consultation failed: ${message}`, "error");
|
|
5946
5965
|
notifyHerdrAdvisorFailure("Advisor consultation failed", message);
|
|
5947
5966
|
} finally {
|
|
@@ -6035,7 +6054,15 @@ var registerManualCommand = (runtime) => {
|
|
|
6035
6054
|
runtime.scoutStatus.register(scoutStatusToken);
|
|
6036
6055
|
runtime.manualConsultations.set(controller, scoutStatusToken);
|
|
6037
6056
|
runtime.pi.appendEntry?.("advisor-manual-call", { progressId, question });
|
|
6038
|
-
|
|
6057
|
+
if (ctx.mode === "print") {
|
|
6058
|
+
await startManualConsultation(runtime, ctx, question, controller, scoutStatusToken, progress, gitContext);
|
|
6059
|
+
return;
|
|
6060
|
+
}
|
|
6061
|
+
(async () => {
|
|
6062
|
+
try {
|
|
6063
|
+
await startManualConsultation(runtime, ctx, question, controller, scoutStatusToken, progress, gitContext);
|
|
6064
|
+
} catch {}
|
|
6065
|
+
})();
|
|
6039
6066
|
}
|
|
6040
6067
|
});
|
|
6041
6068
|
};
|
|
@@ -7769,7 +7796,14 @@ var renderPartialAdvisorResult = (box, result, expanded, theme, context) => {
|
|
|
7769
7796
|
const scoutActive = scout?.status === "calling" || scout?.status === "streaming";
|
|
7770
7797
|
syncRenderPhase(context, scoutActive ? "scout" : "advisor");
|
|
7771
7798
|
if (!context.state.timerId) {
|
|
7772
|
-
context.state.timerId = setInterval(() =>
|
|
7799
|
+
context.state.timerId = setInterval(() => {
|
|
7800
|
+
try {
|
|
7801
|
+
context.invalidate();
|
|
7802
|
+
} catch {
|
|
7803
|
+
clearInterval(context.state.timerId);
|
|
7804
|
+
context.state.timerId = undefined;
|
|
7805
|
+
}
|
|
7806
|
+
}, 80);
|
|
7773
7807
|
}
|
|
7774
7808
|
if (scout) {
|
|
7775
7809
|
renderScoutDetails(box, scout, expanded, theme);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-advisor-flow",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2-dev.1",
|
|
4
4
|
"description": "Advanced Executor/Advisor flow for Pi, fully configurable and extendable.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"advisor",
|
|
@@ -51,26 +51,26 @@
|
|
|
51
51
|
"format:check": "oxfmt --check"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@earendil-works/pi-ai": "0.
|
|
55
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
56
|
-
"@earendil-works/pi-tui": "0.
|
|
57
|
-
"@oxlint/plugins": "1.
|
|
54
|
+
"@earendil-works/pi-ai": "0.99.1",
|
|
55
|
+
"@earendil-works/pi-coding-agent": "0.99.1",
|
|
56
|
+
"@earendil-works/pi-tui": "0.99.1",
|
|
57
|
+
"@oxlint/plugins": "1.86.0",
|
|
58
58
|
"@types/node": "^26.6.3",
|
|
59
59
|
"@typesafe-ai/sdk": "^0.6.0",
|
|
60
60
|
"bun-types": "1.4.2",
|
|
61
61
|
"husky": "^9.1.7",
|
|
62
|
-
"knip": "
|
|
62
|
+
"knip": "6.39.0",
|
|
63
63
|
"lint-staged": "^17.6.0",
|
|
64
|
-
"oxfmt": "0.
|
|
65
|
-
"oxlint": "1.
|
|
64
|
+
"oxfmt": "0.71.0",
|
|
65
|
+
"oxlint": "1.86.0",
|
|
66
66
|
"typebox": "^1.3.34",
|
|
67
67
|
"typescript": "^7.0.2",
|
|
68
|
-
"ultracite": "7.12.
|
|
68
|
+
"ultracite": "7.12.2"
|
|
69
69
|
},
|
|
70
70
|
"peerDependencies": {
|
|
71
|
-
"@earendil-works/pi-ai": "^0.87.1",
|
|
72
|
-
"@earendil-works/pi-coding-agent": "^0.87.1",
|
|
73
|
-
"@earendil-works/pi-tui": "^0.87.1",
|
|
71
|
+
"@earendil-works/pi-ai": "^0.87.1 || ^0.99.1",
|
|
72
|
+
"@earendil-works/pi-coding-agent": "^0.87.1 || ^0.99.1",
|
|
73
|
+
"@earendil-works/pi-tui": "^0.87.1 || ^0.99.1",
|
|
74
74
|
"typebox": "^1.1.38"
|
|
75
75
|
},
|
|
76
76
|
"peerDependenciesMeta": {
|
|
@@ -92,10 +92,10 @@
|
|
|
92
92
|
"safe-buffer": "npm:@socketregistry/safe-buffer@1.0.9"
|
|
93
93
|
},
|
|
94
94
|
"overrides": {
|
|
95
|
-
"brace-expansion": "5.0.
|
|
95
|
+
"brace-expansion": "5.0.12",
|
|
96
96
|
"is-unicode-supported": "npm:@socketregistry/is-unicode-supported@1.0.5",
|
|
97
97
|
"safe-buffer": "npm:@socketregistry/safe-buffer@1.0.9",
|
|
98
|
-
"undici": "8.
|
|
98
|
+
"undici": "8.11.2"
|
|
99
99
|
},
|
|
100
100
|
"lint-staged": {
|
|
101
101
|
"*.{ts,tsx}": [
|
|
@@ -112,18 +112,38 @@ export const registerManualCommand = (runtime: CommandRuntime) => {
|
|
|
112
112
|
runtime.scoutStatus.register(scoutStatusToken);
|
|
113
113
|
runtime.manualConsultations.set(controller, scoutStatusToken);
|
|
114
114
|
runtime.pi.appendEntry?.("advisor-manual-call", { progressId, question });
|
|
115
|
+
// Print mode tears the session down when the handler returns, so the
|
|
116
|
+
// consultation must complete inside the handler to keep its ctx valid.
|
|
117
|
+
if (ctx.mode === "print") {
|
|
118
|
+
await startManualConsultation(
|
|
119
|
+
runtime,
|
|
120
|
+
ctx,
|
|
121
|
+
question,
|
|
122
|
+
controller,
|
|
123
|
+
scoutStatusToken,
|
|
124
|
+
progress,
|
|
125
|
+
gitContext
|
|
126
|
+
);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
115
129
|
// Intentional fire-and-forget: the consultation streams after the command
|
|
116
|
-
// handler returns.
|
|
117
|
-
//
|
|
118
|
-
void
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
130
|
+
// handler returns. A rejected consultation must not escape as an
|
|
131
|
+
// unhandled rejection; its error paths already notify and record.
|
|
132
|
+
void (async () => {
|
|
133
|
+
try {
|
|
134
|
+
await startManualConsultation(
|
|
135
|
+
runtime,
|
|
136
|
+
ctx,
|
|
137
|
+
question,
|
|
138
|
+
controller,
|
|
139
|
+
scoutStatusToken,
|
|
140
|
+
progress,
|
|
141
|
+
gitContext
|
|
142
|
+
);
|
|
143
|
+
} catch {
|
|
144
|
+
// Consultation error paths already notify and record; nothing to add.
|
|
145
|
+
}
|
|
146
|
+
})();
|
|
127
147
|
},
|
|
128
148
|
});
|
|
129
149
|
};
|
|
@@ -7,6 +7,7 @@ import { notifyHerdrAdvisorFailure } from "../herdr.ts";
|
|
|
7
7
|
import { normalizeScreeningQuestion } from "../tools/jev-filter.ts";
|
|
8
8
|
import { appendScoutLifecycleEntry } from "../tools/scout-status.ts";
|
|
9
9
|
import type { ScoutToolDetails } from "../tools/types.ts";
|
|
10
|
+
import { uiAvailable } from "../ui-guard.ts";
|
|
10
11
|
import { advisorUsageCost, snapshotAdvisorUsage } from "../usage.ts";
|
|
11
12
|
import type { AdvisorUsageSnapshot } from "../usage.ts";
|
|
12
13
|
import { notify } from "./runtime.ts";
|
|
@@ -28,9 +29,10 @@ export const startManualConsultation = async (
|
|
|
28
29
|
progress: ManualAdvisorProgressState,
|
|
29
30
|
gitContext?: GitContextLevel
|
|
30
31
|
) => {
|
|
32
|
+
const isPrintMode = ctx.mode === "print";
|
|
31
33
|
progress.phase = "preparing";
|
|
32
34
|
runtime.requestManualRender(ctx);
|
|
33
|
-
if (ctx
|
|
35
|
+
if (uiAvailable(ctx)) {
|
|
34
36
|
const timer = setInterval(() => {
|
|
35
37
|
if (
|
|
36
38
|
controller.signal.aborted ||
|
|
@@ -118,8 +120,10 @@ export const startManualConsultation = async (
|
|
|
118
120
|
{
|
|
119
121
|
// Steer lets the current turn finish its active work; the Executor sees
|
|
120
122
|
// the result before its next model call rather than being interrupted.
|
|
123
|
+
// Print mode has no follow-up turn; triggering one trips pi's turn_end
|
|
124
|
+
// entry resolution during teardown.
|
|
121
125
|
deliverAs: "steer",
|
|
122
|
-
triggerTurn:
|
|
126
|
+
triggerTurn: !isPrintMode,
|
|
123
127
|
}
|
|
124
128
|
);
|
|
125
129
|
} catch (error) {
|
|
@@ -147,7 +151,7 @@ export const startManualConsultation = async (
|
|
|
147
151
|
},
|
|
148
152
|
display: true,
|
|
149
153
|
},
|
|
150
|
-
{ deliverAs: "steer", triggerTurn:
|
|
154
|
+
{ deliverAs: "steer", triggerTurn: !isPrintMode }
|
|
151
155
|
);
|
|
152
156
|
notify(ctx, `Advisor consultation failed: ${message}`, "error");
|
|
153
157
|
notifyHerdrAdvisorFailure("Advisor consultation failed", message);
|
package/src/commands/runtime.ts
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from "../tools/model-access.ts";
|
|
14
14
|
import { ScoutStatusManager } from "../tools/scout-status.ts";
|
|
15
15
|
import { advisorSessionState as defaultAdvisorSessionState } from "../tools/session.ts";
|
|
16
|
+
import { uiAction } from "../ui-guard.ts";
|
|
16
17
|
import type {
|
|
17
18
|
CommandDependencies,
|
|
18
19
|
CommandRuntime as CommandRuntimeContract,
|
|
@@ -25,9 +26,7 @@ export const notify = (
|
|
|
25
26
|
message: string,
|
|
26
27
|
level: "error" | "info" | "warning"
|
|
27
28
|
) => {
|
|
28
|
-
|
|
29
|
-
ctx.ui.notify(message, level);
|
|
30
|
-
}
|
|
29
|
+
uiAction(ctx, (ui) => ui.notify(message, level));
|
|
31
30
|
};
|
|
32
31
|
|
|
33
32
|
const reportManualBudgetExhausted = (ctx: ExtensionContext) => {
|
|
@@ -37,9 +36,7 @@ const reportManualBudgetExhausted = (ctx: ExtensionContext) => {
|
|
|
37
36
|
};
|
|
38
37
|
|
|
39
38
|
const requestManualRender = (ctx: ExtensionContext) => {
|
|
40
|
-
|
|
41
|
-
ctx.ui.setStatus("advisor-manual", undefined);
|
|
42
|
-
}
|
|
39
|
+
uiAction(ctx, (ui) => ui.setStatus("advisor-manual", undefined));
|
|
43
40
|
};
|
|
44
41
|
|
|
45
42
|
class CommandRuntime implements CommandRuntimeContract {
|
|
@@ -130,14 +127,14 @@ class CommandRuntime implements CommandRuntimeContract {
|
|
|
130
127
|
}
|
|
131
128
|
|
|
132
129
|
updateAdvisorUsageStatus(ctx: ExtensionContext) {
|
|
133
|
-
|
|
134
|
-
|
|
130
|
+
uiAction(ctx, (ui) =>
|
|
131
|
+
ui.setStatus(
|
|
135
132
|
"advisor-usage",
|
|
136
133
|
getAdvisorSettings().showUsageFooter
|
|
137
134
|
? this.advisorSessionState.usageStatus()
|
|
138
135
|
: undefined
|
|
139
|
-
)
|
|
140
|
-
|
|
136
|
+
)
|
|
137
|
+
);
|
|
141
138
|
}
|
|
142
139
|
}
|
|
143
140
|
|
|
@@ -100,7 +100,15 @@ const renderPartialAdvisorResult = (
|
|
|
100
100
|
scout?.status === "calling" || scout?.status === "streaming";
|
|
101
101
|
syncRenderPhase(context, scoutActive ? "scout" : "advisor");
|
|
102
102
|
if (!context.state.timerId) {
|
|
103
|
-
context.state.timerId = setInterval(() =>
|
|
103
|
+
context.state.timerId = setInterval(() => {
|
|
104
|
+
try {
|
|
105
|
+
context.invalidate();
|
|
106
|
+
} catch {
|
|
107
|
+
// Renderer context is gone; stop repainting and drop the timer.
|
|
108
|
+
clearInterval(context.state.timerId);
|
|
109
|
+
context.state.timerId = undefined;
|
|
110
|
+
}
|
|
111
|
+
}, 80);
|
|
104
112
|
}
|
|
105
113
|
if (scout) {
|
|
106
114
|
renderScoutDetails(box, scout, expanded, theme);
|
|
@@ -8,6 +8,7 @@ import type { Box } from "@earendil-works/pi-tui";
|
|
|
8
8
|
|
|
9
9
|
import { executorRef, getAdvisorSettings } from "../config/state.ts";
|
|
10
10
|
import type { ScoutLifecycleEvent } from "../scout.ts";
|
|
11
|
+
import { uiAction, uiAvailable } from "../ui-guard.ts";
|
|
11
12
|
import { formatAdvisorUsage, snapshotAdvisorUsage } from "../usage.ts";
|
|
12
13
|
import { renderThinkingMarkdown, SPINNER_FRAMES } from "./render-common.ts";
|
|
13
14
|
import type { ScoutToolDetails } from "./types.ts";
|
|
@@ -91,14 +92,14 @@ export class ScoutStatusManager {
|
|
|
91
92
|
}
|
|
92
93
|
|
|
93
94
|
update(ctx: ExtensionContext, token: symbol, event: ScoutLifecycleEvent) {
|
|
94
|
-
if (this.#retired.has(token) || !ctx
|
|
95
|
+
if (this.#retired.has(token) || !uiAvailable(ctx)) {
|
|
95
96
|
return;
|
|
96
97
|
}
|
|
97
98
|
this.#known.add(token);
|
|
98
99
|
if (event.type === "call" || event.type === "chunk") {
|
|
99
100
|
this.#active.add(token);
|
|
100
101
|
if (this.showStatus) {
|
|
101
|
-
ctx
|
|
102
|
+
uiAction(ctx, (ui) => ui.setStatus("advisor-scout", "Scout curating…"));
|
|
102
103
|
}
|
|
103
104
|
return;
|
|
104
105
|
}
|
|
@@ -109,12 +110,14 @@ export class ScoutStatusManager {
|
|
|
109
110
|
this.#active.delete(token);
|
|
110
111
|
this.#known.delete(token);
|
|
111
112
|
this.#retired.add(token);
|
|
112
|
-
if (!(ctx
|
|
113
|
+
if (!(uiAvailable(ctx) && this.showStatus)) {
|
|
113
114
|
return;
|
|
114
115
|
}
|
|
115
|
-
ctx
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
uiAction(ctx, (ui) =>
|
|
117
|
+
ui.setStatus(
|
|
118
|
+
"advisor-scout",
|
|
119
|
+
this.#active.size > 0 ? "Scout curating…" : undefined
|
|
120
|
+
)
|
|
118
121
|
);
|
|
119
122
|
}
|
|
120
123
|
|
|
@@ -124,8 +127,8 @@ export class ScoutStatusManager {
|
|
|
124
127
|
}
|
|
125
128
|
this.#known.clear();
|
|
126
129
|
this.#active.clear();
|
|
127
|
-
if (
|
|
128
|
-
ctx
|
|
130
|
+
if (this.showStatus) {
|
|
131
|
+
uiAction(ctx, (ui) => ui.setStatus("advisor-scout", undefined));
|
|
129
132
|
}
|
|
130
133
|
}
|
|
131
134
|
}
|
package/src/ui-guard.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
// Pi 0.99 asserts ctx liveness on UI access; post-session reads must no-op.
|
|
4
|
+
const isStaleContext = (error: unknown): error is Error =>
|
|
5
|
+
error instanceof Error && /stale/iu.test(error.message);
|
|
6
|
+
|
|
7
|
+
export const uiAvailable = (ctx: ExtensionContext): boolean => {
|
|
8
|
+
try {
|
|
9
|
+
return ctx.hasUI;
|
|
10
|
+
} catch (error) {
|
|
11
|
+
if (isStaleContext(error)) {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const uiAction = (
|
|
19
|
+
ctx: ExtensionContext,
|
|
20
|
+
action: (ui: ExtensionContext["ui"]) => void
|
|
21
|
+
): void => {
|
|
22
|
+
try {
|
|
23
|
+
if (ctx.hasUI) {
|
|
24
|
+
action(ctx.ui);
|
|
25
|
+
}
|
|
26
|
+
} catch (error) {
|
|
27
|
+
if (!isStaleContext(error)) {
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|