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.
- klaude_code/app/__init__.py +1 -2
- klaude_code/app/runtime.py +13 -41
- klaude_code/cli/list_model.py +27 -10
- klaude_code/cli/main.py +42 -159
- klaude_code/config/assets/builtin_config.yaml +36 -14
- klaude_code/config/config.py +144 -7
- klaude_code/config/select_model.py +38 -13
- klaude_code/config/sub_agent_model_helper.py +217 -0
- klaude_code/const.py +2 -2
- klaude_code/core/agent_profile.py +71 -5
- klaude_code/core/executor.py +75 -0
- klaude_code/core/manager/llm_clients_builder.py +18 -12
- klaude_code/core/prompts/prompt-nano-banana.md +1 -0
- klaude_code/core/tool/shell/command_safety.py +4 -189
- klaude_code/core/tool/sub_agent_tool.py +2 -1
- klaude_code/core/turn.py +1 -1
- klaude_code/llm/anthropic/client.py +8 -5
- klaude_code/llm/anthropic/input.py +54 -29
- klaude_code/llm/google/client.py +2 -2
- klaude_code/llm/google/input.py +23 -2
- klaude_code/llm/openai_compatible/input.py +22 -13
- klaude_code/llm/openai_compatible/stream.py +1 -1
- klaude_code/llm/openrouter/input.py +37 -25
- klaude_code/llm/responses/client.py +1 -1
- klaude_code/llm/responses/input.py +96 -57
- klaude_code/protocol/commands.py +1 -2
- klaude_code/protocol/events/system.py +4 -0
- klaude_code/protocol/message.py +2 -2
- klaude_code/protocol/op.py +17 -0
- klaude_code/protocol/op_handler.py +5 -0
- klaude_code/protocol/sub_agent/AGENTS.md +28 -0
- klaude_code/protocol/sub_agent/__init__.py +10 -14
- klaude_code/protocol/sub_agent/image_gen.py +2 -1
- klaude_code/session/codec.py +2 -6
- klaude_code/session/session.py +9 -1
- klaude_code/skill/assets/create-plan/SKILL.md +3 -5
- klaude_code/tui/command/__init__.py +7 -10
- klaude_code/tui/command/clear_cmd.py +1 -1
- klaude_code/tui/command/command_abc.py +1 -2
- klaude_code/tui/command/copy_cmd.py +1 -2
- klaude_code/tui/command/fork_session_cmd.py +4 -4
- klaude_code/tui/command/model_cmd.py +6 -43
- klaude_code/tui/command/model_select.py +75 -15
- klaude_code/tui/command/refresh_cmd.py +1 -2
- klaude_code/tui/command/resume_cmd.py +3 -4
- klaude_code/tui/command/status_cmd.py +1 -1
- klaude_code/tui/command/sub_agent_model_cmd.py +190 -0
- klaude_code/tui/components/bash_syntax.py +1 -1
- klaude_code/tui/components/common.py +1 -1
- klaude_code/tui/components/developer.py +10 -15
- klaude_code/tui/components/metadata.py +2 -64
- klaude_code/tui/components/rich/cjk_wrap.py +3 -2
- klaude_code/tui/components/rich/status.py +49 -3
- klaude_code/tui/components/rich/theme.py +4 -2
- klaude_code/tui/components/sub_agent.py +25 -46
- klaude_code/tui/components/user_input.py +9 -21
- klaude_code/tui/components/welcome.py +99 -0
- klaude_code/tui/input/prompt_toolkit.py +14 -1
- klaude_code/tui/renderer.py +2 -3
- klaude_code/tui/runner.py +2 -2
- klaude_code/tui/terminal/selector.py +8 -18
- klaude_code/ui/__init__.py +0 -24
- klaude_code/ui/common.py +3 -2
- klaude_code/ui/core/display.py +2 -2
- {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/METADATA +16 -81
- {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/RECORD +68 -67
- klaude_code/tui/command/help_cmd.py +0 -51
- klaude_code/tui/command/prompt-commit.md +0 -82
- klaude_code/tui/command/release_notes_cmd.py +0 -85
- klaude_code/ui/exec_mode.py +0 -60
- {klaude_code-2.1.1.dist-info → klaude_code-2.3.0.dist-info}/WHEEL +0 -0
- {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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
|
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.
|
|
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
|
|
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
|
)
|
klaude_code/session/codec.py
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
from typing import Any,
|
|
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
|
|
24
|
+
if not isinstance(tp, type) or not issubclass(tp, BaseModel):
|
|
29
25
|
continue
|
|
30
26
|
registry[tp.__name__] = tp
|
|
31
27
|
return registry
|
klaude_code/session/session.py
CHANGED
|
@@ -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. **
|
|
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(
|
|
51
|
+
register(SubAgentModelCommand())
|
|
52
|
+
register(ThinkingCommand())
|
|
54
53
|
register(ForkSessionCommand())
|
|
55
|
-
register(ResumeCommand())
|
|
56
54
|
load_prompt_commands()
|
|
57
55
|
register(StatusCommand())
|
|
58
|
-
register(
|
|
59
|
-
register(
|
|
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
|
}
|
|
@@ -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
|
-
|
|
47
|
-
persist_events: bool = True
|
|
46
|
+
persist: bool = True
|
|
48
47
|
|
|
49
48
|
|
|
50
49
|
class CommandABC(ABC):
|
|
@@ -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],
|
|
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],
|
|
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],
|
|
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],
|
|
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 "
|
|
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
|
-
|
|
33
|
+
model_result = await asyncio.to_thread(select_model_interactive, preferred=user_input.text)
|
|
71
34
|
|
|
72
|
-
current_model = agent.
|
|
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=
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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=
|
|
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
|
|
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
|
|
80
|
-
return
|
|
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
|
|
144
|
+
return ModelSelectResult(status=ModelSelectStatus.ERROR)
|
|
@@ -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],
|
|
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],
|
|
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
|
-
|
|
115
|
-
persist_events=False,
|
|
114
|
+
persist=False,
|
|
116
115
|
)
|