klaude-code 2.3.0__py3-none-any.whl → 2.4.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 (53) hide show
  1. klaude_code/cli/list_model.py +3 -3
  2. klaude_code/cli/main.py +2 -2
  3. klaude_code/config/assets/builtin_config.yaml +165 -307
  4. klaude_code/config/config.py +17 -17
  5. klaude_code/config/{select_model.py → model_matcher.py} +7 -7
  6. klaude_code/config/sub_agent_model_helper.py +1 -1
  7. klaude_code/config/thinking.py +2 -2
  8. klaude_code/core/executor.py +59 -52
  9. klaude_code/core/tool/file/diff_builder.py +25 -18
  10. klaude_code/llm/anthropic/client.py +5 -5
  11. klaude_code/llm/client.py +1 -1
  12. klaude_code/llm/codex/client.py +2 -2
  13. klaude_code/llm/google/client.py +6 -6
  14. klaude_code/llm/input_common.py +2 -2
  15. klaude_code/llm/openai_compatible/client.py +3 -3
  16. klaude_code/llm/openai_compatible/stream.py +1 -1
  17. klaude_code/llm/openrouter/client.py +4 -4
  18. klaude_code/llm/openrouter/input.py +1 -3
  19. klaude_code/llm/responses/client.py +5 -5
  20. klaude_code/protocol/events/__init__.py +7 -1
  21. klaude_code/protocol/events/chat.py +10 -0
  22. klaude_code/protocol/llm_param.py +1 -1
  23. klaude_code/protocol/model.py +0 -26
  24. klaude_code/protocol/op.py +0 -5
  25. klaude_code/session/session.py +4 -2
  26. klaude_code/tui/command/clear_cmd.py +0 -1
  27. klaude_code/tui/command/command_abc.py +6 -4
  28. klaude_code/tui/command/copy_cmd.py +10 -10
  29. klaude_code/tui/command/debug_cmd.py +11 -10
  30. klaude_code/tui/command/export_online_cmd.py +18 -23
  31. klaude_code/tui/command/fork_session_cmd.py +39 -43
  32. klaude_code/tui/command/model_cmd.py +5 -7
  33. klaude_code/tui/command/{model_select.py → model_picker.py} +3 -5
  34. klaude_code/tui/command/refresh_cmd.py +0 -1
  35. klaude_code/tui/command/registry.py +15 -21
  36. klaude_code/tui/command/resume_cmd.py +10 -16
  37. klaude_code/tui/command/status_cmd.py +8 -12
  38. klaude_code/tui/command/sub_agent_model_cmd.py +11 -16
  39. klaude_code/tui/command/terminal_setup_cmd.py +8 -11
  40. klaude_code/tui/command/thinking_cmd.py +4 -6
  41. klaude_code/tui/commands.py +5 -0
  42. klaude_code/tui/components/command_output.py +96 -0
  43. klaude_code/tui/components/developer.py +3 -110
  44. klaude_code/tui/components/welcome.py +2 -2
  45. klaude_code/tui/input/prompt_toolkit.py +6 -8
  46. klaude_code/tui/machine.py +5 -0
  47. klaude_code/tui/renderer.py +5 -5
  48. klaude_code/tui/runner.py +0 -6
  49. klaude_code/tui/terminal/selector.py +4 -4
  50. {klaude_code-2.3.0.dist-info → klaude_code-2.4.0.dist-info}/METADATA +21 -74
  51. {klaude_code-2.3.0.dist-info → klaude_code-2.4.0.dist-info}/RECORD +53 -52
  52. {klaude_code-2.3.0.dist-info → klaude_code-2.4.0.dist-info}/WHEEL +0 -0
  53. {klaude_code-2.3.0.dist-info → klaude_code-2.4.0.dist-info}/entry_points.txt +0 -0
@@ -8,7 +8,7 @@ from prompt_toolkit.styles import Style
8
8
 
9
9
  from klaude_code.config.config import load_config
10
10
  from klaude_code.config.sub_agent_model_helper import SubAgentModelHelper, SubAgentModelInfo
11
- from klaude_code.protocol import commands, events, message, model, op
11
+ from klaude_code.protocol import commands, events, message, op
12
12
  from klaude_code.tui.terminal.selector import SelectItem, build_model_select_items, select_one
13
13
 
14
14
  from .command_abc import Agent, CommandABC, CommandResult
@@ -136,12 +136,11 @@ class SubAgentModelCommand(CommandABC):
136
136
  if not sub_agents:
137
137
  return CommandResult(
138
138
  events=[
139
- events.DeveloperMessageEvent(
139
+ events.CommandOutputEvent(
140
140
  session_id=agent.session.id,
141
- item=message.DeveloperMessage(
142
- parts=message.text_parts_from_str("No sub-agents available"),
143
- ui_extra=model.build_command_output_extra(self.name),
144
- ),
141
+ command_name=self.name,
142
+ content="No sub-agents available",
143
+ is_error=True,
145
144
  )
146
145
  ]
147
146
  )
@@ -150,12 +149,10 @@ class SubAgentModelCommand(CommandABC):
150
149
  if selected_sub_agent is None:
151
150
  return CommandResult(
152
151
  events=[
153
- events.DeveloperMessageEvent(
152
+ events.CommandOutputEvent(
154
153
  session_id=agent.session.id,
155
- item=message.DeveloperMessage(
156
- parts=message.text_parts_from_str("(cancelled)"),
157
- ui_extra=model.build_command_output_extra(self.name),
158
- ),
154
+ command_name=self.name,
155
+ content="(cancelled)",
159
156
  )
160
157
  ]
161
158
  )
@@ -166,12 +163,10 @@ class SubAgentModelCommand(CommandABC):
166
163
  if selected_model is None:
167
164
  return CommandResult(
168
165
  events=[
169
- events.DeveloperMessageEvent(
166
+ events.CommandOutputEvent(
170
167
  session_id=agent.session.id,
171
- item=message.DeveloperMessage(
172
- parts=message.text_parts_from_str("(cancelled)"),
173
- ui_extra=model.build_command_output_extra(self.name),
174
- ),
168
+ command_name=self.name,
169
+ content="(cancelled)",
175
170
  )
176
171
  ]
177
172
  )
@@ -2,7 +2,7 @@ import os
2
2
  import subprocess
3
3
  from pathlib import Path
4
4
 
5
- from klaude_code.protocol import commands, events, message, model
5
+ from klaude_code.protocol import commands, events, message
6
6
 
7
7
  from .command_abc import Agent, CommandABC, CommandResult
8
8
 
@@ -226,12 +226,10 @@ class TerminalSetupCommand(CommandABC):
226
226
  """Create success result"""
227
227
  return CommandResult(
228
228
  events=[
229
- events.DeveloperMessageEvent(
229
+ events.CommandOutputEvent(
230
230
  session_id=agent.session.id,
231
- item=message.DeveloperMessage(
232
- parts=message.text_parts_from_str(msg),
233
- ui_extra=model.build_command_output_extra(self.name),
234
- ),
231
+ command_name=self.name,
232
+ content=msg,
235
233
  )
236
234
  ]
237
235
  )
@@ -240,12 +238,11 @@ class TerminalSetupCommand(CommandABC):
240
238
  """Create error result"""
241
239
  return CommandResult(
242
240
  events=[
243
- events.DeveloperMessageEvent(
241
+ events.CommandOutputEvent(
244
242
  session_id=agent.session.id,
245
- item=message.DeveloperMessage(
246
- parts=message.text_parts_from_str(msg),
247
- ui_extra=model.build_command_output_extra(self.name, is_error=True),
248
- ),
243
+ command_name=self.name,
244
+ content=msg,
245
+ is_error=True,
249
246
  )
250
247
  ]
251
248
  )
@@ -3,7 +3,7 @@ import asyncio
3
3
  from prompt_toolkit.styles import Style
4
4
 
5
5
  from klaude_code.config.thinking import get_thinking_picker_data, parse_thinking_value
6
- from klaude_code.protocol import commands, events, llm_param, message, model, op
6
+ from klaude_code.protocol import commands, events, llm_param, message, op
7
7
  from klaude_code.tui.terminal.selector import SelectItem, select_one
8
8
 
9
9
  from .command_abc import Agent, CommandABC, CommandResult
@@ -79,12 +79,10 @@ class ThinkingCommand(CommandABC):
79
79
  if new_thinking is None:
80
80
  return CommandResult(
81
81
  events=[
82
- events.DeveloperMessageEvent(
82
+ events.CommandOutputEvent(
83
83
  session_id=agent.session.id,
84
- item=message.DeveloperMessage(
85
- parts=message.text_parts_from_str("(no change)"),
86
- ui_extra=model.build_command_output_extra(self.name),
87
- ),
84
+ command_name=self.name,
85
+ content="(no change)",
88
86
  )
89
87
  ]
90
88
  )
@@ -38,6 +38,11 @@ class RenderDeveloperMessage(RenderCommand):
38
38
  event: events.DeveloperMessageEvent
39
39
 
40
40
 
41
+ @dataclass(frozen=True, slots=True)
42
+ class RenderCommandOutput(RenderCommand):
43
+ event: events.CommandOutputEvent
44
+
45
+
41
46
  @dataclass(frozen=True, slots=True)
42
47
  class RenderTurnStart(RenderCommand):
43
48
  event: events.TurnStartEvent
@@ -0,0 +1,96 @@
1
+ from rich.console import RenderableType
2
+ from rich.padding import Padding
3
+ from rich.table import Table
4
+ from rich.text import Text
5
+
6
+ from klaude_code.protocol import events, model
7
+ from klaude_code.tui.components.common import truncate_middle
8
+ from klaude_code.tui.components.rich.theme import ThemeKey
9
+
10
+
11
+ def render_command_output(e: events.CommandOutputEvent) -> RenderableType:
12
+ """Render command output content."""
13
+ match e.command_name:
14
+ case "status":
15
+ return _render_status_output(e)
16
+ case "fork-session":
17
+ return _render_fork_session_output(e)
18
+ case _:
19
+ content = e.content or "(no content)"
20
+ style = ThemeKey.TOOL_RESULT if not e.is_error else ThemeKey.ERROR
21
+ return Padding.indent(truncate_middle(content, base_style=style), level=2)
22
+
23
+
24
+ def _format_tokens(tokens: int) -> str:
25
+ """Format token count with K/M suffix for readability."""
26
+ if tokens >= 1_000_000:
27
+ return f"{tokens / 1_000_000:.2f}M"
28
+ if tokens >= 1_000:
29
+ return f"{tokens / 1_000:.1f}K"
30
+ return str(tokens)
31
+
32
+
33
+ def _format_cost(cost: float | None, currency: str = "USD") -> str:
34
+ """Format cost with currency symbol."""
35
+ if cost is None:
36
+ return "-"
37
+ symbol = "Y" if currency == "CNY" else "$"
38
+ if cost < 0.01:
39
+ return f"{symbol}{cost:.4f}"
40
+ return f"{symbol}{cost:.2f}"
41
+
42
+
43
+ def _render_fork_session_output(e: events.CommandOutputEvent) -> RenderableType:
44
+ """Render fork session output with usage instructions."""
45
+ if not isinstance(e.ui_extra, model.SessionIdUIExtra):
46
+ return Padding.indent(Text(e.content, style=ThemeKey.TOOL_RESULT), level=2)
47
+
48
+ grid = Table.grid(padding=(0, 1))
49
+ session_id = e.ui_extra.session_id
50
+ grid.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
51
+
52
+ grid.add_row(Text("Session forked. Resume command copied to clipboard:", style=ThemeKey.TOOL_RESULT))
53
+ grid.add_row(Text(f" klaude --resume-by-id {session_id}", style=ThemeKey.TOOL_RESULT_BOLD))
54
+
55
+ return Padding.indent(grid, level=2)
56
+
57
+
58
+ def _render_status_output(e: events.CommandOutputEvent) -> RenderableType:
59
+ """Render session status with total cost and per-model breakdown."""
60
+ if not isinstance(e.ui_extra, model.SessionStatusUIExtra):
61
+ return Text("(no status data)", style=ThemeKey.TOOL_RESULT)
62
+
63
+ status = e.ui_extra
64
+ usage = status.usage
65
+
66
+ table = Table.grid(padding=(0, 2))
67
+ table.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
68
+ table.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
69
+
70
+ # Total cost line
71
+ table.add_row(
72
+ Text("Total cost:", style=ThemeKey.TOOL_RESULT_BOLD),
73
+ Text(_format_cost(usage.total_cost, usage.currency), style=ThemeKey.TOOL_RESULT_BOLD),
74
+ )
75
+
76
+ # Per-model breakdown
77
+ if status.by_model:
78
+ table.add_row(Text("Usage by model:", style=ThemeKey.TOOL_RESULT_BOLD), "")
79
+ for meta in status.by_model:
80
+ model_label = meta.model_name
81
+ if meta.provider:
82
+ model_label = f"{meta.model_name} ({meta.provider.lower().replace(' ', '-')})"
83
+
84
+ if meta.usage:
85
+ usage_detail = (
86
+ f"{_format_tokens(meta.usage.input_tokens)} input, "
87
+ f"{_format_tokens(meta.usage.output_tokens)} output, "
88
+ f"{_format_tokens(meta.usage.cached_tokens)} cache read, "
89
+ f"{_format_tokens(meta.usage.reasoning_tokens)} thinking, "
90
+ f"({_format_cost(meta.usage.total_cost, meta.usage.currency)})"
91
+ )
92
+ else:
93
+ usage_detail = "(no usage data)"
94
+ table.add_row(f"{model_label}:", usage_detail)
95
+
96
+ return Padding.indent(table, level=2)
@@ -1,29 +1,18 @@
1
1
  from rich.console import Group, RenderableType
2
- from rich.padding import Padding
3
- from rich.table import Table
4
2
  from rich.text import Text
5
3
 
6
- from klaude_code.protocol import commands, events, message, model
7
- from klaude_code.tui.components.common import create_grid, truncate_middle
4
+ from klaude_code.protocol import events, model
5
+ from klaude_code.tui.components.common import create_grid
8
6
  from klaude_code.tui.components.rich.theme import ThemeKey
9
7
  from klaude_code.tui.components.tools import render_path
10
8
 
11
9
  REMINDER_BULLET = " ⧉"
12
10
 
13
11
 
14
- def get_command_output(item: message.DeveloperMessage) -> model.CommandOutput | None:
15
- if not item.ui_extra:
16
- return None
17
- for ui_item in item.ui_extra.items:
18
- if isinstance(ui_item, model.CommandOutputUIItem):
19
- return ui_item.output
20
- return None
21
-
22
-
23
12
  def need_render_developer_message(e: events.DeveloperMessageEvent) -> bool:
24
13
  if not e.item.ui_extra:
25
14
  return False
26
- return any(not isinstance(ui_item, model.CommandOutputUIItem) for ui_item in e.item.ui_extra.items)
15
+ return len(e.item.ui_extra.items) > 0
27
16
 
28
17
 
29
18
  def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
@@ -126,101 +115,5 @@ def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
126
115
  ),
127
116
  )
128
117
  parts.append(grid)
129
- case model.CommandOutputUIItem():
130
- # Rendered via render_command_output
131
- pass
132
118
 
133
119
  return Group(*parts) if parts else Text("")
134
-
135
-
136
- def render_command_output(e: events.DeveloperMessageEvent) -> RenderableType:
137
- """Render developer command output content."""
138
- command_output = get_command_output(e.item)
139
- if not command_output:
140
- return Text("")
141
-
142
- content = message.join_text_parts(e.item.parts)
143
- match command_output.command_name:
144
- case commands.CommandName.STATUS:
145
- return _render_status_output(command_output)
146
- case commands.CommandName.FORK_SESSION:
147
- return _render_fork_session_output(command_output)
148
- case _:
149
- content = content or "(no content)"
150
- style = ThemeKey.TOOL_RESULT if not command_output.is_error else ThemeKey.ERROR
151
- return Padding.indent(truncate_middle(content, base_style=style), level=2)
152
-
153
-
154
- def _format_tokens(tokens: int) -> str:
155
- """Format token count with K/M suffix for readability."""
156
- if tokens >= 1_000_000:
157
- return f"{tokens / 1_000_000:.2f}M"
158
- if tokens >= 1_000:
159
- return f"{tokens / 1_000:.1f}K"
160
- return str(tokens)
161
-
162
-
163
- def _format_cost(cost: float | None, currency: str = "USD") -> str:
164
- """Format cost with currency symbol."""
165
- if cost is None:
166
- return "-"
167
- symbol = "¥" if currency == "CNY" else "$"
168
- if cost < 0.01:
169
- return f"{symbol}{cost:.4f}"
170
- return f"{symbol}{cost:.2f}"
171
-
172
-
173
- def _render_fork_session_output(command_output: model.CommandOutput) -> RenderableType:
174
- """Render fork session output with usage instructions."""
175
- if not isinstance(command_output.ui_extra, model.SessionIdUIExtra):
176
- return Padding.indent(Text("(no session id)", style=ThemeKey.TOOL_RESULT), level=2)
177
-
178
- grid = Table.grid(padding=(0, 1))
179
- session_id = command_output.ui_extra.session_id
180
- grid.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
181
-
182
- grid.add_row(Text("Session forked. Resume command copied to clipboard:", style=ThemeKey.TOOL_RESULT))
183
- grid.add_row(Text(f" klaude --resume-by-id {session_id}", style=ThemeKey.TOOL_RESULT_BOLD))
184
-
185
- return Padding.indent(grid, level=2)
186
-
187
-
188
- def _render_status_output(command_output: model.CommandOutput) -> RenderableType:
189
- """Render session status with total cost and per-model breakdown."""
190
- if not isinstance(command_output.ui_extra, model.SessionStatusUIExtra):
191
- return Text("(no status data)", style=ThemeKey.TOOL_RESULT)
192
-
193
- status = command_output.ui_extra
194
- usage = status.usage
195
-
196
- table = Table.grid(padding=(0, 2))
197
- table.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
198
- table.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
199
-
200
- # Total cost line
201
- table.add_row(
202
- Text("Total cost:", style=ThemeKey.TOOL_RESULT_BOLD),
203
- Text(_format_cost(usage.total_cost, usage.currency), style=ThemeKey.TOOL_RESULT_BOLD),
204
- )
205
-
206
- # Per-model breakdown
207
- if status.by_model:
208
- table.add_row(Text("Usage by model:", style=ThemeKey.TOOL_RESULT_BOLD), "")
209
- for meta in status.by_model:
210
- model_label = meta.model_name
211
- if meta.provider:
212
- model_label = f"{meta.model_name} ({meta.provider.lower().replace(' ', '-')})"
213
-
214
- if meta.usage:
215
- usage_detail = (
216
- f"{_format_tokens(meta.usage.input_tokens)} input, "
217
- f"{_format_tokens(meta.usage.output_tokens)} output, "
218
- f"{_format_tokens(meta.usage.cached_tokens)} cache read, "
219
- f"{_format_tokens(meta.usage.reasoning_tokens)} thinking, "
220
- f"({_format_cost(meta.usage.total_cost, meta.usage.currency)})"
221
- )
222
- else:
223
- usage_detail = "(no usage data)"
224
- table.add_row(f"{model_label}:", usage_detail)
225
-
226
- return Padding.indent(table, level=2)
@@ -38,7 +38,7 @@ def render_welcome(e: events.WelcomeEvent) -> RenderableType:
38
38
  # Model line: model @ provider · params...
39
39
  panel_content.append_text(
40
40
  Text.assemble(
41
- (str(e.llm_config.model), ThemeKey.WELCOME_HIGHLIGHT),
41
+ (str(e.llm_config.model_id), ThemeKey.WELCOME_HIGHLIGHT),
42
42
  (" @ ", ThemeKey.WELCOME_INFO),
43
43
  (e.llm_config.provider_name, ThemeKey.WELCOME_INFO),
44
44
  )
@@ -84,7 +84,7 @@ def render_welcome(e: events.WelcomeEvent) -> RenderableType:
84
84
  (prefix, ThemeKey.LINES),
85
85
  (sub_agent_type.lower().ljust(max_type_len), ThemeKey.WELCOME_INFO),
86
86
  (": ", ThemeKey.LINES),
87
- (str(sub_llm_config.model), ThemeKey.WELCOME_HIGHLIGHT),
87
+ (str(sub_llm_config.model_id), ThemeKey.WELCOME_HIGHLIGHT),
88
88
  (" @ ", ThemeKey.WELCOME_INFO),
89
89
  (sub_llm_config.provider_name, ThemeKey.WELCOME_INFO),
90
90
  )
@@ -26,7 +26,7 @@ from prompt_toolkit.styles import Style
26
26
  from prompt_toolkit.utils import get_cwidth
27
27
 
28
28
  from klaude_code.config import load_config
29
- from klaude_code.config.config import ModelEntry
29
+ from klaude_code.config.model_matcher import match_model_from_config
30
30
  from klaude_code.config.thinking import (
31
31
  format_current_thinking,
32
32
  get_thinking_picker_data,
@@ -452,23 +452,21 @@ class PromptToolkitInput(InputProviderABC):
452
452
  # -------------------------------------------------------------------------
453
453
 
454
454
  def _build_model_picker_items(self) -> tuple[list[SelectItem[str]], str | None]:
455
- config = load_config()
456
- models: list[ModelEntry] = sorted(
457
- config.iter_model_entries(only_available=True),
458
- key=lambda m: (m.model_name.lower(), m.provider.lower()),
459
- )
460
- if not models:
455
+ result = match_model_from_config()
456
+ if result.error_message or not result.filtered_models:
461
457
  return [], None
462
458
 
463
- items = build_model_select_items(models)
459
+ items = build_model_select_items(result.filtered_models)
464
460
 
465
461
  initial = None
466
462
  if self._get_current_model_config_name is not None:
467
463
  with contextlib.suppress(Exception):
468
464
  initial = self._get_current_model_config_name()
469
465
  if initial is None:
466
+ config = load_config()
470
467
  initial = config.main_model
471
468
  if isinstance(initial, str) and initial and "@" not in initial:
469
+ config = load_config()
472
470
  try:
473
471
  resolved = config.resolve_model_location_prefer_available(initial) or config.resolve_model_location(
474
472
  initial
@@ -21,6 +21,7 @@ from klaude_code.tui.commands import (
21
21
  PrintRuleLine,
22
22
  RenderAssistantImage,
23
23
  RenderCommand,
24
+ RenderCommandOutput,
24
25
  RenderDeveloperMessage,
25
26
  RenderError,
26
27
  RenderInterrupt,
@@ -365,6 +366,10 @@ class DisplayStateMachine:
365
366
  cmds.append(RenderDeveloperMessage(e))
366
367
  return cmds
367
368
 
369
+ case events.CommandOutputEvent() as e:
370
+ cmds.append(RenderCommandOutput(e))
371
+ return cmds
372
+
368
373
  case events.TurnStartEvent() as e:
369
374
  cmds.append(RenderTurnStart(e))
370
375
  self._spinner.clear_for_new_turn()
@@ -31,6 +31,7 @@ from klaude_code.tui.commands import (
31
31
  PrintRuleLine,
32
32
  RenderAssistantImage,
33
33
  RenderCommand,
34
+ RenderCommandOutput,
34
35
  RenderDeveloperMessage,
35
36
  RenderError,
36
37
  RenderInterrupt,
@@ -53,6 +54,7 @@ from klaude_code.tui.commands import (
53
54
  TaskClockStart,
54
55
  )
55
56
  from klaude_code.tui.components import assistant as c_assistant
57
+ from klaude_code.tui.components import command_output as c_command_output
56
58
  from klaude_code.tui.components import developer as c_developer
57
59
  from klaude_code.tui.components import errors as c_errors
58
60
  from klaude_code.tui.components import mermaid_viewer as c_mermaid_viewer
@@ -471,7 +473,6 @@ class TUICommandRenderer:
471
473
  self.print()
472
474
  case events.DeveloperMessageEvent() as e:
473
475
  self.display_developer_message(e)
474
- self.display_command_output(e)
475
476
  case events.UserMessageEvent() as e:
476
477
  if is_sub_agent:
477
478
  continue
@@ -504,11 +505,9 @@ class TUICommandRenderer:
504
505
  with self.session_print_context(e.session_id):
505
506
  self.print(c_developer.render_developer_message(e))
506
507
 
507
- def display_command_output(self, e: events.DeveloperMessageEvent) -> None:
508
- if not c_developer.get_command_output(e.item):
509
- return
508
+ def display_command_output(self, e: events.CommandOutputEvent) -> None:
510
509
  with self.session_print_context(e.session_id):
511
- self.print(c_developer.render_command_output(e))
510
+ self.print(c_command_output.render_command_output(e))
512
511
  self.print()
513
512
 
514
513
  def display_welcome(self, event: events.WelcomeEvent) -> None:
@@ -627,6 +626,7 @@ class TUICommandRenderer:
627
626
  self.display_task_start(event)
628
627
  case RenderDeveloperMessage(event=event):
629
628
  self.display_developer_message(event)
629
+ case RenderCommandOutput(event=event):
630
630
  self.display_command_output(event)
631
631
  case RenderTurnStart(event=event):
632
632
  self.display_turn_start(event)
klaude_code/tui/runner.py CHANGED
@@ -76,14 +76,8 @@ async def submit_user_input_payload(
76
76
  if len(run_ops) > 1:
77
77
  raise ValueError("Multiple RunAgentOperation results are not supported")
78
78
 
79
- for run_op in run_ops:
80
- run_op.persist_user_input = cmd_result.persist
81
- run_op.emit_user_message_event = False
82
-
83
79
  if cmd_result.events:
84
80
  for evt in cmd_result.events:
85
- if cmd_result.persist and isinstance(evt, events.DeveloperMessageEvent):
86
- agent.session.append_history([evt.item])
87
81
  await executor.context.emit_event(evt)
88
82
 
89
83
  submitted_ids: list[str] = []
@@ -53,20 +53,20 @@ def build_model_select_items(models: list[Any]) -> list[SelectItem[str]]:
53
53
 
54
54
  items: list[SelectItem[str]] = []
55
55
  for idx, m in enumerate(models, 1):
56
- model_id = m.model_params.model or "N/A"
56
+ model_id_str = m.model_id or "N/A"
57
57
  # Display the base model name only; provider stays in the meta section.
58
58
  display_name = m.model_name
59
59
  first_line_prefix = f"{display_name:<{max_model_name_length}} → "
60
60
  meta_parts: list[str] = [m.provider]
61
- meta_parts.extend(format_model_params(m.model_params))
61
+ meta_parts.extend(format_model_params(m))
62
62
  meta_str = " · ".join(meta_parts)
63
63
  title = [
64
64
  ("class:meta", f"{idx:>{num_width}}. "),
65
65
  ("class:msg", first_line_prefix),
66
- ("class:msg bold", model_id),
66
+ ("class:msg bold", model_id_str),
67
67
  ("class:meta", f" {meta_str}\n"),
68
68
  ]
69
- search_text = f"{m.selector} {m.model_name} {model_id} {m.provider}"
69
+ search_text = f"{m.selector} {m.model_name} {model_id_str} {m.provider}"
70
70
  items.append(SelectItem(title=title, value=m.selector, search_text=search_text))
71
71
 
72
72
  return items
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: Minimal code agent CLI
5
5
  Requires-Dist: anthropic>=0.66.0
6
6
  Requires-Dist: chardet>=5.2.0
@@ -155,95 +155,42 @@ Open in editor:
155
155
  klaude config
156
156
  ```
157
157
 
158
- ##### Adding Models to Built-in Providers
158
+ ##### Model Configuration
159
159
 
160
- You can add custom models to existing built-in providers without redefining the entire provider. Just reference the `provider_name` and add your `model_list`:
160
+ You can add custom models to built-in providers or define new ones. Configuration is inherited from built-in providers by matching `provider_name`.
161
161
 
162
162
  ```yaml
163
163
  # ~/.klaude/klaude-config.yaml
164
164
  provider_list:
165
- - provider_name: openrouter # Reference existing built-in provider
166
- model_list:
167
- - model_name: seed
168
- model_params:
169
- model: bytedance-seed/seed-1.6 # Model ID from OpenRouter
170
- context_limit: 262000
171
- cost:
172
- input: 0.25
173
- output: 2
174
- ```
175
-
176
- **How merging works:**
177
- - Your models are merged with the built-in models for that provider
178
- - You only need `provider_name` and `model_list` - protocol, api_key, etc. are inherited from the built-in config
179
- - To override a built-in model, use the same `model_name` (e.g., `sonnet` to customize the built-in sonnet)
180
-
181
- **More examples:**
182
-
183
- ```yaml
184
- provider_list:
185
- # Add multiple models to OpenRouter
165
+ # Add/Override models for built-in OpenRouter provider
186
166
  - provider_name: openrouter
187
167
  model_list:
188
168
  - model_name: qwen-coder
189
- model_params:
190
- model: qwen/qwen-2.5-coder-32b-instruct
191
- context_limit: 131072
192
- cost:
193
- input: 0.3
194
- output: 0.9
195
- - model_name: llama-405b
196
- model_params:
197
- model: meta-llama/llama-3.1-405b-instruct
198
- context_limit: 131072
199
- cost:
200
- input: 0.8
201
- output: 0.8
202
-
203
- # Add models to Anthropic provider
204
- - provider_name: anthropic
205
- model_list:
206
- - model_name: haiku@ant
207
- model_params:
208
- model: claude-3-5-haiku-20241022
209
- context_limit: 200000
210
- cost:
211
- input: 1.0
212
- output: 5.0
213
- ```
214
-
215
- After adding models, run `klaude list` to verify they appear in the model list.
216
-
217
- ##### Overriding Provider Settings
218
-
219
- Override provider-level settings (like api_key) while keeping built-in models:
220
-
221
- ```yaml
222
- provider_list:
223
- - provider_name: anthropic
224
- api_key: sk-my-custom-key # Override the default ${ANTHROPIC_API_KEY}
225
- # Built-in models (sonnet, opus) are still available
226
- ```
227
-
228
- ##### Adding New Providers
229
-
230
- For providers not in the built-in list, you must specify `protocol`:
231
-
232
- ```yaml
233
- provider_list:
234
- - provider_name: my-azure-openai
169
+ model_id: qwen/qwen-2.5-coder-32b-instruct
170
+ context_limit: 131072
171
+ cost: { input: 0.3, output: 0.9 }
172
+ - model_name: sonnet # Override built-in sonnet params
173
+ model_id: anthropic/claude-3.5-sonnet
174
+ context_limit: 200000
175
+
176
+ # Add a completely new provider
177
+ - provider_name: my-azure
235
178
  protocol: openai
236
179
  api_key: ${AZURE_OPENAI_KEY}
237
180
  base_url: https://my-instance.openai.azure.com/
238
181
  is_azure: true
239
182
  azure_api_version: "2024-02-15-preview"
240
183
  model_list:
241
- - model_name: gpt-4-azure
242
- model_params:
243
- model: gpt-4
244
- context_limit: 128000
184
+ - model_name: gpt-4
185
+ model_id: gpt-4-deploy-name
186
+ context_limit: 128000
245
187
  ```
246
188
 
189
+ **Key Tips:**
190
+ - **Merging**: If `provider_name` matches a built-in provider, settings like `protocol` and `api_key` are inherited.
191
+ - **Overriding**: Use the same `model_name` as a built-in model to override its parameters.
192
+ - **Environment Variables**: Use `${VAR_NAME}` syntax for secrets.
193
+
247
194
  ##### Supported Protocols
248
195
 
249
196
  - `anthropic` - Anthropic Messages API