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.
Files changed (52) hide show
  1. klaude_code/cli/config_cmd.py +1 -5
  2. klaude_code/cli/debug.py +9 -1
  3. klaude_code/cli/list_model.py +170 -129
  4. klaude_code/cli/main.py +76 -19
  5. klaude_code/cli/runtime.py +15 -11
  6. klaude_code/cli/self_update.py +2 -1
  7. klaude_code/cli/session_cmd.py +1 -1
  8. klaude_code/command/__init__.py +3 -0
  9. klaude_code/command/export_online_cmd.py +15 -12
  10. klaude_code/command/fork_session_cmd.py +42 -0
  11. klaude_code/config/__init__.py +3 -1
  12. klaude_code/config/assets/__init__.py +1 -0
  13. klaude_code/config/assets/builtin_config.yaml +233 -0
  14. klaude_code/config/builtin_config.py +37 -0
  15. klaude_code/config/config.py +332 -112
  16. klaude_code/config/select_model.py +46 -8
  17. klaude_code/core/executor.py +6 -3
  18. klaude_code/core/manager/llm_clients_builder.py +4 -1
  19. klaude_code/core/reminders.py +52 -16
  20. klaude_code/core/tool/file/edit_tool.py +4 -4
  21. klaude_code/core/tool/file/write_tool.py +4 -4
  22. klaude_code/core/tool/shell/bash_tool.py +2 -2
  23. klaude_code/core/tool/web/mermaid_tool.md +17 -0
  24. klaude_code/core/tool/web/mermaid_tool.py +2 -2
  25. klaude_code/llm/openai_compatible/stream.py +2 -1
  26. klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
  27. klaude_code/protocol/commands.py +1 -0
  28. klaude_code/protocol/model.py +1 -0
  29. klaude_code/session/export.py +52 -7
  30. klaude_code/session/selector.py +2 -2
  31. klaude_code/session/session.py +26 -4
  32. klaude_code/trace/log.py +7 -1
  33. klaude_code/ui/modes/repl/__init__.py +3 -44
  34. klaude_code/ui/modes/repl/completers.py +39 -7
  35. klaude_code/ui/modes/repl/event_handler.py +8 -6
  36. klaude_code/ui/modes/repl/input_prompt_toolkit.py +33 -66
  37. klaude_code/ui/modes/repl/key_bindings.py +4 -4
  38. klaude_code/ui/modes/repl/renderer.py +1 -6
  39. klaude_code/ui/renderers/common.py +11 -4
  40. klaude_code/ui/renderers/developer.py +17 -0
  41. klaude_code/ui/renderers/diffs.py +1 -1
  42. klaude_code/ui/renderers/errors.py +10 -5
  43. klaude_code/ui/renderers/metadata.py +2 -2
  44. klaude_code/ui/renderers/tools.py +8 -4
  45. klaude_code/ui/rich/markdown.py +5 -5
  46. klaude_code/ui/rich/theme.py +7 -3
  47. klaude_code/ui/terminal/color.py +1 -1
  48. klaude_code/ui/terminal/control.py +4 -4
  49. {klaude_code-1.2.26.dist-info → klaude_code-1.2.28.dist-info}/METADATA +121 -127
  50. {klaude_code-1.2.26.dist-info → klaude_code-1.2.28.dist-info}/RECORD +52 -48
  51. {klaude_code-1.2.26.dist-info → klaude_code-1.2.28.dist-info}/entry_points.txt +1 -0
  52. {klaude_code-1.2.26.dist-info → klaude_code-1.2.28.dist-info}/WHEEL +0 -0
@@ -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
- config = load_config()
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
- subprocess.Popen([editor, str(path)], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
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
@@ -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 _display_codex_status(console: Console) -> None:
14
- """Display Codex OAuth login status."""
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
- console.print(
24
+ elements.append(
22
25
  Text.assemble(
23
- ("Codex Status: ", "bold"),
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
- console.print(
32
+ elements.append(
30
33
  Text.assemble(
31
- ("Codex Status: ", "bold"),
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
- console.print(
41
+ elements.append(
39
42
  Text.assemble(
40
- ("Codex Status: ", "bold"),
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
- console.print(
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 display_models_and_providers(config: Config):
70
- """Display models and providers configuration using rich formatting"""
71
- themes = get_theme(config.theme)
72
- console = Console(theme=themes.app_theme)
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
- # Display providers section
75
- providers_table = Table.grid(padding=(0, 1), expand=True)
76
- providers_table.add_column(width=2, no_wrap=True) # Status
77
- providers_table.add_column(overflow="fold") # Name
78
- providers_table.add_column(overflow="fold") # Protocol
79
- providers_table.add_column(overflow="fold") # Base URL
80
- providers_table.add_column(overflow="fold") # API Key
81
-
82
- # Add header
83
- providers_table.add_row(
84
- Text("", style="bold"),
85
- Text("Name", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
86
- Text("Protocol", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
87
- Text("Base URL", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
88
- Text("API Key", style=f"bold {ThemeKey.CONFIG_TABLE_HEADER}"),
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
- # Add models
119
- for model in config.model_list:
120
- status = Text("✔", style=f"bold {ThemeKey.CONFIG_STATUS_OK}")
121
- if model.model_name == config.main_model:
122
- status = Text("★", style=f"bold {ThemeKey.CONFIG_STATUS_PRIMARY}") # Mark main model
123
-
124
- name = Text(
125
- model.model_name,
126
- style=ThemeKey.CONFIG_STATUS_PRIMARY
127
- if model.model_name == config.main_model
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
- if model.model_params.thinking.budget_tokens is not None:
151
- params.append(
152
- Text.assemble(
153
- ("thinking-budget-tokens", ThemeKey.CONFIG_PARAM_LABEL),
154
- ": ",
155
- str(model.model_params.thinking.budget_tokens),
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
- if model.model_params.provider_routing:
116
+ )
117
+ if model.model_params.thinking.budget_tokens is not None:
159
118
  params.append(
160
119
  Text.assemble(
161
- ("provider-routing", ThemeKey.CONFIG_PARAM_LABEL),
120
+ ("thinking-budget-tokens", ThemeKey.CONFIG_PARAM_LABEL),
162
121
  ": ",
163
- model.model_params.provider_routing.model_dump_json(exclude_none=True),
122
+ str(model.model_params.thinking.budget_tokens),
164
123
  )
165
124
  )
166
- if len(params) == 0:
167
- params.append(Text("N/A", style=ThemeKey.CONFIG_PARAM_LABEL))
168
- models_table.add_row(status, name, model_name, provider, Group(*params))
169
-
170
- # Create panels and display
171
- providers_panel = Panel(
172
- providers_table,
173
- title=Text("Providers Configuration", style="white bold"),
174
- border_style=ThemeKey.CONFIG_PANEL_BORDER,
175
- padding=(0, 1),
176
- title_align="left",
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
- console.print(providers_panel)
188
- console.print()
189
- console.print(models_panel)
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
- console.print(
194
- Text.assemble(
195
- ("Default Model: ", "bold"),
196
- (config.main_model, ThemeKey.CONFIG_STATUS_PRIMARY),
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
- return
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 cfg is not None and session_meta.model_config_name:
279
- if any(m.model_name == session_meta.model_config_name for m in cfg.model_list):
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 cfg is not None and chosen_model is None and session_meta.model_name:
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.model_list
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(
@@ -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 Exception:
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:
@@ -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 Exception:
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
 
@@ -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 Exception:
16
+ except (OSError, OverflowError, ValueError):
17
17
  return str(ts)
18
18
 
19
19
  log(f"Sessions to delete ({len(sessions)}):")