klaude-code 2.1.1__py3-none-any.whl → 2.3.0__py3-none-any.whl

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 (72) hide show
  1. klaude_code/app/__init__.py +1 -2
  2. klaude_code/app/runtime.py +13 -41
  3. klaude_code/cli/list_model.py +27 -10
  4. klaude_code/cli/main.py +42 -159
  5. klaude_code/config/assets/builtin_config.yaml +36 -14
  6. klaude_code/config/config.py +144 -7
  7. klaude_code/config/select_model.py +38 -13
  8. klaude_code/config/sub_agent_model_helper.py +217 -0
  9. klaude_code/const.py +2 -2
  10. klaude_code/core/agent_profile.py +71 -5
  11. klaude_code/core/executor.py +75 -0
  12. klaude_code/core/manager/llm_clients_builder.py +18 -12
  13. klaude_code/core/prompts/prompt-nano-banana.md +1 -0
  14. klaude_code/core/tool/shell/command_safety.py +4 -189
  15. klaude_code/core/tool/sub_agent_tool.py +2 -1
  16. klaude_code/core/turn.py +1 -1
  17. klaude_code/llm/anthropic/client.py +8 -5
  18. klaude_code/llm/anthropic/input.py +54 -29
  19. klaude_code/llm/google/client.py +2 -2
  20. klaude_code/llm/google/input.py +23 -2
  21. klaude_code/llm/openai_compatible/input.py +22 -13
  22. klaude_code/llm/openai_compatible/stream.py +1 -1
  23. klaude_code/llm/openrouter/input.py +37 -25
  24. klaude_code/llm/responses/client.py +1 -1
  25. klaude_code/llm/responses/input.py +96 -57
  26. klaude_code/protocol/commands.py +1 -2
  27. klaude_code/protocol/events/system.py +4 -0
  28. klaude_code/protocol/message.py +2 -2
  29. klaude_code/protocol/op.py +17 -0
  30. klaude_code/protocol/op_handler.py +5 -0
  31. klaude_code/protocol/sub_agent/AGENTS.md +28 -0
  32. klaude_code/protocol/sub_agent/__init__.py +10 -14
  33. klaude_code/protocol/sub_agent/image_gen.py +2 -1
  34. klaude_code/session/codec.py +2 -6
  35. klaude_code/session/session.py +9 -1
  36. klaude_code/skill/assets/create-plan/SKILL.md +3 -5
  37. klaude_code/tui/command/__init__.py +7 -10
  38. klaude_code/tui/command/clear_cmd.py +1 -1
  39. klaude_code/tui/command/command_abc.py +1 -2
  40. klaude_code/tui/command/copy_cmd.py +1 -2
  41. klaude_code/tui/command/fork_session_cmd.py +4 -4
  42. klaude_code/tui/command/model_cmd.py +6 -43
  43. klaude_code/tui/command/model_select.py +75 -15
  44. klaude_code/tui/command/refresh_cmd.py +1 -2
  45. klaude_code/tui/command/resume_cmd.py +3 -4
  46. klaude_code/tui/command/status_cmd.py +1 -1
  47. klaude_code/tui/command/sub_agent_model_cmd.py +190 -0
  48. klaude_code/tui/components/bash_syntax.py +1 -1
  49. klaude_code/tui/components/common.py +1 -1
  50. klaude_code/tui/components/developer.py +10 -15
  51. klaude_code/tui/components/metadata.py +2 -64
  52. klaude_code/tui/components/rich/cjk_wrap.py +3 -2
  53. klaude_code/tui/components/rich/status.py +49 -3
  54. klaude_code/tui/components/rich/theme.py +4 -2
  55. klaude_code/tui/components/sub_agent.py +25 -46
  56. klaude_code/tui/components/user_input.py +9 -21
  57. klaude_code/tui/components/welcome.py +99 -0
  58. klaude_code/tui/input/prompt_toolkit.py +14 -1
  59. klaude_code/tui/renderer.py +2 -3
  60. klaude_code/tui/runner.py +2 -2
  61. klaude_code/tui/terminal/selector.py +8 -18
  62. klaude_code/ui/__init__.py +0 -24
  63. klaude_code/ui/common.py +3 -2
  64. klaude_code/ui/core/display.py +2 -2
  65. {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/METADATA +16 -81
  66. {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/RECORD +68 -67
  67. klaude_code/tui/command/help_cmd.py +0 -51
  68. klaude_code/tui/command/prompt-commit.md +0 -82
  69. klaude_code/tui/command/release_notes_cmd.py +0 -85
  70. klaude_code/ui/exec_mode.py +0 -60
  71. {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/WHEEL +0 -0
  72. {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/entry_points.txt +0 -0
@@ -9,9 +9,11 @@ from klaude_code.protocol import tools
9
9
  if TYPE_CHECKING:
10
10
  from klaude_code.protocol import model
11
11
 
12
- AvailabilityPredicate = Callable[[str], bool]
13
12
  PromptBuilder = Callable[[dict[str, Any]], str]
14
13
 
14
+ # Availability requirement constants
15
+ AVAILABILITY_IMAGE_MODEL = "image_model"
16
+
15
17
 
16
18
  @dataclass
17
19
  class SubAgentResult:
@@ -58,17 +60,13 @@ class SubAgentProfile:
58
60
  # Availability
59
61
  enabled_by_default: bool = True
60
62
  show_in_main_agent: bool = True
61
- target_model_filter: AvailabilityPredicate | None = None
62
63
 
63
64
  # Structured output support: specifies which parameter in the tool schema contains the output schema
64
65
  output_schema_arg: str | None = None
65
66
 
66
- def enabled_for_model(self, model_name: str | None) -> bool:
67
- if not self.enabled_by_default:
68
- return False
69
- if model_name is None or self.target_model_filter is None:
70
- return True
71
- return self.target_model_filter(model_name)
67
+ # Config-based availability requirement (e.g., "image_model" means requires an image model)
68
+ # The actual check is performed in the core layer to avoid circular imports.
69
+ availability_requirement: str | None = None
72
70
 
73
71
 
74
72
  _PROFILES: dict[str, SubAgentProfile] = {}
@@ -87,11 +85,11 @@ def get_sub_agent_profile(sub_agent_type: tools.SubAgentType) -> SubAgentProfile
87
85
  raise KeyError(f"Unknown sub agent type: {sub_agent_type}") from exc
88
86
 
89
87
 
90
- def iter_sub_agent_profiles(enabled_only: bool = False, model_name: str | None = None) -> list[SubAgentProfile]:
88
+ def iter_sub_agent_profiles(enabled_only: bool = False) -> list[SubAgentProfile]:
91
89
  profiles = list(_PROFILES.values())
92
90
  if not enabled_only:
93
91
  return profiles
94
- return [p for p in profiles if p.enabled_for_model(model_name)]
92
+ return [p for p in profiles if p.enabled_by_default]
95
93
 
96
94
 
97
95
  def get_sub_agent_profile_by_tool(tool_name: str) -> SubAgentProfile | None:
@@ -102,11 +100,9 @@ def is_sub_agent_tool(tool_name: str) -> bool:
102
100
  return tool_name in _PROFILES
103
101
 
104
102
 
105
- def sub_agent_tool_names(enabled_only: bool = False, model_name: str | None = None) -> list[str]:
103
+ def sub_agent_tool_names(enabled_only: bool = False) -> list[str]:
106
104
  return [
107
- profile.name
108
- for profile in iter_sub_agent_profiles(enabled_only=enabled_only, model_name=model_name)
109
- if profile.show_in_main_agent
105
+ profile.name for profile in iter_sub_agent_profiles(enabled_only=enabled_only) if profile.show_in_main_agent
110
106
  ]
111
107
 
112
108
 
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Any, cast
4
4
 
5
- from klaude_code.protocol.sub_agent import SubAgentProfile, register_sub_agent
5
+ from klaude_code.protocol.sub_agent import AVAILABILITY_IMAGE_MODEL, SubAgentProfile, register_sub_agent
6
6
 
7
7
  IMAGE_GEN_DESCRIPTION = """\
8
8
  Generate one or more images from a text prompt.
@@ -115,5 +115,6 @@ register_sub_agent(
115
115
  tool_set=(),
116
116
  prompt_builder=_build_image_gen_prompt,
117
117
  active_form="Generating Image",
118
+ availability_requirement=AVAILABILITY_IMAGE_MODEL,
118
119
  )
119
120
  )
@@ -1,17 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import json
4
- from typing import Any, TypeGuard, cast, get_args
4
+ from typing import Any, cast, get_args
5
5
 
6
6
  from pydantic import BaseModel
7
7
 
8
8
  from klaude_code.protocol import message
9
9
 
10
10
 
11
- def _is_basemodel_subclass(tp: object) -> TypeGuard[type[BaseModel]]:
12
- return isinstance(tp, type) and issubclass(tp, BaseModel)
13
-
14
-
15
11
  def _flatten_union(tp: object) -> list[object]:
16
12
  args = list(get_args(tp))
17
13
  if not args:
@@ -25,7 +21,7 @@ def _flatten_union(tp: object) -> list[object]:
25
21
  def _build_type_registry() -> dict[str, type[BaseModel]]:
26
22
  registry: dict[str, type[BaseModel]] = {}
27
23
  for tp in _flatten_union(message.HistoryEvent):
28
- if not _is_basemodel_subclass(tp):
24
+ if not isinstance(tp, type) or not issubclass(tp, BaseModel):
29
25
  continue
30
26
  registry[tp.__name__] = tp
31
27
  return registry
@@ -370,8 +370,16 @@ class Session(BaseModel):
370
370
 
371
371
  has_structured_output = report_back_result is not None
372
372
  task_result = report_back_result if has_structured_output else last_assistant_content
373
+
374
+ if self.sub_agent_state is not None:
375
+ trimmed = (task_result or "").rstrip()
376
+ lines = trimmed.splitlines()
377
+ if not (lines and lines[-1].startswith("agentId:")):
378
+ footer = f"agentId: {self.id} (for resuming to continue this agent's work if needed)"
379
+ task_result = f"{trimmed}\n\n{footer}" if trimmed.strip() else footer
380
+
373
381
  yield events.TaskFinishEvent(
374
- session_id=self.id, task_result=task_result, has_structured_output=has_structured_output
382
+ session_id=self.id, task_result=task_result or "", has_structured_output=has_structured_output
375
383
  )
376
384
 
377
385
  def _iter_sub_agent_history(
@@ -13,6 +13,8 @@ Turn a user prompt into a **single, actionable plan** delivered in the final ass
13
13
 
14
14
  ## Minimal workflow
15
15
 
16
+ Throughout the entire workflow, operate in read-only mode. Do not write or update files.
17
+
16
18
  1. **Scan context quickly**
17
19
  - Read `README.md` and any obvious docs (`docs/`, `CONTRIBUTING.md`, `ARCHITECTURE.md`).
18
20
  - Skim relevant files (the ones most likely touched).
@@ -33,11 +35,7 @@ Turn a user prompt into a **single, actionable plan** delivered in the final ass
33
35
  - Include at least one item for **tests/validation** and one for **edge cases/risk** when applicable.
34
36
  - If there are unknowns, include a tiny **Open questions** section (max 3).
35
37
 
36
- 4. **Write the plan to `plan.md` in the current working directory**
37
- - Use the Write tool to save the plan to `./plan.md`
38
- - If `plan.md` already exists, overwrite it with the new plan
39
-
40
- 5. **Do not preface the plan with meta explanations; output only the plan as per template**
38
+ 4. **Do not preface the plan with meta explanations; output only the plan as per template**
41
39
 
42
40
  ## Plan template (follow exactly)
43
41
 
@@ -35,28 +35,26 @@ def ensure_commands_loaded() -> None:
35
35
  from .export_cmd import ExportCommand
36
36
  from .export_online_cmd import ExportOnlineCommand
37
37
  from .fork_session_cmd import ForkSessionCommand
38
- from .help_cmd import HelpCommand
39
38
  from .model_cmd import ModelCommand
40
39
  from .refresh_cmd import RefreshTerminalCommand
41
- from .release_notes_cmd import ReleaseNotesCommand
42
40
  from .resume_cmd import ResumeCommand
43
41
  from .status_cmd import StatusCommand
42
+ from .sub_agent_model_cmd import SubAgentModelCommand
44
43
  from .terminal_setup_cmd import TerminalSetupCommand
45
44
  from .thinking_cmd import ThinkingCommand
46
45
 
47
46
  # Register in desired display order
47
+ register(CopyCommand())
48
48
  register(ExportCommand())
49
- register(ExportOnlineCommand())
50
49
  register(RefreshTerminalCommand())
51
- register(ThinkingCommand())
52
50
  register(ModelCommand())
53
- register(CopyCommand())
51
+ register(SubAgentModelCommand())
52
+ register(ThinkingCommand())
54
53
  register(ForkSessionCommand())
55
- register(ResumeCommand())
56
54
  load_prompt_commands()
57
55
  register(StatusCommand())
58
- register(HelpCommand())
59
- register(ReleaseNotesCommand())
56
+ register(ResumeCommand())
57
+ register(ExportOnlineCommand())
60
58
  register(TerminalSetupCommand())
61
59
  register(DebugCommand())
62
60
  register(ClearCommand())
@@ -73,12 +71,11 @@ def __getattr__(name: str) -> object:
73
71
  "ExportCommand": "export_cmd",
74
72
  "ExportOnlineCommand": "export_online_cmd",
75
73
  "ForkSessionCommand": "fork_session_cmd",
76
- "HelpCommand": "help_cmd",
77
74
  "ModelCommand": "model_cmd",
78
75
  "RefreshTerminalCommand": "refresh_cmd",
79
- "ReleaseNotesCommand": "release_notes_cmd",
80
76
  "ResumeCommand": "resume_cmd",
81
77
  "StatusCommand": "status_cmd",
78
+ "SubAgentModelCommand": "sub_agent_model_cmd",
82
79
  "TerminalSetupCommand": "terminal_setup_cmd",
83
80
  "ThinkingCommand": "thinking_cmd",
84
81
  }
@@ -22,5 +22,5 @@ class ClearCommand(CommandABC):
22
22
 
23
23
  return CommandResult(
24
24
  operations=[op.ClearSessionOperation(session_id=agent.session.id)],
25
- persist_user_input=False,
25
+ persist=False,
26
26
  )
@@ -43,8 +43,7 @@ class CommandResult(BaseModel):
43
43
  operations: list[op.Operation] | None = None
44
44
 
45
45
  # Persistence controls: some slash commands are UI/control actions and should not be written to session history.
46
- persist_user_input: bool = True
47
- persist_events: bool = True
46
+ persist: bool = True
48
47
 
49
48
 
50
49
  class CommandABC(ABC):
@@ -48,6 +48,5 @@ def _developer_message(agent: Agent, content: str, command_name: commands.Comman
48
48
  ),
49
49
  )
50
50
  ],
51
- persist_user_input=False,
52
- persist_events=False,
51
+ persist=False,
53
52
  )
@@ -211,7 +211,7 @@ class ForkSessionCommand(CommandABC):
211
211
  ui_extra=model.build_command_output_extra(self.name),
212
212
  ),
213
213
  )
214
- return CommandResult(events=[event], persist_user_input=False, persist_events=False)
214
+ return CommandResult(events=[event], persist=False)
215
215
 
216
216
  # Build fork points from conversation history
217
217
  fork_points = _build_fork_points(agent.session.conversation_history)
@@ -234,7 +234,7 @@ class ForkSessionCommand(CommandABC):
234
234
  ),
235
235
  ),
236
236
  )
237
- return CommandResult(events=[event], persist_user_input=False, persist_events=False)
237
+ return CommandResult(events=[event], persist=False)
238
238
 
239
239
  # Interactive selection
240
240
  selected = await asyncio.to_thread(_select_fork_point_sync, fork_points)
@@ -247,7 +247,7 @@ class ForkSessionCommand(CommandABC):
247
247
  ui_extra=model.build_command_output_extra(self.name),
248
248
  ),
249
249
  )
250
- return CommandResult(events=[event], persist_user_input=False, persist_events=False)
250
+ return CommandResult(events=[event], persist=False)
251
251
 
252
252
  # Perform the fork
253
253
  new_session = agent.session.fork(until_index=selected)
@@ -271,4 +271,4 @@ class ForkSessionCommand(CommandABC):
271
271
  ),
272
272
  ),
273
273
  )
274
- return CommandResult(events=[event], persist_user_input=False, persist_events=False)
274
+ return CommandResult(events=[event], persist=False)
@@ -1,46 +1,9 @@
1
1
  import asyncio
2
2
 
3
- from prompt_toolkit.styles import Style
4
-
5
3
  from klaude_code.protocol import commands, events, message, model, op
6
- from klaude_code.tui.terminal.selector import SelectItem, select_one
7
4
 
8
5
  from .command_abc import Agent, CommandABC, CommandResult
9
- from .model_select import select_model_interactive
10
-
11
- SELECT_STYLE = Style(
12
- [
13
- ("instruction", "ansibrightblack"),
14
- ("pointer", "ansigreen"),
15
- ("highlighted", "ansigreen"),
16
- ("text", "ansibrightblack"),
17
- ("question", "bold"),
18
- ]
19
- )
20
-
21
-
22
- def _confirm_change_default_model_sync(selected_model: str) -> bool:
23
- items: list[SelectItem[bool]] = [
24
- SelectItem(title=[("class:text", "No (session only)\n")], value=False, search_text="No"),
25
- SelectItem(
26
- title=[("class:text", "Yes (save as default main_model in ~/.klaude/klaude-config.yaml)\n")],
27
- value=True,
28
- search_text="Yes",
29
- ),
30
- ]
31
-
32
- try:
33
- result = select_one(
34
- message=f"Save '{selected_model}' as default model?",
35
- items=items,
36
- pointer="→",
37
- style=SELECT_STYLE,
38
- use_search_filter=False,
39
- )
40
- except KeyboardInterrupt:
41
- return False
42
-
43
- return bool(result)
6
+ from .model_select import ModelSelectStatus, select_model_interactive
44
7
 
45
8
 
46
9
  class ModelCommand(CommandABC):
@@ -52,7 +15,7 @@ class ModelCommand(CommandABC):
52
15
 
53
16
  @property
54
17
  def summary(self) -> str:
55
- return "Select and switch LLM"
18
+ return "Change model (saves to config)"
56
19
 
57
20
  @property
58
21
  def is_interactive(self) -> bool:
@@ -67,9 +30,10 @@ class ModelCommand(CommandABC):
67
30
  return "model name"
68
31
 
69
32
  async def run(self, agent: Agent, user_input: message.UserInputPayload) -> CommandResult:
70
- selected_model = await asyncio.to_thread(select_model_interactive, preferred=user_input.text)
33
+ model_result = await asyncio.to_thread(select_model_interactive, preferred=user_input.text)
71
34
 
72
- current_model = agent.profile.llm_client.model_name if agent.profile else None
35
+ current_model = agent.session.model_config_name
36
+ selected_model = model_result.model if model_result.status == ModelSelectStatus.SELECTED else None
73
37
  if selected_model is None or selected_model == current_model:
74
38
  return CommandResult(
75
39
  events=[
@@ -82,13 +46,12 @@ class ModelCommand(CommandABC):
82
46
  )
83
47
  ]
84
48
  )
85
- save_as_default = await asyncio.to_thread(_confirm_change_default_model_sync, selected_model)
86
49
  return CommandResult(
87
50
  operations=[
88
51
  op.ChangeModelOperation(
89
52
  session_id=agent.session.id,
90
53
  model_name=selected_model,
91
- save_as_default=save_as_default,
54
+ save_as_default=True,
92
55
  )
93
56
  ]
94
57
  )
@@ -1,58 +1,107 @@
1
1
  """Interactive model selection for CLI."""
2
2
 
3
+ from __future__ import annotations
4
+
3
5
  import sys
6
+ from dataclasses import dataclass
7
+ from enum import Enum
4
8
 
5
9
  from klaude_code.config.config import load_config
6
10
  from klaude_code.config.select_model import match_model_from_config
7
11
  from klaude_code.log import log
8
12
 
9
13
 
10
- def select_model_interactive(preferred: str | None = None) -> str | None:
14
+ class ModelSelectStatus(Enum):
15
+ SELECTED = "selected"
16
+ CANCELLED = "cancelled"
17
+ NO_MATCH = "no_match"
18
+ NO_MODELS = "no_models"
19
+ NON_TTY = "non_tty"
20
+ ERROR = "error"
21
+
22
+
23
+ @dataclass
24
+ class ModelSelectResult:
25
+ status: ModelSelectStatus
26
+ model: str | None = None
27
+
28
+
29
+ def select_model_interactive(
30
+ preferred: str | None = None,
31
+ keywords: list[str] | None = None,
32
+ ) -> ModelSelectResult:
11
33
  """Interactive single-choice model selector.
12
34
 
13
35
  This function combines matching logic with interactive UI selection.
14
36
  For CLI usage.
15
37
 
38
+ If keywords is provided, preferred is ignored and the model list is pre-filtered by model_params.model.
39
+
16
40
  If preferred is provided:
17
41
  - Exact match: return immediately
18
42
  - Single partial match (case-insensitive): return immediately
19
43
  - Otherwise: fall through to interactive selection
20
44
  """
21
- result = match_model_from_config(preferred)
45
+ config = load_config()
46
+ result = match_model_from_config(None if keywords else preferred)
22
47
 
23
48
  if result.error_message:
24
- return None
49
+ return ModelSelectResult(status=ModelSelectStatus.NO_MODELS)
25
50
 
26
51
  if result.matched_model:
27
- return result.matched_model
52
+ return ModelSelectResult(status=ModelSelectStatus.SELECTED, model=result.matched_model)
53
+
54
+ if keywords:
55
+ keywords_lower = [k.lower() for k in keywords]
56
+ filtered_models = [
57
+ m
58
+ for m in result.filtered_models
59
+ if any(kw in (m.model_params.model or "").lower() for kw in keywords_lower)
60
+ ]
61
+ if not filtered_models:
62
+ return ModelSelectResult(status=ModelSelectStatus.NO_MATCH)
63
+ result.filtered_models = filtered_models
64
+ result.filter_hint = ", ".join(keywords)
65
+ result.matched_model = None
28
66
 
29
67
  # Non-interactive environments (CI/pipes) should never enter an interactive prompt.
30
68
  # If we couldn't resolve to a single model deterministically above, fail with a clear hint.
31
69
  if not sys.stdin.isatty() or not sys.stdout.isatty():
32
70
  log(("Error: cannot use interactive model selection without a TTY", "red"))
33
71
  log(("Hint: pass --model <config-name> or set main_model in ~/.klaude/klaude-config.yaml", "yellow"))
34
- if preferred:
72
+ if preferred and not keywords:
35
73
  log((f"Hint: '{preferred}' did not resolve to a single configured model", "yellow"))
36
- return None
74
+ return ModelSelectResult(status=ModelSelectStatus.NON_TTY)
37
75
 
38
76
  # Interactive selection
39
77
  from prompt_toolkit.styles import Style
40
78
 
41
79
  from klaude_code.tui.terminal.selector import build_model_select_items, select_one
42
80
 
43
- config = load_config()
44
- names = [m.model_name for m in result.filtered_models]
81
+ names = [m.selector for m in result.filtered_models]
45
82
 
46
83
  try:
47
84
  items = build_model_select_items(result.filtered_models)
48
85
 
49
86
  message = f"Select a model (filtered by '{result.filter_hint}'):" if result.filter_hint else "Select a model:"
87
+
88
+ initial_value = config.main_model
89
+ if isinstance(initial_value, str) and initial_value and "@" not in initial_value:
90
+ try:
91
+ resolved = config.resolve_model_location_prefer_available(
92
+ initial_value
93
+ ) or config.resolve_model_location(initial_value)
94
+ except ValueError:
95
+ resolved = None
96
+ if resolved is not None:
97
+ initial_value = f"{resolved[0]}@{resolved[1]}"
98
+
50
99
  selected = select_one(
51
100
  message=message,
52
101
  items=items,
53
102
  pointer="→",
54
103
  use_search_filter=True,
55
- initial_value=config.main_model,
104
+ initial_value=initial_value,
56
105
  style=Style(
57
106
  [
58
107
  ("pointer", "ansigreen"),
@@ -69,16 +118,27 @@ def select_model_interactive(preferred: str | None = None) -> str | None:
69
118
  ),
70
119
  )
71
120
  if isinstance(selected, str) and selected in names:
72
- return selected
121
+ return ModelSelectResult(status=ModelSelectStatus.SELECTED, model=selected)
73
122
  except KeyboardInterrupt:
74
- return None
123
+ return ModelSelectResult(status=ModelSelectStatus.CANCELLED)
75
124
  except Exception as e:
76
125
  log((f"Failed to use prompt_toolkit for model selection: {e}", "yellow"))
77
126
  # Never return an unvalidated model name here.
78
127
  # If we can't interactively select, fall back to a known configured model.
79
- if isinstance(preferred, str) and preferred in names:
80
- return preferred
128
+ if result.matched_model and result.matched_model in names:
129
+ return ModelSelectResult(status=ModelSelectStatus.SELECTED, model=result.matched_model)
81
130
  if config.main_model and config.main_model in names:
82
- return config.main_model
131
+ return ModelSelectResult(status=ModelSelectStatus.SELECTED, model=config.main_model)
132
+ if config.main_model and "@" not in config.main_model:
133
+ try:
134
+ resolved = config.resolve_model_location_prefer_available(
135
+ config.main_model
136
+ ) or config.resolve_model_location(config.main_model)
137
+ except ValueError:
138
+ resolved = None
139
+ if resolved is not None:
140
+ selector = f"{resolved[0]}@{resolved[1]}"
141
+ if selector in names:
142
+ return ModelSelectResult(status=ModelSelectStatus.SELECTED, model=selector)
83
143
 
84
- return None
144
+ return ModelSelectResult(status=ModelSelectStatus.ERROR)
@@ -38,6 +38,5 @@ class RefreshTerminalCommand(CommandABC):
38
38
  is_load=False,
39
39
  ),
40
40
  ],
41
- persist_user_input=False,
42
- persist_events=False,
41
+ persist=False,
43
42
  )
@@ -96,7 +96,7 @@ class ResumeCommand(CommandABC):
96
96
  ui_extra=model.build_command_output_extra(self.name, is_error=True),
97
97
  ),
98
98
  )
99
- return CommandResult(events=[event], persist_user_input=False, persist_events=False)
99
+ return CommandResult(events=[event], persist=False)
100
100
 
101
101
  selected_session_id = await asyncio.to_thread(select_session_sync)
102
102
  if selected_session_id is None:
@@ -107,10 +107,9 @@ class ResumeCommand(CommandABC):
107
107
  ui_extra=model.build_command_output_extra(self.name),
108
108
  ),
109
109
  )
110
- return CommandResult(events=[event], persist_user_input=False, persist_events=False)
110
+ return CommandResult(events=[event], persist=False)
111
111
 
112
112
  return CommandResult(
113
113
  operations=[op.ResumeSessionOperation(target_session_id=selected_session_id)],
114
- persist_user_input=False,
115
- persist_events=False,
114
+ persist=False,
116
115
  )
@@ -153,4 +153,4 @@ class StatusCommand(CommandABC):
153
153
  ),
154
154
  )
155
155
 
156
- return CommandResult(events=[event], persist_user_input=False, persist_events=False)
156
+ return CommandResult(events=[event], persist=False)