@oh-my-pi/pi-coding-agent 17.0.1 → 17.0.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 (126) hide show
  1. package/CHANGELOG.md +92 -20
  2. package/dist/cli.js +3485 -3448
  3. package/dist/types/advisor/config.d.ts +14 -6
  4. package/dist/types/advisor/runtime.d.ts +20 -11
  5. package/dist/types/autolearn/controller.d.ts +1 -0
  6. package/dist/types/config/model-discovery.d.ts +17 -0
  7. package/dist/types/config/model-resolver.d.ts +6 -2
  8. package/dist/types/config/settings-schema.d.ts +32 -7
  9. package/dist/types/config/settings.d.ts +50 -0
  10. package/dist/types/cursor.d.ts +11 -0
  11. package/dist/types/discovery/helpers.d.ts +5 -2
  12. package/dist/types/exec/bash-executor.d.ts +2 -0
  13. package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
  14. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  15. package/dist/types/extensibility/extensions/types.d.ts +18 -0
  16. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +40 -0
  17. package/dist/types/extensibility/shared-events.d.ts +6 -0
  18. package/dist/types/extensibility/utils.d.ts +2 -2
  19. package/dist/types/mcp/transports/stdio.d.ts +13 -1
  20. package/dist/types/modes/components/advisor-config.d.ts +8 -1
  21. package/dist/types/modes/components/hook-editor.d.ts +7 -0
  22. package/dist/types/modes/components/model-hub.d.ts +5 -2
  23. package/dist/types/modes/components/session-selector.d.ts +2 -0
  24. package/dist/types/modes/controllers/command-controller.d.ts +8 -0
  25. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  26. package/dist/types/modes/print-mode.d.ts +1 -1
  27. package/dist/types/modes/warp-events.d.ts +24 -0
  28. package/dist/types/modes/warp-events.test.d.ts +1 -0
  29. package/dist/types/plan-mode/model-transition.d.ts +47 -0
  30. package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
  31. package/dist/types/registry/agent-lifecycle.d.ts +26 -1
  32. package/dist/types/sdk.d.ts +13 -2
  33. package/dist/types/session/agent-session.d.ts +34 -10
  34. package/dist/types/session/session-history-format.d.ts +10 -0
  35. package/dist/types/session/session-manager.d.ts +14 -0
  36. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
  37. package/dist/types/task/label.d.ts +1 -1
  38. package/dist/types/telemetry-export.d.ts +34 -9
  39. package/dist/types/tools/approval.d.ts +8 -0
  40. package/dist/types/tools/bash.d.ts +2 -0
  41. package/dist/types/tools/essential-tools.d.ts +29 -0
  42. package/dist/types/tools/image-gen.d.ts +2 -1
  43. package/dist/types/tools/index.d.ts +1 -0
  44. package/dist/types/tools/xdev.d.ts +11 -2
  45. package/dist/types/utils/title-generator.d.ts +2 -1
  46. package/dist/types/web/search/providers/kimi.d.ts +4 -1
  47. package/dist/types/web/search/types.d.ts +1 -1
  48. package/package.json +21 -16
  49. package/src/advisor/__tests__/advisor.test.ts +1304 -42
  50. package/src/advisor/__tests__/config.test.ts +58 -2
  51. package/src/advisor/config.ts +76 -24
  52. package/src/advisor/runtime.ts +445 -92
  53. package/src/autolearn/controller.ts +23 -28
  54. package/src/cli.ts +5 -1
  55. package/src/config/model-discovery.ts +81 -21
  56. package/src/config/model-registry.ts +25 -6
  57. package/src/config/model-resolver.ts +14 -7
  58. package/src/config/settings-schema.ts +42 -6
  59. package/src/config/settings.ts +405 -25
  60. package/src/cursor.ts +20 -3
  61. package/src/debug/report-bundle.ts +40 -4
  62. package/src/discovery/helpers.ts +28 -5
  63. package/src/exec/bash-executor.ts +14 -5
  64. package/src/extensibility/custom-tools/loader.ts +3 -3
  65. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  66. package/src/extensibility/extensions/loader.ts +3 -3
  67. package/src/extensibility/extensions/managed-timers.ts +83 -0
  68. package/src/extensibility/extensions/runner.ts +26 -0
  69. package/src/extensibility/extensions/types.ts +18 -0
  70. package/src/extensibility/extensions/wrapper.ts +2 -1
  71. package/src/extensibility/hooks/loader.ts +3 -3
  72. package/src/extensibility/legacy-pi-coding-agent-shim.ts +96 -1
  73. package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
  74. package/src/extensibility/plugins/manager.ts +2 -2
  75. package/src/extensibility/shared-events.ts +6 -0
  76. package/src/extensibility/utils.ts +91 -25
  77. package/src/irc/bus.ts +22 -3
  78. package/src/launch/broker.ts +3 -2
  79. package/src/launch/client.ts +2 -2
  80. package/src/launch/presence.ts +2 -2
  81. package/src/lsp/client.ts +1 -1
  82. package/src/main.ts +11 -8
  83. package/src/mcp/manager.ts +9 -3
  84. package/src/mcp/transports/stdio.ts +103 -23
  85. package/src/modes/components/advisor-config.ts +65 -3
  86. package/src/modes/components/ask-dialog.ts +1 -1
  87. package/src/modes/components/hook-editor.ts +18 -3
  88. package/src/modes/components/model-hub.ts +138 -42
  89. package/src/modes/components/session-selector.ts +4 -0
  90. package/src/modes/components/status-line/component.test.ts +1 -0
  91. package/src/modes/components/status-line/segments.ts +21 -6
  92. package/src/modes/controllers/command-controller.ts +167 -47
  93. package/src/modes/controllers/event-controller.ts +5 -0
  94. package/src/modes/controllers/extension-ui-controller.ts +4 -22
  95. package/src/modes/controllers/input-controller.ts +12 -12
  96. package/src/modes/controllers/selector-controller.ts +191 -31
  97. package/src/modes/interactive-mode.ts +139 -54
  98. package/src/modes/print-mode.ts +3 -3
  99. package/src/modes/rpc/host-tools.ts +2 -1
  100. package/src/modes/rpc/rpc-mode.ts +19 -4
  101. package/src/modes/warp-events.test.ts +794 -0
  102. package/src/modes/warp-events.ts +232 -0
  103. package/src/plan-mode/model-transition.test.ts +60 -0
  104. package/src/plan-mode/model-transition.ts +51 -0
  105. package/src/registry/agent-lifecycle.ts +133 -18
  106. package/src/sdk.ts +221 -42
  107. package/src/session/agent-session.ts +1285 -348
  108. package/src/session/session-history-format.ts +20 -5
  109. package/src/session/session-manager.ts +48 -0
  110. package/src/slash-commands/builtin-registry.ts +7 -0
  111. package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
  112. package/src/task/executor.ts +1 -1
  113. package/src/task/label.ts +2 -0
  114. package/src/telemetry-export.ts +453 -97
  115. package/src/tools/approval.ts +11 -0
  116. package/src/tools/bash.ts +71 -38
  117. package/src/tools/essential-tools.ts +45 -0
  118. package/src/tools/gh.ts +169 -2
  119. package/src/tools/image-gen.ts +69 -7
  120. package/src/tools/index.ts +7 -5
  121. package/src/tools/read.ts +48 -3
  122. package/src/tools/write.ts +22 -4
  123. package/src/tools/xdev.ts +14 -3
  124. package/src/utils/title-generator.ts +15 -4
  125. package/src/web/search/providers/kimi.ts +18 -12
  126. package/src/web/search/types.ts +6 -1
@@ -36,6 +36,7 @@ import {
36
36
  assistantUsageIsBilled,
37
37
  splitAssistantMessageToolTimeline,
38
38
  } from "../utils/transcript-render-helpers";
39
+ import { isWarpCliAgentProtocolActive } from "../warp-events";
39
40
  import { StreamingRevealController } from "./streaming-reveal";
40
41
  import { streamingStringKeysForTool, ToolArgsRevealController } from "./tool-args-reveal";
41
42
 
@@ -1564,6 +1565,10 @@ export class EventController {
1564
1565
  const notify = settings.get("completion.notify");
1565
1566
  if (notify === "off") return;
1566
1567
 
1568
+ // Warp structured OSC 777 already drives native completion UX when the
1569
+ // protocol is negotiated — avoid a second legacy desktop/OSC-9 toast.
1570
+ if (isWarpCliAgentProtocolActive()) return;
1571
+
1567
1572
  // Skip when the turn was aborted (e.g. ask cancelled with Ctrl+C) or
1568
1573
  // errored — those are not "Task complete" events. Mirrors the gate
1569
1574
  // already used by #currentContextTokens, #handleMessageEnd, and the
@@ -21,7 +21,6 @@ import type {
21
21
  TerminalInputHandler,
22
22
  } from "../../extensibility/extensions";
23
23
  import { getSessionSlashCommands } from "../../extensibility/extensions/get-commands-handler";
24
- import { createExtensionModelQuery } from "../../extensibility/extensions/model-api";
25
24
  import { AskDialogComponent, boundPromptTitle } from "../../modes/components/ask-dialog";
26
25
  import { HookEditorComponent } from "../../modes/components/hook-editor";
27
26
  import { HookInputComponent } from "../../modes/components/hook-input";
@@ -494,32 +493,15 @@ export class ExtensionUiController {
494
493
  if (!uiContext) {
495
494
  return;
496
495
  }
497
- for (const registeredTool of this.ctx.session.extensionRunner?.getAllRegisteredTools() ?? []) {
496
+ const runner = this.ctx.session.extensionRunner;
497
+ for (const registeredTool of runner?.getAllRegisteredTools() ?? []) {
498
498
  if (registeredTool.definition.onSession) {
499
499
  try {
500
500
  await registeredTool.definition.onSession(event, {
501
+ ...runner!.createContext(),
501
502
  ui: uiContext,
502
- getContextUsage: () => this.ctx.session.getContextUsage(),
503
- compact: instructionsOrOptions => this.#compactSession(instructionsOrOptions),
504
503
  hasUI: true,
505
- cwd: this.ctx.sessionManager.getCwd(),
506
- sessionManager: this.ctx.session.sessionManager,
507
- modelRegistry: this.ctx.session.modelRegistry,
508
- model: this.ctx.session.model,
509
- models: createExtensionModelQuery(
510
- this.ctx.session.modelRegistry,
511
- this.ctx.session.settings,
512
- () => this.ctx.session.model,
513
- ),
514
- isIdle: () => !this.ctx.session.isStreaming,
515
- hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
516
- abort: () => {
517
- this.ctx.session.abort({ reason: USER_INTERRUPT_LABEL });
518
- },
519
- shutdown: () => {
520
- // Signal shutdown request
521
- },
522
- getSystemPrompt: () => this.ctx.session.systemPrompt,
504
+ compact: instructionsOrOptions => this.#compactSession(instructionsOrOptions),
523
505
  });
524
506
  } catch (err) {
525
507
  this.showToolError(registeredTool.definition.name, err instanceof Error ? err.message : String(err));
@@ -35,7 +35,6 @@ import { EnhancedPasteController } from "../../utils/enhanced-paste";
35
35
  import { getEditorCommand, openInEditor } from "../../utils/external-editor";
36
36
  import { ensureSupportedImageInput, ImageInputTooLargeError, loadImageInput } from "../../utils/image-loading";
37
37
  import { resizeImage } from "../../utils/image-resize";
38
- import { generateSessionTitle } from "../../utils/title-generator";
39
38
 
40
39
  /**
41
40
  * Slash commands that may carry secrets in their arguments should never be
@@ -392,7 +391,16 @@ export class InputController {
392
391
  this.ctx.editor.onClear = () => this.handleCtrlC();
393
392
  this.ctx.editor.setActionKeys("app.exit", this.ctx.keybindings.getKeys("app.exit"));
394
393
  this.ctx.editor.setActionKeys("app.display.reset", this.ctx.keybindings.getKeys("app.display.reset"));
395
- this.ctx.editor.onDisplayReset = () => this.ctx.ui.resetDisplay();
394
+ this.ctx.editor.onDisplayReset = () => {
395
+ // Explicit user gesture (Ctrl+L): re-query the terminal background once
396
+ // so a mid-session light/dark switch is picked up even on terminals
397
+ // without an end-to-end Mode 2031 notification path (#5352). The
398
+ // appearance callback re-evaluates the auto theme; the repaint below
399
+ // then renders the resolved palette. Bounded to one OSC 11 probe per
400
+ // gesture — no timers, no periodic polling.
401
+ this.ctx.ui.terminal.refreshAppearance?.();
402
+ this.ctx.ui.resetDisplay();
403
+ };
396
404
  this.ctx.editor.onExit = () => this.handleCtrlD();
397
405
  this.ctx.editor.setActionKeys("app.suspend", this.ctx.keybindings.getKeys("app.suspend"));
398
406
  this.ctx.editor.onSuspend = () => this.handleCtrlZ();
@@ -821,16 +829,8 @@ export class InputController {
821
829
  // chance, so titling defers past "hi" instead of latching onto it.
822
830
  if (!this.ctx.sessionManager.getSessionName() && !$env.PI_NO_TITLE && !isLowSignalTitleInput(text)) {
823
831
  this.#showTinyTitleDownloadProgress(this.ctx.settings.get("providers.tinyModel"));
824
- const registry = this.ctx.session.modelRegistry;
825
- generateSessionTitle(
826
- text,
827
- registry,
828
- this.ctx.settings,
829
- this.ctx.session.sessionId,
830
- this.ctx.session.model,
831
- provider => this.ctx.session.agent.metadataForProvider(provider),
832
- this.ctx.session.titleSystemPrompt,
833
- )
832
+ this.ctx.session
833
+ .generateTitle(text)
834
834
  .then(async title => {
835
835
  // Re-check: a concurrent attempt for an earlier message may have
836
836
  // already named the session. Don't clobber it. Terminal title and
@@ -13,7 +13,11 @@ import {
13
13
  saveWatchdogConfigFile,
14
14
  } from "../../advisor";
15
15
  import { reset as resetCapabilities } from "../../capability";
16
- import { formatModelSelectorValue, resolveAdvisorRoleSelection } from "../../config/model-resolver";
16
+ import {
17
+ formatModelSelectorValue,
18
+ resolveAdvisorRoleSelection,
19
+ resolveModelRoleValue,
20
+ } from "../../config/model-resolver";
17
21
  import { getRoleInfo } from "../../config/model-roles";
18
22
  import { settings } from "../../config/settings";
19
23
  import { disableProvider, enableProvider } from "../../discovery";
@@ -46,7 +50,12 @@ import {
46
50
  type ResetUsageAccount,
47
51
  toResetUsageAccounts,
48
52
  } from "../../slash-commands/helpers/reset-usage";
49
- import { AUTO_THINKING, type ConfiguredThinkingLevel } from "../../thinking";
53
+ import {
54
+ AUTO_THINKING,
55
+ type ConfiguredThinkingLevel,
56
+ concreteThinkingLevel,
57
+ parseConfiguredThinkingLevel,
58
+ } from "../../thinking";
50
59
  import {
51
60
  isImageProviderPreference,
52
61
  isSearchProviderId,
@@ -68,7 +77,7 @@ import { ExtensionDashboard } from "../components/extensions";
68
77
  import { HistorySearchComponent } from "../components/history-search";
69
78
  import { LoginDialogComponent } from "../components/login-dialog";
70
79
  import { LogoutAccountSelectorComponent } from "../components/logout-account-selector";
71
- import { ModelHubComponent } from "../components/model-hub";
80
+ import { ModelHubComponent, type ModelRoleSelectionScope } from "../components/model-hub";
72
81
  import { ModelPickerComponent } from "../components/model-picker";
73
82
  import { OAuthSelectorComponent } from "../components/oauth-selector";
74
83
  import { PluginSelectorComponent } from "../components/plugin-selector";
@@ -87,6 +96,15 @@ const MANUAL_LOGIN_PROMPT = "Paste the authorization code (or full redirect URL)
87
96
 
88
97
  export class SelectorController {
89
98
  constructor(private ctx: InteractiveModeContext) {}
99
+ #defaultRoleMutationTail = Promise.resolve();
100
+
101
+ async #acquireDefaultRoleMutation(): Promise<() => void> {
102
+ const previous = this.#defaultRoleMutationTail;
103
+ const { promise, resolve } = Promise.withResolvers<void>();
104
+ this.#defaultRoleMutationTail = previous.then(() => promise);
105
+ await previous;
106
+ return resolve;
107
+ }
90
108
 
91
109
  async #refreshOAuthProviderAuthState(): Promise<void> {
92
110
  const oauthProviders = getOAuthProviders();
@@ -280,6 +298,13 @@ export class SelectorController {
280
298
  close: done,
281
299
  requestRender: () => this.ctx.ui.requestRender(),
282
300
  notify: message => this.ctx.showStatus(message),
301
+ getAdvisorStats: () => this.ctx.session.getAdvisorStats().advisors,
302
+ getUsageReports: async () => this.ctx.session.fetchUsageReports?.() ?? null,
303
+ resolveActiveAccount: (provider, sessionId) =>
304
+ this.ctx.session.modelRegistry.authStorage.getOAuthAccountIdentity(
305
+ provider,
306
+ sessionId ?? this.ctx.session.sessionId,
307
+ ),
283
308
  });
284
309
  overlayHandle = this.ctx.ui.showOverlay(overlay, {
285
310
  anchor: "bottom-center",
@@ -710,55 +735,173 @@ export class SelectorController {
710
735
  this.ctx.session.modelRegistry,
711
736
  this.ctx.session.scopedModels,
712
737
  {
713
- onAssign: async (model, role, thinkingLevel, selector) => {
738
+ onAssign: async (model, role, thinkingLevel, selector, scope?: ModelRoleSelectionScope) => {
739
+ const releaseDefaultMutation = role === "default" ? await this.#acquireDefaultRoleMutation() : undefined;
740
+ const configuredStorage = this.ctx.settings.get("modelRoleStorage");
741
+ const targetScope = configuredStorage === "project" ? (scope ?? "project") : "global";
714
742
  // `auto` is session-global: never baked into a per-role model value
715
743
  // (it can't round-trip through `model:<level>`). Apply it to the session
716
744
  // separately and persist via `defaultThinkingLevel`.
717
745
  const isAuto = thinkingLevel === AUTO_THINKING;
718
746
  const concreteThinking = isAuto || thinkingLevel === undefined ? undefined : thinkingLevel;
719
747
  const selectorValue = selector ?? `${model.provider}/${model.id}`;
748
+ const scopeLabel =
749
+ configuredStorage === "project" ? `${targetScope === "project" ? "Project" : "Global"} ` : "";
750
+ const defaultStatusLabel = configuredStorage === "project" ? `${scopeLabel}default` : "Default";
720
751
  try {
721
752
  if (role === "default") {
722
- const { switched } = await this.ctx.session.setModel(model, role, {
723
- selector,
724
- thinkingLevel: isAuto ? ThinkingLevel.Inherit : concreteThinking,
725
- persist: true,
726
- currentContextTokens,
727
- });
728
- if (isAuto) {
729
- if (switched) {
730
- this.ctx.session.setThinkingLevel(AUTO_THINKING, true);
731
- } else {
753
+ const effectiveProvenance = this.ctx.settings.getModelRoleProvenance("default");
754
+ const shadowedGlobal =
755
+ configuredStorage === "project" &&
756
+ targetScope === "global" &&
757
+ (effectiveProvenance === "project" ||
758
+ effectiveProvenance === "overlay" ||
759
+ (effectiveProvenance === "runtime" &&
760
+ this.ctx.settings.isProjectModelRoleRuntimeOverrideActive("default")));
761
+ const shadowedProject =
762
+ configuredStorage === "project" &&
763
+ targetScope === "project" &&
764
+ effectiveProvenance === "overlay";
765
+ if (shadowedGlobal) {
766
+ this.ctx.settings.setModelRole(
767
+ "default",
768
+ formatModelSelectorValue(selectorValue, concreteThinking),
769
+ );
770
+ if (isAuto) {
732
771
  this.ctx.settings.set("defaultThinkingLevel", AUTO_THINKING);
733
772
  }
734
- } else if (switched && concreteThinking && concreteThinking !== ThinkingLevel.Inherit) {
735
- this.ctx.session.setThinkingLevel(concreteThinking);
736
- }
737
- if (switched) {
773
+ } else if (shadowedProject) {
774
+ this.ctx.settings.setProjectModelRole(
775
+ "default",
776
+ formatModelSelectorValue(selectorValue, concreteThinking),
777
+ );
778
+ if (isAuto) {
779
+ this.ctx.settings.set("defaultThinkingLevel", AUTO_THINKING);
780
+ }
781
+ } else {
782
+ const { switched } = await this.ctx.session.setModel(model, role, {
783
+ selector,
784
+ thinkingLevel: isAuto ? ThinkingLevel.Inherit : concreteThinking,
785
+ persist: targetScope === "global",
786
+ currentContextTokens,
787
+ });
788
+ if (!switched) return;
789
+ if (targetScope === "project") {
790
+ this.ctx.settings.setProjectModelRole(
791
+ "default",
792
+ formatModelSelectorValue(selectorValue, concreteThinking),
793
+ );
794
+ }
795
+ if (isAuto) {
796
+ this.ctx.session.setThinkingLevel(AUTO_THINKING, true);
797
+ } else if (concreteThinking && concreteThinking !== ThinkingLevel.Inherit) {
798
+ this.ctx.session.setThinkingLevel(concreteThinking);
799
+ }
738
800
  this.ctx.statusLine.invalidate();
739
801
  this.ctx.updateEditorBorderColor();
740
802
  }
741
- this.ctx.showStatus(`Default model: ${selector ?? model.id}`);
803
+ this.ctx.showStatus(`${defaultStatusLabel} model: ${selector ?? model.id}`);
742
804
  } else {
743
805
  // Other roles (smol, slow, custom): update settings, not the current model.
744
- this.ctx.settings.setModelRole(role, formatModelSelectorValue(selectorValue, concreteThinking));
806
+ const modelRoleValue = formatModelSelectorValue(selectorValue, concreteThinking);
807
+ if (targetScope === "project") {
808
+ this.ctx.settings.setProjectModelRole(role, modelRoleValue);
809
+ } else {
810
+ this.ctx.settings.setModelRole(role, modelRoleValue);
811
+ }
745
812
  if (isAuto) {
746
813
  this.ctx.session.setThinkingLevel(AUTO_THINKING, true);
747
814
  }
748
815
  const roleInfo = getRoleInfo(role, settings);
749
- this.ctx.showStatus(`${roleInfo?.name ?? role} model: ${selector ?? model.id}`);
816
+ this.ctx.showStatus(
817
+ `${scopeLabel}${roleInfo?.tag ?? roleInfo?.name ?? role} model: ${selector ?? model.id}`,
818
+ );
750
819
  }
751
820
  } catch (error) {
752
821
  this.ctx.showError(error instanceof Error ? error.message : String(error));
822
+ } finally {
823
+ releaseDefaultMutation?.();
824
+ hub?.refreshAfterExternalMutation();
753
825
  }
754
826
  },
755
- onUnassign: role => {
827
+ onUnassign: async (role, scope?: ModelRoleSelectionScope) => {
828
+ const releaseDefaultMutation = role === "default" ? await this.#acquireDefaultRoleMutation() : undefined;
829
+ const configuredStorage = this.ctx.settings.get("modelRoleStorage");
830
+ const targetScope = configuredStorage === "project" ? (scope ?? "project") : "global";
831
+ const scopeLabel =
832
+ configuredStorage === "project" ? `${targetScope === "project" ? "Project" : "Global"} ` : "";
756
833
  try {
757
- this.ctx.settings.setModelRole(role, undefined);
834
+ const previousEffectiveRoleValue =
835
+ role === "default" ? this.ctx.settings.getModelRole("default") : undefined;
836
+ if (targetScope === "project") {
837
+ this.ctx.settings.clearProjectModelRole(role);
838
+ } else {
839
+ this.ctx.settings.setModelRole(role, undefined);
840
+ }
758
841
  const roleInfo = getRoleInfo(role, settings);
759
- this.ctx.showStatus(`${roleInfo?.name ?? role} role cleared — auto-selection applies`);
842
+ this.ctx.showStatus(
843
+ `${scopeLabel}${roleInfo?.tag ?? roleInfo?.name ?? role} role cleared — auto-selection applies`,
844
+ );
845
+ // Clearing either persisted scope can also remove a captured
846
+ // runtime override. When that changes the effective default,
847
+ // resolve the newly exposed persisted layer and switch the live
848
+ // session without writing it back to global settings. Overlay
849
+ // and runtime provenance remain authoritative and session-neutral.
850
+ if (role === "default") {
851
+ const fallbackRoleValue = this.ctx.settings.getModelRole("default");
852
+ const fallbackProvenance = this.ctx.settings.getModelRoleProvenance("default");
853
+ const exposesPersistedFallback =
854
+ fallbackProvenance === "project" || fallbackProvenance === "global";
855
+ if (
856
+ fallbackRoleValue &&
857
+ fallbackRoleValue !== previousEffectiveRoleValue &&
858
+ exposesPersistedFallback
859
+ ) {
860
+ const scopedModels = this.ctx.session.scopedModels.map(sm => sm.model);
861
+ const availableModels =
862
+ scopedModels.length > 0 ? scopedModels : this.ctx.session.getAvailableModels();
863
+ const resolved = resolveModelRoleValue(fallbackRoleValue, availableModels, {
864
+ settings: this.ctx.settings,
865
+ });
866
+ if (resolved.model) {
867
+ const fallbackModel = resolved.model;
868
+ const isAuto = resolved.thinkingLevel === AUTO_THINKING;
869
+ let concreteThinking = concreteThinkingLevel(resolved.thinkingLevel);
870
+ let isAutoFromDefault = false;
871
+ if (!resolved.explicitThinkingLevel && !concreteThinking) {
872
+ const defaultLevel = parseConfiguredThinkingLevel(
873
+ this.ctx.settings.get("defaultThinkingLevel"),
874
+ );
875
+ if (defaultLevel === AUTO_THINKING) {
876
+ isAutoFromDefault = true;
877
+ } else if (defaultLevel) {
878
+ concreteThinking = defaultLevel;
879
+ }
880
+ }
881
+ const effectiveIsAuto = isAuto || isAutoFromDefault;
882
+ const { switched } = await this.ctx.session.setModel(fallbackModel, "default", {
883
+ persist: false,
884
+ thinkingLevel: effectiveIsAuto
885
+ ? ThinkingLevel.Inherit
886
+ : (concreteThinking ?? ThinkingLevel.Inherit),
887
+ currentContextTokens,
888
+ });
889
+ if (!switched) return;
890
+ if (effectiveIsAuto) {
891
+ this.ctx.session.setThinkingLevel(AUTO_THINKING, true);
892
+ } else if (concreteThinking && concreteThinking !== ThinkingLevel.Inherit) {
893
+ this.ctx.session.setThinkingLevel(concreteThinking);
894
+ }
895
+ this.ctx.statusLine.invalidate();
896
+ this.ctx.updateEditorBorderColor();
897
+ }
898
+ }
899
+ }
760
900
  } catch (error) {
761
901
  this.ctx.showError(error instanceof Error ? error.message : String(error));
902
+ } finally {
903
+ releaseDefaultMutation?.();
904
+ hub?.refreshAfterExternalMutation();
762
905
  }
763
906
  },
764
907
  onFallbackChainChange: (role, chain) => {
@@ -773,8 +916,8 @@ export class SelectorController {
773
916
  const roleInfo = getRoleInfo(role, settings);
774
917
  this.ctx.showStatus(
775
918
  chain.length > 0
776
- ? `${roleInfo?.name ?? role} fallbacks: ${chain.join(" → ")}`
777
- : `${roleInfo?.name ?? role} fallbacks cleared`,
919
+ ? `${roleInfo?.tag ?? roleInfo?.name ?? role} fallbacks: ${chain.join(" → ")}`
920
+ : `${roleInfo?.tag ?? roleInfo?.name ?? role} fallbacks cleared`,
778
921
  );
779
922
  } catch (error) {
780
923
  this.ctx.showError(error instanceof Error ? error.message : String(error));
@@ -1124,10 +1267,16 @@ export class SelectorController {
1124
1267
  sessions,
1125
1268
  async (session: SessionInfo) => {
1126
1269
  selector.lockInput();
1270
+ let keepOpen = false;
1127
1271
  try {
1128
- await this.handleResumeSession(session.path);
1272
+ const success = await this.handleResumeSession(session.path);
1273
+ if (!success) {
1274
+ keepOpen = true;
1275
+ selector.unlockInput();
1276
+ this.ctx.ui.requestRender();
1277
+ }
1129
1278
  } finally {
1130
- done();
1279
+ if (!keepOpen) done();
1131
1280
  }
1132
1281
  },
1133
1282
  () => {
@@ -1205,13 +1354,23 @@ export class SelectorController {
1205
1354
  return true;
1206
1355
  }
1207
1356
 
1208
- async handleResumeSession(sessionPath: string): Promise<void> {
1209
- this.ctx.clearTransientSessionUi();
1210
-
1357
+ async handleResumeSession(sessionPath: string, options?: { settingsFlushed?: boolean }): Promise<boolean> {
1211
1358
  const previousCwd = this.ctx.sessionManager.getCwd();
1359
+ // Flush pending settings writes before switching sessions so a save
1360
+ // failure leaves the session, process project dir, and Settings in the
1361
+ // source scope — the switch below mutates the SessionManager cwd.
1362
+ if (!options?.settingsFlushed) {
1363
+ try {
1364
+ await this.ctx.settings.flush();
1365
+ } catch (err) {
1366
+ this.ctx.showError(`Failed to save pending settings: ${err instanceof Error ? err.message : String(err)}`);
1367
+ return false;
1368
+ }
1369
+ }
1212
1370
  // Switch session via AgentSession (emits hook and tool session events). The
1213
1371
  // SessionManager adopts the resumed session's own cwd when it differs.
1214
1372
  await this.ctx.session.switchSession(sessionPath);
1373
+ this.ctx.clearTransientSessionUi();
1215
1374
  const newCwd = this.ctx.sessionManager.getCwd();
1216
1375
  const movedProject = normalizePathForComparison(newCwd) !== normalizePathForComparison(previousCwd);
1217
1376
  if (movedProject) {
@@ -1226,6 +1385,7 @@ export class SelectorController {
1226
1385
  this.ctx.renderInitialMessages({ clearTerminalHistory: true });
1227
1386
  await this.ctx.reloadTodos();
1228
1387
  this.ctx.showStatus(movedProject ? `Resumed session in ${shortenPath(newCwd)}` : "Resumed session");
1388
+ return true;
1229
1389
  }
1230
1390
 
1231
1391
  async handleSessionDeleteCommand(): Promise<void> {