pi-advisor-flow 0.7.0 → 0.8.1-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -18
- package/README.md +9 -8
- package/dist/index.js +1968 -1081
- package/extensions/index.ts +2 -1
- package/package.json +48 -35
- package/src/attachments.ts +11 -10
- package/src/child-session.ts +15 -0
- package/src/commands/activation-preparation.ts +1 -0
- package/src/commands/activation.ts +12 -3
- package/src/commands/lifecycle.ts +7 -1
- 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 +128 -77
- 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/masked-input.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
type Focusable,
|
|
4
|
-
Input,
|
|
5
|
-
truncateToWidth,
|
|
6
|
-
} from "@earendil-works/pi-tui";
|
|
1
|
+
import { Input, truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
|
+
import type { Component, Focusable } from "@earendil-works/pi-tui";
|
|
7
3
|
|
|
8
4
|
export interface MaskedInputOptions {
|
|
9
5
|
onEscape?: () => void;
|
|
@@ -11,9 +7,7 @@ export interface MaskedInputOptions {
|
|
|
11
7
|
placeholder?: string;
|
|
12
8
|
}
|
|
13
9
|
|
|
14
|
-
|
|
15
|
-
cursor: number;
|
|
16
|
-
}
|
|
10
|
+
type InputWithCursor = Input & { cursor: number };
|
|
17
11
|
|
|
18
12
|
/** Single-line secret input rendering bullets; the value never reaches the screen. */
|
|
19
13
|
export class MaskedInput implements Component, Focusable {
|
|
@@ -56,8 +50,8 @@ export class MaskedInput implements Component, Focusable {
|
|
|
56
50
|
|
|
57
51
|
render(width: number): string[] {
|
|
58
52
|
const value = this.input.getValue();
|
|
59
|
-
//
|
|
60
|
-
const { cursor } = this.input as
|
|
53
|
+
// SAFETY: compatibility-only read of Input's private cursor; Input does not expose it.
|
|
54
|
+
const { cursor } = this.input as InputWithCursor;
|
|
61
55
|
const masked = `${"•".repeat(Math.min(cursor, value.length))}█${"•".repeat(Math.max(0, value.length - cursor))}`;
|
|
62
56
|
const placeholder =
|
|
63
57
|
value.length === 0 && this.options.placeholder
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { ModelSelectorAdapter } from "./model-selector-adapter.ts";
|
|
2
|
+
import { SearchableModelList } from "./searchable-model-list.ts";
|
|
3
|
+
import type { SearchableModelMultiSelectorOptions } from "./types.ts";
|
|
4
|
+
|
|
5
|
+
export class SearchableModelMultiSelector extends ModelSelectorAdapter {
|
|
6
|
+
constructor(options: SearchableModelMultiSelectorOptions) {
|
|
7
|
+
super(new SearchableModelList(options));
|
|
8
|
+
}
|
|
9
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { Component, Focusable } from "@earendil-works/pi-tui";
|
|
2
|
+
|
|
3
|
+
import type { SearchableModelList } from "./searchable-model-list.ts";
|
|
4
|
+
|
|
5
|
+
export class ModelSelectorAdapter implements Component, Focusable {
|
|
6
|
+
protected readonly list: SearchableModelList;
|
|
7
|
+
|
|
8
|
+
constructor(list: SearchableModelList) {
|
|
9
|
+
this.list = list;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
get focused(): boolean {
|
|
13
|
+
return this.list.focused;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
set focused(value: boolean) {
|
|
17
|
+
this.list.focused = value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
invalidate(): void {
|
|
21
|
+
this.list.invalidate();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
render(width: number): string[] {
|
|
25
|
+
return this.list.render(width);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
handleInput(keyData: string): void {
|
|
29
|
+
this.list.handleInput(keyData);
|
|
30
|
+
}
|
|
31
|
+
}
|
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",
|