pi-advisor-flow 0.5.0 → 0.5.2
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 +54 -0
- package/README.md +37 -71
- package/extensions/index.ts +7 -7
- package/package.json +19 -6
- package/src/commands/activation-preparation.ts +117 -0
- package/src/commands/activation.ts +135 -0
- package/src/commands/lifecycle.ts +83 -0
- package/src/commands/manual-command.ts +107 -0
- package/src/commands/manual-consultation.ts +144 -0
- package/src/commands/manual-progress.ts +93 -0
- package/src/commands/model-commands.ts +66 -0
- package/src/commands/model-options.ts +181 -0
- package/src/commands/model-picker.ts +110 -0
- package/src/commands/registration.ts +27 -0
- package/src/commands/renderers.ts +76 -0
- package/src/commands/runtime.ts +114 -0
- package/src/commands/settings-commands.ts +80 -0
- package/src/commands/settings-persistence.ts +81 -0
- package/src/commands/types.ts +78 -0
- package/src/commands.ts +9 -946
- package/src/config/args.ts +37 -0
- package/src/config/defaults.ts +190 -0
- package/src/config/state.ts +199 -0
- package/src/config/storage.ts +224 -0
- package/src/config/types.ts +63 -0
- package/src/config/validation.ts +234 -0
- package/src/config.ts +104 -791
- package/src/conversation.ts +4 -2
- package/src/herdr.ts +1 -1
- package/src/model-stream.ts +25 -2
- package/src/scout-context.ts +2 -2
- package/src/scout.ts +1 -1
- package/src/tools/consultation.ts +337 -0
- package/src/tools/gate-policy.ts +132 -0
- package/src/tools/gate-protocol.ts +127 -0
- package/src/tools/loop-gate.ts +210 -0
- package/src/tools/prompts.ts +159 -0
- package/src/tools/register-ask-advisor.ts +225 -0
- package/src/tools/register-lifecycle.ts +89 -0
- package/src/tools/register-outcome.ts +75 -0
- package/src/tools/register-renderers.ts +88 -0
- package/src/tools/registration.ts +35 -0
- package/src/tools/render-advisor-result.ts +168 -0
- package/src/tools/render-common.ts +120 -0
- package/src/tools/scout-status.ts +206 -0
- package/src/tools/session.ts +3 -0
- package/src/tools/types.ts +119 -0
- package/src/tools.ts +42 -1811
- package/src/ui/manual-dialog-render.ts +150 -0
- package/src/ui/manual-dialog.ts +305 -0
- package/src/ui/model-selector.ts +166 -0
- package/src/ui/settings-formatting.ts +133 -0
- package/src/ui/settings-items.ts +323 -0
- package/src/ui/settings-list-adapter.ts +103 -0
- package/src/ui/settings-mutations.ts +80 -0
- package/src/ui/settings-selector.ts +161 -0
- package/src/ui/text-setting-submenu.ts +66 -0
- package/src/ui/types.ts +111 -0
- package/src/ui.ts +9 -1418
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { ContextPreset } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_EFFORT_LEVEL = "Default (Model Default)";
|
|
5
|
+
export const TOGGLE_VALUES = ["On", "Off"];
|
|
6
|
+
export const SIMPLE_MODE_GRADIENT_INTERVAL_MS = 100;
|
|
7
|
+
// Purple steps with a moving light highlight, retained from the original UI.
|
|
8
|
+
const SIMPLE_MODE_GRADIENT_COLORS = [
|
|
9
|
+
[125, 79, 205],
|
|
10
|
+
[143, 96, 218],
|
|
11
|
+
[160, 114, 230],
|
|
12
|
+
[178, 135, 238],
|
|
13
|
+
[195, 157, 245],
|
|
14
|
+
[168, 120, 230],
|
|
15
|
+
[143, 89, 215],
|
|
16
|
+
] as const;
|
|
17
|
+
|
|
18
|
+
export const withCurrentValue = (current: string, values: string[]) =>
|
|
19
|
+
values.includes(current) ? values : [current, ...values];
|
|
20
|
+
|
|
21
|
+
export const numericValues = (current: number, values: number[]) => {
|
|
22
|
+
const all = values.includes(current) ? values : [...values, current];
|
|
23
|
+
return all.sort((a, b) => a - b).map(String);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const maxCallValues = (current: string) => {
|
|
27
|
+
const values = ["0", "1", "2", "3", "5", "10", "25", "50", "∞"];
|
|
28
|
+
if (values.includes(current)) {
|
|
29
|
+
return values;
|
|
30
|
+
}
|
|
31
|
+
const numeric = Number(current);
|
|
32
|
+
const insertionIndex = values.findIndex(
|
|
33
|
+
(value) => value !== "∞" && Number(value) > numeric
|
|
34
|
+
);
|
|
35
|
+
return insertionIndex === -1
|
|
36
|
+
? [...values.slice(0, -1), current, "∞"]
|
|
37
|
+
: [
|
|
38
|
+
...values.slice(0, insertionIndex),
|
|
39
|
+
current,
|
|
40
|
+
...values.slice(insertionIndex),
|
|
41
|
+
];
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const settingValue = (
|
|
45
|
+
value: boolean | undefined,
|
|
46
|
+
defaultValue: boolean
|
|
47
|
+
) => ((value ?? defaultValue) ? "On" : "Off");
|
|
48
|
+
|
|
49
|
+
export const currentContextLabel = (
|
|
50
|
+
presets: ContextPreset[],
|
|
51
|
+
contextMaxChars: number
|
|
52
|
+
): string =>
|
|
53
|
+
presets.find((preset) => preset.value === contextMaxChars)?.label ??
|
|
54
|
+
String(contextMaxChars);
|
|
55
|
+
|
|
56
|
+
export const contextDescription = (
|
|
57
|
+
presets: ContextPreset[],
|
|
58
|
+
contextMaxChars: number
|
|
59
|
+
): string => {
|
|
60
|
+
const exactIndex = presets.findIndex(
|
|
61
|
+
(preset) => preset.value === contextMaxChars
|
|
62
|
+
);
|
|
63
|
+
const selectedIndex =
|
|
64
|
+
exactIndex >= 0
|
|
65
|
+
? exactIndex
|
|
66
|
+
: presets.reduce(
|
|
67
|
+
(closestIndex, preset, index) =>
|
|
68
|
+
Math.abs(preset.value - contextMaxChars) <
|
|
69
|
+
Math.abs(presets[closestIndex].value - contextMaxChars)
|
|
70
|
+
? index
|
|
71
|
+
: closestIndex,
|
|
72
|
+
0
|
|
73
|
+
);
|
|
74
|
+
const selectedPreset = presets[selectedIndex];
|
|
75
|
+
const isFullContext =
|
|
76
|
+
selectedPreset?.value === Number.MAX_SAFE_INTEGER ||
|
|
77
|
+
selectedPreset?.label.toUpperCase() === "FULL" ||
|
|
78
|
+
selectedPreset?.label.toUpperCase() === "ALL";
|
|
79
|
+
const progress = isFullContext
|
|
80
|
+
? 1
|
|
81
|
+
: selectedIndex / Math.max(1, presets.length - 1);
|
|
82
|
+
const meterWidth = 20;
|
|
83
|
+
const marker = Math.round(progress * meterWidth);
|
|
84
|
+
const meter = Array.from({ length: meterWidth + 1 }, (_, index) => {
|
|
85
|
+
if (index === marker) {
|
|
86
|
+
return "●";
|
|
87
|
+
}
|
|
88
|
+
return index < marker ? "━" : "─";
|
|
89
|
+
}).join("");
|
|
90
|
+
const label = currentContextLabel(presets, contextMaxChars);
|
|
91
|
+
const labelWidth = visibleWidth(label);
|
|
92
|
+
const meterPrefix = "none ";
|
|
93
|
+
const markerColumn = meterPrefix.length + marker;
|
|
94
|
+
const labelStart = Math.max(
|
|
95
|
+
0,
|
|
96
|
+
Math.min(
|
|
97
|
+
meterPrefix.length + meter.length + 2 - labelWidth,
|
|
98
|
+
markerColumn - Math.floor((labelWidth - 1) / 2)
|
|
99
|
+
)
|
|
100
|
+
);
|
|
101
|
+
const markerLabel = `${" ".repeat(labelStart)}${label}`;
|
|
102
|
+
const description =
|
|
103
|
+
exactIndex >= 0 ? selectedPreset?.description : "Custom context limit.";
|
|
104
|
+
return `${description ?? "Custom context limit."}\n${meterPrefix}${meter} full\n${markerLabel}`;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const currentEffort = (effort: string | undefined): string =>
|
|
108
|
+
effort || DEFAULT_EFFORT_LEVEL;
|
|
109
|
+
|
|
110
|
+
export const rainbowGradient = (text: string, startedAt: number): string => {
|
|
111
|
+
const frame = Math.floor(
|
|
112
|
+
(Date.now() - startedAt) / SIMPLE_MODE_GRADIENT_INTERVAL_MS
|
|
113
|
+
);
|
|
114
|
+
const shinePosition = frame % (text.length * 2);
|
|
115
|
+
return [...text]
|
|
116
|
+
.map((character, index) => {
|
|
117
|
+
const [baseRed, baseGreen, baseBlue] =
|
|
118
|
+
SIMPLE_MODE_GRADIENT_COLORS[index % SIMPLE_MODE_GRADIENT_COLORS.length];
|
|
119
|
+
const distance = Math.abs(index - shinePosition);
|
|
120
|
+
let brightness = 0;
|
|
121
|
+
if (distance === 0) {
|
|
122
|
+
brightness = 0.7;
|
|
123
|
+
} else if (distance === 1) {
|
|
124
|
+
brightness = 0.35;
|
|
125
|
+
}
|
|
126
|
+
const red = Math.round(baseRed + (255 - baseRed) * brightness);
|
|
127
|
+
const green = Math.round(baseGreen + (255 - baseGreen) * brightness);
|
|
128
|
+
const blue = Math.round(baseBlue + (255 - baseBlue) * brightness);
|
|
129
|
+
return `\x1b[38;2;${red};${green};${blue}m${character}`;
|
|
130
|
+
})
|
|
131
|
+
.join("")
|
|
132
|
+
.concat("\x1b[0m");
|
|
133
|
+
};
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { SettingItem } from "@earendil-works/pi-tui";
|
|
3
|
+
import { isValidAdvisorToolPolicies } from "../config/validation.js";
|
|
4
|
+
import {
|
|
5
|
+
contextDescription,
|
|
6
|
+
currentContextLabel,
|
|
7
|
+
currentEffort,
|
|
8
|
+
maxCallValues,
|
|
9
|
+
numericValues,
|
|
10
|
+
settingValue,
|
|
11
|
+
TOGGLE_VALUES,
|
|
12
|
+
withCurrentValue,
|
|
13
|
+
} from "./settings-formatting.js";
|
|
14
|
+
import { TextSettingSubmenu } from "./text-setting-submenu.js";
|
|
15
|
+
import type {
|
|
16
|
+
AdvisorSettings,
|
|
17
|
+
ContextPreset,
|
|
18
|
+
RenderRequester,
|
|
19
|
+
} from "./types.js";
|
|
20
|
+
|
|
21
|
+
export interface SettingsItemsOptions {
|
|
22
|
+
effortLevels: string[];
|
|
23
|
+
presets: ContextPreset[];
|
|
24
|
+
settings: AdvisorSettings;
|
|
25
|
+
theme: Theme;
|
|
26
|
+
tui: RenderRequester;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const toggle = (
|
|
30
|
+
id: string,
|
|
31
|
+
label: string,
|
|
32
|
+
description: string,
|
|
33
|
+
value: boolean | undefined,
|
|
34
|
+
defaultValue: boolean
|
|
35
|
+
): SettingItem => ({
|
|
36
|
+
currentValue: settingValue(value, defaultValue),
|
|
37
|
+
description,
|
|
38
|
+
id,
|
|
39
|
+
label,
|
|
40
|
+
values: TOGGLE_VALUES,
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export const createSettingsItems = ({
|
|
44
|
+
effortLevels,
|
|
45
|
+
presets,
|
|
46
|
+
settings,
|
|
47
|
+
theme,
|
|
48
|
+
tui,
|
|
49
|
+
}: SettingsItemsOptions): SettingItem[] => {
|
|
50
|
+
const items: SettingItem[] = [
|
|
51
|
+
{
|
|
52
|
+
currentValue: currentContextLabel(presets, settings.contextMaxChars),
|
|
53
|
+
description: contextDescription(presets, settings.contextMaxChars),
|
|
54
|
+
id: "context",
|
|
55
|
+
label: "Context window",
|
|
56
|
+
values: presets.map((preset) => preset.label),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
currentValue: settingValue(settings.simpleMode, false),
|
|
60
|
+
description:
|
|
61
|
+
"Keep the Advisor available on demand without automatic gates or blocks.",
|
|
62
|
+
id: "simpleMode",
|
|
63
|
+
label: "Simple mode",
|
|
64
|
+
values: TOGGLE_VALUES,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
currentValue: settingValue(settings.alwaysOn, false),
|
|
68
|
+
description:
|
|
69
|
+
"Run the Advisor flow automatically for supported agent turns.",
|
|
70
|
+
id: "alwaysOn",
|
|
71
|
+
label: "Always on",
|
|
72
|
+
values: TOGGLE_VALUES,
|
|
73
|
+
},
|
|
74
|
+
];
|
|
75
|
+
if (settings.simpleMode) {
|
|
76
|
+
return items;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
items.push(
|
|
80
|
+
{
|
|
81
|
+
currentValue: currentEffort(settings.effort),
|
|
82
|
+
description: "Reasoning level used for Advisor calls.",
|
|
83
|
+
id: "effort",
|
|
84
|
+
label: "Advisor reasoning",
|
|
85
|
+
values: withCurrentValue(currentEffort(settings.effort), effortLevels),
|
|
86
|
+
},
|
|
87
|
+
toggle(
|
|
88
|
+
"scoutEnabled",
|
|
89
|
+
"Experimental Advisor Scout",
|
|
90
|
+
"Enable the experimental Scout before Advisor calls.",
|
|
91
|
+
settings.scoutEnabled,
|
|
92
|
+
false
|
|
93
|
+
),
|
|
94
|
+
toggle(
|
|
95
|
+
"showUsageDetails",
|
|
96
|
+
"Show usage and cost details",
|
|
97
|
+
"Show token usage and cost details in Advisor responses.",
|
|
98
|
+
settings.showUsageDetails,
|
|
99
|
+
true
|
|
100
|
+
),
|
|
101
|
+
toggle(
|
|
102
|
+
"showUsageFooter",
|
|
103
|
+
"Show usage in footer",
|
|
104
|
+
"Show the current Advisor usage summary in the footer.",
|
|
105
|
+
settings.showUsageFooter,
|
|
106
|
+
false
|
|
107
|
+
),
|
|
108
|
+
toggle(
|
|
109
|
+
"planGate",
|
|
110
|
+
"Plan gate",
|
|
111
|
+
"Ask the Advisor to review implementation plans.",
|
|
112
|
+
settings.planGate,
|
|
113
|
+
true
|
|
114
|
+
),
|
|
115
|
+
toggle(
|
|
116
|
+
"failureGate",
|
|
117
|
+
"Failure gate",
|
|
118
|
+
"Ask the Advisor to review repeated failures.",
|
|
119
|
+
settings.failureGate,
|
|
120
|
+
true
|
|
121
|
+
),
|
|
122
|
+
toggle(
|
|
123
|
+
"completionGate",
|
|
124
|
+
"Completion gate",
|
|
125
|
+
"Ask the Advisor to review work before declaring success.",
|
|
126
|
+
settings.completionGate,
|
|
127
|
+
true
|
|
128
|
+
),
|
|
129
|
+
toggle(
|
|
130
|
+
"collapseResponses",
|
|
131
|
+
"Collapse long responses",
|
|
132
|
+
"Collapse long Advisor responses in the transcript.",
|
|
133
|
+
settings.collapseResponses,
|
|
134
|
+
false
|
|
135
|
+
),
|
|
136
|
+
{
|
|
137
|
+
currentValue: settings.customRule || "None",
|
|
138
|
+
description: "Add a rule that triggers Advisor involvement.",
|
|
139
|
+
id: "customRule",
|
|
140
|
+
label: "Custom invocation",
|
|
141
|
+
submenu: (_currentValue, done) =>
|
|
142
|
+
new TextSettingSubmenu({
|
|
143
|
+
description: "Enter a custom invocation rule.",
|
|
144
|
+
initial: settings.customRule || "",
|
|
145
|
+
onCancel: done,
|
|
146
|
+
onSubmit: (value) => ({ value: value.trim() }),
|
|
147
|
+
theme,
|
|
148
|
+
title: "Custom invocation",
|
|
149
|
+
tui,
|
|
150
|
+
}),
|
|
151
|
+
},
|
|
152
|
+
toggle(
|
|
153
|
+
"blockOnBlocked",
|
|
154
|
+
"Block on critical advice",
|
|
155
|
+
"Block the agent when the Advisor returns a critical decision.",
|
|
156
|
+
settings.blockOnBlocked,
|
|
157
|
+
true
|
|
158
|
+
),
|
|
159
|
+
toggle(
|
|
160
|
+
"autoLoopGate",
|
|
161
|
+
"Automatic loop gate",
|
|
162
|
+
"Ask the Advisor to review repeated equivalent attempts.",
|
|
163
|
+
settings.autoLoopGate,
|
|
164
|
+
true
|
|
165
|
+
),
|
|
166
|
+
{
|
|
167
|
+
currentValue: `After ${settings.loopThreshold ?? 3} repeats`,
|
|
168
|
+
description: "Number of equivalent attempts before automatic review.",
|
|
169
|
+
id: "loopThreshold",
|
|
170
|
+
label: "Loop threshold",
|
|
171
|
+
values: numericValues(
|
|
172
|
+
settings.loopThreshold ?? 3,
|
|
173
|
+
Array.from({ length: 99 }, (_, index) => index + 2)
|
|
174
|
+
).map((value) => `After ${value} repeats`),
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
currentValue:
|
|
178
|
+
settings.maxCallsPerSession === undefined
|
|
179
|
+
? "∞"
|
|
180
|
+
: String(settings.maxCallsPerSession),
|
|
181
|
+
description: "Limit automatic Advisor calls in one session.",
|
|
182
|
+
id: "maxCallsPerSession",
|
|
183
|
+
label: "Max Advisor calls/session",
|
|
184
|
+
values: maxCallValues(
|
|
185
|
+
settings.maxCallsPerSession === undefined
|
|
186
|
+
? "∞"
|
|
187
|
+
: String(settings.maxCallsPerSession)
|
|
188
|
+
),
|
|
189
|
+
},
|
|
190
|
+
toggle(
|
|
191
|
+
"sessionSummary",
|
|
192
|
+
"Session Advisor Summary",
|
|
193
|
+
"Show a local summary when the session ends.",
|
|
194
|
+
settings.sessionSummary,
|
|
195
|
+
false
|
|
196
|
+
),
|
|
197
|
+
{
|
|
198
|
+
currentValue: settings.failureMode ?? "block-session",
|
|
199
|
+
description: "Choose what happens when an Advisor gate fails.",
|
|
200
|
+
id: "failureMode",
|
|
201
|
+
label: "Gate failure mode",
|
|
202
|
+
values: withCurrentValue(settings.failureMode ?? "block-session", [
|
|
203
|
+
"block-session",
|
|
204
|
+
"block-tool",
|
|
205
|
+
"warn-and-continue",
|
|
206
|
+
]),
|
|
207
|
+
},
|
|
208
|
+
toggle(
|
|
209
|
+
"herdrIntegration",
|
|
210
|
+
"Herdr integration",
|
|
211
|
+
"Send Advisor activity to the optional Herdr integration.",
|
|
212
|
+
settings.herdrIntegration,
|
|
213
|
+
true
|
|
214
|
+
),
|
|
215
|
+
{
|
|
216
|
+
currentValue: String(settings.toolResultMaxLines ?? 2000),
|
|
217
|
+
description: "Maximum lines included from a tool result.",
|
|
218
|
+
id: "toolResultMaxLines",
|
|
219
|
+
label: "Tool result lines",
|
|
220
|
+
values: numericValues(
|
|
221
|
+
settings.toolResultMaxLines ?? 2000,
|
|
222
|
+
[0, 500, 1000, 2000, 5000, 10_000]
|
|
223
|
+
),
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
currentValue: String(settings.toolResultMaxBytes ?? 50 * 1024),
|
|
227
|
+
description: "Maximum bytes included from a tool result.",
|
|
228
|
+
id: "toolResultMaxBytes",
|
|
229
|
+
label: "Tool result bytes",
|
|
230
|
+
values: numericValues(settings.toolResultMaxBytes ?? 50 * 1024, [
|
|
231
|
+
0,
|
|
232
|
+
10 * 1024,
|
|
233
|
+
50 * 1024,
|
|
234
|
+
100 * 1024,
|
|
235
|
+
500 * 1024,
|
|
236
|
+
]),
|
|
237
|
+
},
|
|
238
|
+
toggle(
|
|
239
|
+
"redactSecrets",
|
|
240
|
+
"Redact common secrets",
|
|
241
|
+
"Redact common credential patterns before Advisor calls.",
|
|
242
|
+
settings.redactSecrets,
|
|
243
|
+
false
|
|
244
|
+
),
|
|
245
|
+
{
|
|
246
|
+
currentValue: settings.gitContext ?? "summary",
|
|
247
|
+
description: "How much repository context is shared with the Advisor.",
|
|
248
|
+
id: "gitContext",
|
|
249
|
+
label: "Repository context",
|
|
250
|
+
values: withCurrentValue(settings.gitContext ?? "summary", [
|
|
251
|
+
"off",
|
|
252
|
+
"summary",
|
|
253
|
+
"full",
|
|
254
|
+
]),
|
|
255
|
+
},
|
|
256
|
+
{
|
|
257
|
+
currentValue: String(settings.gitContextMaxChars ?? 20_000),
|
|
258
|
+
description: "Maximum repository context characters included.",
|
|
259
|
+
id: "gitContextMaxChars",
|
|
260
|
+
label: "Repository context chars",
|
|
261
|
+
values: numericValues(
|
|
262
|
+
settings.gitContextMaxChars ?? 20_000,
|
|
263
|
+
[0, 5000, 10_000, 20_000, 50_000, 100_000]
|
|
264
|
+
),
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
currentValue: Object.keys(settings.toolPolicies ?? {}).length
|
|
268
|
+
? "Configured"
|
|
269
|
+
: "All tools: full",
|
|
270
|
+
description:
|
|
271
|
+
"Choose which tools are shared in full, summarized, or excluded.",
|
|
272
|
+
id: "toolPolicies",
|
|
273
|
+
label: "Tool disclosure policies",
|
|
274
|
+
submenu: (_currentValue, done) =>
|
|
275
|
+
new TextSettingSubmenu({
|
|
276
|
+
description:
|
|
277
|
+
'Enter a JSON object with "full", "summary", or "exclude" values.',
|
|
278
|
+
initial: JSON.stringify(settings.toolPolicies ?? {}),
|
|
279
|
+
onCancel: done,
|
|
280
|
+
onSubmit: (value) => {
|
|
281
|
+
let parsed: unknown;
|
|
282
|
+
try {
|
|
283
|
+
parsed = JSON.parse(value || "{}");
|
|
284
|
+
} catch {
|
|
285
|
+
return { error: "Enter a valid JSON object." };
|
|
286
|
+
}
|
|
287
|
+
if (!isValidAdvisorToolPolicies(parsed)) {
|
|
288
|
+
return {
|
|
289
|
+
error:
|
|
290
|
+
"Use non-empty tool names with full, summary, or exclude values.",
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return { value: JSON.stringify(parsed) };
|
|
294
|
+
},
|
|
295
|
+
theme,
|
|
296
|
+
title: "Tool disclosure policies",
|
|
297
|
+
tui,
|
|
298
|
+
}),
|
|
299
|
+
},
|
|
300
|
+
toggle(
|
|
301
|
+
"trackedFileContent",
|
|
302
|
+
"Tracked file content",
|
|
303
|
+
"Allow tracked file contents to be sent with Advisor context.",
|
|
304
|
+
settings.trackedFileContent,
|
|
305
|
+
false
|
|
306
|
+
),
|
|
307
|
+
toggle(
|
|
308
|
+
"untrackedContent",
|
|
309
|
+
"Untracked file content",
|
|
310
|
+
"Allow untracked file contents to be sent with Advisor context.",
|
|
311
|
+
settings.untrackedContent,
|
|
312
|
+
false
|
|
313
|
+
),
|
|
314
|
+
toggle(
|
|
315
|
+
"outcomeLogging",
|
|
316
|
+
"Outcome logging (global)",
|
|
317
|
+
"Allow anonymized Advisor outcomes to be logged globally.",
|
|
318
|
+
settings.outcomeLogging,
|
|
319
|
+
false
|
|
320
|
+
)
|
|
321
|
+
);
|
|
322
|
+
return items;
|
|
323
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Component,
|
|
3
|
+
type Focusable,
|
|
4
|
+
type Input,
|
|
5
|
+
Key,
|
|
6
|
+
matchesKey,
|
|
7
|
+
type SettingItem,
|
|
8
|
+
type SettingsList,
|
|
9
|
+
} from "@earendil-works/pi-tui";
|
|
10
|
+
|
|
11
|
+
interface SettingsListPrivateFields {
|
|
12
|
+
filteredItems: SettingItem[];
|
|
13
|
+
searchInput?: Input;
|
|
14
|
+
selectedIndex: number;
|
|
15
|
+
submenuComponent?: Component | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Contains the narrow compatibility access needed for pi-tui's SettingsList.
|
|
20
|
+
* No other UI module should depend on its private fields.
|
|
21
|
+
*/
|
|
22
|
+
export class SettingsListAdapter {
|
|
23
|
+
private readonly list: SettingsList;
|
|
24
|
+
|
|
25
|
+
constructor(list: SettingsList) {
|
|
26
|
+
this.list = list;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
invalidate(): void {
|
|
30
|
+
this.list.invalidate();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
render(width: number): string[] {
|
|
34
|
+
return this.list.render(width);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
handleInput(keyData: string): void {
|
|
38
|
+
this.list.handleInput(keyData);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Compatibility-only view for existing component integration seams. */
|
|
42
|
+
get submenuComponent(): Component | null | undefined {
|
|
43
|
+
return this.privateFields().submenuComponent;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
setFocused(value: boolean): void {
|
|
47
|
+
const fields = this.privateFields();
|
|
48
|
+
const submenu = fields.submenuComponent as Focusable | undefined;
|
|
49
|
+
if (submenu) {
|
|
50
|
+
submenu.focused = value;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
setSelectedId(items: SettingItem[], selectedId: string): void {
|
|
55
|
+
const selectedIndex = items.findIndex((item) => item.id === selectedId);
|
|
56
|
+
if (selectedIndex >= 0) {
|
|
57
|
+
this.privateFields().selectedIndex = selectedIndex;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
changeWithArrow(
|
|
62
|
+
keyData: string,
|
|
63
|
+
onChange: (id: string, value: string) => void
|
|
64
|
+
): boolean {
|
|
65
|
+
let direction = 0;
|
|
66
|
+
if (matchesKey(keyData, Key.left) || keyData === "\u001b[D") {
|
|
67
|
+
direction = -1;
|
|
68
|
+
} else if (matchesKey(keyData, Key.right) || keyData === "\u001b[C") {
|
|
69
|
+
direction = 1;
|
|
70
|
+
}
|
|
71
|
+
if (direction === 0) {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const fields = this.privateFields();
|
|
76
|
+
if (fields.submenuComponent || fields.searchInput?.getValue()) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
const item = fields.filteredItems[fields.selectedIndex];
|
|
80
|
+
if (!item?.values?.length) {
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
const currentIndex = item.values.indexOf(item.currentValue);
|
|
84
|
+
let nextIndex: number;
|
|
85
|
+
if (currentIndex === -1) {
|
|
86
|
+
nextIndex = direction > 0 ? 0 : item.values.length - 1;
|
|
87
|
+
} else {
|
|
88
|
+
nextIndex =
|
|
89
|
+
(currentIndex + direction + item.values.length) % item.values.length;
|
|
90
|
+
}
|
|
91
|
+
const nextValue = item.values[nextIndex];
|
|
92
|
+
if (nextValue === undefined) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
95
|
+
item.currentValue = nextValue;
|
|
96
|
+
onChange(item.id, nextValue);
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
private privateFields(): SettingsListPrivateFields {
|
|
101
|
+
return this.list as unknown as SettingsListPrivateFields;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { AdvisorSettings, ContextPreset, SettingValue } from "./types.js";
|
|
2
|
+
|
|
3
|
+
export const BOOLEAN_SETTING_IDS = new Set([
|
|
4
|
+
"scoutEnabled",
|
|
5
|
+
"showUsageDetails",
|
|
6
|
+
"showUsageFooter",
|
|
7
|
+
"planGate",
|
|
8
|
+
"failureGate",
|
|
9
|
+
"completionGate",
|
|
10
|
+
"collapseResponses",
|
|
11
|
+
"blockOnBlocked",
|
|
12
|
+
"autoLoopGate",
|
|
13
|
+
"sessionSummary",
|
|
14
|
+
"herdrIntegration",
|
|
15
|
+
"redactSecrets",
|
|
16
|
+
"trackedFileContent",
|
|
17
|
+
"untrackedContent",
|
|
18
|
+
"outcomeLogging",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export const mutateAdvisorSettings = (
|
|
22
|
+
settings: AdvisorSettings,
|
|
23
|
+
id: string,
|
|
24
|
+
value: string,
|
|
25
|
+
presets: ContextPreset[]
|
|
26
|
+
): void => {
|
|
27
|
+
switch (id) {
|
|
28
|
+
case "context":
|
|
29
|
+
settings.contextMaxChars =
|
|
30
|
+
presets.find((preset) => preset.label === value)?.value ??
|
|
31
|
+
settings.contextMaxChars;
|
|
32
|
+
break;
|
|
33
|
+
case "simpleMode":
|
|
34
|
+
settings.simpleMode = value === "On";
|
|
35
|
+
break;
|
|
36
|
+
case "alwaysOn":
|
|
37
|
+
settings.alwaysOn = value === "On";
|
|
38
|
+
break;
|
|
39
|
+
case "effort":
|
|
40
|
+
settings.effort = value;
|
|
41
|
+
break;
|
|
42
|
+
case "customRule":
|
|
43
|
+
settings.customRule = value.trim() || undefined;
|
|
44
|
+
break;
|
|
45
|
+
case "toolPolicies":
|
|
46
|
+
settings.toolPolicies = JSON.parse(
|
|
47
|
+
value
|
|
48
|
+
) as AdvisorSettings["toolPolicies"];
|
|
49
|
+
break;
|
|
50
|
+
case "loopThreshold":
|
|
51
|
+
settings.loopThreshold = Number(
|
|
52
|
+
value.replace("After ", "").replace(" repeats", "")
|
|
53
|
+
);
|
|
54
|
+
break;
|
|
55
|
+
case "maxCallsPerSession":
|
|
56
|
+
settings.maxCallsPerSession = value === "∞" ? undefined : Number(value);
|
|
57
|
+
break;
|
|
58
|
+
case "failureMode":
|
|
59
|
+
settings.failureMode = value as AdvisorSettings["failureMode"];
|
|
60
|
+
break;
|
|
61
|
+
case "gitContext":
|
|
62
|
+
settings.gitContext = value as AdvisorSettings["gitContext"];
|
|
63
|
+
break;
|
|
64
|
+
case "toolResultMaxLines":
|
|
65
|
+
settings.toolResultMaxLines = Number(value);
|
|
66
|
+
break;
|
|
67
|
+
case "toolResultMaxBytes":
|
|
68
|
+
settings.toolResultMaxBytes = Number(value);
|
|
69
|
+
break;
|
|
70
|
+
case "gitContextMaxChars":
|
|
71
|
+
settings.gitContextMaxChars = Number(value);
|
|
72
|
+
break;
|
|
73
|
+
default:
|
|
74
|
+
if (BOOLEAN_SETTING_IDS.has(id)) {
|
|
75
|
+
(settings as unknown as Record<string, SettingValue | boolean>)[id] =
|
|
76
|
+
value === "On";
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
};
|