@oh-my-pi/pi-coding-agent 3.37.0 → 4.0.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 (70) hide show
  1. package/CHANGELOG.md +89 -0
  2. package/README.md +44 -3
  3. package/docs/extensions.md +29 -4
  4. package/docs/sdk.md +3 -3
  5. package/package.json +5 -5
  6. package/src/cli/args.ts +8 -0
  7. package/src/config.ts +5 -15
  8. package/src/core/agent-session.ts +193 -47
  9. package/src/core/auth-storage.ts +16 -3
  10. package/src/core/bash-executor.ts +79 -14
  11. package/src/core/custom-commands/types.ts +1 -1
  12. package/src/core/custom-tools/types.ts +1 -1
  13. package/src/core/export-html/index.ts +33 -1
  14. package/src/core/export-html/template.css +99 -0
  15. package/src/core/export-html/template.generated.ts +1 -1
  16. package/src/core/export-html/template.js +133 -8
  17. package/src/core/extensions/index.ts +22 -4
  18. package/src/core/extensions/loader.ts +152 -214
  19. package/src/core/extensions/runner.ts +139 -79
  20. package/src/core/extensions/types.ts +143 -19
  21. package/src/core/extensions/wrapper.ts +5 -8
  22. package/src/core/hooks/types.ts +1 -1
  23. package/src/core/index.ts +2 -1
  24. package/src/core/keybindings.ts +4 -1
  25. package/src/core/model-registry.ts +1 -1
  26. package/src/core/model-resolver.ts +35 -26
  27. package/src/core/sdk.ts +96 -76
  28. package/src/core/settings-manager.ts +45 -14
  29. package/src/core/system-prompt.ts +5 -15
  30. package/src/core/tools/bash.ts +115 -54
  31. package/src/core/tools/find.ts +86 -7
  32. package/src/core/tools/grep.ts +27 -6
  33. package/src/core/tools/index.ts +15 -6
  34. package/src/core/tools/ls.ts +49 -18
  35. package/src/core/tools/render-utils.ts +2 -1
  36. package/src/core/tools/task/worker.ts +35 -12
  37. package/src/core/tools/web-search/auth.ts +37 -32
  38. package/src/core/tools/web-search/providers/anthropic.ts +35 -22
  39. package/src/index.ts +101 -9
  40. package/src/main.ts +60 -20
  41. package/src/migrations.ts +47 -2
  42. package/src/modes/index.ts +2 -2
  43. package/src/modes/interactive/components/assistant-message.ts +25 -7
  44. package/src/modes/interactive/components/bash-execution.ts +5 -0
  45. package/src/modes/interactive/components/branch-summary-message.ts +5 -0
  46. package/src/modes/interactive/components/compaction-summary-message.ts +5 -0
  47. package/src/modes/interactive/components/countdown-timer.ts +38 -0
  48. package/src/modes/interactive/components/custom-editor.ts +8 -0
  49. package/src/modes/interactive/components/custom-message.ts +5 -0
  50. package/src/modes/interactive/components/footer.ts +2 -5
  51. package/src/modes/interactive/components/hook-input.ts +29 -20
  52. package/src/modes/interactive/components/hook-selector.ts +52 -38
  53. package/src/modes/interactive/components/index.ts +39 -0
  54. package/src/modes/interactive/components/login-dialog.ts +160 -0
  55. package/src/modes/interactive/components/model-selector.ts +10 -2
  56. package/src/modes/interactive/components/session-selector.ts +5 -1
  57. package/src/modes/interactive/components/settings-defs.ts +9 -0
  58. package/src/modes/interactive/components/status-line/segments.ts +3 -3
  59. package/src/modes/interactive/components/tool-execution.ts +9 -16
  60. package/src/modes/interactive/components/tree-selector.ts +1 -6
  61. package/src/modes/interactive/interactive-mode.ts +466 -215
  62. package/src/modes/interactive/theme/theme.ts +50 -2
  63. package/src/modes/print-mode.ts +78 -31
  64. package/src/modes/rpc/rpc-mode.ts +186 -78
  65. package/src/modes/rpc/rpc-types.ts +10 -3
  66. package/src/prompts/system-prompt.md +36 -28
  67. package/src/utils/clipboard.ts +90 -50
  68. package/src/utils/image-convert.ts +1 -1
  69. package/src/utils/image-resize.ts +1 -1
  70. package/src/utils/tools-manager.ts +2 -2
@@ -8,14 +8,17 @@
8
8
  * - Interact with the user via UI primitives
9
9
  */
10
10
 
11
- import type { AgentMessage, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
11
+ import type { AgentMessage, AgentToolResult, AgentToolUpdateCallback, ThinkingLevel } from "@oh-my-pi/pi-agent-core";
12
12
  import type { ImageContent, Model, TextContent, ToolResultMessage } from "@oh-my-pi/pi-ai";
13
- import type { Component, KeyId, TUI } from "@oh-my-pi/pi-tui";
13
+ import type { Component, EditorComponent, EditorTheme, KeyId, TUI } from "@oh-my-pi/pi-tui";
14
14
  import type { Static, TSchema } from "@sinclair/typebox";
15
+ import type * as piCodingAgent from "../../index";
15
16
  import type { Theme } from "../../modes/interactive/theme/theme";
17
+ import type { BashResult } from "../bash-executor";
16
18
  import type { CompactionPreparation, CompactionResult } from "../compaction";
17
19
  import type { EventBus } from "../event-bus";
18
20
  import type { ExecOptions, ExecResult } from "../exec";
21
+ import type { KeybindingsManager } from "../keybindings";
19
22
  import type { CustomMessage } from "../messages";
20
23
  import type { ModelRegistry } from "../model-registry";
21
24
  import type {
@@ -26,28 +29,38 @@ import type {
26
29
  SessionManager,
27
30
  } from "../session-manager";
28
31
  import type { BashToolDetails, FindToolDetails, GrepToolDetails, LsToolDetails, ReadToolDetails } from "../tools";
32
+ import type { BashOperations } from "../tools/bash";
29
33
  import type { EditToolDetails } from "../tools/edit";
30
34
 
31
35
  export type { ExecOptions, ExecResult } from "../exec";
32
36
  export type { AgentToolResult, AgentToolUpdateCallback };
37
+ export type { AppAction, KeybindingsManager } from "../keybindings";
33
38
 
34
39
  // ============================================================================
35
40
  // UI Context
36
41
  // ============================================================================
37
42
 
43
+ /**
44
+ * UI dialog options for extensions.
45
+ */
46
+ export interface ExtensionUIDialogOptions {
47
+ signal?: AbortSignal;
48
+ timeout?: number;
49
+ }
50
+
38
51
  /**
39
52
  * UI context for extensions to request interactive UI.
40
53
  * Each mode (interactive, RPC, print) provides its own implementation.
41
54
  */
42
55
  export interface ExtensionUIContext {
43
56
  /** Show a selector and return the user's choice. */
44
- select(title: string, options: string[]): Promise<string | undefined>;
57
+ select(title: string, options: string[], dialogOptions?: ExtensionUIDialogOptions): Promise<string | undefined>;
45
58
 
46
59
  /** Show a confirmation dialog. */
47
- confirm(title: string, message: string): Promise<boolean>;
60
+ confirm(title: string, message: string, dialogOptions?: ExtensionUIDialogOptions): Promise<boolean>;
48
61
 
49
62
  /** Show a text input dialog. */
50
- input(title: string, placeholder?: string): Promise<string | undefined>;
63
+ input(title: string, placeholder?: string, dialogOptions?: ExtensionUIDialogOptions): Promise<string | undefined>;
51
64
 
52
65
  /** Show a notification to the user. */
53
66
  notify(message: string, type?: "info" | "warning" | "error"): void;
@@ -59,6 +72,12 @@ export interface ExtensionUIContext {
59
72
  setWidget(key: string, content: string[] | undefined): void;
60
73
  setWidget(key: string, content: ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) | undefined): void;
61
74
 
75
+ /** Set a custom footer component, or undefined to restore the built-in footer. */
76
+ setFooter(factory: ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) | undefined): void;
77
+
78
+ /** Set a custom header component, or undefined to restore the built-in header. */
79
+ setHeader(factory: ((tui: TUI, theme: Theme) => Component & { dispose?(): void }) | undefined): void;
80
+
62
81
  /** Set the terminal window/tab title. */
63
82
  setTitle(title: string): void;
64
83
 
@@ -67,8 +86,10 @@ export interface ExtensionUIContext {
67
86
  factory: (
68
87
  tui: TUI,
69
88
  theme: Theme,
89
+ keybindings: KeybindingsManager,
70
90
  done: (result: T) => void,
71
91
  ) => (Component & { dispose?(): void }) | Promise<Component & { dispose?(): void }>,
92
+ options?: { overlay?: boolean },
72
93
  ): Promise<T>;
73
94
 
74
95
  /** Set the text in the core input editor. */
@@ -80,8 +101,22 @@ export interface ExtensionUIContext {
80
101
  /** Show a multi-line editor for text editing. */
81
102
  editor(title: string, prefill?: string): Promise<string | undefined>;
82
103
 
104
+ /** Set a custom editor component via factory function, or undefined to restore the default editor. */
105
+ setEditorComponent(
106
+ factory: ((tui: TUI, theme: EditorTheme, keybindings: KeybindingsManager) => EditorComponent) | undefined,
107
+ ): void;
108
+
83
109
  /** Get the current theme for styling. */
84
110
  readonly theme: Theme;
111
+
112
+ /** Get all available themes with names and paths. */
113
+ getAllThemes(): { name: string; path: string | undefined }[];
114
+
115
+ /** Load a theme by name without switching to it. */
116
+ getTheme(name: string): Theme | undefined;
117
+
118
+ /** Set the current theme by name or Theme object. */
119
+ setTheme(theme: string | Theme): { success: boolean; error?: string };
85
120
  }
86
121
 
87
122
  // ============================================================================
@@ -110,6 +145,8 @@ export interface ExtensionContext {
110
145
  abort(): void;
111
146
  /** Whether there are queued messages waiting */
112
147
  hasPendingMessages(): boolean;
148
+ /** Gracefully shutdown and exit. */
149
+ shutdown(): void;
113
150
  /** @deprecated Use hasPendingMessages() instead */
114
151
  hasQueuedMessages(): boolean;
115
152
  }
@@ -299,6 +336,7 @@ export interface BeforeAgentStartEvent {
299
336
  type: "before_agent_start";
300
337
  prompt: string;
301
338
  images?: ImageContent[];
339
+ systemPrompt: string;
302
340
  }
303
341
 
304
342
  /** Fired when an agent loop starts */
@@ -327,6 +365,21 @@ export interface TurnEndEvent {
327
365
  toolResults: ToolResultMessage[];
328
366
  }
329
367
 
368
+ // ============================================================================
369
+ // User Bash Events
370
+ // ============================================================================
371
+
372
+ /** Fired when user executes a bash command via ! or !! prefix */
373
+ export interface UserBashEvent {
374
+ type: "user_bash";
375
+ /** The command to execute */
376
+ command: string;
377
+ /** True if !! prefix was used (excluded from LLM context) */
378
+ excludeFromContext: boolean;
379
+ /** Current working directory */
380
+ cwd: string;
381
+ }
382
+
330
383
  // ============================================================================
331
384
  // Tool Events
332
385
  // ============================================================================
@@ -430,6 +483,7 @@ export type ExtensionEvent =
430
483
  | AgentEndEvent
431
484
  | TurnStartEvent
432
485
  | TurnEndEvent
486
+ | UserBashEvent
433
487
  | ToolCallEvent
434
488
  | ToolResultEvent;
435
489
 
@@ -446,6 +500,14 @@ export interface ToolCallEventResult {
446
500
  reason?: string;
447
501
  }
448
502
 
503
+ /** Result from user_bash event handler */
504
+ export interface UserBashEventResult {
505
+ /** Custom operations to use for execution */
506
+ operations?: BashOperations;
507
+ /** Full replacement: extension handled execution, use this result */
508
+ result?: BashResult;
509
+ }
510
+
449
511
  export interface ToolResultEventResult {
450
512
  content?: (TextContent | ImageContent)[];
451
513
  details?: unknown;
@@ -454,7 +516,8 @@ export interface ToolResultEventResult {
454
516
 
455
517
  export interface BeforeAgentStartEventResult {
456
518
  message?: Pick<CustomMessage, "customType" | "content" | "display" | "details">;
457
- systemPromptAppend?: string;
519
+ /** Replace the system prompt for this turn. If multiple extensions return this, they are chained. */
520
+ systemPrompt?: string;
458
521
  }
459
522
 
460
523
  export interface SessionBeforeSwitchResult {
@@ -526,7 +589,7 @@ export interface ExtensionAPI {
526
589
  typebox: typeof import("@sinclair/typebox");
527
590
 
528
591
  /** Injected pi-coding-agent exports for accessing SDK utilities */
529
- pi: typeof import("../../index.js");
592
+ pi: typeof piCodingAgent;
530
593
 
531
594
  // =========================================================================
532
595
  // Event Subscription
@@ -559,6 +622,7 @@ export interface ExtensionAPI {
559
622
  on(event: "turn_end", handler: ExtensionHandler<TurnEndEvent>): void;
560
623
  on(event: "tool_call", handler: ExtensionHandler<ToolCallEvent, ToolCallEventResult>): void;
561
624
  on(event: "tool_result", handler: ExtensionHandler<ToolResultEvent, ToolResultEventResult>): void;
625
+ on(event: "user_bash", handler: ExtensionHandler<UserBashEvent, UserBashEventResult>): void;
562
626
 
563
627
  // =========================================================================
564
628
  // Tool Registration
@@ -613,6 +677,12 @@ export interface ExtensionAPI {
613
677
  options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
614
678
  ): void;
615
679
 
680
+ /** Send a user message to the agent. Always triggers a turn. */
681
+ sendUserMessage(
682
+ content: string | (TextContent | ImageContent)[],
683
+ options?: { deliverAs?: "steer" | "followUp" },
684
+ ): void;
685
+
616
686
  /** Append a custom entry to the session for state persistence (not sent to LLM). */
617
687
  appendEntry<T = unknown>(customType: string, data?: T): void;
618
688
 
@@ -628,12 +698,21 @@ export interface ExtensionAPI {
628
698
  /** Set the active tools by name. */
629
699
  setActiveTools(toolNames: string[]): void;
630
700
 
701
+ /** Set the current model. Returns false if no API key available. */
702
+ setModel(model: Model<any>): Promise<boolean>;
703
+
704
+ /** Get current thinking level. */
705
+ getThinkingLevel(): ThinkingLevel;
706
+
707
+ /** Set thinking level (clamped to model capabilities). */
708
+ setThinkingLevel(level: ThinkingLevel): void;
709
+
631
710
  /** Shared event bus for extension communication. */
632
711
  events: EventBus;
633
712
  }
634
713
 
635
- /** Extension factory function type. */
636
- export type ExtensionFactory = (pi: ExtensionAPI) => void;
714
+ /** Extension factory function type. Supports both sync and async initialization. */
715
+ export type ExtensionFactory = (pi: ExtensionAPI) => void | Promise<void>;
637
716
 
638
717
  // ============================================================================
639
718
  // Loaded Extension Types
@@ -666,6 +745,11 @@ export type SendMessageHandler = <T = unknown>(
666
745
  options?: { triggerTurn?: boolean; deliverAs?: "steer" | "followUp" | "nextTurn" },
667
746
  ) => void;
668
747
 
748
+ export type SendUserMessageHandler = (
749
+ content: string | (TextContent | ImageContent)[],
750
+ options?: { deliverAs?: "steer" | "followUp" },
751
+ ) => void;
752
+
669
753
  export type AppendEntryHandler = <T = unknown>(customType: string, data?: T) => void;
670
754
 
671
755
  export type GetActiveToolsHandler = () => string[];
@@ -674,8 +758,55 @@ export type GetAllToolsHandler = () => string[];
674
758
 
675
759
  export type SetActiveToolsHandler = (toolNames: string[]) => void;
676
760
 
761
+ export type SetModelHandler = (model: Model<any>) => Promise<boolean>;
762
+
763
+ export type GetThinkingLevelHandler = () => ThinkingLevel;
764
+
765
+ export type SetThinkingLevelHandler = (level: ThinkingLevel) => void;
766
+
767
+ /** Shared state created by loader, used during registration and runtime. */
768
+ export interface ExtensionRuntimeState {
769
+ flagValues: Map<string, boolean | string>;
770
+ }
771
+
772
+ /** Action implementations for ExtensionAPI methods. */
773
+ export interface ExtensionActions {
774
+ sendMessage: SendMessageHandler;
775
+ sendUserMessage: SendUserMessageHandler;
776
+ appendEntry: AppendEntryHandler;
777
+ getActiveTools: GetActiveToolsHandler;
778
+ getAllTools: GetAllToolsHandler;
779
+ setActiveTools: SetActiveToolsHandler;
780
+ setModel: SetModelHandler;
781
+ getThinkingLevel: GetThinkingLevelHandler;
782
+ setThinkingLevel: SetThinkingLevelHandler;
783
+ }
784
+
785
+ /** Actions for ExtensionContext (ctx.* in event handlers). */
786
+ export interface ExtensionContextActions {
787
+ getModel: () => Model<any> | undefined;
788
+ isIdle: () => boolean;
789
+ abort: () => void;
790
+ hasPendingMessages: () => boolean;
791
+ shutdown: () => void;
792
+ }
793
+
794
+ /** Actions for ExtensionCommandContext (ctx.* in command handlers). */
795
+ export interface ExtensionCommandContextActions {
796
+ waitForIdle: () => Promise<void>;
797
+ newSession: (options?: {
798
+ parentSession?: string;
799
+ setup?: (sessionManager: SessionManager) => Promise<void>;
800
+ }) => Promise<{ cancelled: boolean }>;
801
+ branch: (entryId: string) => Promise<{ cancelled: boolean }>;
802
+ navigateTree: (targetId: string, options?: { summarize?: boolean }) => Promise<{ cancelled: boolean }>;
803
+ }
804
+
805
+ /** Full runtime = state + actions. */
806
+ export interface ExtensionRuntime extends ExtensionRuntimeState, ExtensionActions {}
807
+
677
808
  /** Loaded extension with all registered items. */
678
- export interface LoadedExtension {
809
+ export interface Extension {
679
810
  path: string;
680
811
  resolvedPath: string;
681
812
  handlers: Map<string, HandlerFn[]>;
@@ -683,21 +814,14 @@ export interface LoadedExtension {
683
814
  messageRenderers: Map<string, MessageRenderer>;
684
815
  commands: Map<string, RegisteredCommand>;
685
816
  flags: Map<string, ExtensionFlag>;
686
- flagValues: Map<string, boolean | string>;
687
817
  shortcuts: Map<KeyId, ExtensionShortcut>;
688
- setSendMessageHandler: (handler: SendMessageHandler) => void;
689
- setAppendEntryHandler: (handler: AppendEntryHandler) => void;
690
- setGetActiveToolsHandler: (handler: GetActiveToolsHandler) => void;
691
- setGetAllToolsHandler: (handler: GetAllToolsHandler) => void;
692
- setSetActiveToolsHandler: (handler: SetActiveToolsHandler) => void;
693
- setFlagValue: (name: string, value: boolean | string) => void;
694
818
  }
695
819
 
696
820
  /** Result of loading extensions. */
697
821
  export interface LoadExtensionsResult {
698
- extensions: LoadedExtension[];
822
+ extensions: Extension[];
699
823
  errors: Array<{ path: string; error: string }>;
700
- setUIContext(uiContext: ExtensionUIContext, hasUI: boolean): void;
824
+ runtime: ExtensionRuntime;
701
825
  }
702
826
 
703
827
  // ============================================================================
@@ -6,12 +6,12 @@ import type { AgentTool, AgentToolContext, AgentToolUpdateCallback } from "@oh-m
6
6
  import type { ImageContent, TextContent } from "@oh-my-pi/pi-ai";
7
7
  import type { Theme } from "../../modes/interactive/theme/theme";
8
8
  import type { ExtensionRunner } from "./runner";
9
- import type { ExtensionContext, RegisteredTool, ToolCallEventResult, ToolResultEventResult } from "./types";
9
+ import type { RegisteredTool, ToolCallEventResult, ToolResultEventResult } from "./types";
10
10
 
11
11
  /**
12
12
  * Wrap a RegisteredTool into an AgentTool.
13
13
  */
14
- export function wrapRegisteredTool(registeredTool: RegisteredTool, getContext: () => ExtensionContext): AgentTool {
14
+ export function wrapRegisteredTool(registeredTool: RegisteredTool, runner: ExtensionRunner): AgentTool {
15
15
  const { definition } = registeredTool;
16
16
  return {
17
17
  name: definition.name,
@@ -19,7 +19,7 @@ export function wrapRegisteredTool(registeredTool: RegisteredTool, getContext: (
19
19
  description: definition.description,
20
20
  parameters: definition.parameters,
21
21
  execute: (toolCallId, params, signal, onUpdate) =>
22
- definition.execute(toolCallId, params, onUpdate, getContext(), signal),
22
+ definition.execute(toolCallId, params, onUpdate, runner.createContext(), signal),
23
23
  renderCall: definition.renderCall ? (args, theme) => definition.renderCall?.(args, theme as Theme) : undefined,
24
24
  renderResult: definition.renderResult
25
25
  ? (result, options, theme) =>
@@ -35,11 +35,8 @@ export function wrapRegisteredTool(registeredTool: RegisteredTool, getContext: (
35
35
  /**
36
36
  * Wrap all registered tools into AgentTools.
37
37
  */
38
- export function wrapRegisteredTools(
39
- registeredTools: RegisteredTool[],
40
- getContext: () => ExtensionContext,
41
- ): AgentTool[] {
42
- return registeredTools.map((rt) => wrapRegisteredTool(rt, getContext));
38
+ export function wrapRegisteredTools(registeredTools: RegisteredTool[], runner: ExtensionRunner): AgentTool[] {
39
+ return registeredTools.map((rt) => wrapRegisteredTool(rt, runner));
43
40
  }
44
41
 
45
42
  /**
@@ -747,7 +747,7 @@ export interface HookAPI {
747
747
  /** Injected @sinclair/typebox module */
748
748
  typebox: typeof import("@sinclair/typebox");
749
749
  /** Injected pi-coding-agent exports */
750
- pi: typeof import("../../index.js");
750
+ pi: typeof import("../../index");
751
751
  }
752
752
 
753
753
  /**
package/src/core/index.ts CHANGED
@@ -11,7 +11,7 @@ export {
11
11
  type PromptOptions,
12
12
  type SessionStats,
13
13
  } from "./agent-session";
14
- export { type BashExecutorOptions, type BashResult, executeBash } from "./bash-executor";
14
+ export { type BashExecutorOptions, type BashResult, executeBash, executeBashWithOperations } from "./bash-executor";
15
15
  export type { CompactionResult } from "./compaction/index";
16
16
  export {
17
17
  discoverAndLoadExtensions,
@@ -21,6 +21,7 @@ export {
21
21
  type ExtensionFactory,
22
22
  ExtensionRunner,
23
23
  type ExtensionUIContext,
24
+ type ExtensionUIDialogOptions,
24
25
  loadExtensionFromFactory,
25
26
  type ToolDefinition,
26
27
  } from "./extensions/index";
@@ -26,7 +26,8 @@ export type AppAction =
26
26
  | "expandTools"
27
27
  | "toggleThinking"
28
28
  | "externalEditor"
29
- | "followUp";
29
+ | "followUp"
30
+ | "dequeue";
30
31
 
31
32
  /**
32
33
  * All configurable actions.
@@ -56,6 +57,7 @@ export const DEFAULT_APP_KEYBINDINGS: Record<AppAction, KeyId | KeyId[]> = {
56
57
  toggleThinking: "ctrl+t",
57
58
  externalEditor: "ctrl+g",
58
59
  followUp: "alt+enter",
60
+ dequeue: "alt+up",
59
61
  };
60
62
 
61
63
  /**
@@ -80,6 +82,7 @@ const APP_ACTIONS: AppAction[] = [
80
82
  "toggleThinking",
81
83
  "externalEditor",
82
84
  "followUp",
85
+ "dequeue",
83
86
  ];
84
87
 
85
88
  function isAppAction(action: string): action is AppAction {
@@ -384,7 +384,7 @@ export class ModelRegistry {
384
384
  * Find a model by provider and ID.
385
385
  */
386
386
  find(provider: string, modelId: string): Model<Api> | undefined {
387
- return this.models.find((m) => m.provider === provider && m.id === modelId) ?? undefined;
387
+ return this.models.find((m) => m.provider === provider && m.id === modelId);
388
388
  }
389
389
 
390
390
  /**
@@ -25,12 +25,13 @@ export const defaultModelPerProvider: Record<KnownProvider, string> = {
25
25
  cerebras: "zai-glm-4.6",
26
26
  zai: "glm-4.6",
27
27
  mistral: "devstral-medium-latest",
28
- opencode: "claude-sonnet-4-5",
28
+ opencode: "claude-opus-4-5",
29
29
  };
30
30
 
31
31
  export interface ScopedModel {
32
32
  model: Model<Api>;
33
- thinkingLevel: ThinkingLevel;
33
+ thinkingLevel?: ThinkingLevel;
34
+ explicitThinkingLevel: boolean;
34
35
  }
35
36
 
36
37
  /** Priority chain for auto-discovering smol/fast models */
@@ -122,8 +123,10 @@ function tryMatchModel(modelPattern: string, availableModels: Model<Api>[]): Mod
122
123
 
123
124
  export interface ParsedModelResult {
124
125
  model: Model<Api> | undefined;
125
- thinkingLevel: ThinkingLevel;
126
+ /** Thinking level if explicitly specified in pattern, undefined otherwise */
127
+ thinkingLevel?: ThinkingLevel;
126
128
  warning: string | undefined;
129
+ explicitThinkingLevel: boolean;
127
130
  }
128
131
 
129
132
  /**
@@ -132,10 +135,10 @@ export interface ParsedModelResult {
132
135
  *
133
136
  * Algorithm:
134
137
  * 1. Try to match full pattern as a model
135
- * 2. If found, return it with "off" thinking level
138
+ * 2. If found, return it with undefined thinking level
136
139
  * 3. If not found and has colons, split on last colon:
137
140
  * - If suffix is valid thinking level, use it and recurse on prefix
138
- * - If suffix is invalid, warn and recurse on prefix with "off"
141
+ * - If suffix is invalid, warn and recurse on prefix
139
142
  *
140
143
  * @internal Exported for testing
141
144
  */
@@ -143,14 +146,14 @@ export function parseModelPattern(pattern: string, availableModels: Model<Api>[]
143
146
  // Try exact match first
144
147
  const exactMatch = tryMatchModel(pattern, availableModels);
145
148
  if (exactMatch) {
146
- return { model: exactMatch, thinkingLevel: "off", warning: undefined };
149
+ return { model: exactMatch, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
147
150
  }
148
151
 
149
152
  // No match - try splitting on last colon if present
150
153
  const lastColonIndex = pattern.lastIndexOf(":");
151
154
  if (lastColonIndex === -1) {
152
155
  // No colons, pattern simply doesn't match any model
153
- return { model: undefined, thinkingLevel: "off", warning: undefined };
156
+ return { model: undefined, thinkingLevel: undefined, warning: undefined, explicitThinkingLevel: false };
154
157
  }
155
158
 
156
159
  const prefix = pattern.substring(0, lastColonIndex);
@@ -161,26 +164,28 @@ export function parseModelPattern(pattern: string, availableModels: Model<Api>[]
161
164
  const result = parseModelPattern(prefix, availableModels);
162
165
  if (result.model) {
163
166
  // Only use this thinking level if no warning from inner recursion
164
- // (if there was an invalid suffix deeper, we already have "off")
167
+ const explicitThinkingLevel = !result.warning;
165
168
  return {
166
169
  model: result.model,
167
- thinkingLevel: result.warning ? "off" : suffix,
170
+ thinkingLevel: explicitThinkingLevel ? suffix : undefined,
168
171
  warning: result.warning,
172
+ explicitThinkingLevel,
169
173
  };
170
174
  }
171
175
  return result;
172
- } else {
173
- // Invalid suffix - recurse on prefix with "off" and warn
174
- const result = parseModelPattern(prefix, availableModels);
175
- if (result.model) {
176
- return {
177
- model: result.model,
178
- thinkingLevel: "off",
179
- warning: `Invalid thinking level "${suffix}" in pattern "${pattern}". Using "off" instead.`,
180
- };
181
- }
182
- return result;
183
176
  }
177
+
178
+ // Invalid suffix - recurse on prefix and warn
179
+ const result = parseModelPattern(prefix, availableModels);
180
+ if (result.model) {
181
+ return {
182
+ model: result.model,
183
+ thinkingLevel: undefined,
184
+ warning: `Invalid thinking level "${suffix}" in pattern "${pattern}". Using default instead.`,
185
+ explicitThinkingLevel: false,
186
+ };
187
+ }
188
+ return result;
184
189
  }
185
190
 
186
191
  /**
@@ -204,12 +209,14 @@ export async function resolveModelScope(patterns: string[], modelRegistry: Model
204
209
  // Extract optional thinking level suffix (e.g., "provider/*:high")
205
210
  const colonIdx = pattern.lastIndexOf(":");
206
211
  let globPattern = pattern;
207
- let thinkingLevel: ThinkingLevel = "off";
212
+ let thinkingLevel: ThinkingLevel | undefined;
213
+ let explicitThinkingLevel = false;
208
214
 
209
215
  if (colonIdx !== -1) {
210
216
  const suffix = pattern.substring(colonIdx + 1);
211
217
  if (isValidThinkingLevel(suffix)) {
212
218
  thinkingLevel = suffix;
219
+ explicitThinkingLevel = true;
213
220
  globPattern = pattern.substring(0, colonIdx);
214
221
  }
215
222
  }
@@ -228,13 +235,13 @@ export async function resolveModelScope(patterns: string[], modelRegistry: Model
228
235
 
229
236
  for (const model of matchingModels) {
230
237
  if (!scopedModels.find((sm) => modelsAreEqual(sm.model, model))) {
231
- scopedModels.push({ model, thinkingLevel });
238
+ scopedModels.push({ model, thinkingLevel, explicitThinkingLevel });
232
239
  }
233
240
  }
234
241
  continue;
235
242
  }
236
243
 
237
- const { model, thinkingLevel, warning } = parseModelPattern(pattern, availableModels);
244
+ const { model, thinkingLevel, warning, explicitThinkingLevel } = parseModelPattern(pattern, availableModels);
238
245
 
239
246
  if (warning) {
240
247
  console.warn(chalk.yellow(`Warning: ${warning}`));
@@ -247,7 +254,7 @@ export async function resolveModelScope(patterns: string[], modelRegistry: Model
247
254
 
248
255
  // Avoid duplicates
249
256
  if (!scopedModels.find((sm) => modelsAreEqual(sm.model, model))) {
250
- scopedModels.push({ model, thinkingLevel });
257
+ scopedModels.push({ model, thinkingLevel, explicitThinkingLevel });
251
258
  }
252
259
  }
253
260
 
@@ -304,9 +311,11 @@ export async function findInitialModel(options: {
304
311
 
305
312
  // 2. Use first model from scoped models (skip if continuing/resuming)
306
313
  if (scopedModels.length > 0 && !isContinuing) {
314
+ const scoped = scopedModels[0];
315
+ const scopedThinkingLevel = scoped.thinkingLevel ?? defaultThinkingLevel ?? "off";
307
316
  return {
308
- model: scopedModels[0].model,
309
- thinkingLevel: scopedModels[0].thinkingLevel,
317
+ model: scoped.model,
318
+ thinkingLevel: scopedThinkingLevel,
310
319
  fallbackMessage: undefined,
311
320
  };
312
321
  }