@oh-my-pi/pi-coding-agent 13.8.0 → 13.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/package.json +7 -7
  3. package/src/capability/rule.ts +0 -4
  4. package/src/cli/agents-cli.ts +1 -1
  5. package/src/cli/args.ts +7 -12
  6. package/src/commands/launch.ts +3 -2
  7. package/src/config/model-resolver.ts +106 -33
  8. package/src/config/settings-schema.ts +14 -2
  9. package/src/config/settings.ts +1 -17
  10. package/src/discovery/helpers.ts +10 -17
  11. package/src/export/html/template.generated.ts +1 -1
  12. package/src/export/html/template.js +37 -15
  13. package/src/extensibility/extensions/loader.ts +1 -2
  14. package/src/extensibility/extensions/types.ts +2 -1
  15. package/src/main.ts +20 -13
  16. package/src/modes/components/agent-dashboard.ts +12 -13
  17. package/src/modes/components/model-selector.ts +157 -59
  18. package/src/modes/components/read-tool-group.ts +36 -2
  19. package/src/modes/components/settings-defs.ts +11 -8
  20. package/src/modes/components/settings-selector.ts +1 -1
  21. package/src/modes/components/thinking-selector.ts +3 -15
  22. package/src/modes/controllers/selector-controller.ts +21 -7
  23. package/src/modes/rpc/rpc-client.ts +2 -2
  24. package/src/modes/rpc/rpc-types.ts +2 -2
  25. package/src/modes/theme/theme.ts +2 -1
  26. package/src/patch/hashline.ts +26 -3
  27. package/src/patch/index.ts +14 -16
  28. package/src/prompts/tools/read.md +2 -2
  29. package/src/sdk.ts +21 -29
  30. package/src/session/agent-session.ts +44 -37
  31. package/src/task/executor.ts +10 -8
  32. package/src/task/types.ts +1 -2
  33. package/src/tools/read.ts +88 -264
  34. package/src/utils/frontmatter.ts +25 -4
  35. package/src/web/scrapers/choosealicense.ts +1 -1
@@ -63,6 +63,7 @@ interface DashboardAgent extends AgentDefinition {
63
63
  interface ModelResolution {
64
64
  resolved: string;
65
65
  thinkingLevel?: string;
66
+ explicitThinkingLevel: boolean;
66
67
  }
67
68
 
68
69
  interface GeneratedAgentSpec {
@@ -109,6 +110,12 @@ function joinPatterns(patterns: string[]): string {
109
110
  return patterns.join(", ");
110
111
  }
111
112
 
113
+ function formatResolution(resolution: ModelResolution): string {
114
+ const resolved = theme.fg("success", resolution.resolved);
115
+ if (!resolution.explicitThinkingLevel || !resolution.thinkingLevel) return resolved;
116
+ return `${resolved} ${theme.fg("dim", `(${resolution.thinkingLevel})`)}`;
117
+ }
118
+
112
119
  function matchAgent(agent: DashboardAgent, query: string): boolean {
113
120
  const q = query.toLowerCase();
114
121
  if (agent.name.toLowerCase().includes(q)) return true;
@@ -289,10 +296,7 @@ class AgentInspectorPane implements Component {
289
296
  }
290
297
 
291
298
  #formatResolution(resolution: ModelResolution): string {
292
- if (resolution.thinkingLevel && resolution.thinkingLevel !== "off") {
293
- return `${theme.fg("success", resolution.resolved)} ${theme.fg("dim", `(${resolution.thinkingLevel})`)}`;
294
- }
295
- return theme.fg("success", resolution.resolved);
299
+ return formatResolution(resolution);
296
300
  }
297
301
 
298
302
  invalidate(): void {}
@@ -770,7 +774,7 @@ export class AgentDashboard extends Container {
770
774
  #resolvePatterns(patterns: string[]): ModelResolution | undefined {
771
775
  const modelRegistry = this.modelContext.modelRegistry;
772
776
  if (!modelRegistry || patterns.length === 0) return undefined;
773
- const { model, thinkingLevel } = resolveModelOverride(
777
+ const { model, thinkingLevel, explicitThinkingLevel } = resolveModelOverride(
774
778
  patterns,
775
779
  modelRegistry,
776
780
  this.#settingsManager ?? undefined,
@@ -779,6 +783,7 @@ export class AgentDashboard extends Container {
779
783
  return {
780
784
  resolved: formatModelString(model),
781
785
  thinkingLevel,
786
+ explicitThinkingLevel,
782
787
  };
783
788
  }
784
789
 
@@ -930,20 +935,14 @@ export class AgentDashboard extends Container {
930
935
  );
931
936
  this.addChild(
932
937
  new Text(
933
- theme.fg(
934
- "muted",
935
- `Default resolves: ${defaultResolution ? defaultResolution.resolved : "(unresolved)"}`,
936
- ),
938
+ `${theme.fg("muted", "Default resolves:")} ${defaultResolution ? formatResolution(defaultResolution) : theme.fg("dim", "(unresolved)")}`,
937
939
  0,
938
940
  0,
939
941
  ),
940
942
  );
941
943
  this.addChild(
942
944
  new Text(
943
- theme.fg(
944
- "muted",
945
- `Preview effective: ${previewResolution ? previewResolution.resolved : "(unresolved)"}`,
946
- ),
945
+ `${theme.fg("muted", "Preview effective:")} ${previewResolution ? formatResolution(previewResolution) : theme.fg("dim", "(unresolved)")}`,
947
946
  0,
948
947
  0,
949
948
  ),
@@ -1,7 +1,14 @@
1
- import { type Model, modelsAreEqual } from "@oh-my-pi/pi-ai";
1
+ import {
2
+ getAvailableThinkingLevels,
3
+ getThinkingMetadata,
4
+ type Model,
5
+ modelsAreEqual,
6
+ supportsXhigh,
7
+ type ThinkingMode,
8
+ } from "@oh-my-pi/pi-ai";
2
9
  import { Container, Input, matchesKey, Spacer, type Tab, TabBar, Text, type TUI, visibleWidth } from "@oh-my-pi/pi-tui";
3
10
  import { MODEL_ROLE_IDS, MODEL_ROLES, type ModelRegistry, type ModelRole } from "../../config/model-registry";
4
- import { parseModelString } from "../../config/model-resolver";
11
+ import { resolveModelRoleValue } from "../../config/model-resolver";
5
12
  import type { Settings } from "../../config/settings";
6
13
  import { type ThemeColor, theme } from "../../modes/theme/theme";
7
14
  import { fuzzyFilter } from "../../utils/fuzzy";
@@ -25,12 +32,26 @@ interface ScopedModelItem {
25
32
  thinkingLevel: string;
26
33
  }
27
34
 
28
- interface MenuAction {
35
+ interface RoleAssignment {
36
+ model: Model;
37
+ thinkingMode: ThinkingMode;
38
+ }
39
+
40
+ type RoleSelectCallback = (model: Model, role: ModelRole | null, thinkingMode?: ThinkingMode) => void;
41
+ type CancelCallback = () => void;
42
+ interface MenuRoleAction {
29
43
  label: string;
30
44
  role: ModelRole;
31
45
  }
32
46
 
33
- const MENU_ACTIONS: MenuAction[] = MODEL_ROLE_IDS.map(role => ({ label: `Set as ${MODEL_ROLES[role].name}`, role }));
47
+ const MENU_ROLE_ACTIONS: MenuRoleAction[] = MODEL_ROLE_IDS.map(role => {
48
+ const roleInfo = MODEL_ROLES[role];
49
+ const roleLabel = roleInfo.tag ? `${roleInfo.tag} (${roleInfo.name})` : roleInfo.name;
50
+ return {
51
+ label: `Set as ${roleLabel}`,
52
+ role,
53
+ };
54
+ });
34
55
 
35
56
  const ALL_TAB = "ALL";
36
57
 
@@ -50,11 +71,11 @@ export class ModelSelectorComponent extends Container {
50
71
  #allModels: ModelItem[] = [];
51
72
  #filteredModels: ModelItem[] = [];
52
73
  #selectedIndex: number = 0;
53
- #roles: { [key in ModelRole]?: Model } = {};
54
- #settings: Settings;
55
- #modelRegistry: ModelRegistry;
56
- #onSelectCallback: (model: Model, role: ModelRole | null) => void;
57
- #onCancelCallback: () => void;
74
+ #roles = {} as Record<ModelRole, RoleAssignment | undefined>;
75
+ #settings = null as unknown as Settings;
76
+ #modelRegistry = null as unknown as ModelRegistry;
77
+ #onSelectCallback = (() => {}) as RoleSelectCallback;
78
+ #onCancelCallback = (() => {}) as CancelCallback;
58
79
  #errorMessage?: unknown;
59
80
  #tui: TUI;
60
81
  #scopedModels: ReadonlyArray<ScopedModelItem>;
@@ -67,6 +88,8 @@ export class ModelSelectorComponent extends Container {
67
88
  // Context menu state
68
89
  #isMenuOpen: boolean = false;
69
90
  #menuSelectedIndex: number = 0;
91
+ #menuStep: "role" | "thinking" = "role";
92
+ #menuSelectedRole: ModelRole | null = null;
70
93
 
71
94
  constructor(
72
95
  tui: TUI,
@@ -74,7 +97,7 @@ export class ModelSelectorComponent extends Container {
74
97
  settings: Settings,
75
98
  modelRegistry: ModelRegistry,
76
99
  scopedModels: ReadonlyArray<ScopedModelItem>,
77
- onSelect: (model: Model, role: ModelRole | null) => void,
100
+ onSelect: (model: Model, role: ModelRole | null, thinkingMode?: ThinkingMode) => void,
78
101
  onCancel: () => void,
79
102
  options?: { temporaryOnly?: boolean; initialSearchInput?: string },
80
103
  ) {
@@ -157,15 +180,20 @@ export class ModelSelectorComponent extends Container {
157
180
 
158
181
  #loadRoleModels(): void {
159
182
  const allModels = this.#modelRegistry.getAll();
183
+ const matchPreferences = { usageOrder: this.#settings.getStorage()?.getModelUsageOrder() };
160
184
  for (const role of MODEL_ROLE_IDS) {
161
- const modelId = this.#settings.getModelRole(role);
162
- if (!modelId) continue;
163
- const parsed = parseModelString(modelId);
164
- if (parsed) {
165
- const model = allModels.find(m => m.provider === parsed.provider && m.id === parsed.id);
166
- if (model) {
167
- this.#roles[role] = model;
168
- }
185
+ const roleValue = this.#settings.getModelRole(role);
186
+ if (!roleValue) continue;
187
+
188
+ const { model, thinkingLevel, explicitThinkingLevel } = resolveModelRoleValue(roleValue, allModels, {
189
+ settings: this.#settings,
190
+ matchPreferences,
191
+ });
192
+ if (model) {
193
+ this.#roles[role] = {
194
+ model,
195
+ thinkingMode: explicitThinkingLevel && thinkingLevel !== undefined ? thinkingLevel : "inherit",
196
+ };
169
197
  }
170
198
  }
171
199
  }
@@ -179,7 +207,8 @@ export class ModelSelectorComponent extends Container {
179
207
  let i = 0;
180
208
  while (i < MODEL_ROLE_IDS.length) {
181
209
  const role = MODEL_ROLE_IDS[i];
182
- if (this.#roles[role] && modelsAreEqual(this.#roles[role], model.model)) {
210
+ const assigned = this.#roles[role];
211
+ if (assigned && modelsAreEqual(assigned.model, model.model)) {
183
212
  break;
184
213
  }
185
214
  i++;
@@ -373,14 +402,17 @@ export class ModelSelectorComponent extends Container {
373
402
  const isSelected = i === this.#selectedIndex;
374
403
 
375
404
  // Build role badges (inverted: color as background, black text)
376
- const badges: string[] = [];
405
+ const roleBadgeTokens: string[] = [];
377
406
  for (const role of MODEL_ROLE_IDS) {
378
407
  const { tag, color } = MODEL_ROLES[role];
379
- if (tag && modelsAreEqual(this.#roles[role], item.model)) {
380
- badges.push(makeInvertedBadge(tag, color ?? "success"));
381
- }
408
+ const assigned = this.#roles[role];
409
+ if (!tag || !assigned || !modelsAreEqual(assigned.model, item.model)) continue;
410
+
411
+ const badge = makeInvertedBadge(tag, color ?? "success");
412
+ const thinkingLabel = getThinkingMetadata(assigned.thinkingMode).label;
413
+ roleBadgeTokens.push(`${badge} ${theme.fg("dim", `(${thinkingLabel})`)}`);
382
414
  }
383
- const badgeText = badges.length > 0 ? ` ${badges.join(" ")}` : "";
415
+ const badgeText = roleBadgeTokens.length > 0 ? ` ${roleBadgeTokens.join(" ")}` : "";
384
416
 
385
417
  let line = "";
386
418
  if (isSelected) {
@@ -424,17 +456,36 @@ export class ModelSelectorComponent extends Container {
424
456
  this.#listContainer.addChild(new Text(theme.fg("muted", ` Model Name: ${selected.model.name}`), 0, 0));
425
457
  }
426
458
  }
459
+ #getThinkingModesForModel(model: Model): ReadonlyArray<ThinkingMode> {
460
+ return ["inherit", ...getAvailableThinkingLevels(supportsXhigh(model))];
461
+ }
462
+
463
+ #getCurrentRoleThinkingMode(role: ModelRole): ThinkingMode {
464
+ return this.#roles[role]?.thinkingMode ?? "inherit";
465
+ }
466
+
467
+ #getThinkingPreselectIndex(role: ModelRole, model: Model): number {
468
+ const options = this.#getThinkingModesForModel(model);
469
+ const currentMode = this.#getCurrentRoleThinkingMode(role);
470
+ const preferredMode = currentMode === "xhigh" && !options.includes("xhigh") ? "high" : currentMode;
471
+ const foundIndex = options.indexOf(preferredMode);
472
+ return foundIndex >= 0 ? foundIndex : 0;
473
+ }
427
474
 
428
475
  #openMenu(): void {
429
476
  if (this.#filteredModels.length === 0) return;
430
477
 
431
478
  this.#isMenuOpen = true;
479
+ this.#menuStep = "role";
480
+ this.#menuSelectedRole = null;
432
481
  this.#menuSelectedIndex = 0;
433
482
  this.#updateMenu();
434
483
  }
435
484
 
436
485
  #closeMenu(): void {
437
486
  this.#isMenuOpen = false;
487
+ this.#menuStep = "role";
488
+ this.#menuSelectedRole = null;
438
489
  this.#menuContainer.clear();
439
490
  }
440
491
 
@@ -444,35 +495,53 @@ export class ModelSelectorComponent extends Container {
444
495
  const selectedModel = this.#filteredModels[this.#selectedIndex];
445
496
  if (!selectedModel) return;
446
497
 
447
- const headerText = ` Action for: ${selectedModel.id}`;
448
- const hintText = " Enter: confirm Esc: cancel";
449
- const actionLines = MENU_ACTIONS.map((action, index) => {
450
- const prefix = index === this.#menuSelectedIndex ? ` ${theme.nav.cursor} ` : " ";
451
- return `${prefix}${action.label}`;
452
- });
498
+ const showingThinking = this.#menuStep === "thinking" && this.#menuSelectedRole !== null;
499
+ const thinkingOptions = showingThinking ? this.#getThinkingModesForModel(selectedModel.model) : [];
500
+ const optionLines = showingThinking
501
+ ? thinkingOptions.map((thinkingMode, index) => {
502
+ const prefix = index === this.#menuSelectedIndex ? ` ${theme.nav.cursor} ` : " ";
503
+ const label = getThinkingMetadata(thinkingMode).label;
504
+ return `${prefix}${label}`;
505
+ })
506
+ : MENU_ROLE_ACTIONS.map((action, index) => {
507
+ const prefix = index === this.#menuSelectedIndex ? ` ${theme.nav.cursor} ` : " ";
508
+ return `${prefix}${action.label}`;
509
+ });
510
+
511
+ const selectedRoleName = this.#menuSelectedRole ? MODEL_ROLES[this.#menuSelectedRole].name : "";
512
+ const headerText =
513
+ showingThinking && this.#menuSelectedRole
514
+ ? ` Thinking for: ${selectedRoleName} (${selectedModel.id})`
515
+ : ` Action for: ${selectedModel.id}`;
516
+ const hintText = showingThinking ? " Enter: confirm Esc: back" : " Enter: continue Esc: cancel";
453
517
  const menuWidth = Math.max(
454
518
  visibleWidth(headerText),
455
519
  visibleWidth(hintText),
456
- ...actionLines.map(line => visibleWidth(line)),
520
+ ...optionLines.map(line => visibleWidth(line)),
457
521
  );
458
522
 
459
- // Menu header
460
523
  this.#menuContainer.addChild(new Spacer(1));
461
524
  this.#menuContainer.addChild(new Text(theme.fg("border", theme.boxSharp.horizontal.repeat(menuWidth)), 0, 0));
462
- this.#menuContainer.addChild(new Text(theme.fg("text", ` Action for: ${theme.bold(selectedModel.id)}`), 0, 0));
525
+ if (showingThinking && this.#menuSelectedRole) {
526
+ this.#menuContainer.addChild(
527
+ new Text(
528
+ theme.fg("text", ` Thinking for: ${theme.bold(selectedRoleName)} (${theme.bold(selectedModel.id)})`),
529
+ 0,
530
+ 0,
531
+ ),
532
+ );
533
+ } else {
534
+ this.#menuContainer.addChild(
535
+ new Text(theme.fg("text", ` Action for: ${theme.bold(selectedModel.id)}`), 0, 0),
536
+ );
537
+ }
463
538
  this.#menuContainer.addChild(new Spacer(1));
464
539
 
465
- // Menu options
466
- for (let i = 0; i < MENU_ACTIONS.length; i++) {
467
- const action = MENU_ACTIONS[i]!;
540
+ for (let i = 0; i < optionLines.length; i++) {
541
+ const lineText = optionLines[i];
542
+ if (!lineText) continue;
468
543
  const isSelected = i === this.#menuSelectedIndex;
469
-
470
- let line: string;
471
- if (isSelected) {
472
- line = theme.fg("accent", ` ${theme.nav.cursor} ${action.label}`);
473
- } else {
474
- line = theme.fg("muted", ` ${action.label}`);
475
- }
544
+ const line = isSelected ? theme.fg("accent", lineText) : theme.fg("muted", lineText);
476
545
  this.#menuContainer.addChild(new Text(line, 0, 0));
477
546
  }
478
547
 
@@ -532,55 +601,84 @@ export class ModelSelectorComponent extends Container {
532
601
  this.#searchInput.handleInput(keyData);
533
602
  this.#filterModels(this.#searchInput.getValue());
534
603
  }
535
-
536
604
  #handleMenuInput(keyData: string): void {
537
- // Up arrow - navigate menu
605
+ const selectedModel = this.#filteredModels[this.#selectedIndex];
606
+ if (!selectedModel) return;
607
+
608
+ const optionCount =
609
+ this.#menuStep === "thinking" && this.#menuSelectedRole !== null
610
+ ? this.#getThinkingModesForModel(selectedModel.model).length
611
+ : MENU_ROLE_ACTIONS.length;
612
+ if (optionCount === 0) return;
613
+
538
614
  if (matchesKey(keyData, "up")) {
539
- this.#menuSelectedIndex = (this.#menuSelectedIndex - 1 + MENU_ACTIONS.length) % MENU_ACTIONS.length;
615
+ this.#menuSelectedIndex = (this.#menuSelectedIndex - 1 + optionCount) % optionCount;
540
616
  this.#updateMenu();
541
617
  return;
542
618
  }
543
619
 
544
- // Down arrow - navigate menu
545
620
  if (matchesKey(keyData, "down")) {
546
- this.#menuSelectedIndex = (this.#menuSelectedIndex + 1) % MENU_ACTIONS.length;
621
+ this.#menuSelectedIndex = (this.#menuSelectedIndex + 1) % optionCount;
547
622
  this.#updateMenu();
548
623
  return;
549
624
  }
550
625
 
551
- // Enter - confirm selection
552
626
  if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
553
- const selectedModel = this.#filteredModels[this.#selectedIndex];
554
- const action = MENU_ACTIONS[this.#menuSelectedIndex];
555
- if (selectedModel && action) {
556
- this.#handleSelect(selectedModel.model, action.role);
557
- this.#closeMenu();
627
+ if (this.#menuStep === "role") {
628
+ const action = MENU_ROLE_ACTIONS[this.#menuSelectedIndex];
629
+ if (!action) return;
630
+ this.#menuSelectedRole = action.role;
631
+ this.#menuStep = "thinking";
632
+ this.#menuSelectedIndex = this.#getThinkingPreselectIndex(action.role, selectedModel.model);
633
+ this.#updateMenu();
634
+ return;
558
635
  }
636
+
637
+ if (!this.#menuSelectedRole) return;
638
+ const thinkingOptions = this.#getThinkingModesForModel(selectedModel.model);
639
+ const thinkingMode = thinkingOptions[this.#menuSelectedIndex];
640
+ if (!thinkingMode) return;
641
+ this.#handleSelect(selectedModel.model, this.#menuSelectedRole, thinkingMode);
642
+ this.#closeMenu();
559
643
  return;
560
644
  }
561
645
 
562
- // Escape or Ctrl+C - close menu only
563
646
  if (matchesKey(keyData, "escape") || matchesKey(keyData, "esc") || matchesKey(keyData, "ctrl+c")) {
647
+ if (this.#menuStep === "thinking" && this.#menuSelectedRole !== null) {
648
+ this.#menuStep = "role";
649
+ const roleIndex = MENU_ROLE_ACTIONS.findIndex(action => action.role === this.#menuSelectedRole);
650
+ this.#menuSelectedRole = null;
651
+ this.#menuSelectedIndex = roleIndex >= 0 ? roleIndex : 0;
652
+ this.#updateMenu();
653
+ return;
654
+ }
564
655
  this.#closeMenu();
565
656
  return;
566
657
  }
567
658
  }
568
659
 
569
- #handleSelect(model: Model, role: ModelRole | null): void {
660
+ #formatRoleModelValue(model: Model, thinkingMode: ThinkingMode): string {
661
+ const modelKey = `${model.provider}/${model.id}`;
662
+ if (thinkingMode === "inherit") return modelKey;
663
+ return `${modelKey}:${thinkingMode}`;
664
+ }
665
+ #handleSelect(model: Model, role: ModelRole | null, thinkingMode?: ThinkingMode): void {
570
666
  // For temporary role, don't save to settings - just notify caller
571
667
  if (role === null) {
572
668
  this.#onSelectCallback(model, null);
573
669
  return;
574
670
  }
575
671
 
672
+ const selectedThinkingMode = thinkingMode ?? this.#getCurrentRoleThinkingMode(role);
673
+
576
674
  // Save to settings
577
- this.#settings.setModelRole(role, `${model.provider}/${model.id}`);
675
+ this.#settings.setModelRole(role, this.#formatRoleModelValue(model, selectedThinkingMode));
578
676
 
579
677
  // Update local state for UI
580
- this.#roles[role] = model;
678
+ this.#roles[role] = { model, thinkingMode: selectedThinkingMode };
581
679
 
582
680
  // Notify caller (for updating agent state if needed)
583
- this.#onSelectCallback(model, role);
681
+ this.#onSelectCallback(model, role, selectedThinkingMode);
584
682
 
585
683
  // Update list to show new badges
586
684
  this.#updateList();
@@ -11,12 +11,32 @@ type ReadRenderArgs = {
11
11
  limit?: number;
12
12
  };
13
13
 
14
+ type ReadToolSuffixResolution = {
15
+ from: string;
16
+ to: string;
17
+ };
18
+
19
+ type ReadToolResultDetails = {
20
+ suffixResolution?: {
21
+ from?: string;
22
+ to?: string;
23
+ };
24
+ };
25
+
26
+ function getSuffixResolution(details: ReadToolResultDetails | undefined): ReadToolSuffixResolution | undefined {
27
+ if (typeof details?.suffixResolution?.from !== "string" || typeof details.suffixResolution.to !== "string") {
28
+ return undefined;
29
+ }
30
+ return { from: details.suffixResolution.from, to: details.suffixResolution.to };
31
+ }
32
+
14
33
  type ReadEntry = {
15
34
  toolCallId: string;
16
35
  path: string;
17
36
  offset?: number;
18
37
  limit?: number;
19
- status: "pending" | "success" | "error";
38
+ status: "pending" | "success" | "warning" | "error";
39
+ correctedFrom?: string;
20
40
  };
21
41
 
22
42
  export class ReadToolGroupComponent extends Container implements ToolExecutionHandle {
@@ -56,7 +76,15 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
56
76
  const entry = this.#entries.get(toolCallId);
57
77
  if (!entry) return;
58
78
  if (isPartial) return;
59
- entry.status = result.isError ? "error" : "success";
79
+ const details = result.details as ReadToolResultDetails | undefined;
80
+ const suffixResolution = getSuffixResolution(details);
81
+ if (suffixResolution) {
82
+ entry.path = suffixResolution.to;
83
+ entry.correctedFrom = suffixResolution.from;
84
+ } else {
85
+ entry.correctedFrom = undefined;
86
+ }
87
+ entry.status = result.isError ? "error" : suffixResolution ? "warning" : "success";
60
88
  this.#updateDisplay();
61
89
  }
62
90
 
@@ -109,6 +137,9 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
109
137
  const endLine = entry.limit !== undefined ? startLine + entry.limit - 1 : "";
110
138
  pathDisplay += theme.fg("warning", `:${startLine}${endLine ? `-${endLine}` : ""}`);
111
139
  }
140
+ if (entry.correctedFrom) {
141
+ pathDisplay += theme.fg("dim", ` (corrected from ${shortenPath(entry.correctedFrom)})`);
142
+ }
112
143
  return pathDisplay;
113
144
  }
114
145
 
@@ -116,6 +147,9 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
116
147
  if (status === "success") {
117
148
  return theme.fg("success", theme.status.success);
118
149
  }
150
+ if (status === "warning") {
151
+ return theme.fg("warning", theme.status.warning);
152
+ }
119
153
  if (status === "error") {
120
154
  return theme.fg("error", theme.status.error);
121
155
  }
@@ -6,6 +6,8 @@
6
6
  * 1. Add it to settings-schema.ts with a `ui` field
7
7
  * 2. That's it - it appears in the UI automatically
8
8
  */
9
+
10
+ import { getAvailableThinkingLevels, getThinkingMetadata } from "@oh-my-pi/pi-ai";
9
11
  import { TERMINAL } from "@oh-my-pi/pi-tui";
10
12
  import {
11
13
  getDefault,
@@ -167,6 +169,14 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
167
169
  { value: "300", label: "5 minutes" },
168
170
  { value: "600", label: "10 minutes" },
169
171
  ],
172
+ // Read line limit
173
+ "read.defaultLimit": [
174
+ { value: "200", label: "200 lines" },
175
+ { value: "300", label: "300 lines" },
176
+ { value: "500", label: "500 lines" },
177
+ { value: "1000", label: "1000 lines" },
178
+ { value: "5000", label: "5000 lines" },
179
+ ],
170
180
  // Edit fuzzy threshold
171
181
  "edit.fuzzyThreshold": [
172
182
  { value: "0.85", label: "0.85", description: "Lenient" },
@@ -221,14 +231,7 @@ const OPTION_PROVIDERS: Partial<Record<SettingPath, OptionProvider>> = {
221
231
  { value: "on", label: "On", description: "Force websockets for OpenAI Codex models" },
222
232
  ],
223
233
  // Default thinking level
224
- defaultThinkingLevel: [
225
- { value: "off", label: "off", description: "No reasoning" },
226
- { value: "minimal", label: "minimal", description: "Very brief (~1k tokens)" },
227
- { value: "low", label: "low", description: "Light (~2k tokens)" },
228
- { value: "medium", label: "medium", description: "Moderate (~8k tokens)" },
229
- { value: "high", label: "high", description: "Deep (~16k tokens)" },
230
- { value: "xhigh", label: "xhigh", description: "Maximum (~32k tokens)" },
231
- ],
234
+ defaultThinkingLevel: [...getAvailableThinkingLevels().map(getThinkingMetadata)],
232
235
  // Temperature
233
236
  temperature: [
234
237
  { value: "-1", label: "Default", description: "Use provider default" },
@@ -1,4 +1,4 @@
1
- import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
1
+ import type { ThinkingLevel } from "@oh-my-pi/pi-ai";
2
2
  import {
3
3
  Container,
4
4
  matchesKey,
@@ -1,17 +1,9 @@
1
- import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
1
+ import { getThinkingMetadata, type ThinkingLevel } from "@oh-my-pi/pi-ai";
2
+
2
3
  import { Container, type SelectItem, SelectList } from "@oh-my-pi/pi-tui";
3
4
  import { getSelectListTheme } from "../../modes/theme/theme";
4
5
  import { DynamicBorder } from "./dynamic-border";
5
6
 
6
- const LEVEL_DESCRIPTIONS: Record<ThinkingLevel, string> = {
7
- off: "No reasoning",
8
- minimal: "Very brief reasoning (~1k tokens)",
9
- low: "Light reasoning (~2k tokens)",
10
- medium: "Moderate reasoning (~8k tokens)",
11
- high: "Deep reasoning (~16k tokens)",
12
- xhigh: "Maximum reasoning (~32k tokens)",
13
- };
14
-
15
7
  /**
16
8
  * Component that renders a thinking level selector with borders
17
9
  */
@@ -26,11 +18,7 @@ export class ThinkingSelectorComponent extends Container {
26
18
  ) {
27
19
  super();
28
20
 
29
- const thinkingLevels: SelectItem[] = availableLevels.map(level => ({
30
- value: level,
31
- label: level,
32
- description: LEVEL_DESCRIPTIONS[level],
33
- }));
21
+ const thinkingLevels: SelectItem[] = availableLevels.map(getThinkingMetadata);
34
22
 
35
23
  // Add top border
36
24
  this.addChild(new DynamicBorder());
@@ -1,5 +1,4 @@
1
- import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
- import { getOAuthProviders, type OAuthProvider } from "@oh-my-pi/pi-ai";
1
+ import { getOAuthProviders, type OAuthProvider, type ThinkingLevel } from "@oh-my-pi/pi-ai";
3
2
  import type { Component } from "@oh-my-pi/pi-tui";
4
3
  import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
5
4
  import { getAgentDbPath, getProjectDir } from "@oh-my-pi/pi-utils";
@@ -76,7 +75,7 @@ export class SelectorController {
76
75
  this.showSelector(done => {
77
76
  const selector = new SettingsSelectorComponent(
78
77
  {
79
- availableThinkingLevels: this.ctx.session.getAvailableThinkingLevels(),
78
+ availableThinkingLevels: [...this.ctx.session.getAvailableThinkingLevels()],
80
79
  thinkingLevel: this.ctx.session.thinkingLevel,
81
80
  availableThemes,
82
81
  cwd: getProjectDir(),
@@ -342,12 +341,24 @@ export class SelectorController {
342
341
  }
343
342
 
344
343
  // Provider settings - update runtime preferences
345
- case "webSearchProvider":
344
+ case "providers.webSearch":
346
345
  setPreferredSearchProvider(
347
- value as "auto" | "exa" | "jina" | "zai" | "perplexity" | "anthropic" | "gemini" | "codex",
346
+ value as
347
+ | "auto"
348
+ | "exa"
349
+ | "brave"
350
+ | "jina"
351
+ | "kimi"
352
+ | "zai"
353
+ | "perplexity"
354
+ | "anthropic"
355
+ | "gemini"
356
+ | "codex"
357
+ | "kagi"
358
+ | "synthetic",
348
359
  );
349
360
  break;
350
- case "imageProvider":
361
+ case "providers.image":
351
362
  setPreferredImageProvider(value as "auto" | "gemini" | "openrouter");
352
363
  break;
353
364
 
@@ -369,7 +380,7 @@ export class SelectorController {
369
380
  this.ctx.settings,
370
381
  this.ctx.session.modelRegistry,
371
382
  this.ctx.session.scopedModels,
372
- async (model, role) => {
383
+ async (model, role, thinkingMode) => {
373
384
  try {
374
385
  if (role === null) {
375
386
  // Temporary: update agent state but don't persist to settings
@@ -382,6 +393,9 @@ export class SelectorController {
382
393
  } else if (role === "default") {
383
394
  // Default: update agent state and persist
384
395
  await this.ctx.session.setModel(model, role);
396
+ if (thinkingMode && thinkingMode !== "inherit") {
397
+ this.ctx.session.setThinkingLevel(thinkingMode as ThinkingLevel);
398
+ }
385
399
  this.ctx.statusLine.invalidate();
386
400
  this.ctx.updateEditorBorderColor();
387
401
  this.ctx.showStatus(`Default model: ${model.id}`);
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Spawns the agent in RPC mode and provides a typed API for all operations.
5
5
  */
6
- import type { AgentEvent, AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
7
- import type { ImageContent } from "@oh-my-pi/pi-ai";
6
+ import type { AgentEvent, AgentMessage } from "@oh-my-pi/pi-agent-core";
7
+ import type { ImageContent, ThinkingLevel } from "@oh-my-pi/pi-ai";
8
8
  import { isRecord, ptree, readJsonl } from "@oh-my-pi/pi-utils";
9
9
  import type { BashResult } from "../../exec/bash-executor";
10
10
  import type { SessionStats } from "../../session/agent-session";
@@ -4,8 +4,8 @@
4
4
  * Commands are sent as JSON lines on stdin.
5
5
  * Responses and events are emitted as JSON lines on stdout.
6
6
  */
7
- import type { AgentMessage, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
8
- import type { ImageContent, Model } from "@oh-my-pi/pi-ai";
7
+ import type { AgentMessage } from "@oh-my-pi/pi-agent-core";
8
+ import type { ImageContent, Model, ThinkingLevel } from "@oh-my-pi/pi-ai";
9
9
  import type { BashResult } from "../../exec/bash-executor";
10
10
  import type { SessionStats } from "../../session/agent-session";
11
11
  import type { CompactionResult } from "../../session/compaction";