pi-advisor-flow 0.8.3 → 0.9.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 +24 -0
- package/README.md +6 -5
- package/dist/index.js +685 -96
- package/package.json +14 -14
- package/src/attachments.ts +21 -5
- package/src/commands/activation.ts +6 -1
- package/src/commands/lifecycle.ts +6 -0
- package/src/commands/manual-command.ts +44 -12
- package/src/commands/manual-consultation.ts +7 -3
- package/src/commands/model-commands.ts +1 -0
- package/src/commands/runtime.ts +36 -10
- package/src/commands/settings-commands.ts +2 -0
- package/src/commands/settings-persistence.ts +2 -0
- package/src/commands/types.ts +5 -0
- package/src/config/defaults.ts +7 -0
- package/src/config/schema.ts +7 -0
- package/src/config/state.ts +5 -0
- package/src/config/types.ts +1 -0
- package/src/conversation.ts +29 -7
- package/src/image-attachments.ts +118 -0
- package/src/image-validation.ts +288 -0
- package/src/images.ts +151 -0
- package/src/scout-context.ts +1 -0
- package/src/scout-curation.ts +11 -4
- package/src/scout-groups.ts +13 -3
- package/src/scout-types.ts +2 -0
- package/src/tools/consult-context.ts +78 -2
- package/src/tools/consultation.ts +36 -4
- package/src/tools/jev-turn-gate.ts +4 -1
- package/src/tools/model-access.ts +21 -2
- package/src/tools/prompts.ts +14 -2
- package/src/tools/register-ask-advisor.ts +28 -1
- package/src/tools/register-lifecycle.ts +8 -2
- package/src/tools/render-advisor-result.ts +33 -9
- package/src/tools/scout-status.ts +11 -8
- package/src/tools/types.ts +9 -0
- package/src/ui/settings-items.ts +17 -1
- package/src/ui/settings-mutations.ts +1 -0
- package/src/ui/types.ts +1 -0
- package/src/ui-guard.ts +31 -0
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
import type { ImageContent } from "@earendil-works/pi-ai/compat";
|
|
1
4
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
5
|
|
|
3
6
|
import { readTrackedFiles, readUntrackedFiles } from "../attachments.ts";
|
|
@@ -7,12 +10,19 @@ import {
|
|
|
7
10
|
advisorGitContextRef,
|
|
8
11
|
advisorRedactSecretsRef,
|
|
9
12
|
advisorScoutEnabledRef,
|
|
13
|
+
advisorToolPoliciesRef,
|
|
10
14
|
advisorTrackedFileContentRef,
|
|
11
15
|
advisorUntrackedContentRef,
|
|
12
16
|
contextMaxCharsRef,
|
|
13
17
|
} from "../config/state.ts";
|
|
14
18
|
import { clampGitContextLevel, collectGitContext } from "../git.ts";
|
|
15
19
|
import type { GitContextLevel } from "../git.ts";
|
|
20
|
+
import { readImageFiles } from "../image-attachments.ts";
|
|
21
|
+
import {
|
|
22
|
+
ADVISOR_IMAGES_MAX_COUNT,
|
|
23
|
+
ADVISOR_IMAGES_TOTAL_MAX_BYTES,
|
|
24
|
+
selectedConversationImages,
|
|
25
|
+
} from "../images.ts";
|
|
16
26
|
import { readProjectPreferences } from "../preferences.ts";
|
|
17
27
|
import { redactAndCapText, redactSecrets } from "../redaction.ts";
|
|
18
28
|
import { curateAdvisorConversation } from "../scout-curation.ts";
|
|
@@ -38,6 +48,7 @@ export interface ConsultationContextOptions {
|
|
|
38
48
|
includeUntracked?: string[];
|
|
39
49
|
onScout?: (event: ScoutLifecycleEvent) => void;
|
|
40
50
|
signal?: AbortSignal;
|
|
51
|
+
supportsImages?: boolean;
|
|
41
52
|
}
|
|
42
53
|
|
|
43
54
|
export interface ConsultationContext {
|
|
@@ -47,6 +58,10 @@ export interface ConsultationContext {
|
|
|
47
58
|
conversation: string;
|
|
48
59
|
/** Redacted draft text, if a draft was supplied. */
|
|
49
60
|
draftText?: string;
|
|
61
|
+
imageOmissions: number;
|
|
62
|
+
imagePartsSeen: number;
|
|
63
|
+
images: { image: ImageContent; label: string }[];
|
|
64
|
+
supportsImages: boolean;
|
|
50
65
|
/** Redacted project preferences, if present. */
|
|
51
66
|
preferences?: { bytes: number; text: string };
|
|
52
67
|
/** Non-cancelling Scout outcome, when Scout ran. */
|
|
@@ -92,9 +107,12 @@ export const assembleConsultationContext = async (
|
|
|
92
107
|
0,
|
|
93
108
|
contextMaxCharsRef - changeText.length
|
|
94
109
|
);
|
|
110
|
+
const imageNonce = randomUUID();
|
|
95
111
|
const legacyConversation = advisorRequestConversation(
|
|
96
112
|
ctx,
|
|
97
|
-
conversationBudget
|
|
113
|
+
conversationBudget,
|
|
114
|
+
true,
|
|
115
|
+
imageNonce
|
|
98
116
|
);
|
|
99
117
|
const curated = await curateAdvisorConversation(
|
|
100
118
|
ctx,
|
|
@@ -104,7 +122,8 @@ export const assembleConsultationContext = async (
|
|
|
104
122
|
advisorScoutEnabledRef,
|
|
105
123
|
runAdvisorScout,
|
|
106
124
|
options.currentInvocationId,
|
|
107
|
-
conversationBudget
|
|
125
|
+
conversationBudget,
|
|
126
|
+
imageNonce
|
|
108
127
|
);
|
|
109
128
|
const preferences = await readProjectPreferences(
|
|
110
129
|
ctx,
|
|
@@ -135,12 +154,69 @@ export const assembleConsultationContext = async (
|
|
|
135
154
|
untracked.reduce((sum, item) => sum + item.bytes, 0)
|
|
136
155
|
)
|
|
137
156
|
);
|
|
157
|
+
const supportsImages = options.supportsImages ?? false;
|
|
158
|
+
const untrackedImages = await readImageFiles(
|
|
159
|
+
ctx.cwd,
|
|
160
|
+
options.includeUntracked ?? [],
|
|
161
|
+
supportsImages && advisorUntrackedContentRef,
|
|
162
|
+
"untracked",
|
|
163
|
+
ADVISOR_IMAGES_TOTAL_MAX_BYTES,
|
|
164
|
+
ADVISOR_IMAGES_MAX_COUNT
|
|
165
|
+
);
|
|
166
|
+
const trackedImages = await readImageFiles(
|
|
167
|
+
ctx.cwd,
|
|
168
|
+
options.includeTracked ?? [],
|
|
169
|
+
supportsImages && advisorTrackedFileContentRef,
|
|
170
|
+
"tracked",
|
|
171
|
+
ADVISOR_IMAGES_TOTAL_MAX_BYTES -
|
|
172
|
+
untrackedImages.images.reduce((sum, item) => sum + item.bytes, 0),
|
|
173
|
+
ADVISOR_IMAGES_MAX_COUNT - untrackedImages.images.length
|
|
174
|
+
);
|
|
175
|
+
const images = [...untrackedImages.images, ...trackedImages.images].map(
|
|
176
|
+
(item) => ({
|
|
177
|
+
image: item.image,
|
|
178
|
+
label: `File ${JSON.stringify(item.path)}`,
|
|
179
|
+
})
|
|
180
|
+
);
|
|
181
|
+
let remainingBytes =
|
|
182
|
+
ADVISOR_IMAGES_TOTAL_MAX_BYTES -
|
|
183
|
+
[...untrackedImages.images, ...trackedImages.images].reduce(
|
|
184
|
+
(sum, item) => sum + item.bytes,
|
|
185
|
+
0
|
|
186
|
+
);
|
|
187
|
+
let imageOmissions = untrackedImages.omitted + trackedImages.omitted;
|
|
188
|
+
const census = selectedConversationImages(
|
|
189
|
+
ctx,
|
|
190
|
+
curated.conversation,
|
|
191
|
+
advisorToolPoliciesRef,
|
|
192
|
+
curated.scout?.ok === true
|
|
193
|
+
? new Set<string>(curated.selectedEntryIds)
|
|
194
|
+
: undefined,
|
|
195
|
+
imageNonce
|
|
196
|
+
);
|
|
197
|
+
for (const item of census.selected) {
|
|
198
|
+
const bytes = Buffer.from(item.image.data, "base64").length;
|
|
199
|
+
if (
|
|
200
|
+
!supportsImages ||
|
|
201
|
+
images.length >= ADVISOR_IMAGES_MAX_COUNT ||
|
|
202
|
+
bytes > remainingBytes
|
|
203
|
+
) {
|
|
204
|
+
imageOmissions += 1;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
remainingBytes -= bytes;
|
|
208
|
+
images.push({ image: item.image, label: item.marker });
|
|
209
|
+
}
|
|
138
210
|
return {
|
|
139
211
|
changeText,
|
|
140
212
|
conversation: curated.conversation,
|
|
141
213
|
draftText,
|
|
214
|
+
imageOmissions,
|
|
215
|
+
imagePartsSeen: census.imagePartsSeen,
|
|
216
|
+
images,
|
|
142
217
|
preferences,
|
|
143
218
|
scout: curated.scout,
|
|
219
|
+
supportsImages,
|
|
144
220
|
tracked,
|
|
145
221
|
untracked,
|
|
146
222
|
};
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
advisorRef,
|
|
10
10
|
} from "../config/state.ts";
|
|
11
11
|
import { loadConfig } from "../config/storage.ts";
|
|
12
|
+
import { escapeRepositoryText } from "../git.ts";
|
|
12
13
|
import type { GitContextLevel } from "../git.ts";
|
|
13
14
|
import { collectTextStream, resolveConfiguredModel } from "../model-stream.ts";
|
|
14
15
|
import { redactSecrets } from "../redaction.ts";
|
|
@@ -16,7 +17,11 @@ import type { ScoutLifecycleEvent } from "../scout.ts";
|
|
|
16
17
|
import type { ConsultationTrigger, GateTrigger } from "../session-state.ts";
|
|
17
18
|
import { assembleConsultationContext } from "./consult-context.ts";
|
|
18
19
|
import { parseAutomaticDecision } from "./gate-protocol.ts";
|
|
19
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
advisorModelAccessReason,
|
|
22
|
+
sameModelAdvisorDisabled,
|
|
23
|
+
sameModelAdvisorNotice,
|
|
24
|
+
} from "./model-access.ts";
|
|
20
25
|
import {
|
|
21
26
|
ADVISOR_DECISION_SYSTEM,
|
|
22
27
|
ADVISOR_SYSTEM,
|
|
@@ -57,12 +62,18 @@ const collectAdvisorResponse = async (
|
|
|
57
62
|
) => {
|
|
58
63
|
const { ctx, question, signal, systemPrompt } = options;
|
|
59
64
|
loadConfig(ctx);
|
|
65
|
+
if (sameModelAdvisorDisabled(ctx)) {
|
|
66
|
+
throw new Error(sameModelAdvisorNotice);
|
|
67
|
+
}
|
|
60
68
|
const accessReason = advisorModelAccessReason(ctx);
|
|
61
69
|
if (accessReason) {
|
|
62
70
|
throw new Error(accessReason);
|
|
63
71
|
}
|
|
64
72
|
const resolved = await resolveConfiguredModel(ctx, advisorRef, "Advisor");
|
|
65
|
-
const context = await assembleConsultationContext(
|
|
73
|
+
const context = await assembleConsultationContext({
|
|
74
|
+
...options,
|
|
75
|
+
supportsImages: resolved.model.input.includes("image"),
|
|
76
|
+
});
|
|
66
77
|
|
|
67
78
|
// Keep the original question available to local UI callers, but never send
|
|
68
79
|
// its credential-shaped values to the provider when redaction is enabled.
|
|
@@ -70,11 +81,17 @@ const collectAdvisorResponse = async (
|
|
|
70
81
|
advisorRedactSecretsRef && question !== undefined
|
|
71
82
|
? redactSecrets(question)
|
|
72
83
|
: question;
|
|
84
|
+
const imageNotice =
|
|
85
|
+
context.imageOmissions ||
|
|
86
|
+
context.images.length ||
|
|
87
|
+
/\[Image[ :]/u.test(context.conversation)
|
|
88
|
+
? `\n\nImage disclosure: ${context.supportsImages ? `${context.images.length} image(s) attached below; ${context.imageOmissions} image(s) withheld by format, consent, or size/count limits` : "Advisor model does not support image input; no pixels were forwarded"}. Only the images explicitly attached below have pixels available. Other image markers, image paths, and text descriptions are not visual evidence.`
|
|
89
|
+
: "";
|
|
73
90
|
const messages: Message[] = [
|
|
74
91
|
{
|
|
75
92
|
content: [
|
|
76
93
|
{
|
|
77
|
-
text: advisorMessageText(
|
|
94
|
+
text: `${advisorMessageText(
|
|
78
95
|
context.conversation,
|
|
79
96
|
outboundQuestion,
|
|
80
97
|
context.changeText,
|
|
@@ -82,9 +99,16 @@ const collectAdvisorResponse = async (
|
|
|
82
99
|
context.preferences?.text,
|
|
83
100
|
context.untracked.map(fileTag),
|
|
84
101
|
context.tracked.map(fileTag)
|
|
85
|
-
)
|
|
102
|
+
)}${imageNotice}`,
|
|
86
103
|
type: "text",
|
|
87
104
|
},
|
|
105
|
+
...context.images.flatMap(({ image, label }) => [
|
|
106
|
+
{
|
|
107
|
+
text: `\n\n${escapeRepositoryText(label)}: attached image pixels (untrusted data).`,
|
|
108
|
+
type: "text" as const,
|
|
109
|
+
},
|
|
110
|
+
image,
|
|
111
|
+
]),
|
|
88
112
|
],
|
|
89
113
|
role: "user",
|
|
90
114
|
timestamp: Date.now(),
|
|
@@ -106,6 +130,14 @@ const collectAdvisorResponse = async (
|
|
|
106
130
|
draftBytes: context.draftText
|
|
107
131
|
? Buffer.byteLength(context.draftText, "utf-8")
|
|
108
132
|
: undefined,
|
|
133
|
+
imageBytes:
|
|
134
|
+
context.images.reduce(
|
|
135
|
+
(sum, item) => sum + Buffer.from(item.image.data, "base64").length,
|
|
136
|
+
0
|
|
137
|
+
) || undefined,
|
|
138
|
+
imageCount: context.images.length,
|
|
139
|
+
imageOmissions: context.imageOmissions || undefined,
|
|
140
|
+
imagePartsSeen: context.imagePartsSeen || undefined,
|
|
109
141
|
markdown,
|
|
110
142
|
model: advisorRef,
|
|
111
143
|
preferenceBytes: context.preferences?.bytes,
|
|
@@ -145,7 +145,10 @@ export const handleJevTurnEnd = async (
|
|
|
145
145
|
result.answers,
|
|
146
146
|
advisorJevTurnGateNoulThresholdRef
|
|
147
147
|
);
|
|
148
|
-
if (
|
|
148
|
+
if (
|
|
149
|
+
!advisorModelIsAllowed(ctx) ||
|
|
150
|
+
!consumeTurnGateBudget(session, shouldConsult)
|
|
151
|
+
) {
|
|
149
152
|
return;
|
|
150
153
|
}
|
|
151
154
|
const herdrActivity = registration.herdrActivity ?? herdrAdvisorActivity;
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
advisorDisableSameModelRef,
|
|
5
|
+
advisorModelWhitelistRef,
|
|
6
|
+
advisorRef,
|
|
7
|
+
splitRef,
|
|
8
|
+
} from "../config/state.ts";
|
|
4
9
|
|
|
5
10
|
type AdvisorModelAccess =
|
|
6
11
|
| { allowed: true; modelRef?: string }
|
|
@@ -34,9 +39,23 @@ export const advisorModelAccess = (
|
|
|
34
39
|
return denial;
|
|
35
40
|
};
|
|
36
41
|
|
|
42
|
+
export const sameModelAdvisorDisabled = (
|
|
43
|
+
ctx: Pick<ExtensionContext, "model">,
|
|
44
|
+
model: { id: string; provider: string } | undefined = ctx.model
|
|
45
|
+
): boolean => {
|
|
46
|
+
if (!(advisorDisableSameModelRef && advisorRef && model)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const [provider, id] = splitRef(advisorRef);
|
|
50
|
+
return provider === model.provider && id === model.id;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const sameModelAdvisorNotice =
|
|
54
|
+
"Advisor disabled: executor and advisor are the same model.";
|
|
55
|
+
|
|
37
56
|
export const advisorModelIsAllowed = (
|
|
38
57
|
ctx: Pick<ExtensionContext, "model">
|
|
39
|
-
): boolean => advisorModelAccess(ctx).allowed;
|
|
58
|
+
): boolean => advisorModelAccess(ctx).allowed && !sameModelAdvisorDisabled(ctx);
|
|
40
59
|
|
|
41
60
|
export const advisorModelAccessReason = (
|
|
42
61
|
ctx: Pick<ExtensionContext, "model">
|
package/src/tools/prompts.ts
CHANGED
|
@@ -97,8 +97,20 @@ export const advisorRepositoryContext = (
|
|
|
97
97
|
*/
|
|
98
98
|
export const advisorRequestConversation = (
|
|
99
99
|
ctx: ExtensionContext,
|
|
100
|
-
maxChars = contextMaxCharsRef
|
|
101
|
-
|
|
100
|
+
maxChars = contextMaxCharsRef,
|
|
101
|
+
describeImages = false,
|
|
102
|
+
imageNonce = ""
|
|
103
|
+
) =>
|
|
104
|
+
recentConversation(
|
|
105
|
+
ctx,
|
|
106
|
+
maxChars,
|
|
107
|
+
undefined,
|
|
108
|
+
undefined,
|
|
109
|
+
undefined,
|
|
110
|
+
undefined,
|
|
111
|
+
describeImages,
|
|
112
|
+
imageNonce
|
|
113
|
+
);
|
|
102
114
|
|
|
103
115
|
export const advisorInvocationGuidelines = () => {
|
|
104
116
|
if (isSimpleMode()) {
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
getAdvisorSettings,
|
|
11
11
|
isSimpleMode,
|
|
12
12
|
} from "../config/state.ts";
|
|
13
|
+
import { loadConfig } from "../config/storage.ts";
|
|
13
14
|
import { notifyHerdrAdvisorFailure } from "../herdr.ts";
|
|
14
15
|
import {
|
|
15
16
|
ADVISOR_STREAM_UPDATE_INTERVAL_MS,
|
|
@@ -22,7 +23,11 @@ import {
|
|
|
22
23
|
} from "../usage.ts";
|
|
23
24
|
import { notifyLocalFailure, updateAdvisorUsageStatus } from "./gate-policy.ts";
|
|
24
25
|
import { normalizeScreeningQuestion, screeningSkipText } from "./jev-filter.ts";
|
|
25
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
advisorModelAccessReason,
|
|
28
|
+
sameModelAdvisorDisabled,
|
|
29
|
+
sameModelAdvisorNotice,
|
|
30
|
+
} from "./model-access.ts";
|
|
26
31
|
import { renderAdvisorResult } from "./render-advisor-result.ts";
|
|
27
32
|
import {
|
|
28
33
|
renderAdvisorCallBox,
|
|
@@ -73,7 +78,22 @@ export const registerAskAdvisorTool = ({
|
|
|
73
78
|
description:
|
|
74
79
|
"Consult the on-demand Advisor model for strategic guidance. Call with an empty object for a contextual review; attach an optional draft for concrete plan or completion review. If the Advisor explicitly names a missing file, you may make a sequential follow-up call with includeTrackedFiles when enabled and relevant.",
|
|
75
80
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
81
|
+
const skipSameModel = () => {
|
|
82
|
+
session.releaseCall(_id);
|
|
83
|
+
reservedCalls.delete(_id);
|
|
84
|
+
return {
|
|
85
|
+
content: [{ text: sameModelAdvisorNotice, type: "text" as const }],
|
|
86
|
+
details: {
|
|
87
|
+
skipReason: sameModelAdvisorNotice,
|
|
88
|
+
text: sameModelAdvisorNotice,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
};
|
|
76
92
|
try {
|
|
93
|
+
loadConfig(ctx);
|
|
94
|
+
if (sameModelAdvisorDisabled(ctx)) {
|
|
95
|
+
return skipSameModel();
|
|
96
|
+
}
|
|
77
97
|
assertAdvisorModelAccess(ctx);
|
|
78
98
|
} catch (error) {
|
|
79
99
|
session.releaseCall(_id);
|
|
@@ -128,6 +148,9 @@ export const registerAskAdvisorTool = ({
|
|
|
128
148
|
},
|
|
129
149
|
};
|
|
130
150
|
}
|
|
151
|
+
if (sameModelAdvisorDisabled(ctx)) {
|
|
152
|
+
return skipSameModel();
|
|
153
|
+
}
|
|
131
154
|
if (
|
|
132
155
|
!simpleMode &&
|
|
133
156
|
!session.canConsult(getAdvisorMaxCallsPerSession(), _id)
|
|
@@ -217,6 +240,10 @@ export const registerAskAdvisorTool = ({
|
|
|
217
240
|
adviceId: result.adviceId,
|
|
218
241
|
advisor: result.model,
|
|
219
242
|
draftBytes: result.draftBytes,
|
|
243
|
+
imageBytes: result.imageBytes,
|
|
244
|
+
imageCount: result.imageCount,
|
|
245
|
+
imageOmissions: result.imageOmissions,
|
|
246
|
+
imagePartsSeen: result.imagePartsSeen,
|
|
220
247
|
preferenceBytes: result.preferenceBytes,
|
|
221
248
|
question: resolveAdvisorRequest(params.question),
|
|
222
249
|
scout: scoutDetails,
|
|
@@ -6,7 +6,10 @@ import {
|
|
|
6
6
|
import { loadConfig } from "../config/storage.ts";
|
|
7
7
|
import { reserveAdvisorCall } from "./gate-policy.ts";
|
|
8
8
|
import { handleAutomaticGate } from "./loop-gate.ts";
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
advisorModelAccess,
|
|
11
|
+
sameModelAdvisorDisabled,
|
|
12
|
+
} from "./model-access.ts";
|
|
10
13
|
import { advisorInvocationGuidelines } from "./prompts.ts";
|
|
11
14
|
import type { ToolRegistrationContext } from "./types.ts";
|
|
12
15
|
|
|
@@ -73,7 +76,7 @@ export const registerToolLifecycle = ({
|
|
|
73
76
|
return;
|
|
74
77
|
}
|
|
75
78
|
loadConfig(ctx);
|
|
76
|
-
if (!advisorModelAccess(ctx).allowed) {
|
|
79
|
+
if (!advisorModelAccess(ctx).allowed || sameModelAdvisorDisabled(ctx)) {
|
|
77
80
|
return;
|
|
78
81
|
}
|
|
79
82
|
const guidelines = advisorInvocationGuidelines();
|
|
@@ -107,6 +110,9 @@ export const registerToolLifecycle = ({
|
|
|
107
110
|
// tool errors; every other tool must proceed without Advisor gating.
|
|
108
111
|
return;
|
|
109
112
|
}
|
|
113
|
+
if (sameModelAdvisorDisabled(ctx)) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
110
116
|
const accessBlock = modelAccessBlock(event.toolName, ctx);
|
|
111
117
|
if (accessBlock) {
|
|
112
118
|
return accessBlock;
|
|
@@ -30,23 +30,39 @@ const syncRenderPhase = (context: AdvisorToolContext, phase: string) => {
|
|
|
30
30
|
context.state.phase = phase;
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
+
const formatByteSize = (bytes: number): string => {
|
|
34
|
+
if (bytes >= 1024 ** 3) {
|
|
35
|
+
return `${(bytes / 1024 ** 3).toFixed(1)} GiB`;
|
|
36
|
+
}
|
|
37
|
+
if (bytes >= 1024 ** 2) {
|
|
38
|
+
return `${(bytes / 1024 ** 2).toFixed(1)} MiB`;
|
|
39
|
+
}
|
|
40
|
+
if (bytes >= 1024) {
|
|
41
|
+
return `${Math.round(bytes / 1024)} KiB`;
|
|
42
|
+
}
|
|
43
|
+
return `${bytes} B`;
|
|
44
|
+
};
|
|
45
|
+
|
|
33
46
|
const attachmentLabels = (details: AdvisorToolDetails | undefined) =>
|
|
34
47
|
[
|
|
35
48
|
details?.draftBytes
|
|
36
|
-
? `Draft attached · ${details.draftBytes}
|
|
49
|
+
? `Draft attached · ${formatByteSize(details.draftBytes)}`
|
|
37
50
|
: undefined,
|
|
38
51
|
details?.preferenceBytes
|
|
39
|
-
? `Project preferences attached · ${details.preferenceBytes}
|
|
52
|
+
? `Project preferences attached · ${formatByteSize(details.preferenceBytes)}`
|
|
40
53
|
: undefined,
|
|
41
54
|
details?.trackedBytes
|
|
42
|
-
? `Tracked files attached · ${details.trackedBytes}
|
|
55
|
+
? `Tracked files attached · ${formatByteSize(details.trackedBytes)}`
|
|
43
56
|
: undefined,
|
|
44
57
|
details?.untrackedBytes
|
|
45
|
-
? `Untracked files attached · ${details.untrackedBytes}
|
|
58
|
+
? `Untracked files attached · ${formatByteSize(details.untrackedBytes)}`
|
|
59
|
+
: undefined,
|
|
60
|
+
details?.imageCount
|
|
61
|
+
? `${details.imageCount} image${details.imageCount === 1 ? "" : "s"} attached${details.imageBytes ? ` · ${formatByteSize(details.imageBytes)}` : ""}${details.imageOmissions ? ` · ${details.imageOmissions} withheld` : ""}`
|
|
46
62
|
: undefined,
|
|
47
63
|
].filter((label): label is string => label !== undefined);
|
|
48
64
|
|
|
49
|
-
const
|
|
65
|
+
const renderSkipBox = (
|
|
50
66
|
box: Box,
|
|
51
67
|
result: AgentToolResult<AdvisorToolDetails>,
|
|
52
68
|
expanded: boolean,
|
|
@@ -55,7 +71,7 @@ const renderJevSkipBox = (
|
|
|
55
71
|
const details = advisorResultDetails(result);
|
|
56
72
|
const lines = [
|
|
57
73
|
theme.fg("dim", theme.bold("◆ ADVISOR · SKIPPED")),
|
|
58
|
-
theme.fg("dim", ` ${details?.jev?.reason ?? ""}`),
|
|
74
|
+
theme.fg("dim", ` ${details?.skipReason ?? details?.jev?.reason ?? ""}`),
|
|
59
75
|
];
|
|
60
76
|
box.addChild(new Text(lines.join("\n"), 0, 0));
|
|
61
77
|
box.addChild(
|
|
@@ -84,7 +100,15 @@ const renderPartialAdvisorResult = (
|
|
|
84
100
|
scout?.status === "calling" || scout?.status === "streaming";
|
|
85
101
|
syncRenderPhase(context, scoutActive ? "scout" : "advisor");
|
|
86
102
|
if (!context.state.timerId) {
|
|
87
|
-
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);
|
|
88
112
|
}
|
|
89
113
|
if (scout) {
|
|
90
114
|
renderScoutDetails(box, scout, expanded, theme);
|
|
@@ -157,8 +181,8 @@ const renderFinalAdvisorResult = (
|
|
|
157
181
|
context.state.timerId = undefined;
|
|
158
182
|
}
|
|
159
183
|
const details = advisorResultDetails(result);
|
|
160
|
-
if (details?.jev?.skipped) {
|
|
161
|
-
|
|
184
|
+
if (details?.jev?.skipped || details?.skipReason) {
|
|
185
|
+
renderSkipBox(box, result, expanded, theme);
|
|
162
186
|
return;
|
|
163
187
|
}
|
|
164
188
|
if (details?.scout) {
|
|
@@ -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/tools/types.ts
CHANGED
|
@@ -47,6 +47,10 @@ export interface AdvisorGateFailure {
|
|
|
47
47
|
export interface AdvisorConsultationResult {
|
|
48
48
|
adviceId: string;
|
|
49
49
|
draftBytes?: number;
|
|
50
|
+
imageBytes?: number;
|
|
51
|
+
imageCount?: number;
|
|
52
|
+
imageOmissions?: number;
|
|
53
|
+
imagePartsSeen?: number;
|
|
50
54
|
markdown: string;
|
|
51
55
|
model: string;
|
|
52
56
|
preferenceBytes?: number;
|
|
@@ -83,6 +87,10 @@ export interface AdvisorToolDetails {
|
|
|
83
87
|
adviceId?: string;
|
|
84
88
|
advisor?: string;
|
|
85
89
|
draftBytes?: number;
|
|
90
|
+
imageBytes?: number;
|
|
91
|
+
imageCount?: number;
|
|
92
|
+
imageOmissions?: number;
|
|
93
|
+
imagePartsSeen?: number;
|
|
86
94
|
jev?: {
|
|
87
95
|
kind: JevSkipKind;
|
|
88
96
|
reason: string;
|
|
@@ -91,6 +99,7 @@ export interface AdvisorToolDetails {
|
|
|
91
99
|
preferenceBytes?: number;
|
|
92
100
|
question?: string;
|
|
93
101
|
scout?: ScoutToolDetails;
|
|
102
|
+
skipReason?: string;
|
|
94
103
|
text?: string;
|
|
95
104
|
thinking?: string;
|
|
96
105
|
trackedBytes?: number;
|
package/src/ui/settings-items.ts
CHANGED
|
@@ -284,7 +284,16 @@ export const createSettingsItems = ({
|
|
|
284
284
|
},
|
|
285
285
|
];
|
|
286
286
|
if (settings.simpleMode) {
|
|
287
|
-
items.push(
|
|
287
|
+
items.push(
|
|
288
|
+
modelWhitelist,
|
|
289
|
+
toggle(
|
|
290
|
+
"disableSameModel",
|
|
291
|
+
"Disable same-model Advisor",
|
|
292
|
+
"Skip advice when the active Executor and Advisor use the same provider/model; turn off to allow higher-effort same-model reviews.",
|
|
293
|
+
settings.disableSameModel,
|
|
294
|
+
true
|
|
295
|
+
)
|
|
296
|
+
);
|
|
288
297
|
return items;
|
|
289
298
|
}
|
|
290
299
|
|
|
@@ -297,6 +306,13 @@ export const createSettingsItems = ({
|
|
|
297
306
|
values: withCurrentValue(currentEffort(settings.effort), effortLevels),
|
|
298
307
|
},
|
|
299
308
|
modelWhitelist,
|
|
309
|
+
toggle(
|
|
310
|
+
"disableSameModel",
|
|
311
|
+
"Disable same-model Advisor",
|
|
312
|
+
"Skip advice when the active Executor and Advisor use the same provider/model; turn off to allow higher-effort same-model reviews.",
|
|
313
|
+
settings.disableSameModel,
|
|
314
|
+
true
|
|
315
|
+
),
|
|
300
316
|
toggle(
|
|
301
317
|
"scoutEnabled",
|
|
302
318
|
"Experimental Advisor Scout",
|
package/src/ui/types.ts
CHANGED
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
|
+
};
|