pi-advisor-flow 0.1.8 → 0.2.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/src/ui.ts CHANGED
@@ -1,21 +1,30 @@
1
- import { Input, Key, matchesKey, fuzzyFilter, truncateToWidth, type Component, type Focusable } from "@earendil-works/pi-tui";
2
- import { Box, Text } from "@earendil-works/pi-tui";
3
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import {
3
+ type Component,
4
+ type Focusable,
5
+ fuzzyFilter,
6
+ Input,
7
+ Key,
8
+ matchesKey,
9
+ truncateToWidth,
10
+ } from "@earendil-works/pi-tui";
4
11
 
5
12
  export class SearchableModelSelector implements Component, Focusable {
6
- private tui: any;
7
- private searchInput: Input;
8
- private allOptions: string[];
13
+ private readonly tui: any;
14
+ private readonly searchInput: Input;
15
+ private readonly allOptions: string[];
9
16
  private filteredOptions: string[];
10
17
  private selectedIndex = 0;
11
- private title: string;
12
- private onSelect: (value: string) => void;
13
- private onCancel: () => void;
14
- private theme: Theme;
15
- private keybindings: any;
18
+ private readonly title: string;
19
+ private readonly onSelect: (value: string) => void;
20
+ private readonly onCancel: () => void;
21
+ private readonly theme: Theme;
22
+ private readonly keybindings: any;
16
23
  private _focused = false;
17
24
 
18
- public get focused(): boolean { return this._focused; }
25
+ public get focused(): boolean {
26
+ return this._focused;
27
+ }
19
28
  public set focused(val: boolean) {
20
29
  this._focused = val;
21
30
  this.searchInput.focused = val;
@@ -41,35 +50,62 @@ export class SearchableModelSelector implements Component, Focusable {
41
50
  this.filteredOptions = this.allOptions;
42
51
  }
43
52
 
44
- invalidate(): void { this.searchInput.invalidate(); }
53
+ invalidate(): void {
54
+ this.searchInput.invalidate();
55
+ }
45
56
 
46
57
  render(width: number): string[] {
47
58
  const lines: string[] = ["═".repeat(width)];
48
59
  lines.push(` ${this.theme.fg("accent", this.theme.bold(this.title))}`);
49
60
  const inputLines = this.searchInput.render(width - 12);
50
- lines.push(` ${this.theme.fg("accent", "Search: ")}${inputLines[0] || ""}`);
61
+ lines.push(
62
+ ` ${this.theme.fg("accent", "Search: ")}${inputLines[0] || ""}`
63
+ );
51
64
  lines.push("");
52
65
 
53
66
  const query = this.searchInput.getValue().trim();
54
- this.filteredOptions = query ? fuzzyFilter(this.allOptions, query, (item) => item) : this.allOptions;
55
- this.selectedIndex = Math.min(this.selectedIndex, Math.max(0, this.filteredOptions.length - 1));
67
+ this.filteredOptions = query
68
+ ? fuzzyFilter(this.allOptions, query, (item) => item)
69
+ : this.allOptions;
70
+ this.selectedIndex = Math.min(
71
+ this.selectedIndex,
72
+ Math.max(0, this.filteredOptions.length - 1)
73
+ );
56
74
 
57
75
  const maxVisible = 10;
58
76
  const total = this.filteredOptions.length;
59
77
  if (total === 0) {
60
- lines.push(" " + this.theme.fg("muted", "No matching models found."));
78
+ lines.push(` ${this.theme.fg("muted", "No matching models found.")}`);
61
79
  } else {
62
- const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(maxVisible / 2), total - maxVisible));
80
+ const startIndex = Math.max(
81
+ 0,
82
+ Math.min(
83
+ this.selectedIndex - Math.floor(maxVisible / 2),
84
+ total - maxVisible
85
+ )
86
+ );
63
87
  const endIndex = Math.min(startIndex + maxVisible, total);
64
88
  for (let i = startIndex; i < endIndex; i++) {
65
89
  const item = this.filteredOptions[i];
66
- if (i === this.selectedIndex) lines.push(` ${this.theme.fg("accent", "→ ")}${this.theme.fg("accent", item)}`);
67
- else lines.push(` ${this.theme.fg("text", item)}`);
90
+ if (i === this.selectedIndex) {
91
+ lines.push(
92
+ ` ${this.theme.fg("accent", "→ ")}${this.theme.fg("accent", item)}`
93
+ );
94
+ } else {
95
+ lines.push(` ${this.theme.fg("text", item)}`);
96
+ }
97
+ }
98
+ if (total > maxVisible) {
99
+ lines.push(
100
+ " " +
101
+ this.theme.fg("muted", ` (${this.selectedIndex + 1}/${total})`)
102
+ );
68
103
  }
69
- if (total > maxVisible) lines.push(" " + this.theme.fg("muted", ` (${this.selectedIndex + 1}/${total})`));
70
104
  }
71
105
  lines.push("");
72
- lines.push(` ${this.theme.fg("muted", "Type to search · ↑↓: navigate · Enter: select · Esc: cancel")}`);
106
+ lines.push(
107
+ ` ${this.theme.fg("muted", "Type to search · ↑↓: navigate · Enter: select · Esc: cancel")}`
108
+ );
73
109
  lines.push("═".repeat(width));
74
110
  return lines;
75
111
  }
@@ -77,14 +113,36 @@ export class SearchableModelSelector implements Component, Focusable {
77
113
  handleInput(keyData: string): void {
78
114
  const kb = this.keybindings;
79
115
  if (kb.matches(keyData, "tui.select.up") || keyData === "\u001b[A") {
80
- if (this.filteredOptions.length > 0) this.selectedIndex = this.selectedIndex === 0 ? this.filteredOptions.length - 1 : this.selectedIndex - 1;
116
+ if (this.filteredOptions.length > 0) {
117
+ this.selectedIndex =
118
+ this.selectedIndex === 0
119
+ ? this.filteredOptions.length - 1
120
+ : this.selectedIndex - 1;
121
+ }
81
122
  this.tui.requestRender();
82
- } else if (kb.matches(keyData, "tui.select.down") || keyData === "\u001b[B") {
83
- if (this.filteredOptions.length > 0) this.selectedIndex = this.selectedIndex === this.filteredOptions.length - 1 ? 0 : this.selectedIndex + 1;
123
+ } else if (
124
+ kb.matches(keyData, "tui.select.down") ||
125
+ keyData === "\u001b[B"
126
+ ) {
127
+ if (this.filteredOptions.length > 0) {
128
+ this.selectedIndex =
129
+ this.selectedIndex === this.filteredOptions.length - 1
130
+ ? 0
131
+ : this.selectedIndex + 1;
132
+ }
84
133
  this.tui.requestRender();
85
- } else if (kb.matches(keyData, "tui.select.confirm") || keyData === "\n" || keyData === "\r") {
86
- if (this.filteredOptions.length > 0) this.onSelect(this.filteredOptions[this.selectedIndex]);
87
- } else if (kb.matches(keyData, "tui.select.cancel") || keyData === "\u001b") {
134
+ } else if (
135
+ kb.matches(keyData, "tui.select.confirm") ||
136
+ keyData === "\n" ||
137
+ keyData === "\r"
138
+ ) {
139
+ if (this.filteredOptions.length > 0) {
140
+ this.onSelect(this.filteredOptions[this.selectedIndex]);
141
+ }
142
+ } else if (
143
+ kb.matches(keyData, "tui.select.cancel") ||
144
+ keyData === "\u001b"
145
+ ) {
88
146
  this.onCancel();
89
147
  } else {
90
148
  this.searchInput.handleInput(keyData);
@@ -94,50 +152,72 @@ export class SearchableModelSelector implements Component, Focusable {
94
152
  }
95
153
  }
96
154
 
97
- export type ContextPreset = { label: string; value: number; description: string };
155
+ export interface ContextPreset {
156
+ description: string;
157
+ label: string;
158
+ value: number;
159
+ }
98
160
 
99
- export type AdvisorSettings = {
161
+ export interface AdvisorSettings {
162
+ autoLoopGate?: boolean;
163
+ blockOnBlocked?: boolean;
164
+ collapseResponses: boolean;
165
+ completionGate: boolean;
100
166
  contextMaxChars: number;
167
+ customRule?: string;
101
168
  effort?: string;
102
- planGate: boolean;
103
169
  failureGate: boolean;
104
- completionGate: boolean;
105
- collapseResponses: boolean;
106
- customRule?: string;
107
- blockOnBlocked?: boolean;
108
- autoLoopGate?: boolean;
170
+ failureMode?: "block-session" | "block-tool" | "warn-and-continue";
171
+ herdrIntegration?: boolean;
109
172
  loopThreshold?: number;
110
173
  maxCallsPerSession?: number;
174
+ planGate: boolean;
111
175
  sessionSummary?: boolean;
112
- };
176
+ toolResultMaxBytes?: number;
177
+ toolResultMaxLines?: number;
178
+ }
113
179
 
114
180
  export class AdvisorSettingsSelector implements Component, Focusable {
115
181
  private selectedRow = 0;
116
182
  private contextIndex: number;
117
183
  private effortIndex: number;
118
- private settings: AdvisorSettings;
119
- private customInput = new Input();
184
+ private readonly settings: AdvisorSettings;
185
+ private readonly customInput = new Input();
120
186
  private editingCustom = false;
121
187
  private _focused = false;
122
188
 
123
- public get focused() { return this._focused; }
189
+ public get focused() {
190
+ return this._focused;
191
+ }
124
192
  public set focused(value: boolean) {
125
193
  this._focused = value;
126
194
  this.customInput.focused = value && this.editingCustom;
127
195
  }
128
196
 
129
- constructor(private options: {
130
- tui: any;
131
- theme: Theme;
132
- presets: ContextPreset[];
133
- effortLevels: string[];
134
- initial: AdvisorSettings;
135
- onSave: (settings: AdvisorSettings) => void;
136
- onCancel: () => void;
137
- }) {
197
+ constructor(
198
+ private readonly options: {
199
+ tui: any;
200
+ theme: Theme;
201
+ presets: ContextPreset[];
202
+ effortLevels: string[];
203
+ initial: AdvisorSettings;
204
+ onSave: (settings: AdvisorSettings) => void;
205
+ onCancel: () => void;
206
+ }
207
+ ) {
138
208
  this.settings = { ...options.initial };
139
- this.contextIndex = Math.max(0, options.presets.findIndex((preset) => preset.value === this.settings.contextMaxChars));
140
- this.effortIndex = Math.max(0, options.effortLevels.indexOf(this.settings.effort || "Default (Model Default)"));
209
+ this.contextIndex = Math.max(
210
+ 0,
211
+ options.presets.findIndex(
212
+ (preset) => preset.value === this.settings.contextMaxChars
213
+ )
214
+ );
215
+ this.effortIndex = Math.max(
216
+ 0,
217
+ options.effortLevels.indexOf(
218
+ this.settings.effort || "Default (Model Default)"
219
+ )
220
+ );
141
221
  this.customInput.onSubmit = (value) => {
142
222
  this.settings.customRule = value.trim() || undefined;
143
223
  this.editingCustom = false;
@@ -151,46 +231,116 @@ export class AdvisorSettingsSelector implements Component, Focusable {
151
231
  };
152
232
  }
153
233
 
154
- invalidate(): void { this.options.tui.requestRender(); }
234
+ invalidate(): void {
235
+ this.options.tui.requestRender();
236
+ }
155
237
 
156
- private currentContext() { return this.options.presets[this.contextIndex]; }
238
+ private currentContext() {
239
+ return this.options.presets[this.contextIndex];
240
+ }
157
241
  private row(label: string, value: string, index: number) {
158
242
  const { theme } = this.options;
159
243
  const prefix = index === this.selectedRow ? theme.fg("accent", "›") : " ";
160
244
  const text = `${prefix} ${label.padEnd(28)} ${value}`;
161
- return index === this.selectedRow ? theme.fg("accent", theme.bold(text)) : theme.fg("text", text);
245
+ return index === this.selectedRow
246
+ ? theme.fg("accent", theme.bold(text))
247
+ : theme.fg("text", text);
162
248
  }
163
249
 
164
250
  render(width: number): string[] {
165
251
  const { theme, presets } = this.options;
166
252
  const trackWidth = Math.max(24, Math.min(60, width - 4));
167
- const positions = presets.map((_, index) => Math.round(index * (trackWidth - 1) / (presets.length - 1)));
253
+ const positions = presets.map((_, index) =>
254
+ Math.round((index * (trackWidth - 1)) / Math.max(1, presets.length - 1))
255
+ );
168
256
  const track = Array.from({ length: trackWidth }, () => "─");
169
257
  track[positions[this.contextIndex]] = "▲";
170
258
  const labels = Array.from({ length: trackWidth }, () => " ");
171
259
  for (let index = 0; index < presets.length; index++) {
172
260
  const label = presets[index].label;
173
- const start = Math.max(0, Math.min(trackWidth - label.length, positions[index] - Math.floor(label.length / 2)));
174
- for (let char = 0; char < label.length; char++) labels[start + char] = label[char];
261
+ const start = Math.max(
262
+ 0,
263
+ Math.min(
264
+ trackWidth - label.length,
265
+ positions[index] - Math.floor(label.length / 2)
266
+ )
267
+ );
268
+ for (let char = 0; char < label.length; char++) {
269
+ labels[start + char] = label[char];
270
+ }
175
271
  }
176
272
  const heading = `Recent history${" ".repeat(Math.max(1, trackWidth - "Recent history".length - "Full branch".length))}Full branch`;
177
- const onOff = (value: boolean) => value ? "On" : "Off";
273
+ const onOff = (value: boolean) => (value ? "On" : "Off");
178
274
  const rows = [
179
275
  this.row("Context window", this.currentContext().label, 0),
180
- this.row("Advisor reasoning", this.options.effortLevels[this.effortIndex], 1),
276
+ this.row(
277
+ "Advisor reasoning",
278
+ this.options.effortLevels[this.effortIndex],
279
+ 1
280
+ ),
181
281
  this.row("Plan gate", onOff(this.settings.planGate), 2),
182
282
  this.row("Failure gate", onOff(this.settings.failureGate), 3),
183
283
  this.row("Completion gate", onOff(this.settings.completionGate), 4),
184
- this.row("Collapse long responses", onOff(this.settings.collapseResponses), 5),
284
+ this.row(
285
+ "Collapse long responses",
286
+ onOff(this.settings.collapseResponses),
287
+ 5
288
+ ),
185
289
  this.row("Custom invocation", this.settings.customRule || "None", 6),
186
- this.row("Block on critical advice", onOff(this.settings.blockOnBlocked ?? true), 7),
187
- this.row("Automatic loop gate", onOff(this.settings.autoLoopGate ?? true), 8),
188
- this.row("Loop threshold", `After ${this.settings.loopThreshold ?? 3} repeats`, 9),
189
- this.row("Max Advisor calls/session", this.settings.maxCallsPerSession === undefined ? "∞" : String(this.settings.maxCallsPerSession), 10),
190
- this.row("Session Advisor Summary", onOff(this.settings.sessionSummary ?? true), 11),
290
+ this.row(
291
+ "Block on critical advice",
292
+ onOff(this.settings.blockOnBlocked ?? true),
293
+ 7
294
+ ),
295
+ this.row(
296
+ "Automatic loop gate",
297
+ onOff(this.settings.autoLoopGate ?? true),
298
+ 8
299
+ ),
300
+ this.row(
301
+ "Loop threshold",
302
+ `After ${this.settings.loopThreshold ?? 3} repeats`,
303
+ 9
304
+ ),
305
+ this.row(
306
+ "Max Advisor calls/session",
307
+ this.settings.maxCallsPerSession === undefined
308
+ ? "∞"
309
+ : String(this.settings.maxCallsPerSession),
310
+ 10
311
+ ),
312
+ this.row(
313
+ "Session Advisor Summary",
314
+ onOff(this.settings.sessionSummary ?? true),
315
+ 11
316
+ ),
317
+ this.row(
318
+ "Gate failure mode",
319
+ this.settings.failureMode ?? "block-session",
320
+ 12
321
+ ),
322
+ this.row(
323
+ "Herdr integration",
324
+ onOff(this.settings.herdrIntegration ?? true),
325
+ 13
326
+ ),
327
+ this.row(
328
+ "Tool result lines",
329
+ String(this.settings.toolResultMaxLines ?? 2000),
330
+ 14
331
+ ),
332
+ this.row(
333
+ "Tool result bytes",
334
+ String(this.settings.toolResultMaxBytes ?? 50 * 1024),
335
+ 15
336
+ ),
191
337
  ];
192
- if (this.editingCustom) rows.push(` ${this.customInput.render(Math.max(10, width - 6))[0] || ""}`);
193
- rows.push(this.row("Save changes", "", 12));
338
+ if (this.editingCustom) {
339
+ rows.push(
340
+ ` ${this.customInput.render(Math.max(10, width - 6))[0] || ""}`
341
+ );
342
+ }
343
+ rows.push(this.row("Save changes", "", 16));
194
344
  return [
195
345
  theme.fg("accent", theme.bold(" Advisor settings")),
196
346
  "",
@@ -213,7 +363,7 @@ export class AdvisorSettingsSelector implements Component, Focusable {
213
363
  if (matchesKey(keyData, Key.up)) {
214
364
  this.selectedRow = Math.max(0, this.selectedRow - 1);
215
365
  } else if (matchesKey(keyData, Key.down)) {
216
- this.selectedRow = Math.min(12, this.selectedRow + 1);
366
+ this.selectedRow = Math.min(16, this.selectedRow + 1);
217
367
  } else if (matchesKey(keyData, Key.left)) {
218
368
  this.adjust(-1);
219
369
  } else if (matchesKey(keyData, Key.right)) {
@@ -226,32 +376,118 @@ export class AdvisorSettingsSelector implements Component, Focusable {
226
376
  tui.requestRender();
227
377
  return;
228
378
  }
229
- if (this.selectedRow === 12) return this.options.onSave({ ...this.settings, contextMaxChars: this.currentContext().value, effort: this.options.effortLevels[this.effortIndex] });
379
+ if (this.selectedRow === 16) {
380
+ return this.options.onSave({
381
+ ...this.settings,
382
+ contextMaxChars: this.currentContext().value,
383
+ effort: this.options.effortLevels[this.effortIndex],
384
+ });
385
+ }
230
386
  this.adjust(1);
231
387
  } else if (matchesKey(keyData, Key.escape)) {
232
388
  return this.options.onCancel();
233
- } else return;
389
+ } else {
390
+ return;
391
+ }
234
392
  tui.requestRender();
235
393
  }
236
394
 
237
395
  private adjust(direction: number) {
238
396
  switch (this.selectedRow) {
239
- case 0: this.contextIndex = Math.max(0, Math.min(this.options.presets.length - 1, this.contextIndex + direction)); break;
240
- case 1: this.effortIndex = Math.max(0, Math.min(this.options.effortLevels.length - 1, this.effortIndex + direction)); break;
241
- case 2: this.settings.planGate = !this.settings.planGate; break;
242
- case 3: this.settings.failureGate = !this.settings.failureGate; break;
243
- case 4: this.settings.completionGate = !this.settings.completionGate; break;
244
- case 5: this.settings.collapseResponses = !this.settings.collapseResponses; break;
245
- case 7: this.settings.blockOnBlocked = !(this.settings.blockOnBlocked ?? true); break;
246
- case 8: this.settings.autoLoopGate = !(this.settings.autoLoopGate ?? true); break;
247
- case 9: this.settings.loopThreshold = Math.max(2, (this.settings.loopThreshold ?? 3) + direction); break;
397
+ case 0:
398
+ this.contextIndex = Math.max(
399
+ 0,
400
+ Math.min(
401
+ this.options.presets.length - 1,
402
+ this.contextIndex + direction
403
+ )
404
+ );
405
+ break;
406
+ case 1:
407
+ this.effortIndex = Math.max(
408
+ 0,
409
+ Math.min(
410
+ this.options.effortLevels.length - 1,
411
+ this.effortIndex + direction
412
+ )
413
+ );
414
+ break;
415
+ case 2:
416
+ this.settings.planGate = !this.settings.planGate;
417
+ break;
418
+ case 3:
419
+ this.settings.failureGate = !this.settings.failureGate;
420
+ break;
421
+ case 4:
422
+ this.settings.completionGate = !this.settings.completionGate;
423
+ break;
424
+ case 5:
425
+ this.settings.collapseResponses = !this.settings.collapseResponses;
426
+ break;
427
+ case 7:
428
+ this.settings.blockOnBlocked = !(this.settings.blockOnBlocked ?? true);
429
+ break;
430
+ case 8:
431
+ this.settings.autoLoopGate = !(this.settings.autoLoopGate ?? true);
432
+ break;
433
+ case 9:
434
+ this.settings.loopThreshold = Math.max(
435
+ 2,
436
+ (this.settings.loopThreshold ?? 3) + direction
437
+ );
438
+ break;
248
439
  case 10: {
249
440
  const values = [undefined, 0, 1, 2, 3, 5, 10, 25, 50];
250
- const index = Math.max(0, values.indexOf(this.settings.maxCallsPerSession));
251
- this.settings.maxCallsPerSession = values[Math.max(0, Math.min(values.length - 1, index + direction))];
441
+ const index = Math.max(
442
+ 0,
443
+ values.indexOf(this.settings.maxCallsPerSession)
444
+ );
445
+ this.settings.maxCallsPerSession =
446
+ values[Math.max(0, Math.min(values.length - 1, index + direction))];
447
+ break;
448
+ }
449
+ case 11:
450
+ this.settings.sessionSummary = !(this.settings.sessionSummary ?? true);
451
+ break;
452
+ case 12: {
453
+ const modes: AdvisorSettings["failureMode"][] = [
454
+ "block-session",
455
+ "block-tool",
456
+ "warn-and-continue",
457
+ ];
458
+ const index = Math.max(
459
+ 0,
460
+ modes.indexOf(this.settings.failureMode ?? "block-session")
461
+ );
462
+ this.settings.failureMode =
463
+ modes[Math.max(0, Math.min(modes.length - 1, index + direction))];
464
+ break;
465
+ }
466
+ case 13:
467
+ this.settings.herdrIntegration = !(
468
+ this.settings.herdrIntegration ?? true
469
+ );
470
+ break;
471
+ case 14: {
472
+ const values = [0, 500, 1000, 2000, 5000, 10_000];
473
+ const index = Math.max(
474
+ 0,
475
+ values.indexOf(this.settings.toolResultMaxLines ?? 2000)
476
+ );
477
+ this.settings.toolResultMaxLines =
478
+ values[Math.max(0, Math.min(values.length - 1, index + direction))];
479
+ break;
480
+ }
481
+ case 15: {
482
+ const values = [0, 10 * 1024, 50 * 1024, 100 * 1024, 500 * 1024];
483
+ const index = Math.max(
484
+ 0,
485
+ values.indexOf(this.settings.toolResultMaxBytes ?? 50 * 1024)
486
+ );
487
+ this.settings.toolResultMaxBytes =
488
+ values[Math.max(0, Math.min(values.length - 1, index + direction))];
252
489
  break;
253
490
  }
254
- case 11: this.settings.sessionSummary = !(this.settings.sessionSummary ?? true); break;
255
491
  }
256
492
  }
257
493
  }