klaude-code 1.2.26__py3-none-any.whl → 1.2.28__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/cli/config_cmd.py +1 -5
- klaude_code/cli/debug.py +9 -1
- klaude_code/cli/list_model.py +170 -129
- klaude_code/cli/main.py +76 -19
- klaude_code/cli/runtime.py +15 -11
- klaude_code/cli/self_update.py +2 -1
- klaude_code/cli/session_cmd.py +1 -1
- klaude_code/command/__init__.py +3 -0
- klaude_code/command/export_online_cmd.py +15 -12
- klaude_code/command/fork_session_cmd.py +42 -0
- klaude_code/config/__init__.py +3 -1
- klaude_code/config/assets/__init__.py +1 -0
- klaude_code/config/assets/builtin_config.yaml +233 -0
- klaude_code/config/builtin_config.py +37 -0
- klaude_code/config/config.py +332 -112
- klaude_code/config/select_model.py +46 -8
- klaude_code/core/executor.py +6 -3
- klaude_code/core/manager/llm_clients_builder.py +4 -1
- klaude_code/core/reminders.py +52 -16
- klaude_code/core/tool/file/edit_tool.py +4 -4
- klaude_code/core/tool/file/write_tool.py +4 -4
- klaude_code/core/tool/shell/bash_tool.py +2 -2
- klaude_code/core/tool/web/mermaid_tool.md +17 -0
- klaude_code/core/tool/web/mermaid_tool.py +2 -2
- klaude_code/llm/openai_compatible/stream.py +2 -1
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
- klaude_code/protocol/commands.py +1 -0
- klaude_code/protocol/model.py +1 -0
- klaude_code/session/export.py +52 -7
- klaude_code/session/selector.py +2 -2
- klaude_code/session/session.py +26 -4
- klaude_code/trace/log.py +7 -1
- klaude_code/ui/modes/repl/__init__.py +3 -44
- klaude_code/ui/modes/repl/completers.py +39 -7
- klaude_code/ui/modes/repl/event_handler.py +8 -6
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +33 -66
- klaude_code/ui/modes/repl/key_bindings.py +4 -4
- klaude_code/ui/modes/repl/renderer.py +1 -6
- klaude_code/ui/renderers/common.py +11 -4
- klaude_code/ui/renderers/developer.py +17 -0
- klaude_code/ui/renderers/diffs.py +1 -1
- klaude_code/ui/renderers/errors.py +10 -5
- klaude_code/ui/renderers/metadata.py +2 -2
- klaude_code/ui/renderers/tools.py +8 -4
- klaude_code/ui/rich/markdown.py +5 -5
- klaude_code/ui/rich/theme.py +7 -3
- klaude_code/ui/terminal/color.py +1 -1
- klaude_code/ui/terminal/control.py +4 -4
- {klaude_code-1.2.26.dist-info → klaude_code-1.2.28.dist-info}/METADATA +121 -127
- {klaude_code-1.2.26.dist-info → klaude_code-1.2.28.dist-info}/RECORD +52 -48
- {klaude_code-1.2.26.dist-info → klaude_code-1.2.28.dist-info}/entry_points.txt +1 -0
- {klaude_code-1.2.26.dist-info → klaude_code-1.2.28.dist-info}/WHEEL +0 -0
klaude_code/cli/config_cmd.py
CHANGED
|
@@ -16,8 +16,6 @@ def list_models() -> None:
|
|
|
16
16
|
from klaude_code.ui.terminal.color import is_light_terminal_background
|
|
17
17
|
|
|
18
18
|
config = load_config()
|
|
19
|
-
if config is None:
|
|
20
|
-
raise typer.Exit(1)
|
|
21
19
|
|
|
22
20
|
# Auto-detect theme when not explicitly set in config, to match other CLI entrypoints.
|
|
23
21
|
if config.theme is None:
|
|
@@ -60,9 +58,7 @@ def edit_config() -> None:
|
|
|
60
58
|
editor = "xdg-open"
|
|
61
59
|
|
|
62
60
|
# Ensure config file exists
|
|
63
|
-
|
|
64
|
-
if config is None:
|
|
65
|
-
raise typer.Exit(1)
|
|
61
|
+
load_config()
|
|
66
62
|
|
|
67
63
|
try:
|
|
68
64
|
if editor == "open -a TextEdit":
|
klaude_code/cli/debug.py
CHANGED
|
@@ -63,7 +63,15 @@ def open_log_file_in_editor(path: Path) -> None:
|
|
|
63
63
|
editor = "xdg-open"
|
|
64
64
|
|
|
65
65
|
try:
|
|
66
|
-
|
|
66
|
+
# Detach stdin to prevent the editor from interfering with terminal input state.
|
|
67
|
+
# Without this, the spawned process inherits the parent's TTY and can disrupt
|
|
68
|
+
# prompt_toolkit's keyboard handling (e.g., history navigation with up/down keys).
|
|
69
|
+
subprocess.Popen(
|
|
70
|
+
[editor, str(path)],
|
|
71
|
+
stdin=subprocess.DEVNULL,
|
|
72
|
+
stdout=subprocess.DEVNULL,
|
|
73
|
+
stderr=subprocess.DEVNULL,
|
|
74
|
+
)
|
|
67
75
|
except FileNotFoundError:
|
|
68
76
|
log((f"Error: Editor '{editor}' not found", "red"))
|
|
69
77
|
except Exception as exc: # pragma: no cover - best effort
|
klaude_code/cli/list_model.py
CHANGED
|
@@ -6,44 +6,47 @@ from rich.table import Table
|
|
|
6
6
|
from rich.text import Text
|
|
7
7
|
|
|
8
8
|
from klaude_code.config import Config
|
|
9
|
+
from klaude_code.config.config import ModelConfig, ProviderConfig
|
|
10
|
+
from klaude_code.protocol.llm_param import LLMClientProtocol
|
|
9
11
|
from klaude_code.protocol.sub_agent import iter_sub_agent_profiles
|
|
10
12
|
from klaude_code.ui.rich.theme import ThemeKey, get_theme
|
|
11
13
|
|
|
12
14
|
|
|
13
|
-
def
|
|
14
|
-
"""
|
|
15
|
+
def _get_codex_status_elements() -> list[Text]:
|
|
16
|
+
"""Get Codex OAuth login status as Text elements for panel display."""
|
|
15
17
|
from klaude_code.auth.codex.token_manager import CodexTokenManager
|
|
16
18
|
|
|
19
|
+
elements: list[Text] = []
|
|
17
20
|
token_manager = CodexTokenManager()
|
|
18
21
|
state = token_manager.get_state()
|
|
19
22
|
|
|
20
23
|
if state is None:
|
|
21
|
-
|
|
24
|
+
elements.append(
|
|
22
25
|
Text.assemble(
|
|
23
|
-
("
|
|
26
|
+
("Status: ", "bold"),
|
|
24
27
|
("Not logged in", ThemeKey.CONFIG_STATUS_ERROR),
|
|
25
28
|
(" (run 'klaude login codex' to authenticate)", "dim"),
|
|
26
29
|
)
|
|
27
30
|
)
|
|
28
31
|
elif state.is_expired():
|
|
29
|
-
|
|
32
|
+
elements.append(
|
|
30
33
|
Text.assemble(
|
|
31
|
-
("
|
|
34
|
+
("Status: ", "bold"),
|
|
32
35
|
("Token expired", ThemeKey.CONFIG_STATUS_ERROR),
|
|
33
36
|
(" (run 'klaude login codex' to re-authenticate)", "dim"),
|
|
34
37
|
)
|
|
35
38
|
)
|
|
36
39
|
else:
|
|
37
40
|
expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.UTC)
|
|
38
|
-
|
|
41
|
+
elements.append(
|
|
39
42
|
Text.assemble(
|
|
40
|
-
("
|
|
43
|
+
("Status: ", "bold"),
|
|
41
44
|
("Logged in", ThemeKey.CONFIG_STATUS_OK),
|
|
42
45
|
(f" (account: {state.account_id[:8]}..., expires: {expires_dt.strftime('%Y-%m-%d %H:%M UTC')})", "dim"),
|
|
43
46
|
)
|
|
44
47
|
)
|
|
45
48
|
|
|
46
|
-
|
|
49
|
+
elements.append(
|
|
47
50
|
Text.assemble(
|
|
48
51
|
("Visit ", "dim"),
|
|
49
52
|
(
|
|
@@ -53,6 +56,7 @@ def _display_codex_status(console: Console) -> None:
|
|
|
53
56
|
(" for up-to-date information on rate limits and credits", "dim"),
|
|
54
57
|
)
|
|
55
58
|
)
|
|
59
|
+
return elements
|
|
56
60
|
|
|
57
61
|
|
|
58
62
|
def mask_api_key(api_key: str | None) -> str:
|
|
@@ -66,136 +70,179 @@ def mask_api_key(api_key: str | None) -> str:
|
|
|
66
70
|
return f"{api_key[:6]} … {api_key[-6:]}"
|
|
67
71
|
|
|
68
72
|
|
|
69
|
-
def
|
|
70
|
-
"""
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
def format_api_key_display(provider: ProviderConfig) -> Text:
|
|
74
|
+
"""Format API key display with warning if env var is not set."""
|
|
75
|
+
env_var = provider.get_api_key_env_var()
|
|
76
|
+
resolved_key = provider.get_resolved_api_key()
|
|
73
77
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
Text(
|
|
89
|
-
|
|
78
|
+
if env_var:
|
|
79
|
+
# Using ${ENV_VAR} syntax
|
|
80
|
+
if resolved_key:
|
|
81
|
+
return Text.assemble(
|
|
82
|
+
(f"${{{env_var}}} = ", "dim"),
|
|
83
|
+
(mask_api_key(resolved_key), ""),
|
|
84
|
+
)
|
|
85
|
+
else:
|
|
86
|
+
return Text.assemble(
|
|
87
|
+
(f"${{{env_var}}} ", ""),
|
|
88
|
+
("(not set)", ThemeKey.CONFIG_STATUS_ERROR),
|
|
89
|
+
)
|
|
90
|
+
elif provider.api_key:
|
|
91
|
+
# Plain API key
|
|
92
|
+
return Text(mask_api_key(provider.api_key))
|
|
93
|
+
else:
|
|
94
|
+
return Text("N/A")
|
|
90
95
|
|
|
91
|
-
# Add providers
|
|
92
|
-
for provider in config.provider_list:
|
|
93
|
-
status = Text("✔", style=f"bold {ThemeKey.CONFIG_STATUS_OK}")
|
|
94
|
-
name = Text(provider.provider_name, style=ThemeKey.CONFIG_ITEM_NAME)
|
|
95
|
-
protocol = Text(str(provider.protocol.value), style="")
|
|
96
|
-
base_url = Text(provider.base_url or "N/A", style="")
|
|
97
|
-
api_key = Text(mask_api_key(provider.api_key), style="")
|
|
98
|
-
|
|
99
|
-
providers_table.add_row(status, name, protocol, base_url, api_key)
|
|
100
|
-
|
|
101
|
-
# Display models section
|
|
102
|
-
models_table = Table.grid(padding=(0, 1), expand=True)
|
|
103
|
-
models_table.add_column(width=2, no_wrap=True) # Status
|
|
104
|
-
models_table.add_column(overflow="fold", ratio=1) # Name
|
|
105
|
-
models_table.add_column(overflow="fold", ratio=2) # Model
|
|
106
|
-
models_table.add_column(overflow="fold", ratio=2) # Provider
|
|
107
|
-
models_table.add_column(overflow="fold", ratio=3) # Params
|
|
108
|
-
|
|
109
|
-
# Add header
|
|
110
|
-
models_table.add_row(
|
|
111
|
-
Text("", style="bold"),
|
|
112
|
-
Text("Name", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
|
|
113
|
-
Text("Model", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
|
|
114
|
-
Text("Provider", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
|
|
115
|
-
Text("Params", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
|
|
116
|
-
)
|
|
117
96
|
|
|
118
|
-
|
|
119
|
-
for model
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
else ThemeKey.CONFIG_ITEM_NAME,
|
|
129
|
-
)
|
|
130
|
-
model_name = Text(model.model_params.model or "N/A", style="")
|
|
131
|
-
provider = Text(model.provider, style="")
|
|
132
|
-
params: list[Text] = []
|
|
133
|
-
if model.model_params.thinking:
|
|
134
|
-
if model.model_params.thinking.reasoning_effort is not None:
|
|
135
|
-
params.append(
|
|
136
|
-
Text.assemble(
|
|
137
|
-
("reason-effort", ThemeKey.CONFIG_PARAM_LABEL),
|
|
138
|
-
": ",
|
|
139
|
-
model.model_params.thinking.reasoning_effort,
|
|
140
|
-
)
|
|
141
|
-
)
|
|
142
|
-
if model.model_params.thinking.reasoning_summary is not None:
|
|
143
|
-
params.append(
|
|
144
|
-
Text.assemble(
|
|
145
|
-
("reason-summary", ThemeKey.CONFIG_PARAM_LABEL),
|
|
146
|
-
": ",
|
|
147
|
-
model.model_params.thinking.reasoning_summary,
|
|
148
|
-
)
|
|
97
|
+
def _get_model_params_display(model: ModelConfig) -> list[Text]:
|
|
98
|
+
"""Get display elements for model parameters."""
|
|
99
|
+
params: list[Text] = []
|
|
100
|
+
if model.model_params.thinking:
|
|
101
|
+
if model.model_params.thinking.reasoning_effort is not None:
|
|
102
|
+
params.append(
|
|
103
|
+
Text.assemble(
|
|
104
|
+
("reason-effort", ThemeKey.CONFIG_PARAM_LABEL),
|
|
105
|
+
": ",
|
|
106
|
+
model.model_params.thinking.reasoning_effort,
|
|
149
107
|
)
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
108
|
+
)
|
|
109
|
+
if model.model_params.thinking.reasoning_summary is not None:
|
|
110
|
+
params.append(
|
|
111
|
+
Text.assemble(
|
|
112
|
+
("reason-summary", ThemeKey.CONFIG_PARAM_LABEL),
|
|
113
|
+
": ",
|
|
114
|
+
model.model_params.thinking.reasoning_summary,
|
|
157
115
|
)
|
|
158
|
-
|
|
116
|
+
)
|
|
117
|
+
if model.model_params.thinking.budget_tokens is not None:
|
|
159
118
|
params.append(
|
|
160
119
|
Text.assemble(
|
|
161
|
-
("
|
|
120
|
+
("thinking-budget-tokens", ThemeKey.CONFIG_PARAM_LABEL),
|
|
162
121
|
": ",
|
|
163
|
-
model.model_params.
|
|
122
|
+
str(model.model_params.thinking.budget_tokens),
|
|
164
123
|
)
|
|
165
124
|
)
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
)
|
|
125
|
+
if model.model_params.provider_routing:
|
|
126
|
+
params.append(
|
|
127
|
+
Text.assemble(
|
|
128
|
+
("provider-routing", ThemeKey.CONFIG_PARAM_LABEL),
|
|
129
|
+
": ",
|
|
130
|
+
model.model_params.provider_routing.model_dump_json(exclude_none=True),
|
|
131
|
+
)
|
|
132
|
+
)
|
|
133
|
+
if len(params) == 0:
|
|
134
|
+
params.append(Text("N/A", style=ThemeKey.CONFIG_PARAM_LABEL))
|
|
135
|
+
return params
|
|
178
136
|
|
|
179
|
-
models_panel = Panel(
|
|
180
|
-
models_table,
|
|
181
|
-
title=Text("Models Configuration", style="white bold"),
|
|
182
|
-
border_style=ThemeKey.CONFIG_PANEL_BORDER,
|
|
183
|
-
padding=(0, 1),
|
|
184
|
-
title_align="left",
|
|
185
|
-
)
|
|
186
137
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
138
|
+
def display_models_and_providers(config: Config):
|
|
139
|
+
"""Display models and providers configuration using rich formatting"""
|
|
140
|
+
themes = get_theme(config.theme)
|
|
141
|
+
console = Console(theme=themes.app_theme)
|
|
142
|
+
|
|
143
|
+
# Display each provider as a separate panel
|
|
144
|
+
for provider in config.provider_list:
|
|
145
|
+
# Provider info section
|
|
146
|
+
provider_info = Table.grid(padding=(0, 1))
|
|
147
|
+
provider_info.add_column(width=12)
|
|
148
|
+
provider_info.add_column()
|
|
149
|
+
|
|
150
|
+
provider_info.add_row(
|
|
151
|
+
Text("Protocol:", style=ThemeKey.CONFIG_PARAM_LABEL),
|
|
152
|
+
Text(provider.protocol.value),
|
|
153
|
+
)
|
|
154
|
+
if provider.base_url:
|
|
155
|
+
provider_info.add_row(
|
|
156
|
+
Text("Base URL:", style=ThemeKey.CONFIG_PARAM_LABEL),
|
|
157
|
+
Text(provider.base_url or "N/A"),
|
|
158
|
+
)
|
|
159
|
+
if provider.api_key:
|
|
160
|
+
provider_info.add_row(
|
|
161
|
+
Text("API Key:", style=ThemeKey.CONFIG_PARAM_LABEL),
|
|
162
|
+
format_api_key_display(provider),
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Check if provider has valid API key
|
|
166
|
+
provider_available = not provider.is_api_key_missing()
|
|
167
|
+
|
|
168
|
+
# Models table for this provider
|
|
169
|
+
models_table = Table.grid(padding=(0, 1), expand=True)
|
|
170
|
+
models_table.add_column(width=2, no_wrap=True) # Status
|
|
171
|
+
models_table.add_column(overflow="fold", ratio=1) # Name
|
|
172
|
+
models_table.add_column(overflow="fold", ratio=2) # Model
|
|
173
|
+
models_table.add_column(overflow="fold", ratio=3) # Params
|
|
174
|
+
|
|
175
|
+
# Add header
|
|
176
|
+
models_table.add_row(
|
|
177
|
+
Text("", style="bold"),
|
|
178
|
+
Text("Name", style=ThemeKey.CONFIG_TABLE_HEADER),
|
|
179
|
+
Text("Model", style=ThemeKey.CONFIG_TABLE_HEADER),
|
|
180
|
+
Text("Params", style=ThemeKey.CONFIG_TABLE_HEADER),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
# Add models for this provider
|
|
184
|
+
for model in provider.model_list:
|
|
185
|
+
if not provider_available:
|
|
186
|
+
# Provider API key not set - show as unavailable
|
|
187
|
+
status = Text("-", style="dim")
|
|
188
|
+
name = Text(model.model_name, style="dim")
|
|
189
|
+
model_id = Text(model.model_params.model or "N/A", style="dim")
|
|
190
|
+
params = [Text("(unavailable)", style="dim")]
|
|
191
|
+
elif model.model_name == config.main_model:
|
|
192
|
+
status = Text("★", style=ThemeKey.CONFIG_STATUS_PRIMARY)
|
|
193
|
+
name = Text(model.model_name, style=ThemeKey.CONFIG_STATUS_PRIMARY)
|
|
194
|
+
model_id = Text(model.model_params.model or "N/A", style="")
|
|
195
|
+
params = _get_model_params_display(model)
|
|
196
|
+
else:
|
|
197
|
+
status = Text("✔", style=ThemeKey.CONFIG_STATUS_OK)
|
|
198
|
+
name = Text(model.model_name, style=ThemeKey.CONFIG_ITEM_NAME)
|
|
199
|
+
model_id = Text(model.model_params.model or "N/A", style="")
|
|
200
|
+
params = _get_model_params_display(model)
|
|
201
|
+
|
|
202
|
+
models_table.add_row(status, name, model_id, Group(*params))
|
|
203
|
+
|
|
204
|
+
# Create panel content with provider info and models
|
|
205
|
+
panel_elements = [
|
|
206
|
+
provider_info,
|
|
207
|
+
Text(""), # Spacer
|
|
208
|
+
Text("Models:", style=ThemeKey.CONFIG_TABLE_HEADER),
|
|
209
|
+
models_table,
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
# Add Codex status if this is a codex provider
|
|
213
|
+
if provider.protocol == LLMClientProtocol.CODEX:
|
|
214
|
+
panel_elements.append(Text("")) # Spacer
|
|
215
|
+
panel_elements.extend(_get_codex_status_elements())
|
|
216
|
+
|
|
217
|
+
panel_content = Group(*panel_elements)
|
|
218
|
+
|
|
219
|
+
panel = Panel(
|
|
220
|
+
panel_content,
|
|
221
|
+
title=Text(f"Provider: {provider.provider_name}", style="white bold"),
|
|
222
|
+
border_style=ThemeKey.CONFIG_PANEL_BORDER,
|
|
223
|
+
padding=(0, 1),
|
|
224
|
+
title_align="left",
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
console.print(panel)
|
|
228
|
+
console.print()
|
|
190
229
|
|
|
191
230
|
# Display main model info
|
|
192
231
|
console.print()
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
(
|
|
196
|
-
|
|
232
|
+
if config.main_model:
|
|
233
|
+
console.print(
|
|
234
|
+
Text.assemble(
|
|
235
|
+
("Default Model: ", "bold"),
|
|
236
|
+
(config.main_model, ThemeKey.CONFIG_STATUS_PRIMARY),
|
|
237
|
+
)
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
console.print(
|
|
241
|
+
Text.assemble(
|
|
242
|
+
("Default Model: ", "bold"),
|
|
243
|
+
("(not set)", ThemeKey.CONFIG_STATUS_ERROR),
|
|
244
|
+
)
|
|
197
245
|
)
|
|
198
|
-
)
|
|
199
246
|
|
|
200
247
|
for profile in iter_sub_agent_profiles():
|
|
201
248
|
sub_model_name = config.sub_agent_models.get(profile.name)
|
|
@@ -207,9 +254,3 @@ def display_models_and_providers(config: Config):
|
|
|
207
254
|
(sub_model_name, ThemeKey.CONFIG_STATUS_PRIMARY),
|
|
208
255
|
)
|
|
209
256
|
)
|
|
210
|
-
|
|
211
|
-
# Display Codex login status if any codex provider is configured
|
|
212
|
-
has_codex_provider = any(p.protocol.value == "codex" for p in config.provider_list)
|
|
213
|
-
if has_codex_provider:
|
|
214
|
-
console.print()
|
|
215
|
-
_display_codex_status(console)
|
klaude_code/cli/main.py
CHANGED
|
@@ -16,6 +16,10 @@ from klaude_code.trace import DebugType, prepare_debug_log_file
|
|
|
16
16
|
|
|
17
17
|
def set_terminal_title(title: str) -> None:
|
|
18
18
|
"""Set terminal window title using ANSI escape sequence."""
|
|
19
|
+
# Never write terminal control sequences when stdout is not a TTY (pipes/CI/redirects).
|
|
20
|
+
# This avoids corrupting machine-readable output (e.g., JSON streaming) and log captures.
|
|
21
|
+
if not sys.stdout.isatty():
|
|
22
|
+
return
|
|
19
23
|
sys.stdout.write(f"\033]0;{title}\007")
|
|
20
24
|
sys.stdout.flush()
|
|
21
25
|
|
|
@@ -143,13 +147,28 @@ def exec_command(
|
|
|
143
147
|
raise typer.Exit(1)
|
|
144
148
|
|
|
145
149
|
from klaude_code.cli.runtime import AppInitConfig, run_exec
|
|
150
|
+
from klaude_code.config import load_config
|
|
146
151
|
from klaude_code.config.select_model import select_model_from_config
|
|
147
152
|
|
|
148
153
|
chosen_model = model
|
|
149
154
|
if model or select_model:
|
|
150
155
|
chosen_model = select_model_from_config(preferred=model)
|
|
151
156
|
if chosen_model is None:
|
|
152
|
-
|
|
157
|
+
raise typer.Exit(1)
|
|
158
|
+
else:
|
|
159
|
+
# Check if main_model is configured; if not, trigger interactive selection
|
|
160
|
+
config = load_config()
|
|
161
|
+
if config.main_model is None:
|
|
162
|
+
chosen_model = select_model_from_config()
|
|
163
|
+
if chosen_model is None:
|
|
164
|
+
raise typer.Exit(1)
|
|
165
|
+
# Save the selection as default
|
|
166
|
+
config.main_model = chosen_model
|
|
167
|
+
from klaude_code.config.config import config_path
|
|
168
|
+
from klaude_code.trace import log
|
|
169
|
+
|
|
170
|
+
asyncio.run(config.save())
|
|
171
|
+
log(f"Saved main_model={chosen_model} to {config_path}", style="cyan")
|
|
153
172
|
|
|
154
173
|
debug_enabled, debug_filters, log_path = prepare_debug_logging(debug, debug_filter)
|
|
155
174
|
|
|
@@ -227,9 +246,43 @@ def main_callback(
|
|
|
227
246
|
) -> None:
|
|
228
247
|
# Only run interactive mode when no subcommand is invoked
|
|
229
248
|
if ctx.invoked_subcommand is None:
|
|
249
|
+
from klaude_code.trace import log
|
|
250
|
+
|
|
251
|
+
resume_by_id_value = resume_by_id.strip() if resume_by_id is not None else None
|
|
252
|
+
if resume_by_id_value == "":
|
|
253
|
+
log(("Error: --resume-by-id cannot be empty", "red"))
|
|
254
|
+
raise typer.Exit(2)
|
|
255
|
+
|
|
256
|
+
if resume_by_id_value is not None and (resume or continue_):
|
|
257
|
+
log(("Error: --resume-by-id cannot be combined with --resume/--continue", "red"))
|
|
258
|
+
raise typer.Exit(2)
|
|
259
|
+
|
|
260
|
+
if resume_by_id_value is not None and not Session.exists(resume_by_id_value):
|
|
261
|
+
log((f"Error: session id '{resume_by_id_value}' not found for this project", "red"))
|
|
262
|
+
log(("Hint: run `klaude --resume` to select an existing session", "yellow"))
|
|
263
|
+
raise typer.Exit(2)
|
|
264
|
+
|
|
265
|
+
# In non-interactive environments, default to exec-mode behavior.
|
|
266
|
+
# This allows: echo "..." | klaude
|
|
267
|
+
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
268
|
+
if continue_ or resume or resume_by_id is not None:
|
|
269
|
+
log(("Error: --continue/--resume options require a TTY", "red"))
|
|
270
|
+
log(("Hint: use `klaude exec` for non-interactive usage", "yellow"))
|
|
271
|
+
raise typer.Exit(2)
|
|
272
|
+
|
|
273
|
+
exec_command(
|
|
274
|
+
input_content="",
|
|
275
|
+
model=model,
|
|
276
|
+
select_model=select_model,
|
|
277
|
+
debug=debug,
|
|
278
|
+
debug_filter=debug_filter,
|
|
279
|
+
vanilla=vanilla,
|
|
280
|
+
stream_json=False,
|
|
281
|
+
)
|
|
282
|
+
return
|
|
283
|
+
|
|
230
284
|
from klaude_code.cli.runtime import AppInitConfig, run_interactive
|
|
231
285
|
from klaude_code.config.select_model import select_model_from_config
|
|
232
|
-
from klaude_code.trace import log
|
|
233
286
|
|
|
234
287
|
update_terminal_title()
|
|
235
288
|
|
|
@@ -243,15 +296,6 @@ def main_callback(
|
|
|
243
296
|
# session_id=None means create a new session
|
|
244
297
|
session_id: str | None = None
|
|
245
298
|
|
|
246
|
-
resume_by_id_value = resume_by_id.strip() if resume_by_id is not None else None
|
|
247
|
-
if resume_by_id_value == "":
|
|
248
|
-
log(("Error: --resume-by-id cannot be empty", "red"))
|
|
249
|
-
raise typer.Exit(2)
|
|
250
|
-
|
|
251
|
-
if resume_by_id_value is not None and (resume or continue_):
|
|
252
|
-
log(("Error: --resume-by-id cannot be combined with --resume/--continue", "red"))
|
|
253
|
-
raise typer.Exit(2)
|
|
254
|
-
|
|
255
299
|
if resume:
|
|
256
300
|
session_id = resume_select_session()
|
|
257
301
|
if session_id is None:
|
|
@@ -261,10 +305,6 @@ def main_callback(
|
|
|
261
305
|
session_id = Session.most_recent_session_id()
|
|
262
306
|
|
|
263
307
|
if resume_by_id_value is not None:
|
|
264
|
-
if not Session.exists(resume_by_id_value):
|
|
265
|
-
log((f"Error: session id '{resume_by_id_value}' not found for this project", "red"))
|
|
266
|
-
log(("Hint: run `klaude --resume` to select an existing session", "yellow"))
|
|
267
|
-
raise typer.Exit(2)
|
|
268
308
|
session_id = resume_by_id_value
|
|
269
309
|
# If still no session_id, leave as None to create a new session
|
|
270
310
|
|
|
@@ -275,8 +315,8 @@ def main_callback(
|
|
|
275
315
|
session_meta = Session.load_meta(session_id)
|
|
276
316
|
cfg = load_config()
|
|
277
317
|
|
|
278
|
-
if
|
|
279
|
-
if any(m.model_name == session_meta.model_config_name for m in cfg.
|
|
318
|
+
if session_meta.model_config_name:
|
|
319
|
+
if any(m.model_name == session_meta.model_config_name for m in cfg.iter_model_entries()):
|
|
280
320
|
chosen_model = session_meta.model_config_name
|
|
281
321
|
else:
|
|
282
322
|
log(
|
|
@@ -286,17 +326,34 @@ def main_callback(
|
|
|
286
326
|
)
|
|
287
327
|
)
|
|
288
328
|
|
|
289
|
-
if
|
|
329
|
+
if chosen_model is None and session_meta.model_name:
|
|
290
330
|
raw_model = session_meta.model_name.strip()
|
|
291
331
|
if raw_model:
|
|
292
332
|
matches = [
|
|
293
333
|
m.model_name
|
|
294
|
-
for m in cfg.
|
|
334
|
+
for m in cfg.iter_model_entries()
|
|
295
335
|
if (m.model_params.model or "").strip().lower() == raw_model.lower()
|
|
296
336
|
]
|
|
297
337
|
if len(matches) == 1:
|
|
298
338
|
chosen_model = matches[0]
|
|
299
339
|
|
|
340
|
+
# If still no model, check main_model; if not configured, trigger interactive selection
|
|
341
|
+
if chosen_model is None:
|
|
342
|
+
from klaude_code.config import load_config
|
|
343
|
+
|
|
344
|
+
cfg = load_config()
|
|
345
|
+
if cfg.main_model is None:
|
|
346
|
+
chosen_model = select_model_from_config()
|
|
347
|
+
if chosen_model is None:
|
|
348
|
+
raise typer.Exit(1)
|
|
349
|
+
# Save the selection as default
|
|
350
|
+
cfg.main_model = chosen_model
|
|
351
|
+
from klaude_code.config.config import config_path
|
|
352
|
+
from klaude_code.trace import log
|
|
353
|
+
|
|
354
|
+
asyncio.run(cfg.save())
|
|
355
|
+
log(f"Saved main_model={chosen_model} to {config_path}", style="dim")
|
|
356
|
+
|
|
300
357
|
debug_enabled, debug_filters, log_path = prepare_debug_logging(debug, debug_filter)
|
|
301
358
|
|
|
302
359
|
init_config = AppInitConfig(
|
klaude_code/cli/runtime.py
CHANGED
|
@@ -62,8 +62,6 @@ async def initialize_app_components(init_config: AppInitConfig) -> AppComponents
|
|
|
62
62
|
set_debug_logging(init_config.debug, filters=init_config.debug_filters)
|
|
63
63
|
|
|
64
64
|
config = load_config()
|
|
65
|
-
if config is None:
|
|
66
|
-
raise typer.Exit(1)
|
|
67
65
|
|
|
68
66
|
# Initialize LLM clients
|
|
69
67
|
try:
|
|
@@ -160,7 +158,7 @@ async def initialize_session(
|
|
|
160
158
|
def _backfill_session_model_config(
|
|
161
159
|
agent: Agent | None,
|
|
162
160
|
model_override: str | None,
|
|
163
|
-
default_model: str,
|
|
161
|
+
default_model: str | None,
|
|
164
162
|
is_new_session: bool,
|
|
165
163
|
) -> None:
|
|
166
164
|
"""Backfill model_config_name and model_thinking on newly created sessions."""
|
|
@@ -169,7 +167,7 @@ def _backfill_session_model_config(
|
|
|
169
167
|
|
|
170
168
|
if model_override is not None:
|
|
171
169
|
agent.session.model_config_name = model_override
|
|
172
|
-
elif is_new_session:
|
|
170
|
+
elif is_new_session and default_model is not None:
|
|
173
171
|
agent.session.model_config_name = default_model
|
|
174
172
|
else:
|
|
175
173
|
return
|
|
@@ -257,12 +255,8 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
|
|
|
257
255
|
|
|
258
256
|
# Create status provider for bottom toolbar
|
|
259
257
|
def _status_provider() -> REPLStatusSnapshot:
|
|
260
|
-
# Check for updates (returns None if uv not available)
|
|
261
258
|
update_message = get_update_message()
|
|
262
|
-
|
|
263
|
-
return build_repl_status_snapshot(
|
|
264
|
-
agent=components.executor.context.current_agent, update_message=update_message
|
|
265
|
-
)
|
|
259
|
+
return build_repl_status_snapshot(update_message)
|
|
266
260
|
|
|
267
261
|
# Set up input provider for interactive mode
|
|
268
262
|
def _stop_rich_bottom_ui() -> None:
|
|
@@ -278,9 +272,19 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
|
|
|
278
272
|
display.wrapped_display.renderer.spinner_stop()
|
|
279
273
|
display.wrapped_display.renderer.stop_bottom_live()
|
|
280
274
|
|
|
275
|
+
# Pass the pre-detected theme to avoid redundant TTY queries.
|
|
276
|
+
# Querying the terminal background again after questionary's interactive selection
|
|
277
|
+
# can interfere with prompt_toolkit's terminal state and break history navigation.
|
|
278
|
+
is_light_background: bool | None = None
|
|
279
|
+
if components.theme == "light":
|
|
280
|
+
is_light_background = True
|
|
281
|
+
elif components.theme == "dark":
|
|
282
|
+
is_light_background = False
|
|
283
|
+
|
|
281
284
|
input_provider: ui.InputProviderABC = ui.PromptToolkitInput(
|
|
282
285
|
status_provider=_status_provider,
|
|
283
286
|
pre_prompt=_stop_rich_bottom_ui,
|
|
287
|
+
is_light_background=is_light_background,
|
|
284
288
|
)
|
|
285
289
|
|
|
286
290
|
# --- Custom Ctrl+C handler: double-press within 2s to exit, single press shows toast ---
|
|
@@ -305,8 +309,8 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
|
|
|
305
309
|
printer.print(Text(f" {MSG} ", style="bold yellow reverse"))
|
|
306
310
|
else:
|
|
307
311
|
print(MSG, file=sys.stderr)
|
|
308
|
-
except
|
|
309
|
-
# Fallback if themed print is unavailable
|
|
312
|
+
except (AttributeError, TypeError, RuntimeError):
|
|
313
|
+
# Fallback if themed print is unavailable (e.g., display not ready or Rich internal error)
|
|
310
314
|
print(MSG, file=sys.stderr)
|
|
311
315
|
|
|
312
316
|
def _hide_progress() -> None:
|
klaude_code/cli/self_update.py
CHANGED
|
@@ -174,7 +174,8 @@ def _print_version() -> None:
|
|
|
174
174
|
ver = pkg_version(PACKAGE_NAME)
|
|
175
175
|
except PackageNotFoundError:
|
|
176
176
|
ver = "unknown"
|
|
177
|
-
except
|
|
177
|
+
except (ValueError, TypeError):
|
|
178
|
+
# Catch invalid package name format or type errors
|
|
178
179
|
ver = "unknown"
|
|
179
180
|
print(f"{PACKAGE_NAME} {ver}")
|
|
180
181
|
|
klaude_code/cli/session_cmd.py
CHANGED
|
@@ -13,7 +13,7 @@ def _session_confirm(sessions: list[Session.SessionMetaBrief], message: str) ->
|
|
|
13
13
|
def _fmt(ts: float) -> str:
|
|
14
14
|
try:
|
|
15
15
|
return time.strftime("%m-%d %H:%M:%S", time.localtime(ts))
|
|
16
|
-
except
|
|
16
|
+
except (OSError, OverflowError, ValueError):
|
|
17
17
|
return str(ts)
|
|
18
18
|
|
|
19
19
|
log(f"Sessions to delete ({len(sessions)}):")
|