pi-advisor-flow 0.6.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.
Files changed (93) hide show
  1. package/CHANGELOG.md +34 -17
  2. package/README.md +10 -8
  3. package/dist/index.js +2852 -868
  4. package/extensions/index.ts +2 -1
  5. package/package.json +55 -40
  6. package/src/attachments.ts +11 -10
  7. package/src/commands/activation-preparation.ts +1 -0
  8. package/src/commands/activation.ts +2 -0
  9. package/src/commands/lifecycle.ts +1 -0
  10. package/src/commands/manual-command.ts +7 -1
  11. package/src/commands/manual-consultation.ts +103 -79
  12. package/src/commands/manual-progress.ts +7 -5
  13. package/src/commands/model-commands.ts +1 -1
  14. package/src/commands/model-options.ts +14 -3
  15. package/src/commands/model-picker.ts +1 -0
  16. package/src/commands/registration.ts +1 -0
  17. package/src/commands/renderers.ts +67 -55
  18. package/src/commands/runtime.ts +16 -14
  19. package/src/commands/settings-commands.ts +10 -4
  20. package/src/commands/settings-persistence.ts +64 -2
  21. package/src/commands/types.ts +2 -0
  22. package/src/commands.ts +0 -1
  23. package/src/config/args.ts +1 -1
  24. package/src/config/defaults.ts +83 -1
  25. package/src/config/schema.ts +152 -15
  26. package/src/config/state.ts +80 -2
  27. package/src/config/storage.ts +38 -22
  28. package/src/config/types.ts +45 -3
  29. package/src/config/validation.ts +33 -21
  30. package/src/config.ts +3 -0
  31. package/src/content-utils.ts +24 -4
  32. package/src/conversation.ts +16 -15
  33. package/src/git.ts +10 -3
  34. package/src/herdr-block.ts +83 -0
  35. package/src/herdr-shared.ts +108 -0
  36. package/src/herdr.ts +30 -192
  37. package/src/jev/client.ts +338 -0
  38. package/src/jev/failure.ts +19 -0
  39. package/src/jev/key-store.ts +267 -0
  40. package/src/jev/ledger.ts +228 -0
  41. package/src/jev/questions.ts +122 -0
  42. package/src/jev/state.ts +50 -0
  43. package/src/jev/transport.ts +63 -0
  44. package/src/model-stream.ts +35 -37
  45. package/src/outcomes.ts +20 -12
  46. package/src/pi-settings.ts +4 -9
  47. package/src/preferences.ts +4 -2
  48. package/src/redaction.ts +10 -12
  49. package/src/scout-context.ts +59 -60
  50. package/src/scout-curation.ts +4 -13
  51. package/src/scout-groups.ts +178 -175
  52. package/src/scout-protocol.ts +74 -70
  53. package/src/scout-reconstruct.ts +1 -1
  54. package/src/scout.ts +115 -73
  55. package/src/session-state.ts +148 -39
  56. package/src/tool-result-cap.ts +18 -17
  57. package/src/tools/consult-context.ts +7 -15
  58. package/src/tools/consultation.ts +14 -4
  59. package/src/tools/gate-policy.ts +1 -0
  60. package/src/tools/gate-protocol.ts +36 -23
  61. package/src/tools/jev-filter.ts +204 -0
  62. package/src/tools/jev-turn-gate.ts +203 -0
  63. package/src/tools/loop-gate.ts +32 -20
  64. package/src/tools/model-access.ts +46 -0
  65. package/src/tools/outage-notifier.ts +30 -0
  66. package/src/tools/prompts.ts +20 -25
  67. package/src/tools/register-ask-advisor.ts +73 -17
  68. package/src/tools/register-lifecycle.ts +22 -0
  69. package/src/tools/register-outcome.ts +2 -0
  70. package/src/tools/register-renderers.ts +149 -73
  71. package/src/tools/registration.ts +15 -0
  72. package/src/tools/render-advisor-result.ts +77 -35
  73. package/src/tools/render-common.ts +16 -19
  74. package/src/tools/scout-status.ts +42 -27
  75. package/src/tools/types.ts +11 -0
  76. package/src/tools.ts +0 -1
  77. package/src/ui/jev-setup-submenu.ts +350 -0
  78. package/src/ui/manual-dialog-render.ts +1 -0
  79. package/src/ui/manual-dialog.ts +39 -43
  80. package/src/ui/masked-input.ts +62 -0
  81. package/src/ui/model-multi-selector.ts +9 -0
  82. package/src/ui/model-selector-adapter.ts +31 -0
  83. package/src/ui/model-selector.ts +14 -161
  84. package/src/ui/searchable-model-list.ts +230 -0
  85. package/src/ui/settings-formatting.ts +24 -14
  86. package/src/ui/settings-items.ts +200 -2
  87. package/src/ui/settings-list-adapter.ts +15 -12
  88. package/src/ui/settings-mutations.ts +169 -47
  89. package/src/ui/settings-selector.ts +18 -9
  90. package/src/ui/text-setting-submenu.ts +3 -6
  91. package/src/ui/types.ts +25 -0
  92. package/src/ui.ts +0 -1
  93. package/src/usage.ts +29 -20
@@ -1,167 +1,20 @@
1
- import type { Theme } from "@earendil-works/pi-coding-agent";
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 implements Component, Focusable {
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
- this.tui = options.tui;
37
- this.title = options.title;
38
- this.currentOption =
39
- options.currentOption &&
40
- options.allOptions.includes(options.currentOption)
41
- ? options.currentOption
42
- : undefined;
43
- this.allOptions = this.currentOption
44
- ? [
45
- this.currentOption,
46
- ...options.allOptions.filter((item) => item !== this.currentOption),
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.sort((a, b) => a - b).map(String);
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 >= 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
- );
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 >= 0 ? selectedPreset?.description : "Custom context limit.";
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 `\x1b[38;2;${red};${green};${blue}m${character}`;
139
+ return `\u001B[38;2;${red};${green};${blue}m${character}`;
130
140
  })
131
141
  .join("")
132
- .concat("\x1b[0m");
142
+ .concat("\u001B[0m");
133
143
  };