pi-advisor-flow 0.2.3 → 0.2.4
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 +12 -4
- package/package.json +1 -1
- package/src/commands.ts +107 -30
- package/src/config.ts +66 -4
- package/src/conversation.ts +11 -3
- package/src/tools.ts +66 -14
- package/src/ui.ts +313 -97
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ Restart or reload Pi after installation, then run `/advisor` in a session.
|
|
|
36
36
|
|
|
37
37
|
1. Run `/advisor` to enable the flow and register `ask_advisor`.
|
|
38
38
|
2. Run `/advisor-models` to choose the Executor and Advisor models.
|
|
39
|
-
3. Run `/advisor-settings` to set context, gates, privacy controls, and output limits.
|
|
39
|
+
3. Run `/advisor-settings` to set Simple mode, persistent activation, context, gates, privacy controls, and output limits.
|
|
40
40
|
|
|
41
41
|
Enable with models in one command when preferred:
|
|
42
42
|
|
|
@@ -62,7 +62,7 @@ The Executor calls `ask_advisor({})` for a general review of the current task an
|
|
|
62
62
|
|
|
63
63
|
Use the Advisor after the Executor has investigated and formed a candidate direction. It is intended to challenge assumptions, expose risks, and confirm the next verification step—not to replace the Executor's work.
|
|
64
64
|
|
|
65
|
-
Normal consultations preserve the provider's final Markdown.
|
|
65
|
+
Normal consultations preserve the provider's final Markdown and never block execution. When the Advisor has no material concern or recommendation, it may begin with the exact first line `Verdict: sound`; Pi renders that response with the static `◆ ADVISOR · SOUND` header, for both `ask_advisor` results and `/advisor-manual`.
|
|
66
66
|
|
|
67
67
|
## Automatic loop gate
|
|
68
68
|
|
|
@@ -125,7 +125,9 @@ All fields are optional. This example shows the available settings and their nor
|
|
|
125
125
|
"advisorBlockOnBlocked": true,
|
|
126
126
|
"gateFailureMode": "block-session",
|
|
127
127
|
|
|
128
|
-
"advisorSessionSummary":
|
|
128
|
+
"advisorSessionSummary": false,
|
|
129
|
+
"simpleMode": false,
|
|
130
|
+
"alwaysOn": false,
|
|
129
131
|
"advisorHerdrIntegration": true,
|
|
130
132
|
"advisorToolResultMaxLines": 2000,
|
|
131
133
|
"advisorToolResultMaxBytes": 51200,
|
|
@@ -138,6 +140,12 @@ All fields are optional. This example shows the available settings and their nor
|
|
|
138
140
|
}
|
|
139
141
|
```
|
|
140
142
|
|
|
143
|
+
### Simple mode and persistent activation
|
|
144
|
+
|
|
145
|
+
- `simpleMode` defaults to `false`. When enabled, `ask_advisor` and `/advisor-manual` remain available for voluntary second opinions, while plan/failure/completion rules, loop gates, blocking, call budgets, and session summaries are disabled. Context limits, result caps, redaction, and tool disclosure policies still apply.
|
|
146
|
+
- `alwaysOn` defaults to `false`. When enabled, Pi restores the configured Executor and activates `ask_advisor` for new, resumed, forked, and reloaded sessions. While the Advisor flow is active, an explicit `/model` selection becomes the persisted Executor for the next activation; a model restored with a session does not change the saved Executor. `/advisor-off` turns `alwaysOn` off so the flow stays disabled in later sessions.
|
|
147
|
+
- In Simple mode, settings keeps the Context window/history slider alongside Simple mode and Always on; advanced values remain saved and take effect when Simple mode is disabled.
|
|
148
|
+
|
|
141
149
|
### Context and limits
|
|
142
150
|
|
|
143
151
|
- `contextMaxChars` defaults to `15000`. It preserves complete semantic entries and adds an omission marker rather than splitting a message.
|
|
@@ -161,7 +169,7 @@ Redaction and output limits reduce accidental disclosure; they are not a data-cl
|
|
|
161
169
|
|
|
162
170
|
### Session summary and Herdr
|
|
163
171
|
|
|
164
|
-
The optional Session Advisor Summary is local and in-memory only
|
|
172
|
+
The optional Session Advisor Summary defaults to off. When enabled, it is local and in-memory only, appears after a non-blocked settled run, and is never persisted.
|
|
165
173
|
|
|
166
174
|
It distinguishes regular Markdown advice from gate decisions and records the trigger, model, usage/cost when available, failures, budget, and execution effect.
|
|
167
175
|
|
package/package.json
CHANGED
package/src/commands.ts
CHANGED
|
@@ -8,9 +8,11 @@ import {
|
|
|
8
8
|
advisorEffortRef,
|
|
9
9
|
advisorMaxCallsPerSessionRef,
|
|
10
10
|
advisorRef,
|
|
11
|
+
alwaysOnRef,
|
|
11
12
|
executorEffortRef,
|
|
12
13
|
executorRef,
|
|
13
14
|
getAdvisorSettings,
|
|
15
|
+
isSimpleMode,
|
|
14
16
|
loadConfig,
|
|
15
17
|
parseArgs,
|
|
16
18
|
saveConfig,
|
|
@@ -32,9 +34,11 @@ import {
|
|
|
32
34
|
setAdvisorToolPoliciesRef,
|
|
33
35
|
setAdvisorToolResultMaxBytesRef,
|
|
34
36
|
setAdvisorToolResultMaxLinesRef,
|
|
37
|
+
setAlwaysOnRef,
|
|
35
38
|
setContextMaxCharsRef,
|
|
36
39
|
setExecutorEffortRef,
|
|
37
40
|
setExecutorRef,
|
|
41
|
+
setSimpleModeRef,
|
|
38
42
|
splitRef,
|
|
39
43
|
} from "./config.js";
|
|
40
44
|
import { herdrAdvisorActivity, notifyHerdrAdvisorFailure } from "./herdr.js";
|
|
@@ -42,7 +46,9 @@ import {
|
|
|
42
46
|
adviceForDisplay,
|
|
43
47
|
advisorSessionState,
|
|
44
48
|
consultAdvisor,
|
|
49
|
+
hasSoundVerdict,
|
|
45
50
|
renderAdvisorCallBox,
|
|
51
|
+
renderAdvisorResponseHeader,
|
|
46
52
|
resolveAdvisorRequest,
|
|
47
53
|
} from "./tools.js";
|
|
48
54
|
import {
|
|
@@ -196,25 +202,47 @@ export const registerCommands = (
|
|
|
196
202
|
});
|
|
197
203
|
};
|
|
198
204
|
|
|
199
|
-
|
|
205
|
+
/** Resolves both models and their auth, or reports why activation cannot proceed. */
|
|
206
|
+
const resolveActivationModels = async (ctx: ExtensionContext) => {
|
|
207
|
+
const executor = findConfiguredModel(ctx, executorRef);
|
|
208
|
+
if (!executor) {
|
|
209
|
+
return { error: `Executor model not found: ${executorRef}` };
|
|
210
|
+
}
|
|
211
|
+
const advisor = findConfiguredModel(ctx, advisorRef);
|
|
212
|
+
if (!advisor) {
|
|
213
|
+
return { error: `Advisor model not found: ${advisorRef}` };
|
|
214
|
+
}
|
|
215
|
+
const advisorAuth = await ctx.modelRegistry.getApiKeyAndHeaders(advisor);
|
|
216
|
+
if (!(advisorAuth.ok && advisorAuth.apiKey)) {
|
|
217
|
+
return { error: `No API key for Advisor ${advisorRef}` };
|
|
218
|
+
}
|
|
219
|
+
if (!(await pi.setModel(executor))) {
|
|
220
|
+
return { error: `No API key for Executor ${executorRef}` };
|
|
221
|
+
}
|
|
222
|
+
return {};
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const activateAdvisor = async (
|
|
226
|
+
args: string,
|
|
227
|
+
ctx: ExtensionContext,
|
|
228
|
+
announce = true
|
|
229
|
+
) => {
|
|
200
230
|
loadConfig(ctx);
|
|
201
231
|
const argumentError = parseArgs(args);
|
|
202
232
|
if (argumentError) {
|
|
203
233
|
notify(ctx, argumentError, "error");
|
|
204
234
|
return;
|
|
205
235
|
}
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
208
|
-
notify(ctx,
|
|
209
|
-
return;
|
|
210
|
-
}
|
|
211
|
-
if (!findConfiguredModel(ctx, advisorRef)) {
|
|
212
|
-
notify(ctx, `Advisor model not found: ${advisorRef}`, "error");
|
|
236
|
+
const { error } = await resolveActivationModels(ctx);
|
|
237
|
+
if (error) {
|
|
238
|
+
notify(ctx, error, "error");
|
|
213
239
|
return;
|
|
214
240
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
241
|
+
// parseArgs only mutates in-memory refs, and every later loadConfig resets
|
|
242
|
+
// them from disk. Persist supplied arguments once they are known to resolve,
|
|
243
|
+
// so an unusable model reference is never written to the configuration.
|
|
244
|
+
if (args.trim()) {
|
|
245
|
+
saveConfig(ctx);
|
|
218
246
|
}
|
|
219
247
|
if (executorEffortRef) {
|
|
220
248
|
pi.setThinkingLevel(executorEffortRef as ThinkingLevel);
|
|
@@ -222,11 +250,13 @@ export const registerCommands = (
|
|
|
222
250
|
if (!flowEnabled()) {
|
|
223
251
|
pi.setActiveTools([...pi.getActiveTools(), "ask_advisor"]);
|
|
224
252
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
253
|
+
if (announce) {
|
|
254
|
+
notify(
|
|
255
|
+
ctx,
|
|
256
|
+
`Advisor flow ready — Executor: ${executorRef} (thinking: ${executorEffortRef || "default"}) · Advisor: ${advisorRef} (thinking: ${advisorEffortRef || "default"})`,
|
|
257
|
+
"info"
|
|
258
|
+
);
|
|
259
|
+
}
|
|
230
260
|
};
|
|
231
261
|
|
|
232
262
|
pi.registerEntryRenderer?.(
|
|
@@ -244,17 +274,22 @@ export const registerCommands = (
|
|
|
244
274
|
| { advisor?: string; text?: string }
|
|
245
275
|
| undefined;
|
|
246
276
|
const box = new Box(1, 1, (text) => theme.bg("customMessageBg", text));
|
|
247
|
-
box.addChild(
|
|
248
|
-
new Text(theme.fg("warning", theme.bold("◆ ADVISOR RESPONSE")), 0, 0)
|
|
249
|
-
);
|
|
250
|
-
if (details?.advisor) {
|
|
251
|
-
box.addChild(new Text(theme.fg("dim", ` ${details.advisor}`), 0, 0));
|
|
252
|
-
}
|
|
253
277
|
const advice =
|
|
254
278
|
details?.text ??
|
|
255
279
|
(typeof message.content === "string"
|
|
256
280
|
? message.content
|
|
257
281
|
: "(Advisor returned no advice.)");
|
|
282
|
+
// Manual consultations must render exactly like an Executor ask_advisor call.
|
|
283
|
+
box.addChild(
|
|
284
|
+
new Text(
|
|
285
|
+
renderAdvisorResponseHeader(hasSoundVerdict(advice), theme),
|
|
286
|
+
0,
|
|
287
|
+
0
|
|
288
|
+
)
|
|
289
|
+
);
|
|
290
|
+
if (details?.advisor) {
|
|
291
|
+
box.addChild(new Text(theme.fg("dim", ` ${details.advisor}`), 0, 0));
|
|
292
|
+
}
|
|
258
293
|
box.addChild(
|
|
259
294
|
new Markdown(
|
|
260
295
|
adviceForDisplay(advice, expanded),
|
|
@@ -267,6 +302,34 @@ export const registerCommands = (
|
|
|
267
302
|
}
|
|
268
303
|
);
|
|
269
304
|
|
|
305
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
306
|
+
// A malformed advisor.json or a provider auth failure must not reject a
|
|
307
|
+
// lifecycle handler and break session startup.
|
|
308
|
+
try {
|
|
309
|
+
loadConfig(ctx);
|
|
310
|
+
if (alwaysOnRef) {
|
|
311
|
+
await activateAdvisor("", ctx, false);
|
|
312
|
+
}
|
|
313
|
+
} catch (error) {
|
|
314
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
315
|
+
notify(ctx, `Advisor activation failed: ${message}`, "error");
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
pi.on("model_select", (event, ctx) => {
|
|
320
|
+
// Only an explicit user selection redefines the Executor. "restore" replays a
|
|
321
|
+
// stored session model and would otherwise overwrite saved configuration.
|
|
322
|
+
if (event.source !== "set" || !flowEnabled()) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const selected = `${event.model.provider}/${event.model.id}`;
|
|
326
|
+
if (selected === executorRef) {
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
setExecutorRef(selected);
|
|
330
|
+
saveConfig(ctx);
|
|
331
|
+
});
|
|
332
|
+
|
|
270
333
|
pi.on("session_shutdown", () => {
|
|
271
334
|
for (const controller of manualConsultations) {
|
|
272
335
|
controller.abort();
|
|
@@ -280,13 +343,20 @@ export const registerCommands = (
|
|
|
280
343
|
"Consult the Advisor in parallel; accepts an optional focused question and fans its response out to the Executor",
|
|
281
344
|
handler: (args, ctx) => {
|
|
282
345
|
loadConfig(ctx);
|
|
283
|
-
if (
|
|
346
|
+
if (
|
|
347
|
+
!(
|
|
348
|
+
isSimpleMode() ||
|
|
349
|
+
advisorSessionState.canConsult(advisorMaxCallsPerSessionRef)
|
|
350
|
+
)
|
|
351
|
+
) {
|
|
284
352
|
const message = "Advisor call budget exhausted for this session.";
|
|
285
353
|
notify(ctx, message, "warning");
|
|
286
354
|
notifyHerdrAdvisorFailure("Advisor budget exhausted", message);
|
|
287
355
|
return Promise.resolve();
|
|
288
356
|
}
|
|
289
|
-
|
|
357
|
+
if (!isSimpleMode()) {
|
|
358
|
+
advisorSessionState.consumeCall();
|
|
359
|
+
}
|
|
290
360
|
const question = resolveAdvisorRequest(args);
|
|
291
361
|
// A single visible progress surface avoids competing consultations overwriting
|
|
292
362
|
// each other's streamed state. A newer manual request replaces the previous one.
|
|
@@ -427,7 +497,9 @@ export const registerCommands = (
|
|
|
427
497
|
setAdvisorAutoLoopGateRef(settings.autoLoopGate ?? true);
|
|
428
498
|
setAdvisorLoopThresholdRef(settings.loopThreshold ?? 3);
|
|
429
499
|
setAdvisorMaxCallsPerSessionRef(settings.maxCallsPerSession);
|
|
430
|
-
setAdvisorSessionSummaryRef(settings.sessionSummary ??
|
|
500
|
+
setAdvisorSessionSummaryRef(settings.sessionSummary ?? false);
|
|
501
|
+
setSimpleModeRef(settings.simpleMode ?? false);
|
|
502
|
+
setAlwaysOnRef(settings.alwaysOn ?? false);
|
|
431
503
|
setAdvisorFailureModeRef(settings.failureMode ?? "block-session");
|
|
432
504
|
setAdvisorHerdrIntegrationRef(settings.herdrIntegration ?? true);
|
|
433
505
|
setAdvisorToolResultMaxLinesRef(settings.toolResultMaxLines ?? 2000);
|
|
@@ -445,12 +517,17 @@ export const registerCommands = (
|
|
|
445
517
|
pi.setActiveTools(
|
|
446
518
|
pi.getActiveTools().filter((name) => name !== "ask_advisor")
|
|
447
519
|
);
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
);
|
|
520
|
+
// Leaving alwaysOn set would silently reactivate the flow next session.
|
|
521
|
+
const wasAlwaysOn = alwaysOnRef;
|
|
522
|
+
if (wasAlwaysOn) {
|
|
523
|
+
setAlwaysOnRef(false);
|
|
524
|
+
saveConfig(ctx);
|
|
453
525
|
}
|
|
526
|
+
notify(
|
|
527
|
+
ctx,
|
|
528
|
+
`Advisor flow disabled. Current model unchanged.${wasAlwaysOn ? " Always on turned off." : ""}`,
|
|
529
|
+
"info"
|
|
530
|
+
);
|
|
454
531
|
return Promise.resolve();
|
|
455
532
|
},
|
|
456
533
|
});
|
package/src/config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import {
|
|
4
4
|
CONFIG_DIR_NAME,
|
|
@@ -45,7 +45,9 @@ export let advisorBlockOnBlockedRef = true;
|
|
|
45
45
|
export let advisorAutoLoopGateRef = true;
|
|
46
46
|
export let advisorLoopThresholdRef = 3;
|
|
47
47
|
export let advisorMaxCallsPerSessionRef: number | undefined;
|
|
48
|
-
export let advisorSessionSummaryRef =
|
|
48
|
+
export let advisorSessionSummaryRef = false;
|
|
49
|
+
export let simpleModeRef = false;
|
|
50
|
+
export let alwaysOnRef = false;
|
|
49
51
|
export let advisorFailureModeRef: GateFailureMode = "block-session";
|
|
50
52
|
export let advisorHerdrIntegrationRef = true;
|
|
51
53
|
export let advisorToolResultMaxLinesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES;
|
|
@@ -107,6 +109,14 @@ export const setAdvisorMaxCallsPerSessionRef = (value: number | undefined) => {
|
|
|
107
109
|
export const setAdvisorSessionSummaryRef = (enabled: boolean) => {
|
|
108
110
|
advisorSessionSummaryRef = enabled;
|
|
109
111
|
};
|
|
112
|
+
export const setSimpleModeRef = (enabled: boolean) => {
|
|
113
|
+
simpleModeRef = enabled;
|
|
114
|
+
};
|
|
115
|
+
export const setAlwaysOnRef = (enabled: boolean) => {
|
|
116
|
+
alwaysOnRef = enabled;
|
|
117
|
+
};
|
|
118
|
+
/** Whether advanced Advisor automation is suppressed for this session. */
|
|
119
|
+
export const isSimpleMode = () => simpleModeRef;
|
|
110
120
|
export const isValidGateFailureMode = (
|
|
111
121
|
value: unknown
|
|
112
122
|
): value is GateFailureMode =>
|
|
@@ -140,6 +150,7 @@ export const setAdvisorToolPoliciesRef = (policies: AdvisorToolPolicies) => {
|
|
|
140
150
|
* imported mutable bindings, which can be snapshotted by extension loaders.
|
|
141
151
|
*/
|
|
142
152
|
export const getAdvisorSettings = () => ({
|
|
153
|
+
alwaysOn: alwaysOnRef,
|
|
143
154
|
autoLoopGate: advisorAutoLoopGateRef,
|
|
144
155
|
blockOnBlocked: advisorBlockOnBlockedRef,
|
|
145
156
|
collapseResponses: advisorCollapseResponsesRef,
|
|
@@ -155,6 +166,7 @@ export const getAdvisorSettings = () => ({
|
|
|
155
166
|
planGate: advisorPlanGateRef,
|
|
156
167
|
redactSecrets: advisorRedactSecretsRef,
|
|
157
168
|
sessionSummary: advisorSessionSummaryRef,
|
|
169
|
+
simpleMode: simpleModeRef,
|
|
158
170
|
toolPolicies: { ...advisorToolPoliciesRef },
|
|
159
171
|
toolResultMaxBytes: advisorToolResultMaxBytesRef,
|
|
160
172
|
toolResultMaxLines: advisorToolResultMaxLinesRef,
|
|
@@ -190,10 +202,12 @@ export interface AdvisorConfig {
|
|
|
190
202
|
advisorToolPolicies?: AdvisorToolPolicies;
|
|
191
203
|
advisorToolResultMaxBytes?: number;
|
|
192
204
|
advisorToolResultMaxLines?: number;
|
|
205
|
+
alwaysOn?: boolean;
|
|
193
206
|
contextMaxChars?: number;
|
|
194
207
|
executor?: string;
|
|
195
208
|
executorEffort?: string;
|
|
196
209
|
gateFailureMode?: GateFailureMode;
|
|
210
|
+
simpleMode?: boolean;
|
|
197
211
|
}
|
|
198
212
|
|
|
199
213
|
const CONFIG_KEYS = new Set<keyof AdvisorConfig>([
|
|
@@ -210,6 +224,8 @@ const CONFIG_KEYS = new Set<keyof AdvisorConfig>([
|
|
|
210
224
|
"advisorMaxCallsPerSession",
|
|
211
225
|
"advisorPlanGate",
|
|
212
226
|
"advisorSessionSummary",
|
|
227
|
+
"simpleMode",
|
|
228
|
+
"alwaysOn",
|
|
213
229
|
"advisorToolResultMaxBytes",
|
|
214
230
|
"advisorToolResultMaxLines",
|
|
215
231
|
"advisorRedactSecrets",
|
|
@@ -227,6 +243,8 @@ const BOOLEAN_CONFIG_KEYS = [
|
|
|
227
243
|
"advisorBlockOnBlocked",
|
|
228
244
|
"advisorAutoLoopGate",
|
|
229
245
|
"advisorSessionSummary",
|
|
246
|
+
"simpleMode",
|
|
247
|
+
"alwaysOn",
|
|
230
248
|
"advisorHerdrIntegration",
|
|
231
249
|
"advisorRedactSecrets",
|
|
232
250
|
] as const;
|
|
@@ -385,7 +403,9 @@ const resetDefaults = () => {
|
|
|
385
403
|
advisorAutoLoopGateRef = true;
|
|
386
404
|
advisorLoopThresholdRef = 3;
|
|
387
405
|
advisorMaxCallsPerSessionRef = undefined;
|
|
388
|
-
advisorSessionSummaryRef =
|
|
406
|
+
advisorSessionSummaryRef = false;
|
|
407
|
+
simpleModeRef = false;
|
|
408
|
+
alwaysOnRef = false;
|
|
389
409
|
advisorFailureModeRef = "block-session";
|
|
390
410
|
advisorHerdrIntegrationRef = true;
|
|
391
411
|
advisorToolResultMaxLinesRef = DEFAULT_ADVISOR_TOOL_RESULT_MAX_LINES;
|
|
@@ -458,6 +478,8 @@ const applyConfig = (config: AdvisorConfig) => {
|
|
|
458
478
|
"advisorSessionSummary",
|
|
459
479
|
setAdvisorSessionSummaryRef
|
|
460
480
|
);
|
|
481
|
+
applyOptionalConfig(config, "simpleMode", setSimpleModeRef);
|
|
482
|
+
applyOptionalConfig(config, "alwaysOn", setAlwaysOnRef);
|
|
461
483
|
applyOptionalConfig(config, "gateFailureMode", setAdvisorFailureModeRef);
|
|
462
484
|
applyOptionalConfig(
|
|
463
485
|
config,
|
|
@@ -492,6 +514,43 @@ const readConfig = (path: string): AdvisorConfig => {
|
|
|
492
514
|
}
|
|
493
515
|
};
|
|
494
516
|
|
|
517
|
+
// loadConfig runs on every tool call and every consultation. Caching the parsed
|
|
518
|
+
// file by path and stat identity removes that read and parse from the hot path
|
|
519
|
+
// while still applying the full reset-then-apply sequence on each call.
|
|
520
|
+
let configCache:
|
|
521
|
+
| { config: AdvisorConfig; identity: string; path: string }
|
|
522
|
+
| undefined;
|
|
523
|
+
|
|
524
|
+
/** Drops the parsed-configuration cache; the next load re-reads from disk. */
|
|
525
|
+
export const resetConfigCache = () => {
|
|
526
|
+
configCache = undefined;
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Nanosecond timestamps plus size and inode so a same-millisecond or
|
|
531
|
+
* same-size external rewrite cannot be served from the cache. An unstattable
|
|
532
|
+
* path yields a unique value, which always forces a re-read.
|
|
533
|
+
*/
|
|
534
|
+
const configIdentity = (path: string): string => {
|
|
535
|
+
try {
|
|
536
|
+
const stats = statSync(path, { bigint: true });
|
|
537
|
+
return `${stats.mtimeNs}:${stats.ctimeNs}:${stats.size}:${stats.ino}:${stats.dev}`;
|
|
538
|
+
} catch {
|
|
539
|
+
return `unstattable:${process.hrtime.bigint()}`;
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
|
|
543
|
+
const readConfigCached = (path: string): AdvisorConfig => {
|
|
544
|
+
const identity = configIdentity(path);
|
|
545
|
+
const cached = configCache;
|
|
546
|
+
if (cached && cached.path === path && cached.identity === identity) {
|
|
547
|
+
return cached.config;
|
|
548
|
+
}
|
|
549
|
+
const config = readConfig(path);
|
|
550
|
+
configCache = { config, identity, path };
|
|
551
|
+
return config;
|
|
552
|
+
};
|
|
553
|
+
|
|
495
554
|
export const loadConfig = (ctx: ExtensionContext) => {
|
|
496
555
|
resetDefaults();
|
|
497
556
|
const path = configPaths(ctx).find(
|
|
@@ -501,7 +560,7 @@ export const loadConfig = (ctx: ExtensionContext) => {
|
|
|
501
560
|
if (!path) {
|
|
502
561
|
return null;
|
|
503
562
|
}
|
|
504
|
-
applyConfig(
|
|
563
|
+
applyConfig(readConfigCached(path));
|
|
505
564
|
return path;
|
|
506
565
|
};
|
|
507
566
|
|
|
@@ -547,9 +606,12 @@ export const saveConfig = (ctx: ExtensionContext) => {
|
|
|
547
606
|
advisorToolPolicies: advisorToolPoliciesRef,
|
|
548
607
|
advisorToolResultMaxBytes: advisorToolResultMaxBytesRef,
|
|
549
608
|
advisorToolResultMaxLines: advisorToolResultMaxLinesRef,
|
|
609
|
+
alwaysOn: alwaysOnRef,
|
|
550
610
|
gateFailureMode: advisorFailureModeRef,
|
|
611
|
+
simpleMode: simpleModeRef,
|
|
551
612
|
};
|
|
552
613
|
writeFileSync(path, `${JSON.stringify(data, null, 2)}\n`);
|
|
614
|
+
resetConfigCache();
|
|
553
615
|
return path;
|
|
554
616
|
};
|
|
555
617
|
|
package/src/conversation.ts
CHANGED
|
@@ -256,10 +256,18 @@ const selectRecentEntries = (entries: string[], maxChars: number): string => {
|
|
|
256
256
|
return joined;
|
|
257
257
|
}
|
|
258
258
|
const selected: string[] = [];
|
|
259
|
+
// Track the joined length instead of re-joining the accumulator each step;
|
|
260
|
+
// long branches with a large maxChars are otherwise quadratic in bytes.
|
|
261
|
+
let selectedLength = 0;
|
|
259
262
|
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
260
|
-
const
|
|
261
|
-
|
|
262
|
-
|
|
263
|
+
const entry = entries[index];
|
|
264
|
+
const candidateLength =
|
|
265
|
+
selectedLength +
|
|
266
|
+
entry.length +
|
|
267
|
+
(selected.length === 0 ? 0 : separator.length);
|
|
268
|
+
if (candidateLength <= maxChars || selected.length === 0) {
|
|
269
|
+
selected.unshift(entry);
|
|
270
|
+
selectedLength = candidateLength;
|
|
263
271
|
} else {
|
|
264
272
|
break;
|
|
265
273
|
}
|
package/src/tools.ts
CHANGED
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
advisorRef,
|
|
31
31
|
advisorSessionSummaryRef,
|
|
32
32
|
contextMaxCharsRef,
|
|
33
|
+
isSimpleMode,
|
|
33
34
|
loadConfig,
|
|
34
35
|
splitRef,
|
|
35
36
|
} from "./config.js";
|
|
@@ -69,8 +70,15 @@ export const SPINNER_FRAMES = [
|
|
|
69
70
|
];
|
|
70
71
|
export const resolveAdvisorRequest = (question?: string) =>
|
|
71
72
|
question?.trim() || undefined;
|
|
72
|
-
export const advisorMessageText = (conversation: string, question?: string) =>
|
|
73
|
-
`${conversation ? `<conversation>\n${conversation}\n</conversation>` : ""}${question ? `\n\nTargeted focus:\n${question}` : ""}`;
|
|
73
|
+
export const advisorMessageText = (conversation: string, question?: string) => {
|
|
74
|
+
const text = `${conversation ? `<conversation>\n${conversation}\n</conversation>` : ""}${question ? `\n\nTargeted focus:\n${question}` : ""}`;
|
|
75
|
+
// A zero context limit with no targeted focus would otherwise send an empty
|
|
76
|
+
// user message, which several providers reject outright.
|
|
77
|
+
return (
|
|
78
|
+
text.trim() ||
|
|
79
|
+
"No conversation context is available. State that you cannot review without context."
|
|
80
|
+
);
|
|
81
|
+
};
|
|
74
82
|
|
|
75
83
|
/** The sole reconstructed-context boundary for outgoing Advisor requests. */
|
|
76
84
|
export const advisorRequestConversation = (ctx: ExtensionContext) =>
|
|
@@ -96,6 +104,19 @@ export const renderAdvisorCallBox = (
|
|
|
96
104
|
};
|
|
97
105
|
|
|
98
106
|
const COLLAPSED_ADVICE_LINES = 12;
|
|
107
|
+
// The system prompt requires this exact first line, so the match is exact too.
|
|
108
|
+
const SOUND_VERDICT = /^Verdict:\s*sound$/;
|
|
109
|
+
|
|
110
|
+
export const hasSoundVerdict = (advice: string) =>
|
|
111
|
+
SOUND_VERDICT.test(
|
|
112
|
+
(advice.split("\n").find((line) => line.trim()) ?? "").trim()
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
/** The single Advisor response header shared by tool and manual renderers. */
|
|
116
|
+
export const renderAdvisorResponseHeader = (sound: boolean, theme: Theme) =>
|
|
117
|
+
sound
|
|
118
|
+
? theme.fg("accent", theme.bold("◆ ADVISOR · SOUND"))
|
|
119
|
+
: theme.fg("warning", theme.bold("◆ ADVISOR RESPONSE"));
|
|
99
120
|
|
|
100
121
|
export const adviceForDisplay = (advice: string, expanded: boolean) => {
|
|
101
122
|
if (!advisorCollapseResponsesRef || expanded) {
|
|
@@ -109,6 +130,11 @@ export const adviceForDisplay = (advice: string, expanded: boolean) => {
|
|
|
109
130
|
};
|
|
110
131
|
|
|
111
132
|
export const advisorInvocationGuidelines = () => {
|
|
133
|
+
if (isSimpleMode()) {
|
|
134
|
+
return [
|
|
135
|
+
"When uncertain and normal available tools cannot resolve it, call ask_advisor for a second opinion.",
|
|
136
|
+
];
|
|
137
|
+
}
|
|
112
138
|
const guidelines: string[] = [];
|
|
113
139
|
if (advisorPlanGateRef) {
|
|
114
140
|
guidelines.push(
|
|
@@ -141,6 +167,7 @@ export const ADVISOR_SYSTEM = [
|
|
|
141
167
|
"You already have the relevant reconstructed conversation context. No question or other input from the Executor is needed for a general review.",
|
|
142
168
|
"When no targeted focus is supplied, proactively review the task, risks, proposed direction, and validation from the context. Do not ask the Executor for a question, clarification, more input, or confirmation.",
|
|
143
169
|
"The context may be truncated, so state any material uncertainty and make the best recommendation you can from what is present.",
|
|
170
|
+
"When the implementation is fully sound based on the supplied evidence and you have no material concern or recommended change, begin with exactly `Verdict: sound`. Do not use that verdict when uncertainty, a risk, or a recommendation remains.",
|
|
144
171
|
"You do not act or take over planning. Answer the Executor's request directly in concise, human-readable Markdown. State uncertainty plainly and never claim verification that the supplied evidence does not show.",
|
|
145
172
|
].join(" ");
|
|
146
173
|
|
|
@@ -193,6 +220,7 @@ export const advisorUsageCost = (usage: unknown): number | undefined => {
|
|
|
193
220
|
|
|
194
221
|
const DECISION_LINE = /^Decision\s*:\s*(proceed|revise|blocked)\s*$/i;
|
|
195
222
|
const ANY_DECISION_LINE = /^Decision\s*:\s*(.*?)\s*$/i;
|
|
223
|
+
const CODE_FENCE = /^(?:```|~~~)/;
|
|
196
224
|
const LINE_BREAK = /\r?\n/;
|
|
197
225
|
|
|
198
226
|
export const parseAutomaticDecision = (
|
|
@@ -221,8 +249,19 @@ export const parseAutomaticDecision = (
|
|
|
221
249
|
};
|
|
222
250
|
}
|
|
223
251
|
const decision = match[1].toLowerCase() as GateDecision;
|
|
252
|
+
let insideFence = false;
|
|
224
253
|
for (const line of lines.slice(nonEmpty + 1)) {
|
|
225
|
-
const
|
|
254
|
+
const trimmed = line.trim();
|
|
255
|
+
// A decision quoted inside a fenced example is illustrative, not a second
|
|
256
|
+
// decision, and must not fail the gate.
|
|
257
|
+
if (CODE_FENCE.test(trimmed)) {
|
|
258
|
+
insideFence = !insideFence;
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (insideFence) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const subsequent = ANY_DECISION_LINE.exec(trimmed);
|
|
226
265
|
if (!subsequent) {
|
|
227
266
|
continue;
|
|
228
267
|
}
|
|
@@ -453,7 +492,7 @@ const reserveAdvisorCall = (
|
|
|
453
492
|
session: AdvisorSessionState,
|
|
454
493
|
reservedCalls: Set<string>
|
|
455
494
|
): ToolCallEventResult | undefined => {
|
|
456
|
-
if (event.toolName !== "ask_advisor") {
|
|
495
|
+
if (event.toolName !== "ask_advisor" || isSimpleMode()) {
|
|
457
496
|
return;
|
|
458
497
|
}
|
|
459
498
|
if (!session.canConsult(advisorMaxCallsPerSessionRef)) {
|
|
@@ -521,6 +560,7 @@ const handleAutomaticGate = async (
|
|
|
521
560
|
session: AdvisorSessionState
|
|
522
561
|
): Promise<ToolCallEventResult | undefined> => {
|
|
523
562
|
if (
|
|
563
|
+
isSimpleMode() ||
|
|
524
564
|
event.toolName === "ask_advisor" ||
|
|
525
565
|
!advisorAutoLoopGateRef ||
|
|
526
566
|
!session.recordToolCall(
|
|
@@ -665,7 +705,8 @@ const renderFinalAdvisorResult = (
|
|
|
665
705
|
context.state.timerId = undefined;
|
|
666
706
|
}
|
|
667
707
|
const details = advisorResultDetails(result);
|
|
668
|
-
const
|
|
708
|
+
const advice = details?.text || textFrom(result.content);
|
|
709
|
+
const lines = [renderAdvisorResponseHeader(hasSoundVerdict(advice), theme)];
|
|
669
710
|
if (details?.advisor) {
|
|
670
711
|
lines.push(theme.fg("dim", ` ${details.advisor}`));
|
|
671
712
|
}
|
|
@@ -678,13 +719,15 @@ const renderFinalAdvisorResult = (
|
|
|
678
719
|
)
|
|
679
720
|
);
|
|
680
721
|
}
|
|
681
|
-
const advice
|
|
682
|
-
details?.text ||
|
|
683
|
-
textFrom(result.content) ||
|
|
684
|
-
"(Advisor returned no advice.)";
|
|
722
|
+
const displayAdvice = advice || "(Advisor returned no advice.)";
|
|
685
723
|
box.addChild(new Text(lines.join("\n"), 0, 0));
|
|
686
724
|
box.addChild(
|
|
687
|
-
new Markdown(
|
|
725
|
+
new Markdown(
|
|
726
|
+
adviceForDisplay(displayAdvice, expanded),
|
|
727
|
+
0,
|
|
728
|
+
0,
|
|
729
|
+
getMarkdownTheme()
|
|
730
|
+
)
|
|
688
731
|
);
|
|
689
732
|
};
|
|
690
733
|
|
|
@@ -779,7 +822,9 @@ export const registerAdvisorTool = (pi: ExtensionAPI) => {
|
|
|
779
822
|
}
|
|
780
823
|
loadConfig(ctx);
|
|
781
824
|
const guidelines = advisorInvocationGuidelines();
|
|
782
|
-
const budget =
|
|
825
|
+
const budget = isSimpleMode()
|
|
826
|
+
? undefined
|
|
827
|
+
: session.remainingCalls(advisorMaxCallsPerSessionRef);
|
|
783
828
|
if (budget !== undefined) {
|
|
784
829
|
guidelines.push(
|
|
785
830
|
`Advisor calls remaining this session: ${budget}.\nReserve calls for material decisions, repeated failures, or final review.`
|
|
@@ -797,7 +842,14 @@ export const registerAdvisorTool = (pi: ExtensionAPI) => {
|
|
|
797
842
|
return;
|
|
798
843
|
}
|
|
799
844
|
loadConfig(ctx);
|
|
800
|
-
if (
|
|
845
|
+
if (isSimpleMode()) {
|
|
846
|
+
// Simple mode does not gate, so a block recorded before it was enabled must
|
|
847
|
+
// not linger in session or Herdr state and misreport the session as blocked.
|
|
848
|
+
if (session.blocked) {
|
|
849
|
+
session.clearBlocked();
|
|
850
|
+
herdrAdvisorBlock.clear();
|
|
851
|
+
}
|
|
852
|
+
} else if (session.blocked) {
|
|
801
853
|
return {
|
|
802
854
|
block: true,
|
|
803
855
|
reason: session.blockedReason ?? "Advisor session is blocked.",
|
|
@@ -811,7 +863,7 @@ export const registerAdvisorTool = (pi: ExtensionAPI) => {
|
|
|
811
863
|
});
|
|
812
864
|
|
|
813
865
|
pi.on("agent_settled", (_event, ctx) => {
|
|
814
|
-
if (session.blocked || !advisorSessionSummaryRef) {
|
|
866
|
+
if (isSimpleMode() || session.blocked || !advisorSessionSummaryRef) {
|
|
815
867
|
return;
|
|
816
868
|
}
|
|
817
869
|
const summary = session.summary(advisorMaxCallsPerSessionRef);
|
|
@@ -829,7 +881,7 @@ export const registerAdvisorTool = (pi: ExtensionAPI) => {
|
|
|
829
881
|
description:
|
|
830
882
|
"Consult the on-demand Advisor model for strategic guidance. Call with an empty object for a context-aware review; add question only for a genuinely targeted focus.",
|
|
831
883
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
832
|
-
if (!reservedCalls.delete(_id)) {
|
|
884
|
+
if (!(reservedCalls.delete(_id) || isSimpleMode())) {
|
|
833
885
|
if (!session.canConsult(advisorMaxCallsPerSessionRef)) {
|
|
834
886
|
throw new Error("Advisor call budget exhausted for this session.");
|
|
835
887
|
}
|
package/src/ui.ts
CHANGED
|
@@ -185,6 +185,7 @@ export interface ContextPreset {
|
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
export interface AdvisorSettings {
|
|
188
|
+
alwaysOn?: boolean;
|
|
188
189
|
autoLoopGate?: boolean;
|
|
189
190
|
blockOnBlocked?: boolean;
|
|
190
191
|
collapseResponses: boolean;
|
|
@@ -200,11 +201,68 @@ export interface AdvisorSettings {
|
|
|
200
201
|
planGate: boolean;
|
|
201
202
|
redactSecrets?: boolean;
|
|
202
203
|
sessionSummary?: boolean;
|
|
204
|
+
simpleMode?: boolean;
|
|
203
205
|
toolPolicies?: Record<string, "full" | "summary" | "exclude">;
|
|
204
206
|
toolResultMaxBytes?: number;
|
|
205
207
|
toolResultMaxLines?: number;
|
|
206
208
|
}
|
|
207
209
|
|
|
210
|
+
type AdvisorSettingsRow =
|
|
211
|
+
| "simpleMode"
|
|
212
|
+
| "alwaysOn"
|
|
213
|
+
| "context"
|
|
214
|
+
| "effort"
|
|
215
|
+
| "planGate"
|
|
216
|
+
| "failureGate"
|
|
217
|
+
| "completionGate"
|
|
218
|
+
| "collapseResponses"
|
|
219
|
+
| "customRule"
|
|
220
|
+
| "blockOnBlocked"
|
|
221
|
+
| "autoLoopGate"
|
|
222
|
+
| "loopThreshold"
|
|
223
|
+
| "maxCallsPerSession"
|
|
224
|
+
| "sessionSummary"
|
|
225
|
+
| "failureMode"
|
|
226
|
+
| "herdrIntegration"
|
|
227
|
+
| "toolResultMaxLines"
|
|
228
|
+
| "toolResultMaxBytes"
|
|
229
|
+
| "redactSecrets"
|
|
230
|
+
| "toolPolicies"
|
|
231
|
+
| "save";
|
|
232
|
+
|
|
233
|
+
const ADVANCED_ROWS: AdvisorSettingsRow[] = [
|
|
234
|
+
"context",
|
|
235
|
+
"effort",
|
|
236
|
+
"planGate",
|
|
237
|
+
"failureGate",
|
|
238
|
+
"completionGate",
|
|
239
|
+
"collapseResponses",
|
|
240
|
+
"customRule",
|
|
241
|
+
"blockOnBlocked",
|
|
242
|
+
"autoLoopGate",
|
|
243
|
+
"loopThreshold",
|
|
244
|
+
"maxCallsPerSession",
|
|
245
|
+
"sessionSummary",
|
|
246
|
+
"failureMode",
|
|
247
|
+
"herdrIntegration",
|
|
248
|
+
"toolResultMaxLines",
|
|
249
|
+
"toolResultMaxBytes",
|
|
250
|
+
"redactSecrets",
|
|
251
|
+
"toolPolicies",
|
|
252
|
+
];
|
|
253
|
+
|
|
254
|
+
const SIMPLE_MODE_GRADIENT_INTERVAL_MS = 100;
|
|
255
|
+
// #763FCD and nearby lighter/darker purple steps; white is the moving shine.
|
|
256
|
+
const SIMPLE_MODE_GRADIENT_COLORS = [
|
|
257
|
+
[125, 79, 205],
|
|
258
|
+
[143, 96, 218],
|
|
259
|
+
[160, 114, 230],
|
|
260
|
+
[178, 135, 238],
|
|
261
|
+
[195, 157, 245],
|
|
262
|
+
[168, 120, 230],
|
|
263
|
+
[143, 89, 215],
|
|
264
|
+
] as const;
|
|
265
|
+
|
|
208
266
|
export class AdvisorSettingsSelector implements Component, Focusable {
|
|
209
267
|
private selectedRow: number;
|
|
210
268
|
private contextIndex: number;
|
|
@@ -215,6 +273,8 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
215
273
|
private editingCustom: boolean;
|
|
216
274
|
private editingPolicies: boolean;
|
|
217
275
|
private policiesError: string | undefined;
|
|
276
|
+
private simpleModeGradientStartedAt: number | undefined;
|
|
277
|
+
private simpleModeGradientTimer: ReturnType<typeof setInterval> | undefined;
|
|
218
278
|
private _focused = false;
|
|
219
279
|
private readonly options: AdvisorSettingsSelectorOptions;
|
|
220
280
|
|
|
@@ -233,6 +293,7 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
233
293
|
this.editingPolicies = false;
|
|
234
294
|
this.policiesError = undefined;
|
|
235
295
|
this.options = options;
|
|
296
|
+
// Retain every advanced value even when Simple mode hides its controls.
|
|
236
297
|
this.settings = { ...options.initial };
|
|
237
298
|
this.contextIndex = Math.max(
|
|
238
299
|
0,
|
|
@@ -246,6 +307,9 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
246
307
|
this.settings.effort || "Default (Model Default)"
|
|
247
308
|
)
|
|
248
309
|
);
|
|
310
|
+
if (this.settings.simpleMode) {
|
|
311
|
+
this.startSimpleModeGradient();
|
|
312
|
+
}
|
|
249
313
|
this.customInput.onSubmit = (value) => {
|
|
250
314
|
this.settings.customRule = value.trim() || undefined;
|
|
251
315
|
this.editingCustom = false;
|
|
@@ -289,6 +353,25 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
289
353
|
this.options.tui.requestRender();
|
|
290
354
|
}
|
|
291
355
|
|
|
356
|
+
/** Called by TUI teardown when it supports component disposal. */
|
|
357
|
+
dispose(): void {
|
|
358
|
+
this.stopSimpleModeGradient();
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
private visibleRows(): AdvisorSettingsRow[] {
|
|
362
|
+
return this.settings.simpleMode
|
|
363
|
+
? ["context", "simpleMode", "alwaysOn", "save"]
|
|
364
|
+
: ["simpleMode", "alwaysOn", ...ADVANCED_ROWS, "save"];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private selectedRowId(): AdvisorSettingsRow {
|
|
368
|
+
return this.visibleRows()[this.selectedRow] ?? "simpleMode";
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
private focusRow(rowId: AdvisorSettingsRow): void {
|
|
372
|
+
this.selectedRow = Math.max(0, this.visibleRows().indexOf(rowId));
|
|
373
|
+
}
|
|
374
|
+
|
|
292
375
|
private currentContext(): ContextPreset {
|
|
293
376
|
const preset = this.options.presets.find(
|
|
294
377
|
(item) => item.value === this.settings.contextMaxChars
|
|
@@ -301,125 +384,167 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
301
384
|
}
|
|
302
385
|
);
|
|
303
386
|
}
|
|
387
|
+
|
|
304
388
|
private currentEffort() {
|
|
305
389
|
return this.settings.effort || "Default (Model Default)";
|
|
306
390
|
}
|
|
307
|
-
|
|
391
|
+
|
|
392
|
+
private row(label: string, value: string, rowId: AdvisorSettingsRow): string {
|
|
308
393
|
const { theme } = this.options;
|
|
309
|
-
const
|
|
394
|
+
const selected = rowId === this.selectedRowId();
|
|
395
|
+
const prefix = selected ? theme.fg("accent", "›") : " ";
|
|
310
396
|
const text = `${prefix} ${label.padEnd(28)} ${value}`;
|
|
311
|
-
|
|
397
|
+
if (
|
|
398
|
+
rowId === "simpleMode" &&
|
|
399
|
+
this.settings.simpleMode &&
|
|
400
|
+
this.simpleModeGradientStartedAt !== undefined
|
|
401
|
+
) {
|
|
402
|
+
return `${prefix} ${this.rainbowGradient(label)}${" ".repeat(Math.max(0, 28 - label.length))} ${value}`;
|
|
403
|
+
}
|
|
404
|
+
return selected
|
|
312
405
|
? theme.fg("accent", theme.bold(text))
|
|
313
406
|
: theme.fg("text", text);
|
|
314
407
|
}
|
|
315
408
|
|
|
316
|
-
private
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
]
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
409
|
+
private rainbowGradient(text: string): string {
|
|
410
|
+
const frame = Math.floor(
|
|
411
|
+
(Date.now() - (this.simpleModeGradientStartedAt ?? 0)) /
|
|
412
|
+
SIMPLE_MODE_GRADIENT_INTERVAL_MS
|
|
413
|
+
);
|
|
414
|
+
const shinePosition = frame % (text.length * 2);
|
|
415
|
+
return [...text]
|
|
416
|
+
.map((character, index) => {
|
|
417
|
+
const [baseRed, baseGreen, baseBlue] =
|
|
418
|
+
SIMPLE_MODE_GRADIENT_COLORS[
|
|
419
|
+
index % SIMPLE_MODE_GRADIENT_COLORS.length
|
|
420
|
+
];
|
|
421
|
+
const distance = Math.abs(index - shinePosition);
|
|
422
|
+
let brightness = 0;
|
|
423
|
+
if (distance === 0) {
|
|
424
|
+
brightness = 0.7;
|
|
425
|
+
} else if (distance === 1) {
|
|
426
|
+
brightness = 0.35;
|
|
427
|
+
}
|
|
428
|
+
const red = Math.round(baseRed + (255 - baseRed) * brightness);
|
|
429
|
+
const green = Math.round(baseGreen + (255 - baseGreen) * brightness);
|
|
430
|
+
const blue = Math.round(baseBlue + (255 - baseBlue) * brightness);
|
|
431
|
+
return `\x1b[38;2;${red};${green};${blue}m${character}`;
|
|
432
|
+
})
|
|
433
|
+
.join("")
|
|
434
|
+
.concat("\x1b[0m");
|
|
327
435
|
}
|
|
328
436
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
);
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
Math.min(
|
|
343
|
-
trackWidth - label.length,
|
|
344
|
-
positions[index] - Math.floor(label.length / 2)
|
|
345
|
-
)
|
|
346
|
-
);
|
|
347
|
-
for (let char = 0; char < label.length; char += 1) {
|
|
348
|
-
labels[start + char] = label[char];
|
|
349
|
-
}
|
|
437
|
+
private startSimpleModeGradient(): void {
|
|
438
|
+
this.stopSimpleModeGradient();
|
|
439
|
+
this.simpleModeGradientStartedAt = Date.now();
|
|
440
|
+
this.simpleModeGradientTimer = setInterval(() => {
|
|
441
|
+
this.options.tui.requestRender();
|
|
442
|
+
}, SIMPLE_MODE_GRADIENT_INTERVAL_MS);
|
|
443
|
+
this.simpleModeGradientTimer.unref?.();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
private stopSimpleModeGradient(): void {
|
|
447
|
+
if (this.simpleModeGradientTimer) {
|
|
448
|
+
clearInterval(this.simpleModeGradientTimer);
|
|
449
|
+
this.simpleModeGradientTimer = undefined;
|
|
350
450
|
}
|
|
351
|
-
|
|
451
|
+
this.simpleModeGradientStartedAt = undefined;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
private save(): void {
|
|
455
|
+
this.stopSimpleModeGradient();
|
|
456
|
+
this.options.onSave({
|
|
457
|
+
...this.settings,
|
|
458
|
+
contextMaxChars: this.currentContext().value,
|
|
459
|
+
effort: this.currentEffort(),
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private cancel(): void {
|
|
464
|
+
this.stopSimpleModeGradient();
|
|
465
|
+
this.options.onCancel();
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
private advancedRows(width: number): string[] {
|
|
352
469
|
const onOff = (value: boolean) => (value ? "On" : "Off");
|
|
353
470
|
const rows = [
|
|
354
|
-
this.row("Context window", this.currentContext().label,
|
|
355
|
-
this.row("Advisor reasoning", this.currentEffort(),
|
|
356
|
-
this.row("Plan gate", onOff(this.settings.planGate),
|
|
357
|
-
this.row("Failure gate", onOff(this.settings.failureGate),
|
|
358
|
-
this.row(
|
|
471
|
+
this.row("Context window", this.currentContext().label, "context"),
|
|
472
|
+
this.row("Advisor reasoning", this.currentEffort(), "effort"),
|
|
473
|
+
this.row("Plan gate", onOff(this.settings.planGate), "planGate"),
|
|
474
|
+
this.row("Failure gate", onOff(this.settings.failureGate), "failureGate"),
|
|
475
|
+
this.row(
|
|
476
|
+
"Completion gate",
|
|
477
|
+
onOff(this.settings.completionGate),
|
|
478
|
+
"completionGate"
|
|
479
|
+
),
|
|
359
480
|
this.row(
|
|
360
481
|
"Collapse long responses",
|
|
361
482
|
onOff(this.settings.collapseResponses),
|
|
362
|
-
|
|
483
|
+
"collapseResponses"
|
|
484
|
+
),
|
|
485
|
+
this.row(
|
|
486
|
+
"Custom invocation",
|
|
487
|
+
this.settings.customRule || "None",
|
|
488
|
+
"customRule"
|
|
363
489
|
),
|
|
364
|
-
this.row("Custom invocation", this.settings.customRule || "None", 6),
|
|
365
490
|
this.row(
|
|
366
491
|
"Block on critical advice",
|
|
367
492
|
onOff(this.settings.blockOnBlocked ?? true),
|
|
368
|
-
|
|
493
|
+
"blockOnBlocked"
|
|
369
494
|
),
|
|
370
495
|
this.row(
|
|
371
496
|
"Automatic loop gate",
|
|
372
497
|
onOff(this.settings.autoLoopGate ?? true),
|
|
373
|
-
|
|
498
|
+
"autoLoopGate"
|
|
374
499
|
),
|
|
375
500
|
this.row(
|
|
376
501
|
"Loop threshold",
|
|
377
502
|
`After ${this.settings.loopThreshold ?? 3} repeats`,
|
|
378
|
-
|
|
503
|
+
"loopThreshold"
|
|
379
504
|
),
|
|
380
505
|
this.row(
|
|
381
506
|
"Max Advisor calls/session",
|
|
382
507
|
this.settings.maxCallsPerSession === undefined
|
|
383
508
|
? "∞"
|
|
384
509
|
: String(this.settings.maxCallsPerSession),
|
|
385
|
-
|
|
510
|
+
"maxCallsPerSession"
|
|
386
511
|
),
|
|
387
512
|
this.row(
|
|
388
513
|
"Session Advisor Summary",
|
|
389
|
-
onOff(this.settings.sessionSummary ??
|
|
390
|
-
|
|
514
|
+
onOff(this.settings.sessionSummary ?? false),
|
|
515
|
+
"sessionSummary"
|
|
391
516
|
),
|
|
392
517
|
this.row(
|
|
393
518
|
"Gate failure mode",
|
|
394
519
|
this.settings.failureMode ?? "block-session",
|
|
395
|
-
|
|
520
|
+
"failureMode"
|
|
396
521
|
),
|
|
397
522
|
this.row(
|
|
398
523
|
"Herdr integration",
|
|
399
524
|
onOff(this.settings.herdrIntegration ?? true),
|
|
400
|
-
|
|
525
|
+
"herdrIntegration"
|
|
401
526
|
),
|
|
402
527
|
this.row(
|
|
403
528
|
"Tool result lines",
|
|
404
529
|
String(this.settings.toolResultMaxLines ?? 2000),
|
|
405
|
-
|
|
530
|
+
"toolResultMaxLines"
|
|
406
531
|
),
|
|
407
532
|
this.row(
|
|
408
533
|
"Tool result bytes",
|
|
409
534
|
String(this.settings.toolResultMaxBytes ?? 50 * 1024),
|
|
410
|
-
|
|
535
|
+
"toolResultMaxBytes"
|
|
411
536
|
),
|
|
412
537
|
this.row(
|
|
413
538
|
"Redact common secrets",
|
|
414
539
|
onOff(this.settings.redactSecrets ?? false),
|
|
415
|
-
|
|
540
|
+
"redactSecrets"
|
|
416
541
|
),
|
|
417
542
|
this.row(
|
|
418
543
|
"Tool disclosure policies",
|
|
419
544
|
Object.keys(this.settings.toolPolicies ?? {}).length
|
|
420
545
|
? "Exact names configured"
|
|
421
546
|
: "All tools: full",
|
|
422
|
-
|
|
547
|
+
"toolPolicies"
|
|
423
548
|
),
|
|
424
549
|
];
|
|
425
550
|
if (this.editingCustom) {
|
|
@@ -427,19 +552,96 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
427
552
|
` ${this.customInput.render(Math.max(10, width - 6))[0] || ""}`
|
|
428
553
|
);
|
|
429
554
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
555
|
+
if (this.editingPolicies) {
|
|
556
|
+
rows.push(
|
|
557
|
+
` ${this.policiesInput.render(Math.max(10, width - 6))[0] || ""}`
|
|
558
|
+
);
|
|
559
|
+
if (this.policiesError) {
|
|
560
|
+
rows.push(` ${this.options.theme.fg("error", this.policiesError)}`);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
rows.push(this.row("Save changes", "", "save"));
|
|
564
|
+
return rows;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
render(width: number): string[] {
|
|
568
|
+
const { theme, presets } = this.options;
|
|
569
|
+
const simpleMode = this.settings.simpleMode ?? false;
|
|
570
|
+
const modeRows = [
|
|
571
|
+
this.row("Simple mode", simpleMode ? "On" : "Off", "simpleMode"),
|
|
572
|
+
this.row("Always on", this.settings.alwaysOn ? "On" : "Off", "alwaysOn"),
|
|
573
|
+
];
|
|
574
|
+
const lines = [theme.fg("accent", theme.bold(" Advisor settings")), ""];
|
|
575
|
+
if (simpleMode) {
|
|
576
|
+
const trackWidth = Math.max(24, Math.min(60, width - 4));
|
|
577
|
+
const positions = presets.map((_, index) =>
|
|
578
|
+
Math.round((index * (trackWidth - 1)) / Math.max(1, presets.length - 1))
|
|
579
|
+
);
|
|
580
|
+
const track = Array.from({ length: trackWidth }, () => "─");
|
|
581
|
+
track[positions[this.contextIndex]] = "▲";
|
|
582
|
+
const labels = Array.from({ length: trackWidth }, () => " ");
|
|
583
|
+
for (let index = 0; index < presets.length; index += 1) {
|
|
584
|
+
const { label } = presets[index];
|
|
585
|
+
const start = Math.max(
|
|
586
|
+
0,
|
|
587
|
+
Math.min(
|
|
588
|
+
trackWidth - label.length,
|
|
589
|
+
positions[index] - Math.floor(label.length / 2)
|
|
590
|
+
)
|
|
591
|
+
);
|
|
592
|
+
for (let char = 0; char < label.length; char += 1) {
|
|
593
|
+
labels[start + char] = label[char];
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
lines.push(
|
|
597
|
+
` ${this.row("Context window", this.currentContext().label, "context")}`
|
|
598
|
+
);
|
|
599
|
+
lines.push(` ${theme.fg("muted", "Recent history")}`);
|
|
600
|
+
lines.push(` ${theme.fg("muted", track.join(""))}`);
|
|
601
|
+
lines.push(` ${theme.fg("text", labels.join(""))}`);
|
|
602
|
+
lines.push("");
|
|
603
|
+
lines.push(...modeRows.map((line) => ` ${line}`));
|
|
604
|
+
lines.push(` ${this.row("Save changes", "", "save")}`);
|
|
605
|
+
lines.push("");
|
|
606
|
+
lines.push(
|
|
607
|
+
` ${theme.fg("muted", "↑/↓ select · ←/→ adjust · Enter saves · Esc cancels")}`
|
|
608
|
+
);
|
|
609
|
+
return lines.map((line) => truncateToWidth(line, width));
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const trackWidth = Math.max(24, Math.min(60, width - 4));
|
|
613
|
+
const positions = presets.map((_, index) =>
|
|
614
|
+
Math.round((index * (trackWidth - 1)) / Math.max(1, presets.length - 1))
|
|
615
|
+
);
|
|
616
|
+
const track = Array.from({ length: trackWidth }, () => "─");
|
|
617
|
+
track[positions[this.contextIndex]] = "▲";
|
|
618
|
+
const labels = Array.from({ length: trackWidth }, () => " ");
|
|
619
|
+
for (let index = 0; index < presets.length; index += 1) {
|
|
620
|
+
const { label } = presets[index];
|
|
621
|
+
const start = Math.max(
|
|
622
|
+
0,
|
|
623
|
+
Math.min(
|
|
624
|
+
trackWidth - label.length,
|
|
625
|
+
positions[index] - Math.floor(label.length / 2)
|
|
626
|
+
)
|
|
627
|
+
);
|
|
628
|
+
for (let char = 0; char < label.length; char += 1) {
|
|
629
|
+
labels[start + char] = label[char];
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
const heading = `Recent history${" ".repeat(Math.max(1, trackWidth - "Recent history".length - "Full branch".length))}Full branch`;
|
|
633
|
+
lines.push(` ${theme.fg("muted", heading)}`);
|
|
634
|
+
lines.push(` ${theme.fg("muted", track.join(""))}`);
|
|
635
|
+
lines.push(` ${theme.fg("text", labels.join(""))}`);
|
|
636
|
+
lines.push("");
|
|
637
|
+
lines.push(
|
|
638
|
+
...[...modeRows, ...this.advancedRows(width)].map((line) => ` ${line}`)
|
|
639
|
+
);
|
|
640
|
+
lines.push("");
|
|
641
|
+
lines.push(
|
|
642
|
+
` ${theme.fg("muted", "↑/↓ select · ←/→ adjust · Enter edits or saves · Esc cancels")}`
|
|
643
|
+
);
|
|
644
|
+
return lines.map((line) => truncateToWidth(line, width));
|
|
443
645
|
}
|
|
444
646
|
|
|
445
647
|
handleInput(keyData: string): void {
|
|
@@ -455,20 +657,24 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
455
657
|
if (matchesKey(keyData, Key.up)) {
|
|
456
658
|
this.selectedRow = Math.max(0, this.selectedRow - 1);
|
|
457
659
|
} else if (matchesKey(keyData, Key.down)) {
|
|
458
|
-
this.selectedRow = Math.min(
|
|
660
|
+
this.selectedRow = Math.min(
|
|
661
|
+
this.visibleRows().length - 1,
|
|
662
|
+
this.selectedRow + 1
|
|
663
|
+
);
|
|
459
664
|
} else if (matchesKey(keyData, Key.left)) {
|
|
460
665
|
this.adjust(-1);
|
|
461
666
|
} else if (matchesKey(keyData, Key.right)) {
|
|
462
667
|
this.adjust(1);
|
|
463
668
|
} else if (matchesKey(keyData, Key.enter)) {
|
|
464
|
-
|
|
669
|
+
const row = this.selectedRowId();
|
|
670
|
+
if (row === "customRule") {
|
|
465
671
|
this.editingCustom = true;
|
|
466
672
|
this.customInput.setValue(this.settings.customRule || "");
|
|
467
673
|
this.customInput.focused = this.focused;
|
|
468
674
|
tui.requestRender();
|
|
469
675
|
return;
|
|
470
676
|
}
|
|
471
|
-
if (
|
|
677
|
+
if (row === "toolPolicies") {
|
|
472
678
|
this.editingPolicies = true;
|
|
473
679
|
this.policiesError = undefined;
|
|
474
680
|
this.policiesInput.setValue(
|
|
@@ -478,17 +684,13 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
478
684
|
tui.requestRender();
|
|
479
685
|
return;
|
|
480
686
|
}
|
|
481
|
-
if (
|
|
482
|
-
this.
|
|
483
|
-
...this.settings,
|
|
484
|
-
contextMaxChars: this.currentContext().value,
|
|
485
|
-
effort: this.currentEffort(),
|
|
486
|
-
});
|
|
687
|
+
if (row === "save") {
|
|
688
|
+
this.save();
|
|
487
689
|
return;
|
|
488
690
|
}
|
|
489
691
|
this.adjust(1);
|
|
490
692
|
} else if (matchesKey(keyData, Key.escape)) {
|
|
491
|
-
this.
|
|
693
|
+
this.cancel();
|
|
492
694
|
return;
|
|
493
695
|
} else {
|
|
494
696
|
return;
|
|
@@ -496,9 +698,23 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
496
698
|
tui.requestRender();
|
|
497
699
|
}
|
|
498
700
|
|
|
499
|
-
private adjust(direction: number) {
|
|
500
|
-
switch (this.
|
|
501
|
-
case
|
|
701
|
+
private adjust(direction: number): void {
|
|
702
|
+
switch (this.selectedRowId()) {
|
|
703
|
+
case "simpleMode":
|
|
704
|
+
this.settings.simpleMode = !(this.settings.simpleMode ?? false);
|
|
705
|
+
if (this.settings.simpleMode) {
|
|
706
|
+
this.startSimpleModeGradient();
|
|
707
|
+
} else {
|
|
708
|
+
this.stopSimpleModeGradient();
|
|
709
|
+
}
|
|
710
|
+
// The visible row set changes with the mode, so keep the cursor on the
|
|
711
|
+
// row the user is actually operating rather than on its old index.
|
|
712
|
+
this.focusRow("simpleMode");
|
|
713
|
+
break;
|
|
714
|
+
case "alwaysOn":
|
|
715
|
+
this.settings.alwaysOn = !(this.settings.alwaysOn ?? false);
|
|
716
|
+
break;
|
|
717
|
+
case "context":
|
|
502
718
|
this.contextIndex = Math.max(
|
|
503
719
|
0,
|
|
504
720
|
Math.min(
|
|
@@ -509,7 +725,7 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
509
725
|
this.settings.contextMaxChars =
|
|
510
726
|
this.options.presets[this.contextIndex].value;
|
|
511
727
|
break;
|
|
512
|
-
case
|
|
728
|
+
case "effort":
|
|
513
729
|
this.effortIndex = Math.max(
|
|
514
730
|
0,
|
|
515
731
|
Math.min(
|
|
@@ -519,31 +735,31 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
519
735
|
);
|
|
520
736
|
this.settings.effort = this.options.effortLevels[this.effortIndex];
|
|
521
737
|
break;
|
|
522
|
-
case
|
|
738
|
+
case "planGate":
|
|
523
739
|
this.settings.planGate = !this.settings.planGate;
|
|
524
740
|
break;
|
|
525
|
-
case
|
|
741
|
+
case "failureGate":
|
|
526
742
|
this.settings.failureGate = !this.settings.failureGate;
|
|
527
743
|
break;
|
|
528
|
-
case
|
|
744
|
+
case "completionGate":
|
|
529
745
|
this.settings.completionGate = !this.settings.completionGate;
|
|
530
746
|
break;
|
|
531
|
-
case
|
|
747
|
+
case "collapseResponses":
|
|
532
748
|
this.settings.collapseResponses = !this.settings.collapseResponses;
|
|
533
749
|
break;
|
|
534
|
-
case
|
|
750
|
+
case "blockOnBlocked":
|
|
535
751
|
this.settings.blockOnBlocked = !(this.settings.blockOnBlocked ?? true);
|
|
536
752
|
break;
|
|
537
|
-
case
|
|
753
|
+
case "autoLoopGate":
|
|
538
754
|
this.settings.autoLoopGate = !(this.settings.autoLoopGate ?? true);
|
|
539
755
|
break;
|
|
540
|
-
case
|
|
756
|
+
case "loopThreshold":
|
|
541
757
|
this.settings.loopThreshold = Math.max(
|
|
542
758
|
2,
|
|
543
759
|
(this.settings.loopThreshold ?? 3) + direction
|
|
544
760
|
);
|
|
545
761
|
break;
|
|
546
|
-
case
|
|
762
|
+
case "maxCallsPerSession": {
|
|
547
763
|
const values = [undefined, 0, 1, 2, 3, 5, 10, 25, 50];
|
|
548
764
|
const index = Math.max(
|
|
549
765
|
0,
|
|
@@ -553,10 +769,10 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
553
769
|
values[Math.max(0, Math.min(values.length - 1, index + direction))];
|
|
554
770
|
break;
|
|
555
771
|
}
|
|
556
|
-
case
|
|
557
|
-
this.settings.sessionSummary = !(this.settings.sessionSummary ??
|
|
772
|
+
case "sessionSummary":
|
|
773
|
+
this.settings.sessionSummary = !(this.settings.sessionSummary ?? false);
|
|
558
774
|
break;
|
|
559
|
-
case
|
|
775
|
+
case "failureMode": {
|
|
560
776
|
const modes: AdvisorSettings["failureMode"][] = [
|
|
561
777
|
"block-session",
|
|
562
778
|
"block-tool",
|
|
@@ -570,15 +786,15 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
570
786
|
modes[Math.max(0, Math.min(modes.length - 1, index + direction))];
|
|
571
787
|
break;
|
|
572
788
|
}
|
|
573
|
-
case
|
|
789
|
+
case "herdrIntegration":
|
|
574
790
|
this.settings.herdrIntegration = !(
|
|
575
791
|
this.settings.herdrIntegration ?? true
|
|
576
792
|
);
|
|
577
793
|
break;
|
|
578
|
-
case
|
|
794
|
+
case "redactSecrets":
|
|
579
795
|
this.settings.redactSecrets = !(this.settings.redactSecrets ?? false);
|
|
580
796
|
break;
|
|
581
|
-
case
|
|
797
|
+
case "toolResultMaxLines": {
|
|
582
798
|
const values = [0, 500, 1000, 2000, 5000, 10_000];
|
|
583
799
|
const index = Math.max(
|
|
584
800
|
0,
|
|
@@ -588,7 +804,7 @@ export class AdvisorSettingsSelector implements Component, Focusable {
|
|
|
588
804
|
values[Math.max(0, Math.min(values.length - 1, index + direction))];
|
|
589
805
|
break;
|
|
590
806
|
}
|
|
591
|
-
case
|
|
807
|
+
case "toolResultMaxBytes": {
|
|
592
808
|
const values = [0, 10 * 1024, 50 * 1024, 100 * 1024, 500 * 1024];
|
|
593
809
|
const index = Math.max(
|
|
594
810
|
0,
|