klaude-code 1.8.0__py3-none-any.whl → 2.0.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 (142) hide show
  1. klaude_code/auth/base.py +97 -0
  2. klaude_code/auth/claude/__init__.py +6 -0
  3. klaude_code/auth/claude/exceptions.py +9 -0
  4. klaude_code/auth/claude/oauth.py +172 -0
  5. klaude_code/auth/claude/token_manager.py +26 -0
  6. klaude_code/auth/codex/token_manager.py +10 -50
  7. klaude_code/cli/auth_cmd.py +127 -46
  8. klaude_code/cli/config_cmd.py +4 -2
  9. klaude_code/cli/cost_cmd.py +14 -9
  10. klaude_code/cli/list_model.py +248 -200
  11. klaude_code/cli/main.py +1 -1
  12. klaude_code/cli/runtime.py +7 -5
  13. klaude_code/cli/self_update.py +1 -1
  14. klaude_code/cli/session_cmd.py +1 -1
  15. klaude_code/command/clear_cmd.py +6 -2
  16. klaude_code/command/command_abc.py +2 -2
  17. klaude_code/command/debug_cmd.py +4 -4
  18. klaude_code/command/export_cmd.py +2 -2
  19. klaude_code/command/export_online_cmd.py +12 -12
  20. klaude_code/command/fork_session_cmd.py +29 -23
  21. klaude_code/command/help_cmd.py +4 -4
  22. klaude_code/command/model_cmd.py +4 -4
  23. klaude_code/command/model_select.py +1 -1
  24. klaude_code/command/prompt-commit.md +82 -0
  25. klaude_code/command/prompt_command.py +3 -3
  26. klaude_code/command/refresh_cmd.py +2 -2
  27. klaude_code/command/registry.py +7 -5
  28. klaude_code/command/release_notes_cmd.py +4 -4
  29. klaude_code/command/resume_cmd.py +15 -11
  30. klaude_code/command/status_cmd.py +4 -4
  31. klaude_code/command/terminal_setup_cmd.py +8 -8
  32. klaude_code/command/thinking_cmd.py +4 -4
  33. klaude_code/config/assets/builtin_config.yaml +52 -3
  34. klaude_code/config/builtin_config.py +16 -5
  35. klaude_code/config/config.py +31 -7
  36. klaude_code/config/thinking.py +4 -4
  37. klaude_code/const.py +146 -91
  38. klaude_code/core/agent.py +3 -12
  39. klaude_code/core/executor.py +21 -13
  40. klaude_code/core/manager/sub_agent_manager.py +71 -7
  41. klaude_code/core/prompt.py +1 -1
  42. klaude_code/core/prompts/prompt-sub-agent-image-gen.md +1 -0
  43. klaude_code/core/prompts/prompt-sub-agent-web.md +27 -1
  44. klaude_code/core/reminders.py +88 -69
  45. klaude_code/core/task.py +44 -45
  46. klaude_code/core/tool/file/apply_patch_tool.py +9 -9
  47. klaude_code/core/tool/file/diff_builder.py +3 -5
  48. klaude_code/core/tool/file/edit_tool.py +23 -23
  49. klaude_code/core/tool/file/move_tool.py +43 -43
  50. klaude_code/core/tool/file/read_tool.py +44 -39
  51. klaude_code/core/tool/file/write_tool.py +14 -14
  52. klaude_code/core/tool/report_back_tool.py +4 -4
  53. klaude_code/core/tool/shell/bash_tool.py +23 -23
  54. klaude_code/core/tool/skill/skill_tool.py +7 -7
  55. klaude_code/core/tool/sub_agent_tool.py +38 -9
  56. klaude_code/core/tool/todo/todo_write_tool.py +8 -8
  57. klaude_code/core/tool/todo/update_plan_tool.py +6 -6
  58. klaude_code/core/tool/tool_abc.py +2 -2
  59. klaude_code/core/tool/tool_context.py +27 -0
  60. klaude_code/core/tool/tool_runner.py +88 -42
  61. klaude_code/core/tool/truncation.py +38 -20
  62. klaude_code/core/tool/web/mermaid_tool.py +6 -7
  63. klaude_code/core/tool/web/web_fetch_tool.py +68 -30
  64. klaude_code/core/tool/web/web_search_tool.py +15 -17
  65. klaude_code/core/turn.py +120 -73
  66. klaude_code/llm/anthropic/client.py +104 -44
  67. klaude_code/llm/anthropic/input.py +116 -108
  68. klaude_code/llm/bedrock/client.py +8 -5
  69. klaude_code/llm/claude/__init__.py +3 -0
  70. klaude_code/llm/claude/client.py +105 -0
  71. klaude_code/llm/client.py +4 -3
  72. klaude_code/llm/codex/client.py +16 -10
  73. klaude_code/llm/google/client.py +122 -60
  74. klaude_code/llm/google/input.py +94 -108
  75. klaude_code/llm/image.py +123 -0
  76. klaude_code/llm/input_common.py +136 -189
  77. klaude_code/llm/openai_compatible/client.py +17 -7
  78. klaude_code/llm/openai_compatible/input.py +36 -66
  79. klaude_code/llm/openai_compatible/stream.py +119 -67
  80. klaude_code/llm/openai_compatible/tool_call_accumulator.py +23 -11
  81. klaude_code/llm/openrouter/client.py +34 -9
  82. klaude_code/llm/openrouter/input.py +63 -64
  83. klaude_code/llm/openrouter/reasoning.py +22 -24
  84. klaude_code/llm/registry.py +20 -15
  85. klaude_code/llm/responses/client.py +107 -45
  86. klaude_code/llm/responses/input.py +115 -98
  87. klaude_code/llm/usage.py +52 -25
  88. klaude_code/protocol/__init__.py +1 -0
  89. klaude_code/protocol/events.py +16 -12
  90. klaude_code/protocol/llm_param.py +22 -3
  91. klaude_code/protocol/message.py +250 -0
  92. klaude_code/protocol/model.py +94 -281
  93. klaude_code/protocol/op.py +2 -2
  94. klaude_code/protocol/sub_agent/__init__.py +2 -2
  95. klaude_code/protocol/sub_agent/explore.py +10 -0
  96. klaude_code/protocol/sub_agent/image_gen.py +119 -0
  97. klaude_code/protocol/sub_agent/task.py +10 -0
  98. klaude_code/protocol/sub_agent/web.py +10 -0
  99. klaude_code/session/codec.py +6 -6
  100. klaude_code/session/export.py +261 -62
  101. klaude_code/session/selector.py +7 -24
  102. klaude_code/session/session.py +125 -53
  103. klaude_code/session/store.py +5 -32
  104. klaude_code/session/templates/export_session.html +1 -1
  105. klaude_code/session/templates/mermaid_viewer.html +1 -1
  106. klaude_code/trace/log.py +11 -6
  107. klaude_code/ui/core/input.py +1 -1
  108. klaude_code/ui/core/stage_manager.py +1 -8
  109. klaude_code/ui/modes/debug/display.py +2 -2
  110. klaude_code/ui/modes/repl/clipboard.py +2 -2
  111. klaude_code/ui/modes/repl/completers.py +18 -10
  112. klaude_code/ui/modes/repl/event_handler.py +136 -127
  113. klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
  114. klaude_code/ui/modes/repl/key_bindings.py +1 -1
  115. klaude_code/ui/modes/repl/renderer.py +107 -15
  116. klaude_code/ui/renderers/assistant.py +2 -2
  117. klaude_code/ui/renderers/common.py +65 -7
  118. klaude_code/ui/renderers/developer.py +7 -6
  119. klaude_code/ui/renderers/diffs.py +11 -11
  120. klaude_code/ui/renderers/mermaid_viewer.py +49 -2
  121. klaude_code/ui/renderers/metadata.py +39 -31
  122. klaude_code/ui/renderers/sub_agent.py +57 -16
  123. klaude_code/ui/renderers/thinking.py +37 -2
  124. klaude_code/ui/renderers/tools.py +180 -165
  125. klaude_code/ui/rich/live.py +3 -1
  126. klaude_code/ui/rich/markdown.py +39 -7
  127. klaude_code/ui/rich/quote.py +76 -1
  128. klaude_code/ui/rich/status.py +14 -8
  129. klaude_code/ui/rich/theme.py +13 -6
  130. klaude_code/ui/terminal/image.py +34 -0
  131. klaude_code/ui/terminal/notifier.py +2 -1
  132. klaude_code/ui/terminal/progress_bar.py +4 -4
  133. klaude_code/ui/terminal/selector.py +22 -4
  134. klaude_code/ui/utils/common.py +55 -0
  135. {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.dist-info}/METADATA +28 -6
  136. klaude_code-2.0.0.dist-info/RECORD +229 -0
  137. klaude_code/command/prompt-jj-describe.md +0 -32
  138. klaude_code/core/prompts/prompt-sub-agent-oracle.md +0 -22
  139. klaude_code/protocol/sub_agent/oracle.py +0 -91
  140. klaude_code-1.8.0.dist-info/RECORD +0 -219
  141. {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.dist-info}/WHEEL +0 -0
  142. {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.dist-info}/entry_points.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  import datetime
2
2
 
3
+ from rich.box import HORIZONTALS
3
4
  from rich.console import Console, Group
4
- from rich.panel import Panel
5
5
  from rich.table import Table
6
6
  from rich.text import Text
7
7
 
@@ -9,65 +9,127 @@ from klaude_code.config import Config
9
9
  from klaude_code.config.config import ModelConfig, ProviderConfig, parse_env_var_syntax
10
10
  from klaude_code.protocol.llm_param import LLMClientProtocol
11
11
  from klaude_code.protocol.sub_agent import iter_sub_agent_profiles
12
+ from klaude_code.ui.rich.quote import Quote
12
13
  from klaude_code.ui.rich.theme import ThemeKey, get_theme
14
+ from klaude_code.ui.utils.common import format_model_params
13
15
 
14
16
 
15
- def _get_codex_status_elements() -> list[Text]:
16
- """Get Codex OAuth login status as Text elements for panel display."""
17
+ def _get_codex_status_rows() -> list[tuple[Text, Text]]:
18
+ """Get Codex OAuth login status as (label, value) tuples for table display."""
17
19
  from klaude_code.auth.codex.token_manager import CodexTokenManager
18
20
 
19
- elements: list[Text] = []
21
+ rows: list[tuple[Text, Text]] = []
20
22
  token_manager = CodexTokenManager()
21
23
  state = token_manager.get_state()
22
24
 
23
25
  if state is None:
24
- elements.append(
25
- Text.assemble(
26
- ("Status: ", "bold"),
27
- ("Not logged in", ThemeKey.CONFIG_STATUS_ERROR),
28
- (" (run 'klaude login codex' to authenticate)", "dim"),
26
+ rows.append(
27
+ (
28
+ Text("Status", style=ThemeKey.CONFIG_PARAM_LABEL),
29
+ Text.assemble(
30
+ ("Not logged in", ThemeKey.CONFIG_STATUS_ERROR),
31
+ (" (run 'klaude login codex' to authenticate)", "dim"),
32
+ ),
29
33
  )
30
34
  )
31
35
  elif state.is_expired():
32
- elements.append(
33
- Text.assemble(
34
- ("Status: ", "bold"),
35
- ("Token expired", ThemeKey.CONFIG_STATUS_ERROR),
36
- (" (run 'klaude login codex' to re-authenticate)", "dim"),
36
+ rows.append(
37
+ (
38
+ Text("Status", style=ThemeKey.CONFIG_PARAM_LABEL),
39
+ Text.assemble(
40
+ ("Token expired", ThemeKey.CONFIG_STATUS_ERROR),
41
+ (" (run 'klaude login codex' to re-authenticate)", "dim"),
42
+ ),
37
43
  )
38
44
  )
39
45
  else:
40
46
  expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.UTC)
41
- elements.append(
42
- Text.assemble(
43
- ("Status: ", "bold"),
44
- ("Logged in", ThemeKey.CONFIG_STATUS_OK),
45
- (f" (account: {state.account_id[:8]}..., expires: {expires_dt.strftime('%Y-%m-%d %H:%M UTC')})", "dim"),
47
+ rows.append(
48
+ (
49
+ Text("Status", style=ThemeKey.CONFIG_PARAM_LABEL),
50
+ Text.assemble(
51
+ ("Logged in", ThemeKey.CONFIG_STATUS_OK),
52
+ (
53
+ f" (account: {state.account_id[:8]}…, expires: {expires_dt.strftime('%Y-%m-%d %H:%M UTC')})",
54
+ "dim",
55
+ ),
56
+ ),
46
57
  )
47
58
  )
48
59
 
49
- elements.append(
50
- Text.assemble(
51
- ("Visit ", "dim"),
52
- (
60
+ rows.append(
61
+ (
62
+ Text("Usage", style="dim"),
63
+ Text(
53
64
  "https://chatgpt.com/codex/settings/usage",
54
- "blue underline link https://chatgpt.com/codex/settings/usage",
65
+ style="blue link https://chatgpt.com/codex/settings/usage",
55
66
  ),
56
- (" for up-to-date information on rate limits and credits", "dim"),
57
67
  )
58
68
  )
59
- return elements
69
+ return rows
70
+
71
+
72
+ def _get_claude_status_rows() -> list[tuple[Text, Text]]:
73
+ """Get Claude OAuth login status as (label, value) tuples for table display."""
74
+ from klaude_code.auth.claude.token_manager import ClaudeTokenManager
75
+
76
+ rows: list[tuple[Text, Text]] = []
77
+ token_manager = ClaudeTokenManager()
78
+ state = token_manager.get_state()
79
+
80
+ if state is None:
81
+ rows.append(
82
+ (
83
+ Text("Status", style=ThemeKey.CONFIG_PARAM_LABEL),
84
+ Text.assemble(
85
+ ("Not logged in", ThemeKey.CONFIG_STATUS_ERROR),
86
+ (" (run 'klaude login claude' to authenticate)", "dim"),
87
+ ),
88
+ )
89
+ )
90
+ elif state.is_expired():
91
+ rows.append(
92
+ (
93
+ Text("Status", style=ThemeKey.CONFIG_PARAM_LABEL),
94
+ Text.assemble(
95
+ ("Token expired", ThemeKey.CONFIG_STATUS_ERROR),
96
+ (" (will refresh automatically on use; run 'klaude login claude' if refresh fails)", "dim"),
97
+ ),
98
+ )
99
+ )
100
+ else:
101
+ expires_dt = datetime.datetime.fromtimestamp(state.expires_at, tz=datetime.UTC)
102
+ rows.append(
103
+ (
104
+ Text("Status", style=ThemeKey.CONFIG_PARAM_LABEL),
105
+ Text.assemble(
106
+ ("Logged in", ThemeKey.CONFIG_STATUS_OK),
107
+ (f" (expires: {expires_dt.strftime('%Y-%m-%d %H:%M UTC')})", "dim"),
108
+ ),
109
+ )
110
+ )
111
+
112
+ rows.append(
113
+ (
114
+ Text("Usage", style="dim"),
115
+ Text(
116
+ "https://claude.ai/settings/usage",
117
+ style="blue link https://claude.ai/settings/usage",
118
+ ),
119
+ )
120
+ )
121
+ return rows
60
122
 
61
123
 
62
124
  def mask_api_key(api_key: str | None) -> str:
63
125
  """Mask API key to show only first 6 and last 6 characters with *** in between"""
64
- if not api_key or api_key == "N/A":
65
- return "N/A"
126
+ if not api_key:
127
+ return ""
66
128
 
67
129
  if len(api_key) <= 12:
68
130
  return api_key
69
131
 
70
- return f"{api_key[:6]} {api_key[-6:]}"
132
+ return f"{api_key[:6]}…{api_key[-6:]}"
71
133
 
72
134
 
73
135
  def format_api_key_display(provider: ProviderConfig) -> Text:
@@ -91,7 +153,7 @@ def format_api_key_display(provider: ProviderConfig) -> Text:
91
153
  # Plain API key
92
154
  return Text(mask_api_key(provider.api_key))
93
155
  else:
94
- return Text("N/A")
156
+ return Text("")
95
157
 
96
158
 
97
159
  def format_env_var_display(value: str | None) -> Text:
@@ -114,194 +176,180 @@ def format_env_var_display(value: str | None) -> Text:
114
176
  # Plain value
115
177
  return Text(mask_api_key(value))
116
178
  else:
117
- return Text("N/A")
179
+ return Text("")
118
180
 
119
181
 
120
182
  def _get_model_params_display(model: ModelConfig) -> list[Text]:
121
183
  """Get display elements for model parameters."""
122
- params: list[Text] = []
123
- if model.model_params.thinking:
124
- if model.model_params.thinking.reasoning_effort is not None:
125
- params.append(
126
- Text.assemble(
127
- ("reason-effort", ThemeKey.CONFIG_PARAM_LABEL),
128
- ": ",
129
- model.model_params.thinking.reasoning_effort,
130
- )
131
- )
132
- if model.model_params.thinking.reasoning_summary is not None:
133
- params.append(
134
- Text.assemble(
135
- ("reason-summary", ThemeKey.CONFIG_PARAM_LABEL),
136
- ": ",
137
- model.model_params.thinking.reasoning_summary,
138
- )
139
- )
140
- if model.model_params.thinking.budget_tokens is not None:
141
- params.append(
142
- Text.assemble(
143
- ("thinking-budget-tokens", ThemeKey.CONFIG_PARAM_LABEL),
144
- ": ",
145
- str(model.model_params.thinking.budget_tokens),
146
- )
147
- )
148
- if model.model_params.provider_routing:
149
- params.append(
150
- Text.assemble(
151
- ("provider-routing", ThemeKey.CONFIG_PARAM_LABEL),
152
- ": ",
153
- model.model_params.provider_routing.model_dump_json(exclude_none=True),
154
- )
184
+ param_strings = format_model_params(model.model_params)
185
+ if param_strings:
186
+ return [Text(s) for s in param_strings]
187
+ return [Text("")]
188
+
189
+
190
+ def _build_provider_info_panel(provider: ProviderConfig, available: bool) -> Quote:
191
+ """Build a Quote containing provider name and information using a two-column grid."""
192
+ # Provider name as title
193
+ if available:
194
+ title = Text(provider.provider_name, style=ThemeKey.CONFIG_PROVIDER)
195
+ else:
196
+ title = Text.assemble(
197
+ (provider.provider_name, ThemeKey.CONFIG_PROVIDER),
198
+ (" (Unavailable)", ThemeKey.CONFIG_STATUS_ERROR),
155
199
  )
156
- if len(params) == 0:
157
- params.append(Text("N/A", style=ThemeKey.CONFIG_PARAM_LABEL))
158
- return params
159
200
 
201
+ # Build info table with two columns
202
+ info_table = Table.grid(padding=(0, 2))
203
+ info_table.add_column("Label", style=ThemeKey.CONFIG_PARAM_LABEL)
204
+ info_table.add_column("Value")
205
+
206
+ # Protocol
207
+ info_table.add_row(Text("Protocol"), Text(provider.protocol.value))
208
+
209
+ # Base URL (if set)
210
+ if provider.base_url:
211
+ info_table.add_row(Text("Base URL"), Text(provider.base_url))
212
+
213
+ # API key (if set)
214
+ if provider.api_key:
215
+ info_table.add_row(Text("API key"), format_api_key_display(provider))
216
+
217
+ # AWS Bedrock parameters
218
+ if provider.protocol == LLMClientProtocol.BEDROCK:
219
+ if provider.aws_access_key:
220
+ info_table.add_row(Text("AWS key"), format_env_var_display(provider.aws_access_key))
221
+ if provider.aws_secret_key:
222
+ info_table.add_row(Text("AWS secret"), format_env_var_display(provider.aws_secret_key))
223
+ if provider.aws_region:
224
+ info_table.add_row(Text("AWS region"), format_env_var_display(provider.aws_region))
225
+ if provider.aws_session_token:
226
+ info_table.add_row(Text("AWS token"), format_env_var_display(provider.aws_session_token))
227
+ if provider.aws_profile:
228
+ info_table.add_row(Text("AWS profile"), format_env_var_display(provider.aws_profile))
229
+
230
+ # OAuth status rows
231
+ if provider.protocol == LLMClientProtocol.CODEX_OAUTH:
232
+ for label, value in _get_codex_status_rows():
233
+ info_table.add_row(label, value)
234
+ if provider.protocol == LLMClientProtocol.CLAUDE_OAUTH:
235
+ for label, value in _get_claude_status_rows():
236
+ info_table.add_row(label, value)
237
+
238
+ return Quote(
239
+ Group(title, info_table),
240
+ style=ThemeKey.LINES,
241
+ prefix="┃ ",
242
+ )
160
243
 
161
- def display_models_and_providers(config: Config):
162
- """Display models and providers configuration using rich formatting"""
163
- themes = get_theme(config.theme)
164
- console = Console(theme=themes.app_theme)
165
244
 
166
- # Display each provider as a separate panel
167
- for provider in config.provider_list:
168
- # Provider info section
169
- provider_info = Table.grid(padding=(0, 1))
170
- provider_info.add_column(width=12)
171
- provider_info.add_column()
245
+ def _build_models_table(
246
+ provider: ProviderConfig, main_model: str | None, sub_agent_models: dict[str, str] | None = None
247
+ ) -> Table:
248
+ """Build a table for models under a provider."""
249
+ provider_available = not provider.is_api_key_missing()
172
250
 
173
- provider_info.add_row(
174
- Text("Protocol:", style=ThemeKey.CONFIG_PARAM_LABEL),
175
- Text(provider.protocol.value),
176
- )
177
- if provider.base_url:
178
- provider_info.add_row(
179
- Text("Base URL:", style=ThemeKey.CONFIG_PARAM_LABEL),
180
- Text(provider.base_url or "N/A"),
181
- )
182
- if provider.api_key:
183
- provider_info.add_row(
184
- Text("API Key:", style=ThemeKey.CONFIG_PARAM_LABEL),
185
- format_api_key_display(provider),
186
- )
251
+ # Build reverse mapping: model_name -> list of agent roles using it
252
+ model_to_agents: dict[str, list[str]] = {}
253
+ if sub_agent_models:
254
+ for agent_role, model_name in sub_agent_models.items():
255
+ if model_name not in model_to_agents:
256
+ model_to_agents[model_name] = []
257
+ model_to_agents[model_name].append(agent_role)
187
258
 
188
- # AWS Bedrock parameters
189
- if provider.protocol == LLMClientProtocol.BEDROCK:
190
- if provider.aws_access_key:
191
- provider_info.add_row(
192
- Text("AWS Key:", style=ThemeKey.CONFIG_PARAM_LABEL),
193
- format_env_var_display(provider.aws_access_key),
194
- )
195
- if provider.aws_secret_key:
196
- provider_info.add_row(
197
- Text("AWS Secret:", style=ThemeKey.CONFIG_PARAM_LABEL),
198
- format_env_var_display(provider.aws_secret_key),
199
- )
200
- if provider.aws_region:
201
- provider_info.add_row(
202
- Text("AWS Region:", style=ThemeKey.CONFIG_PARAM_LABEL),
203
- format_env_var_display(provider.aws_region),
204
- )
205
- if provider.aws_session_token:
206
- provider_info.add_row(
207
- Text("AWS Token:", style=ThemeKey.CONFIG_PARAM_LABEL),
208
- format_env_var_display(provider.aws_session_token),
209
- )
210
- if provider.aws_profile:
211
- provider_info.add_row(
212
- Text("AWS Profile:", style=ThemeKey.CONFIG_PARAM_LABEL),
213
- format_env_var_display(provider.aws_profile),
259
+ models_table = Table.grid(
260
+ padding=(0, 2),
261
+ )
262
+ models_table.add_column("Model Name", min_width=12)
263
+ models_table.add_column("Model ID", min_width=20, style=ThemeKey.CONFIG_MODEL_ID)
264
+ models_table.add_column("Params", style="dim")
265
+
266
+ model_count = len(provider.model_list)
267
+ for i, model in enumerate(provider.model_list):
268
+ is_last = i == model_count - 1
269
+ prefix = " └─ " if is_last else " ├─ "
270
+
271
+ if not provider_available:
272
+ name = Text.assemble((prefix, ThemeKey.LINES), (model.model_name, "dim"))
273
+ model_id = Text(model.model_params.model or "", style="dim")
274
+ params = Text("(unavailable)", style="dim")
275
+ else:
276
+ # Build role tags for this model
277
+ roles: list[str] = []
278
+ if model.model_name == main_model:
279
+ roles.append("default")
280
+ if model.model_name in model_to_agents:
281
+ roles.extend(role.lower() for role in model_to_agents[model.model_name])
282
+
283
+ if roles:
284
+ name = Text.assemble(
285
+ (prefix, ThemeKey.LINES),
286
+ (model.model_name, ThemeKey.CONFIG_STATUS_PRIMARY),
287
+ (f" ({', '.join(roles)})", "dim"),
214
288
  )
289
+ else:
290
+ name = Text.assemble((prefix, ThemeKey.LINES), (model.model_name, ThemeKey.CONFIG_ITEM_NAME))
291
+ model_id = Text(model.model_params.model or "")
292
+ params = Text(" · ").join(_get_model_params_display(model))
215
293
 
216
- # Check if provider has valid API key
217
- provider_available = not provider.is_api_key_missing()
294
+ models_table.add_row(name, model_id, params)
218
295
 
219
- # Models table for this provider
220
- models_table = Table.grid(padding=(0, 1), expand=True)
221
- models_table.add_column(width=2, no_wrap=True) # Status
222
- models_table.add_column(overflow="fold", ratio=2) # Name
223
- models_table.add_column(overflow="fold", ratio=3) # Model
224
- models_table.add_column(overflow="fold", ratio=4) # Params
225
-
226
- # Add header
227
- models_table.add_row(
228
- Text("", style="bold"),
229
- Text("Name", style=ThemeKey.CONFIG_TABLE_HEADER),
230
- Text("Model", style=ThemeKey.CONFIG_TABLE_HEADER),
231
- Text("Params", style=ThemeKey.CONFIG_TABLE_HEADER),
232
- )
296
+ return models_table
233
297
 
234
- # Add models for this provider
235
- for model in provider.model_list:
236
- if not provider_available:
237
- # Provider API key not set - show as unavailable
238
- status = Text("-", style="dim")
239
- name = Text(model.model_name, style="dim")
240
- model_id = Text(model.model_params.model or "N/A", style="dim")
241
- params = [Text("(unavailable)", style="dim")]
242
- elif model.model_name == config.main_model:
243
- status = Text("★", style=ThemeKey.CONFIG_STATUS_PRIMARY)
244
- name = Text(model.model_name, style=ThemeKey.CONFIG_STATUS_PRIMARY)
245
- model_id = Text(model.model_params.model or "N/A", style="")
246
- params = _get_model_params_display(model)
247
- else:
248
- status = Text("✔", style=ThemeKey.CONFIG_STATUS_OK)
249
- name = Text(model.model_name, style=ThemeKey.CONFIG_ITEM_NAME)
250
- model_id = Text(model.model_params.model or "N/A", style="")
251
- params = _get_model_params_display(model)
252
-
253
- models_table.add_row(status, name, model_id, Group(*params))
254
-
255
- # Create panel content with provider info and models
256
- panel_elements = [
257
- provider_info,
258
- Text(""), # Spacer
259
- Text("Models:", style=ThemeKey.CONFIG_TABLE_HEADER),
260
- models_table,
261
- ]
262
-
263
- # Add Codex status if this is a codex provider
264
- if provider.protocol == LLMClientProtocol.CODEX:
265
- panel_elements.append(Text("")) # Spacer
266
- panel_elements.extend(_get_codex_status_elements())
267
-
268
- panel_content = Group(*panel_elements)
269
-
270
- panel = Panel(
271
- panel_content,
272
- title=Text(f"Provider: {provider.provider_name}", style=ThemeKey.CONFIG_PROVIDER),
273
- border_style=ThemeKey.CONFIG_PANEL_BORDER,
274
- padding=(0, 1),
275
- title_align="left",
276
- )
277
298
 
278
- console.print(panel)
279
- console.print()
299
+ def _display_agent_models_table(config: Config, console: Console) -> None:
300
+ """Display model assignments as a table."""
301
+ console.print(Text(" Agent Models:", style=ThemeKey.CONFIG_TABLE_HEADER))
302
+ agent_table = Table(
303
+ box=HORIZONTALS,
304
+ show_header=True,
305
+ header_style=ThemeKey.CONFIG_TABLE_HEADER,
306
+ padding=(0, 2),
307
+ border_style=ThemeKey.LINES,
308
+ )
309
+ agent_table.add_column("Role", style="bold", min_width=10)
310
+ agent_table.add_column("Model", style=ThemeKey.CONFIG_STATUS_PRIMARY)
280
311
 
281
- # Display main model info
282
- console.print()
312
+ # Default model
283
313
  if config.main_model:
284
- console.print(
285
- Text.assemble(
286
- ("Default Model: ", "bold"),
287
- (config.main_model, ThemeKey.CONFIG_STATUS_PRIMARY),
288
- )
289
- )
314
+ agent_table.add_row("Default", config.main_model)
290
315
  else:
291
- console.print(
292
- Text.assemble(
293
- ("Default Model: ", "bold"),
294
- ("(not set)", ThemeKey.CONFIG_STATUS_ERROR),
295
- )
296
- )
316
+ agent_table.add_row("Default", Text("(not set)", style=ThemeKey.CONFIG_STATUS_ERROR))
297
317
 
318
+ # Sub-agent models
298
319
  for profile in iter_sub_agent_profiles():
299
320
  sub_model_name = config.sub_agent_models.get(profile.name)
300
- if not sub_model_name:
301
- continue
302
- console.print(
303
- Text.assemble(
304
- (f"{profile.name} Model: ", "bold"),
305
- (sub_model_name, ThemeKey.CONFIG_STATUS_PRIMARY),
306
- )
307
- )
321
+ if sub_model_name:
322
+ agent_table.add_row(profile.name, sub_model_name)
323
+
324
+ console.print(agent_table)
325
+
326
+
327
+ def display_models_and_providers(config: Config, *, show_all: bool = False):
328
+ """Display models and providers configuration using rich formatting"""
329
+ themes = get_theme(config.theme)
330
+ console = Console(theme=themes.app_theme)
331
+
332
+ # Display model assignments as a table
333
+ _display_agent_models_table(config, console)
334
+ console.print()
335
+
336
+ # Sort providers: available (api_key set) first, unavailable (api_key not set) last
337
+ sorted_providers = sorted(config.provider_list, key=lambda p: (p.is_api_key_missing(), p.provider_name))
338
+
339
+ # Filter out unavailable providers unless show_all is True
340
+ if not show_all:
341
+ sorted_providers = [p for p in sorted_providers if not p.is_api_key_missing()]
342
+
343
+ # Display each provider with its models table
344
+ for provider in sorted_providers:
345
+ provider_available = not provider.is_api_key_missing()
346
+
347
+ # Provider info panel
348
+ provider_panel = _build_provider_info_panel(provider, provider_available)
349
+ console.print(provider_panel)
350
+ console.print()
351
+
352
+ # Models table for this provider
353
+ models_table = _build_models_table(provider, config.main_model, config.sub_agent_models)
354
+ console.print(models_table)
355
+ console.print("\n")
klaude_code/cli/main.py CHANGED
@@ -286,7 +286,7 @@ def main_callback(
286
286
  raise typer.Exit(2)
287
287
 
288
288
  # In non-interactive environments, default to exec-mode behavior.
289
- # This allows: echo "..." | klaude
289
+ # This allows: echo "" | klaude
290
290
  if not sys.stdin.isatty() or not sys.stdout.isatty():
291
291
  if continue_ or resume or resume_by_id is not None:
292
292
  log(("Error: --continue/--resume options require a TTY", "red"))
@@ -17,8 +17,8 @@ from klaude_code.core.agent import Agent, DefaultModelProfileProvider, VanillaMo
17
17
  from klaude_code.core.executor import Executor
18
18
  from klaude_code.core.manager import build_llm_clients
19
19
  from klaude_code.protocol import events, llm_param, op
20
- from klaude_code.protocol import model as protocol_model
21
- from klaude_code.protocol.model import UserInputPayload
20
+ from klaude_code.protocol import message as protocol_message
21
+ from klaude_code.protocol.message import UserInputPayload
22
22
  from klaude_code.session.session import Session, close_default_store
23
23
  from klaude_code.trace import DebugType, log, set_debug_logging
24
24
  from klaude_code.ui.modes.repl import build_repl_status_snapshot
@@ -105,9 +105,11 @@ async def submit_user_input_payload(
105
105
  if result.persist_user_input:
106
106
  agent.session.append_history(
107
107
  [
108
- protocol_model.UserMessageItem(
109
- content=persisted_user_input.text,
110
- images=persisted_user_input.images,
108
+ protocol_message.UserMessage(
109
+ parts=protocol_message.parts_from_text_and_images(
110
+ persisted_user_input.text,
111
+ persisted_user_input.images,
112
+ )
111
113
  )
112
114
  ]
113
115
  )
@@ -228,7 +228,7 @@ def update_command(
228
228
  log(f"To update, install uv and run `uv tool upgrade {PACKAGE_NAME}`.")
229
229
  raise typer.Exit(1)
230
230
 
231
- log(f"Running `uv tool upgrade {PACKAGE_NAME}`...")
231
+ log(f"Running `uv tool upgrade {PACKAGE_NAME}`…")
232
232
  result = subprocess.run(["uv", "tool", "upgrade", PACKAGE_NAME], check=False)
233
233
  if result.returncode != 0:
234
234
  log((f"Error: update failed (exit code {result.returncode}).", "red"))
@@ -25,7 +25,7 @@ def _session_confirm(sessions: list[Session.SessionMetaBrief], message: str) ->
25
25
  first_msg_text = s.user_messages[0] if s.user_messages else ""
26
26
  first_msg = first_msg_text.strip().replace("\n", " ")[:50]
27
27
  if len(first_msg_text) > 50:
28
- first_msg += "..."
28
+ first_msg += ""
29
29
  log(f" {_fmt(s.updated_at)} {msg_count_display:>3} msgs {first_msg}")
30
30
 
31
31
  items: list[SelectItem[bool]] = [
@@ -1,5 +1,5 @@
1
1
  from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
2
- from klaude_code.protocol import commands, model, op
2
+ from klaude_code.protocol import commands, message, op
3
3
 
4
4
 
5
5
  class ClearCommand(CommandABC):
@@ -13,8 +13,12 @@ class ClearCommand(CommandABC):
13
13
  def summary(self) -> str:
14
14
  return "Clear conversation history and free up context"
15
15
 
16
- async def run(self, agent: Agent, user_input: model.UserInputPayload) -> CommandResult:
16
+ async def run(self, agent: Agent, user_input: message.UserInputPayload) -> CommandResult:
17
17
  del user_input # unused
18
+ import os
19
+
20
+ os.system("cls" if os.name == "nt" else "clear")
21
+
18
22
  return CommandResult(
19
23
  operations=[op.ClearSessionOperation(session_id=agent.session.id)],
20
24
  persist_user_input=False,
@@ -4,7 +4,7 @@ from typing import Protocol
4
4
  from pydantic import BaseModel
5
5
 
6
6
  from klaude_code.llm import LLMClientABC
7
- from klaude_code.protocol import commands, llm_param, model, op
7
+ from klaude_code.protocol import commands, llm_param, message, op
8
8
  from klaude_code.protocol import events as protocol_events
9
9
  from klaude_code.session.session import Session
10
10
 
@@ -78,7 +78,7 @@ class CommandABC(ABC):
78
78
  return "additional instructions"
79
79
 
80
80
  @abstractmethod
81
- async def run(self, agent: Agent, user_input: model.UserInputPayload) -> CommandResult:
81
+ async def run(self, agent: Agent, user_input: message.UserInputPayload) -> CommandResult:
82
82
  """
83
83
  Execute the command.
84
84
 
@@ -1,5 +1,5 @@
1
1
  from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
2
- from klaude_code.protocol import commands, events, model
2
+ from klaude_code.protocol import commands, events, message, model
3
3
  from klaude_code.trace import DebugType, get_current_log_file, is_debug_enabled, set_debug_logging
4
4
 
5
5
 
@@ -45,7 +45,7 @@ class DebugCommand(CommandABC):
45
45
  def placeholder(self) -> str:
46
46
  return "filter types"
47
47
 
48
- async def run(self, agent: Agent, user_input: model.UserInputPayload) -> CommandResult:
48
+ async def run(self, agent: Agent, user_input: message.UserInputPayload) -> CommandResult:
49
49
  raw = user_input.text.strip()
50
50
 
51
51
  # /debug (no args) - enable debug
@@ -70,8 +70,8 @@ class DebugCommand(CommandABC):
70
70
  events=[
71
71
  events.DeveloperMessageEvent(
72
72
  session_id=agent.session.id,
73
- item=model.DeveloperMessageItem(
74
- content=content,
73
+ item=message.DeveloperMessage(
74
+ parts=message.text_parts_from_str(content),
75
75
  command_output=model.CommandOutput(command_name=self.name),
76
76
  ),
77
77
  )
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from pathlib import Path
4
4
 
5
5
  from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
6
- from klaude_code.protocol import commands, model, op
6
+ from klaude_code.protocol import commands, message, op
7
7
 
8
8
 
9
9
  class ExportCommand(CommandABC):
@@ -29,7 +29,7 @@ class ExportCommand(CommandABC):
29
29
  def is_interactive(self) -> bool:
30
30
  return False
31
31
 
32
- async def run(self, agent: Agent, user_input: model.UserInputPayload) -> CommandResult:
32
+ async def run(self, agent: Agent, user_input: message.UserInputPayload) -> CommandResult:
33
33
  output_path = self._normalize_output_path(user_input.text, agent)
34
34
  return CommandResult(
35
35
  operations=[