pi-advisor-flow 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -18
- package/README.md +9 -8
- package/dist/index.js +1390 -1068
- package/extensions/index.ts +2 -1
- package/package.json +48 -35
- package/src/attachments.ts +11 -10
- package/src/commands/activation-preparation.ts +1 -0
- package/src/commands/activation.ts +2 -0
- package/src/commands/lifecycle.ts +1 -0
- package/src/commands/manual-command.ts +7 -1
- package/src/commands/manual-consultation.ts +102 -92
- package/src/commands/manual-progress.ts +7 -5
- package/src/commands/model-commands.ts +1 -1
- package/src/commands/model-options.ts +14 -3
- package/src/commands/model-picker.ts +1 -0
- package/src/commands/registration.ts +1 -0
- package/src/commands/renderers.ts +67 -55
- package/src/commands/runtime.ts +16 -14
- package/src/commands/settings-commands.ts +10 -4
- package/src/commands/settings-persistence.ts +16 -2
- package/src/commands/types.ts +1 -0
- package/src/commands.ts +0 -1
- package/src/config/args.ts +1 -1
- package/src/config/defaults.ts +8 -1
- package/src/config/schema.ts +42 -17
- package/src/config/state.ts +12 -3
- package/src/config/storage.ts +34 -23
- package/src/config/types.ts +16 -3
- package/src/config/validation.ts +33 -21
- package/src/config.ts +3 -0
- package/src/content-utils.ts +24 -4
- package/src/conversation.ts +16 -15
- package/src/git.ts +10 -3
- package/src/herdr-block.ts +83 -0
- package/src/herdr-shared.ts +108 -0
- package/src/herdr.ts +30 -192
- package/src/jev/client.ts +75 -85
- package/src/jev/failure.ts +19 -0
- package/src/jev/key-store.ts +26 -11
- package/src/jev/ledger.ts +58 -64
- package/src/jev/questions.ts +26 -23
- package/src/jev/state.ts +15 -12
- package/src/jev/transport.ts +8 -3
- package/src/model-stream.ts +35 -37
- package/src/outcomes.ts +15 -11
- package/src/pi-settings.ts +4 -9
- package/src/preferences.ts +4 -2
- package/src/redaction.ts +10 -12
- package/src/scout-context.ts +59 -60
- package/src/scout-curation.ts +4 -13
- package/src/scout-groups.ts +178 -175
- package/src/scout-protocol.ts +74 -70
- package/src/scout-reconstruct.ts +1 -1
- package/src/scout.ts +115 -73
- package/src/session-state.ts +64 -49
- package/src/tool-result-cap.ts +18 -17
- package/src/tools/consult-context.ts +7 -15
- package/src/tools/consultation.ts +14 -4
- package/src/tools/gate-policy.ts +1 -0
- package/src/tools/gate-protocol.ts +36 -23
- package/src/tools/jev-filter.ts +43 -41
- package/src/tools/jev-turn-gate.ts +29 -9
- package/src/tools/loop-gate.ts +31 -20
- package/src/tools/model-access.ts +46 -0
- package/src/tools/prompts.ts +20 -25
- package/src/tools/register-ask-advisor.ts +36 -16
- package/src/tools/register-lifecycle.ts +22 -0
- package/src/tools/register-outcome.ts +2 -0
- package/src/tools/register-renderers.ts +146 -128
- package/src/tools/registration.ts +12 -14
- package/src/tools/render-advisor-result.ts +35 -22
- package/src/tools/render-common.ts +16 -19
- package/src/tools/scout-status.ts +42 -27
- package/src/tools/types.ts +1 -0
- package/src/tools.ts +0 -1
- package/src/ui/jev-setup-submenu.ts +65 -59
- package/src/ui/manual-dialog-render.ts +1 -0
- package/src/ui/manual-dialog.ts +39 -43
- package/src/ui/masked-input.ts +5 -11
- package/src/ui/model-multi-selector.ts +9 -0
- package/src/ui/model-selector-adapter.ts +31 -0
- package/src/ui/model-selector.ts +14 -161
- package/src/ui/searchable-model-list.ts +230 -0
- package/src/ui/settings-formatting.ts +24 -14
- package/src/ui/settings-items.ts +38 -1
- package/src/ui/settings-list-adapter.ts +15 -12
- package/src/ui/settings-mutations.ts +157 -70
- package/src/ui/settings-selector.ts +18 -9
- package/src/ui/text-setting-submenu.ts +3 -6
- package/src/ui/types.ts +14 -0
- package/src/ui.ts +0 -1
- package/src/usage.ts +26 -19
package/src/ui/model-selector.ts
CHANGED
|
@@ -1,167 +1,20 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
type Component,
|
|
4
|
-
type Focusable,
|
|
5
|
-
fuzzyFilter,
|
|
6
|
-
Input,
|
|
7
|
-
type Keybindings,
|
|
8
|
-
type KeybindingsManager,
|
|
9
|
-
truncateToWidth,
|
|
10
|
-
} from "@earendil-works/pi-tui";
|
|
1
|
+
import { ModelSelectorAdapter } from "./model-selector-adapter.ts";
|
|
2
|
+
import { SearchableModelList } from "./searchable-model-list.ts";
|
|
11
3
|
import type { SearchableModelSelectorOptions } from "./types.ts";
|
|
12
4
|
|
|
13
|
-
export class SearchableModelSelector
|
|
14
|
-
private readonly tui: SearchableModelSelectorOptions["tui"];
|
|
15
|
-
private readonly searchInput: Input;
|
|
16
|
-
private readonly allOptions: string[];
|
|
17
|
-
private readonly currentOption: string | undefined;
|
|
18
|
-
private filteredOptions: string[];
|
|
19
|
-
private selectedIndex = 0;
|
|
20
|
-
private readonly title: string;
|
|
21
|
-
private readonly onSelect: (value: string) => void;
|
|
22
|
-
private readonly onCancel: () => void;
|
|
23
|
-
private readonly theme: Theme;
|
|
24
|
-
private readonly keybindings: KeybindingsManager;
|
|
25
|
-
private _focused = false;
|
|
26
|
-
|
|
27
|
-
get focused(): boolean {
|
|
28
|
-
return this._focused;
|
|
29
|
-
}
|
|
30
|
-
set focused(val: boolean) {
|
|
31
|
-
this._focused = val;
|
|
32
|
-
this.searchInput.focused = val;
|
|
33
|
-
}
|
|
34
|
-
|
|
5
|
+
export class SearchableModelSelector extends ModelSelectorAdapter {
|
|
35
6
|
constructor(options: SearchableModelSelectorOptions) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
]
|
|
48
|
-
: options.allOptions;
|
|
49
|
-
this.theme = options.theme;
|
|
50
|
-
this.keybindings = options.keybindings;
|
|
51
|
-
this.onSelect = options.onSelect;
|
|
52
|
-
this.onCancel = options.onCancel;
|
|
53
|
-
this.searchInput = new Input();
|
|
54
|
-
this.filteredOptions = this.allOptions;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
invalidate(): void {
|
|
58
|
-
this.searchInput.invalidate();
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
render(width: number): string[] {
|
|
62
|
-
const lines: string[] = [this.theme.fg("border", "─".repeat(width))];
|
|
63
|
-
lines.push(` ${this.theme.fg("accent", this.theme.bold(this.title))}`);
|
|
64
|
-
const inputLines = this.searchInput.render(Math.max(1, width - 10));
|
|
65
|
-
lines.push(
|
|
66
|
-
` ${this.theme.fg("accent", "Search: ")}${inputLines[0] || ""}`
|
|
67
|
-
);
|
|
68
|
-
lines.push("");
|
|
69
|
-
|
|
70
|
-
const query = this.searchInput.getValue().trim();
|
|
71
|
-
this.filteredOptions = query
|
|
72
|
-
? fuzzyFilter(this.allOptions, query, (item) => item)
|
|
73
|
-
: this.allOptions;
|
|
74
|
-
this.selectedIndex = Math.min(
|
|
75
|
-
this.selectedIndex,
|
|
76
|
-
Math.max(0, this.filteredOptions.length - 1)
|
|
77
|
-
);
|
|
78
|
-
|
|
79
|
-
const maxVisible = 10;
|
|
80
|
-
const total = this.filteredOptions.length;
|
|
81
|
-
if (total === 0) {
|
|
82
|
-
lines.push(` ${this.theme.fg("muted", "No matching models found.")}`);
|
|
83
|
-
} else {
|
|
84
|
-
const startIndex = Math.max(
|
|
85
|
-
0,
|
|
86
|
-
Math.min(
|
|
87
|
-
this.selectedIndex - Math.floor(maxVisible / 2),
|
|
88
|
-
total - maxVisible
|
|
89
|
-
)
|
|
90
|
-
);
|
|
91
|
-
const endIndex = Math.min(startIndex + maxVisible, total);
|
|
92
|
-
for (let i = startIndex; i < endIndex; i += 1) {
|
|
93
|
-
const item = this.filteredOptions[i];
|
|
94
|
-
const tick = item === this.currentOption ? "✓ " : " ";
|
|
95
|
-
if (i === this.selectedIndex) {
|
|
96
|
-
lines.push(
|
|
97
|
-
` ${this.theme.fg("accent", "→ ")}${this.theme.fg("accent", `${tick}${item}`)}`
|
|
98
|
-
);
|
|
99
|
-
} else {
|
|
100
|
-
lines.push(` ${this.theme.fg("text", `${tick}${item}`)}`);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
if (total > maxVisible) {
|
|
104
|
-
lines.push(
|
|
105
|
-
" " +
|
|
106
|
-
this.theme.fg("muted", ` (${this.selectedIndex + 1}/${total})`)
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
lines.push("");
|
|
111
|
-
lines.push(
|
|
112
|
-
` ${this.theme.fg("dim", "Type to search · ↑↓: navigate · Enter: select · Esc: cancel")}`
|
|
7
|
+
super(
|
|
8
|
+
new SearchableModelList({
|
|
9
|
+
...options,
|
|
10
|
+
currentOptions: options.currentOption ? [options.currentOption] : [],
|
|
11
|
+
multiSelect: false,
|
|
12
|
+
onSelect: ([value]) => {
|
|
13
|
+
if (value !== undefined) {
|
|
14
|
+
options.onSelect(value);
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
})
|
|
113
18
|
);
|
|
114
|
-
lines.push(this.theme.fg("border", "─".repeat(width)));
|
|
115
|
-
return lines.map((line) => truncateToWidth(line, width));
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
handleInput(keyData: string): void {
|
|
119
|
-
if (this.matchesAction(keyData, "tui.select.up", "\u001b[A")) {
|
|
120
|
-
this.moveSelection(-1);
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
if (this.matchesAction(keyData, "tui.select.down", "\u001b[B")) {
|
|
124
|
-
this.moveSelection(1);
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
if (
|
|
128
|
-
this.matchesAction(keyData, "tui.select.confirm", "\n") ||
|
|
129
|
-
keyData === "\r"
|
|
130
|
-
) {
|
|
131
|
-
if (this.filteredOptions.length > 0) {
|
|
132
|
-
this.onSelect(this.filteredOptions[this.selectedIndex]);
|
|
133
|
-
}
|
|
134
|
-
return;
|
|
135
|
-
}
|
|
136
|
-
if (this.matchesAction(keyData, "tui.select.cancel", "\u001b")) {
|
|
137
|
-
this.onCancel();
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
this.searchInput.handleInput(keyData);
|
|
141
|
-
this.selectedIndex = 0;
|
|
142
|
-
this.tui.requestRender();
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
private matchesAction(
|
|
146
|
-
keyData: string,
|
|
147
|
-
action: keyof Keybindings,
|
|
148
|
-
fallback: string
|
|
149
|
-
) {
|
|
150
|
-
return this.keybindings.matches(keyData, action) || keyData === fallback;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
private moveSelection(direction: -1 | 1) {
|
|
154
|
-
if (this.filteredOptions.length > 0) {
|
|
155
|
-
const lastIndex = this.filteredOptions.length - 1;
|
|
156
|
-
const nextIndex = this.selectedIndex + direction;
|
|
157
|
-
if (nextIndex < 0) {
|
|
158
|
-
this.selectedIndex = lastIndex;
|
|
159
|
-
} else if (nextIndex > lastIndex) {
|
|
160
|
-
this.selectedIndex = 0;
|
|
161
|
-
} else {
|
|
162
|
-
this.selectedIndex = nextIndex;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
this.tui.requestRender();
|
|
166
19
|
}
|
|
167
20
|
}
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { fuzzyFilter, Input, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import type {
|
|
4
|
+
Component,
|
|
5
|
+
Focusable,
|
|
6
|
+
Keybindings,
|
|
7
|
+
KeybindingsManager,
|
|
8
|
+
} from "@earendil-works/pi-tui";
|
|
9
|
+
|
|
10
|
+
import type { RenderRequester } from "./types.ts";
|
|
11
|
+
|
|
12
|
+
interface SearchableModelListOptions {
|
|
13
|
+
allOptions: string[];
|
|
14
|
+
currentOptions: string[];
|
|
15
|
+
keybindings: KeybindingsManager;
|
|
16
|
+
multiSelect: boolean;
|
|
17
|
+
onCancel: () => void;
|
|
18
|
+
onSelect: (values: string[]) => void;
|
|
19
|
+
theme: Theme;
|
|
20
|
+
title: string;
|
|
21
|
+
tui: RenderRequester;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class SearchableModelList implements Component, Focusable {
|
|
25
|
+
private readonly tui: RenderRequester;
|
|
26
|
+
private readonly searchInput: Input;
|
|
27
|
+
private readonly allOptions: string[];
|
|
28
|
+
private readonly currentOption: string | undefined;
|
|
29
|
+
private readonly multiSelect: boolean;
|
|
30
|
+
private readonly selected = new Set<string>();
|
|
31
|
+
private filteredOptions: string[];
|
|
32
|
+
private selectedIndex = 0;
|
|
33
|
+
private readonly title: string;
|
|
34
|
+
private readonly onSelect: (values: string[]) => void;
|
|
35
|
+
private readonly onCancel: () => void;
|
|
36
|
+
private readonly theme: Theme;
|
|
37
|
+
private readonly keybindings: KeybindingsManager;
|
|
38
|
+
private _focused = false;
|
|
39
|
+
|
|
40
|
+
get focused(): boolean {
|
|
41
|
+
return this._focused;
|
|
42
|
+
}
|
|
43
|
+
set focused(val: boolean) {
|
|
44
|
+
this._focused = val;
|
|
45
|
+
this.searchInput.focused = val;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
constructor(options: SearchableModelListOptions) {
|
|
49
|
+
this.multiSelect = options.multiSelect;
|
|
50
|
+
this.tui = options.tui;
|
|
51
|
+
this.title = options.title;
|
|
52
|
+
const [requestedCurrentOption] = options.currentOptions;
|
|
53
|
+
this.currentOption =
|
|
54
|
+
!this.multiSelect &&
|
|
55
|
+
requestedCurrentOption &&
|
|
56
|
+
options.allOptions.includes(requestedCurrentOption)
|
|
57
|
+
? requestedCurrentOption
|
|
58
|
+
: undefined;
|
|
59
|
+
let allOptions: string[];
|
|
60
|
+
if (this.multiSelect) {
|
|
61
|
+
allOptions = [...new Set(options.allOptions)].toSorted((left, right) =>
|
|
62
|
+
left.localeCompare(right)
|
|
63
|
+
);
|
|
64
|
+
} else if (this.currentOption) {
|
|
65
|
+
allOptions = [
|
|
66
|
+
this.currentOption,
|
|
67
|
+
...options.allOptions.filter((item) => item !== this.currentOption),
|
|
68
|
+
];
|
|
69
|
+
} else {
|
|
70
|
+
allOptions = [...options.allOptions];
|
|
71
|
+
}
|
|
72
|
+
this.allOptions = allOptions;
|
|
73
|
+
if (this.multiSelect) {
|
|
74
|
+
for (const value of options.currentOptions) {
|
|
75
|
+
if (this.allOptions.includes(value)) {
|
|
76
|
+
this.selected.add(value);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
this.theme = options.theme;
|
|
81
|
+
this.keybindings = options.keybindings;
|
|
82
|
+
this.onSelect = options.onSelect;
|
|
83
|
+
this.onCancel = options.onCancel;
|
|
84
|
+
this.searchInput = new Input();
|
|
85
|
+
this.filteredOptions = this.allOptions;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
invalidate(): void {
|
|
89
|
+
this.searchInput.invalidate();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
render(width: number): string[] {
|
|
93
|
+
const lines: string[] = [this.theme.fg("border", "─".repeat(width))];
|
|
94
|
+
lines.push(` ${this.theme.fg("accent", this.theme.bold(this.title))}`);
|
|
95
|
+
const inputLines = this.searchInput.render(Math.max(1, width - 10));
|
|
96
|
+
lines.push(
|
|
97
|
+
` ${this.theme.fg("accent", "Search: ")}${inputLines[0] || ""}`,
|
|
98
|
+
""
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
const query = this.searchInput.getValue().trim();
|
|
102
|
+
this.filteredOptions = query
|
|
103
|
+
? fuzzyFilter(this.allOptions, query, (item) => item)
|
|
104
|
+
: this.allOptions;
|
|
105
|
+
this.selectedIndex = Math.min(
|
|
106
|
+
this.selectedIndex,
|
|
107
|
+
Math.max(0, this.filteredOptions.length - 1)
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
const maxVisible = 10;
|
|
111
|
+
const total = this.filteredOptions.length;
|
|
112
|
+
if (total === 0) {
|
|
113
|
+
lines.push(` ${this.theme.fg("muted", "No matching models found.")}`);
|
|
114
|
+
} else {
|
|
115
|
+
const startIndex = Math.max(
|
|
116
|
+
0,
|
|
117
|
+
Math.min(
|
|
118
|
+
this.selectedIndex - Math.floor(maxVisible / 2),
|
|
119
|
+
total - maxVisible
|
|
120
|
+
)
|
|
121
|
+
);
|
|
122
|
+
const endIndex = Math.min(startIndex + maxVisible, total);
|
|
123
|
+
for (let i = startIndex; i < endIndex; i += 1) {
|
|
124
|
+
lines.push(this.renderOption(this.filteredOptions[i], i));
|
|
125
|
+
}
|
|
126
|
+
if (total > maxVisible) {
|
|
127
|
+
lines.push(
|
|
128
|
+
` ${this.theme.fg(
|
|
129
|
+
"muted",
|
|
130
|
+
` (${this.selectedIndex + 1}/${total})`
|
|
131
|
+
)}`
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
lines.push(
|
|
136
|
+
"",
|
|
137
|
+
` ${this.theme.fg("dim", this.interactionHint())}`,
|
|
138
|
+
this.theme.fg("border", "─".repeat(width))
|
|
139
|
+
);
|
|
140
|
+
return lines.map((line) => truncateToWidth(line, width));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
handleInput(keyData: string): void {
|
|
144
|
+
if (this.matchesAction(keyData, "tui.select.up", "\u001B[A")) {
|
|
145
|
+
this.moveSelection(-1);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
if (this.matchesAction(keyData, "tui.select.down", "\u001B[B")) {
|
|
149
|
+
this.moveSelection(1);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
if (this.multiSelect && keyData === " ") {
|
|
153
|
+
const item = this.filteredOptions[this.selectedIndex];
|
|
154
|
+
if (item) {
|
|
155
|
+
if (this.selected.has(item)) {
|
|
156
|
+
this.selected.delete(item);
|
|
157
|
+
} else {
|
|
158
|
+
this.selected.add(item);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
this.tui.requestRender();
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (
|
|
165
|
+
this.matchesAction(keyData, "tui.select.confirm", "\n") ||
|
|
166
|
+
keyData === "\r"
|
|
167
|
+
) {
|
|
168
|
+
if (this.multiSelect) {
|
|
169
|
+
this.onSelect(
|
|
170
|
+
this.allOptions.filter((item) => this.selected.has(item))
|
|
171
|
+
);
|
|
172
|
+
} else {
|
|
173
|
+
const selectedOption = this.filteredOptions[this.selectedIndex];
|
|
174
|
+
if (selectedOption !== undefined) {
|
|
175
|
+
this.onSelect([selectedOption]);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
if (this.matchesAction(keyData, "tui.select.cancel", "\u001B")) {
|
|
181
|
+
this.onCancel();
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
this.searchInput.handleInput(keyData);
|
|
185
|
+
this.selectedIndex = 0;
|
|
186
|
+
this.tui.requestRender();
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private interactionHint(): string {
|
|
190
|
+
return this.multiSelect
|
|
191
|
+
? "Type to search · ↑↓: navigate · Space: toggle · Enter: apply · Esc: cancel"
|
|
192
|
+
: "Type to search · ↑↓: navigate · Enter: select · Esc: cancel";
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private renderOption(item: string, index: number): string {
|
|
196
|
+
let tick = " ";
|
|
197
|
+
if (
|
|
198
|
+
this.multiSelect ? this.selected.has(item) : item === this.currentOption
|
|
199
|
+
) {
|
|
200
|
+
tick = "✓ ";
|
|
201
|
+
}
|
|
202
|
+
if (index === this.selectedIndex) {
|
|
203
|
+
return ` ${this.theme.fg("accent", `→ ${tick}${item}`)}`;
|
|
204
|
+
}
|
|
205
|
+
return ` ${this.theme.fg("text", `${tick}${item}`)}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private matchesAction(
|
|
209
|
+
keyData: string,
|
|
210
|
+
action: keyof Keybindings,
|
|
211
|
+
fallback: string
|
|
212
|
+
) {
|
|
213
|
+
return this.keybindings.matches(keyData, action) || keyData === fallback;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private moveSelection(direction: -1 | 1) {
|
|
217
|
+
if (this.filteredOptions.length > 0) {
|
|
218
|
+
const lastIndex = this.filteredOptions.length - 1;
|
|
219
|
+
const nextIndex = this.selectedIndex + direction;
|
|
220
|
+
if (nextIndex < 0) {
|
|
221
|
+
this.selectedIndex = lastIndex;
|
|
222
|
+
} else if (nextIndex > lastIndex) {
|
|
223
|
+
this.selectedIndex = 0;
|
|
224
|
+
} else {
|
|
225
|
+
this.selectedIndex = nextIndex;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
this.tui.requestRender();
|
|
229
|
+
}
|
|
230
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { visibleWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
2
3
|
import { DEFAULT_EFFORT_LEVEL } from "../commands/model-options.ts";
|
|
3
4
|
import type { ContextPreset } from "./types.ts";
|
|
4
5
|
|
|
@@ -20,7 +21,7 @@ export const withCurrentValue = (current: string, values: string[]) =>
|
|
|
20
21
|
|
|
21
22
|
export const numericValues = (current: number, values: number[]) => {
|
|
22
23
|
const all = values.includes(current) ? values : [...values, current];
|
|
23
|
-
return all.
|
|
24
|
+
return all.toSorted((a, b) => a - b).map(String);
|
|
24
25
|
};
|
|
25
26
|
|
|
26
27
|
export const maxCallValues = (current: string) => {
|
|
@@ -53,6 +54,22 @@ export const currentContextLabel = (
|
|
|
53
54
|
presets.find((preset) => preset.value === contextMaxChars)?.label ??
|
|
54
55
|
String(contextMaxChars);
|
|
55
56
|
|
|
57
|
+
const closestPresetIndex = (
|
|
58
|
+
presets: ContextPreset[],
|
|
59
|
+
contextMaxChars: number
|
|
60
|
+
): number => {
|
|
61
|
+
let closestIndex = 0;
|
|
62
|
+
for (let index = 0; index < presets.length; index += 1) {
|
|
63
|
+
if (
|
|
64
|
+
Math.abs(presets[index].value - contextMaxChars) <
|
|
65
|
+
Math.abs(presets[closestIndex].value - contextMaxChars)
|
|
66
|
+
) {
|
|
67
|
+
closestIndex = index;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return closestIndex;
|
|
71
|
+
};
|
|
72
|
+
|
|
56
73
|
export const contextDescription = (
|
|
57
74
|
presets: ContextPreset[],
|
|
58
75
|
contextMaxChars: number
|
|
@@ -61,16 +78,9 @@ export const contextDescription = (
|
|
|
61
78
|
(preset) => preset.value === contextMaxChars
|
|
62
79
|
);
|
|
63
80
|
const selectedIndex =
|
|
64
|
-
exactIndex
|
|
65
|
-
?
|
|
66
|
-
:
|
|
67
|
-
(closestIndex, preset, index) =>
|
|
68
|
-
Math.abs(preset.value - contextMaxChars) <
|
|
69
|
-
Math.abs(presets[closestIndex].value - contextMaxChars)
|
|
70
|
-
? index
|
|
71
|
-
: closestIndex,
|
|
72
|
-
0
|
|
73
|
-
);
|
|
81
|
+
exactIndex === -1
|
|
82
|
+
? closestPresetIndex(presets, contextMaxChars)
|
|
83
|
+
: exactIndex;
|
|
74
84
|
const selectedPreset = presets[selectedIndex];
|
|
75
85
|
const isFullContext =
|
|
76
86
|
selectedPreset?.value === Number.MAX_SAFE_INTEGER ||
|
|
@@ -100,7 +110,7 @@ export const contextDescription = (
|
|
|
100
110
|
);
|
|
101
111
|
const markerLabel = `${" ".repeat(labelStart)}${label}`;
|
|
102
112
|
const description =
|
|
103
|
-
exactIndex
|
|
113
|
+
exactIndex === -1 ? "Custom context limit." : selectedPreset?.description;
|
|
104
114
|
return `${description ?? "Custom context limit."}\n${meterPrefix}${meter} full\n${markerLabel}`;
|
|
105
115
|
};
|
|
106
116
|
|
|
@@ -126,8 +136,8 @@ export const rainbowGradient = (text: string, startedAt: number): string => {
|
|
|
126
136
|
const red = Math.round(baseRed + (255 - baseRed) * brightness);
|
|
127
137
|
const green = Math.round(baseGreen + (255 - baseGreen) * brightness);
|
|
128
138
|
const blue = Math.round(baseBlue + (255 - baseBlue) * brightness);
|
|
129
|
-
return `\
|
|
139
|
+
return `\u001B[38;2;${red};${green};${blue}m${character}`;
|
|
130
140
|
})
|
|
131
141
|
.join("")
|
|
132
|
-
.concat("\
|
|
142
|
+
.concat("\u001B[0m");
|
|
133
143
|
};
|
package/src/ui/settings-items.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import
|
|
2
|
+
import { getKeybindings } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { KeybindingsManager, SettingItem } from "@earendil-works/pi-tui";
|
|
4
|
+
|
|
3
5
|
import {
|
|
4
6
|
DEFAULT_JEV_DIGEST_MAX_CHARS,
|
|
5
7
|
DEFAULT_JEV_FILTER_NOUL_MARGIN,
|
|
@@ -13,6 +15,7 @@ import {
|
|
|
13
15
|
} from "../config/types.ts";
|
|
14
16
|
import { isValidAdvisorToolPolicies } from "../config/validation.ts";
|
|
15
17
|
import { JevSetupSubmenu } from "./jev-setup-submenu.ts";
|
|
18
|
+
import { SearchableModelMultiSelector } from "./model-multi-selector.ts";
|
|
16
19
|
import {
|
|
17
20
|
contextDescription,
|
|
18
21
|
currentContextLabel,
|
|
@@ -32,6 +35,7 @@ import type {
|
|
|
32
35
|
|
|
33
36
|
export interface SettingsItemsOptions {
|
|
34
37
|
effortLevels: string[];
|
|
38
|
+
modelWhitelist: SettingItem;
|
|
35
39
|
presets: ContextPreset[];
|
|
36
40
|
settings: AdvisorSettings;
|
|
37
41
|
theme: Theme;
|
|
@@ -200,8 +204,39 @@ const jevItems = (
|
|
|
200
204
|
},
|
|
201
205
|
];
|
|
202
206
|
|
|
207
|
+
export const advisorModelWhitelistItem = (
|
|
208
|
+
settings: AdvisorSettings,
|
|
209
|
+
modelRefs: string[] | undefined,
|
|
210
|
+
keybindings: KeybindingsManager | undefined,
|
|
211
|
+
theme: Theme,
|
|
212
|
+
tui: RenderRequester
|
|
213
|
+
): SettingItem => ({
|
|
214
|
+
currentValue: settings.modelWhitelist?.length
|
|
215
|
+
? settings.modelWhitelist.join(", ")
|
|
216
|
+
: "Any model",
|
|
217
|
+
description:
|
|
218
|
+
"Only the exact provider/model references listed here may call the Advisor; an empty list allows every model.",
|
|
219
|
+
id: "modelWhitelist",
|
|
220
|
+
label: "Advisor model whitelist",
|
|
221
|
+
submenu: (_currentValue, done) =>
|
|
222
|
+
new SearchableModelMultiSelector({
|
|
223
|
+
allOptions: [
|
|
224
|
+
...new Set([...(modelRefs ?? []), ...(settings.modelWhitelist ?? [])]),
|
|
225
|
+
],
|
|
226
|
+
currentOptions: settings.modelWhitelist ?? [],
|
|
227
|
+
keybindings: keybindings ?? getKeybindings(),
|
|
228
|
+
multiSelect: true,
|
|
229
|
+
onCancel: done,
|
|
230
|
+
onSelect: (values) => done(values.join(",")),
|
|
231
|
+
theme,
|
|
232
|
+
title: "Advisor model whitelist",
|
|
233
|
+
tui,
|
|
234
|
+
}),
|
|
235
|
+
});
|
|
236
|
+
|
|
203
237
|
export const createSettingsItems = ({
|
|
204
238
|
effortLevels,
|
|
239
|
+
modelWhitelist,
|
|
205
240
|
presets,
|
|
206
241
|
settings,
|
|
207
242
|
theme,
|
|
@@ -233,6 +268,7 @@ export const createSettingsItems = ({
|
|
|
233
268
|
},
|
|
234
269
|
];
|
|
235
270
|
if (settings.simpleMode) {
|
|
271
|
+
items.push(modelWhitelist);
|
|
236
272
|
return items;
|
|
237
273
|
}
|
|
238
274
|
|
|
@@ -244,6 +280,7 @@ export const createSettingsItems = ({
|
|
|
244
280
|
label: "Advisor reasoning",
|
|
245
281
|
values: withCurrentValue(currentEffort(settings.effort), effortLevels),
|
|
246
282
|
},
|
|
283
|
+
modelWhitelist,
|
|
247
284
|
toggle(
|
|
248
285
|
"scoutEnabled",
|
|
249
286
|
"Experimental Advisor Scout",
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
type SettingsList,
|
|
1
|
+
import { Key, matchesKey } from "@earendil-works/pi-tui";
|
|
2
|
+
import type {
|
|
3
|
+
Component,
|
|
4
|
+
Focusable,
|
|
5
|
+
Input,
|
|
6
|
+
SettingItem,
|
|
7
|
+
SettingsList,
|
|
9
8
|
} from "@earendil-works/pi-tui";
|
|
10
9
|
|
|
11
10
|
interface SettingsListPrivateFields {
|
|
@@ -15,6 +14,8 @@ interface SettingsListPrivateFields {
|
|
|
15
14
|
submenuComponent?: Component | null;
|
|
16
15
|
}
|
|
17
16
|
|
|
17
|
+
type SettingsListWithPrivateFields = SettingsList & SettingsListPrivateFields;
|
|
18
|
+
|
|
18
19
|
/**
|
|
19
20
|
* Contains the narrow compatibility access needed for pi-tui's SettingsList.
|
|
20
21
|
* No other UI module should depend on its private fields.
|
|
@@ -45,6 +46,7 @@ export class SettingsListAdapter {
|
|
|
45
46
|
|
|
46
47
|
setFocused(value: boolean): void {
|
|
47
48
|
const fields = this.privateFields();
|
|
49
|
+
// SAFETY: a SettingsList submenu component is registered only when focusable.
|
|
48
50
|
const submenu = fields.submenuComponent as Focusable | undefined;
|
|
49
51
|
if (submenu) {
|
|
50
52
|
submenu.focused = value;
|
|
@@ -53,7 +55,7 @@ export class SettingsListAdapter {
|
|
|
53
55
|
|
|
54
56
|
setSelectedId(items: SettingItem[], selectedId: string): void {
|
|
55
57
|
const selectedIndex = items.findIndex((item) => item.id === selectedId);
|
|
56
|
-
if (selectedIndex
|
|
58
|
+
if (selectedIndex !== -1) {
|
|
57
59
|
this.privateFields().selectedIndex = selectedIndex;
|
|
58
60
|
}
|
|
59
61
|
}
|
|
@@ -63,9 +65,9 @@ export class SettingsListAdapter {
|
|
|
63
65
|
onChange: (id: string, value: string) => void
|
|
64
66
|
): boolean {
|
|
65
67
|
let direction = 0;
|
|
66
|
-
if (matchesKey(keyData, Key.left) || keyData === "\
|
|
68
|
+
if (matchesKey(keyData, Key.left) || keyData === "\u001B[D") {
|
|
67
69
|
direction = -1;
|
|
68
|
-
} else if (matchesKey(keyData, Key.right) || keyData === "\
|
|
70
|
+
} else if (matchesKey(keyData, Key.right) || keyData === "\u001B[C") {
|
|
69
71
|
direction = 1;
|
|
70
72
|
}
|
|
71
73
|
if (direction === 0) {
|
|
@@ -98,6 +100,7 @@ export class SettingsListAdapter {
|
|
|
98
100
|
}
|
|
99
101
|
|
|
100
102
|
private privateFields(): SettingsListPrivateFields {
|
|
101
|
-
|
|
103
|
+
// SAFETY: compatibility-only read of SettingsList's private layout fields.
|
|
104
|
+
return this.list as SettingsListWithPrivateFields;
|
|
102
105
|
}
|
|
103
106
|
}
|