klaude-code 2.5.1__py3-none-any.whl → 2.5.3__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 (58) hide show
  1. klaude_code/.DS_Store +0 -0
  2. klaude_code/cli/auth_cmd.py +2 -13
  3. klaude_code/cli/cost_cmd.py +10 -10
  4. klaude_code/cli/list_model.py +8 -0
  5. klaude_code/cli/main.py +41 -8
  6. klaude_code/cli/session_cmd.py +2 -11
  7. klaude_code/config/assets/builtin_config.yaml +45 -26
  8. klaude_code/config/config.py +30 -7
  9. klaude_code/config/model_matcher.py +3 -3
  10. klaude_code/config/sub_agent_model_helper.py +1 -1
  11. klaude_code/const.py +2 -1
  12. klaude_code/core/agent_profile.py +1 -0
  13. klaude_code/core/executor.py +4 -0
  14. klaude_code/core/loaded_skills.py +36 -0
  15. klaude_code/core/tool/context.py +1 -3
  16. klaude_code/core/tool/file/edit_tool.py +1 -1
  17. klaude_code/core/tool/file/read_tool.py +2 -2
  18. klaude_code/core/tool/file/write_tool.py +1 -1
  19. klaude_code/core/turn.py +19 -7
  20. klaude_code/llm/anthropic/client.py +97 -60
  21. klaude_code/llm/anthropic/input.py +20 -9
  22. klaude_code/llm/google/client.py +223 -148
  23. klaude_code/llm/google/input.py +44 -36
  24. klaude_code/llm/openai_compatible/stream.py +109 -99
  25. klaude_code/llm/openrouter/reasoning.py +4 -29
  26. klaude_code/llm/partial_message.py +2 -32
  27. klaude_code/llm/responses/client.py +99 -81
  28. klaude_code/llm/responses/input.py +11 -25
  29. klaude_code/llm/stream_parts.py +94 -0
  30. klaude_code/log.py +57 -0
  31. klaude_code/protocol/events/system.py +3 -0
  32. klaude_code/protocol/llm_param.py +1 -0
  33. klaude_code/session/export.py +259 -91
  34. klaude_code/session/templates/export_session.html +141 -59
  35. klaude_code/skill/.DS_Store +0 -0
  36. klaude_code/skill/assets/.DS_Store +0 -0
  37. klaude_code/skill/loader.py +1 -0
  38. klaude_code/tui/command/fork_session_cmd.py +14 -23
  39. klaude_code/tui/command/model_picker.py +2 -17
  40. klaude_code/tui/command/refresh_cmd.py +2 -0
  41. klaude_code/tui/command/resume_cmd.py +2 -18
  42. klaude_code/tui/command/sub_agent_model_cmd.py +5 -19
  43. klaude_code/tui/command/thinking_cmd.py +2 -14
  44. klaude_code/tui/components/common.py +1 -1
  45. klaude_code/tui/components/metadata.py +22 -21
  46. klaude_code/tui/components/rich/markdown.py +8 -0
  47. klaude_code/tui/components/rich/quote.py +36 -8
  48. klaude_code/tui/components/rich/theme.py +2 -0
  49. klaude_code/tui/components/welcome.py +32 -0
  50. klaude_code/tui/input/prompt_toolkit.py +3 -1
  51. klaude_code/tui/machine.py +19 -1
  52. klaude_code/tui/renderer.py +3 -4
  53. klaude_code/tui/terminal/selector.py +174 -31
  54. {klaude_code-2.5.1.dist-info → klaude_code-2.5.3.dist-info}/METADATA +1 -1
  55. {klaude_code-2.5.1.dist-info → klaude_code-2.5.3.dist-info}/RECORD +57 -53
  56. klaude_code/skill/assets/jj-workspace/SKILL.md +0 -20
  57. {klaude_code-2.5.1.dist-info → klaude_code-2.5.3.dist-info}/WHEEL +0 -0
  58. {klaude_code-2.5.1.dist-info → klaude_code-2.5.3.dist-info}/entry_points.txt +0 -0
klaude_code/.DS_Store ADDED
Binary file
@@ -4,20 +4,9 @@ import datetime
4
4
  import webbrowser
5
5
 
6
6
  import typer
7
- from prompt_toolkit.styles import Style
8
7
 
9
8
  from klaude_code.log import log
10
- from klaude_code.tui.terminal.selector import SelectItem, select_one
11
-
12
- _SELECT_STYLE = Style(
13
- [
14
- ("instruction", "ansibrightblack"),
15
- ("pointer", "ansigreen"),
16
- ("highlighted", "ansigreen"),
17
- ("text", "ansibrightblack"),
18
- ("question", "bold"),
19
- ]
20
- )
9
+ from klaude_code.tui.terminal.selector import DEFAULT_PICKER_STYLE, SelectItem, select_one
21
10
 
22
11
 
23
12
  def _select_provider() -> str | None:
@@ -30,7 +19,7 @@ def _select_provider() -> str | None:
30
19
  message="Select provider to login:",
31
20
  items=items,
32
21
  pointer="→",
33
- style=_SELECT_STYLE,
22
+ style=DEFAULT_PICKER_STYLE,
34
23
  use_search_filter=False,
35
24
  )
36
25
 
@@ -183,14 +183,14 @@ def render_cost_table(daily_stats: dict[str, DailyStats]) -> Table:
183
183
  box=ASCII_HORIZONAL,
184
184
  )
185
185
 
186
- table.add_column("Date", style="cyan", no_wrap=True)
187
- table.add_column("Model", no_wrap=True)
188
- table.add_column("Input", justify="right", no_wrap=True)
189
- table.add_column("Output", justify="right", no_wrap=True)
190
- table.add_column("Cache", justify="right", no_wrap=True)
191
- table.add_column("Total", justify="right", no_wrap=True)
192
- table.add_column("USD", justify="right", no_wrap=True)
193
- table.add_column("CNY", justify="right", no_wrap=True)
186
+ table.add_column("Date", style="cyan")
187
+ table.add_column("Model", overflow="ellipsis")
188
+ table.add_column("Input", justify="right")
189
+ table.add_column("Output", justify="right")
190
+ table.add_column("Cache", justify="right")
191
+ table.add_column("Total", justify="right")
192
+ table.add_column("USD", justify="right")
193
+ table.add_column("CNY", justify="right")
194
194
 
195
195
  # Sort dates
196
196
  sorted_dates = sorted(daily_stats.keys())
@@ -222,7 +222,7 @@ def render_cost_table(daily_stats: dict[str, DailyStats]) -> Table:
222
222
 
223
223
  table.add_row(
224
224
  format_date_display(date_str) if first_row else "",
225
- f"- {model_name}",
225
+ f"{model_name}",
226
226
  format_tokens(stats.input_tokens),
227
227
  format_tokens(stats.output_tokens),
228
228
  format_tokens(stats.cached_tokens),
@@ -276,7 +276,7 @@ def render_cost_table(daily_stats: dict[str, DailyStats]) -> Table:
276
276
  usd_str, cny_str = format_cost_dual(stats.cost_usd, stats.cost_cny)
277
277
  table.add_row(
278
278
  "",
279
- f"- {model_name}",
279
+ f"{model_name}",
280
280
  format_tokens(stats.input_tokens),
281
281
  format_tokens(stats.output_tokens),
282
282
  format_tokens(stats.cached_tokens),
@@ -288,6 +288,14 @@ def _build_models_table(
288
288
  name = Text.assemble((prefix, ThemeKey.LINES), (model.model_name, "dim"))
289
289
  model_id = Text(model.model_id or "", style="dim")
290
290
  params = Text("(unavailable)", style="dim")
291
+ elif model.disabled:
292
+ name = Text.assemble(
293
+ (prefix, ThemeKey.LINES),
294
+ (model.model_name, "dim strike"),
295
+ (" (disabled)", "dim"),
296
+ )
297
+ model_id = Text(model.model_id or "", style="dim")
298
+ params = Text(" · ").join(_get_model_params_display(model))
291
299
  else:
292
300
  # Build role tags for this model
293
301
  roles: list[str] = []
klaude_code/cli/main.py CHANGED
@@ -13,13 +13,46 @@ from klaude_code.session import Session
13
13
  from klaude_code.tui.command.resume_cmd import select_session_sync
14
14
  from klaude_code.ui.terminal.title import update_terminal_title
15
15
 
16
- ENV_HELP = """\
17
- Environment Variables:
18
-
19
- KLAUDE_READ_GLOBAL_LINE_CAP Max lines to read (default: 2000)
20
-
21
- KLAUDE_READ_MAX_CHARS Max total chars to read (default: 50000)
22
- """
16
+ ENV_HELP_LINES = [
17
+ "Environment Variables:",
18
+ "",
19
+ "Provider API keys (built-in config):",
20
+ " ANTHROPIC_API_KEY Anthropic API key",
21
+ " OPENAI_API_KEY OpenAI API key",
22
+ " OPENROUTER_API_KEY OpenRouter API key",
23
+ " GOOGLE_API_KEY Google API key (Gemini)",
24
+ " DEEPSEEK_API_KEY DeepSeek API key",
25
+ " MOONSHOT_API_KEY Moonshot API key (Kimi)",
26
+ "",
27
+ "AWS credentials (Bedrock):",
28
+ " AWS_ACCESS_KEY_ID AWS access key id",
29
+ " AWS_SECRET_ACCESS_KEY AWS secret access key",
30
+ " AWS_REGION AWS region",
31
+ "",
32
+ "Tool limits (Read):",
33
+ " KLAUDE_READ_GLOBAL_LINE_CAP Max lines to read (default: 2000)",
34
+ " KLAUDE_READ_MAX_CHARS Max total chars to read (default: 50000)",
35
+ " KLAUDE_READ_MAX_IMAGE_BYTES Max image bytes to read (default: 4MB)",
36
+ " KLAUDE_IMAGE_OUTPUT_MAX_BYTES Max decoded image bytes (default: 64MB)",
37
+ "",
38
+ "Notifications / testing:",
39
+ " KLAUDE_NOTIFY Set to 0/off/false/disable(d) to disable task notifications",
40
+ " KLAUDE_TEST_SIGNAL In tmux, emit `tmux wait-for -S <channel>` on task completion",
41
+ " TMUX Auto-detected; required for KLAUDE_TEST_SIGNAL",
42
+ "",
43
+ "Editor / terminal integration:",
44
+ " EDITOR Preferred editor for `klaude config`",
45
+ " TERM Terminal identification (auto-detected)",
46
+ " TERM_PROGRAM Terminal identification (auto-detected)",
47
+ " WT_SESSION Terminal hint (auto-detected)",
48
+ " VTE_VERSION Terminal hint (auto-detected)",
49
+ " GHOSTTY_RESOURCES_DIR Ghostty detection (auto-detected)",
50
+ "",
51
+ "Compatibility:",
52
+ " ANTHROPIC_AUTH_TOKEN Reserved by anthropic SDK; temporarily unset during client init",
53
+ ]
54
+
55
+ ENV_HELP = "\n\n".join(ENV_HELP_LINES)
23
56
 
24
57
  app = typer.Typer(
25
58
  add_completion=False,
@@ -192,7 +225,7 @@ def main_callback(
192
225
  if raw_model:
193
226
  matches = [
194
227
  m.selector
195
- for m in cfg.iter_model_entries()
228
+ for m in cfg.iter_model_entries(only_available=True, include_disabled=False)
196
229
  if (m.model_id or "").strip().lower() == raw_model.lower()
197
230
  ]
198
231
  if len(matches) == 1:
@@ -9,9 +9,7 @@ from klaude_code.session import Session
9
9
  def _session_confirm(sessions: list[Session.SessionMetaBrief], message: str) -> bool:
10
10
  """Show session list and confirm deletion using prompt_toolkit."""
11
11
 
12
- from prompt_toolkit.styles import Style
13
-
14
- from klaude_code.tui.terminal.selector import SelectItem, select_one
12
+ from klaude_code.tui.terminal.selector import DEFAULT_PICKER_STYLE, SelectItem, select_one
15
13
 
16
14
  def _fmt(ts: float) -> str:
17
15
  try:
@@ -37,14 +35,7 @@ def _session_confirm(sessions: list[Session.SessionMetaBrief], message: str) ->
37
35
  message=message,
38
36
  items=items,
39
37
  pointer="→",
40
- style=Style(
41
- [
42
- ("question", "bold"),
43
- ("pointer", "ansigreen"),
44
- ("highlighted", "ansigreen"),
45
- ("text", ""),
46
- ]
47
- ),
38
+ style=DEFAULT_PICKER_STYLE,
48
39
  use_search_filter=False,
49
40
  )
50
41
  return bool(result)
@@ -1,4 +1,3 @@
1
- ---
2
1
  # Built-in provider and model configurations
3
2
  # Users can start using klaude by simply setting environment variables
4
3
  # (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.) without manual configuration.
@@ -25,32 +24,58 @@ provider_list:
25
24
  protocol: responses
26
25
  api_key: ${OPENAI_API_KEY}
27
26
  model_list:
28
- - model_name: gpt-5.2
27
+ - model_name: gpt-5.2-high
29
28
  model_id: gpt-5.2
30
29
  max_tokens: 128000
31
30
  context_limit: 400000
32
31
  verbosity: high
33
32
  thinking:
34
33
  reasoning_effort: high
34
+ reasoning_summary: detailed
35
+ cost: {input: 1.75, output: 14, cache_read: 0.17}
36
+ - model_name: gpt-5.2-medium
37
+ model_id: gpt-5.2
38
+ context_limit: 400000
39
+ verbosity: high
40
+ thinking:
41
+ reasoning_effort: medium
42
+ reasoning_summary: concise
43
+ cost: {input: 1.75, output: 14, cache_read: 0.17}
44
+ - model_name: gpt-5.2-low
45
+ model_id: gpt-5.2
46
+ context_limit: 400000
47
+ verbosity: low
48
+ thinking:
49
+ reasoning_effort: low
50
+ reasoning_summary: concise
51
+ cost: {input: 1.75, output: 14, cache_read: 0.17}
52
+ - model_name: gpt-5.2-fast
53
+ model_id: gpt-5.2
54
+ context_limit: 400000
55
+ verbosity: low
56
+ thinking:
57
+ reasoning_effort: none
35
58
  cost: {input: 1.75, output: 14, cache_read: 0.17}
36
- - provider_name: openrouter
37
- protocol: openrouter
38
- api_key: ${OPENROUTER_API_KEY}
39
- model_list:
40
59
  - model_name: gpt-5.1-codex-max
41
- model_id: openai/gpt-5.1-codex-max
60
+ model_id: gpt-5.1-codex-max
42
61
  max_tokens: 128000
43
62
  context_limit: 400000
44
63
  thinking:
45
64
  reasoning_effort: medium
65
+ reasoning_summary: detailed
46
66
  cost: {input: 1.25, output: 10, cache_read: 0.13}
47
- - model_name: gpt-5.2
67
+ - provider_name: openrouter
68
+ protocol: openrouter
69
+ api_key: ${OPENROUTER_API_KEY}
70
+ model_list:
71
+ - model_name: gpt-5.2-high
48
72
  model_id: openai/gpt-5.2
49
73
  max_tokens: 128000
50
74
  context_limit: 400000
51
75
  verbosity: high
52
76
  thinking:
53
77
  reasoning_effort: high
78
+ reasoning_summary: detailed
54
79
  cost: {input: 1.75, output: 14, cache_read: 0.17}
55
80
  - model_name: gpt-5.2-medium
56
81
  model_id: openai/gpt-5.2
@@ -59,22 +84,7 @@ provider_list:
59
84
  verbosity: high
60
85
  thinking:
61
86
  reasoning_effort: medium
62
- cost: {input: 1.75, output: 14, cache_read: 0.17}
63
- - model_name: gpt-5.2-low
64
- model_id: openai/gpt-5.2
65
- max_tokens: 128000
66
- context_limit: 400000
67
- verbosity: low
68
- thinking:
69
- reasoning_effort: low
70
- cost: {input: 1.75, output: 14, cache_read: 0.17}
71
- - model_name: gpt-5.2-fast
72
- model_id: openai/gpt-5.2
73
- max_tokens: 128000
74
- context_limit: 400000
75
- verbosity: low
76
- thinking:
77
- reasoning_effort: none
87
+ reasoning_summary: concise
78
88
  cost: {input: 1.75, output: 14, cache_read: 0.17}
79
89
  - model_name: kimi
80
90
  model_id: moonshotai/kimi-k2-thinking
@@ -96,7 +106,6 @@ provider_list:
96
106
  - model_name: opus
97
107
  model_id: anthropic/claude-4.5-opus
98
108
  context_limit: 200000
99
- verbosity: high
100
109
  thinking:
101
110
  type: enabled
102
111
  budget_tokens: 2048
@@ -166,10 +175,14 @@ provider_list:
166
175
  - model_name: gemini-pro
167
176
  model_id: gemini-3-pro-preview
168
177
  context_limit: 1048576
178
+ thinking:
179
+ reasoning_effort: high
169
180
  cost: {input: 2, output: 12, cache_read: 0.2}
170
181
  - model_name: gemini-flash
171
182
  model_id: gemini-3-flash-preview
172
183
  context_limit: 1048576
184
+ thinking:
185
+ reasoning_effort: medium
173
186
  cost: {input: 0.5, output: 3, cache_read: 0.05}
174
187
  - model_name: nano-banana-pro
175
188
  model_id: gemini-3-pro-image-preview
@@ -178,6 +191,13 @@ provider_list:
178
191
  - image
179
192
  - text
180
193
  cost: {input: 2, output: 12, cache_read: 0.2, image: 120}
194
+ - model_name: nano-banana
195
+ model_id: gemini-2.5-flash-image
196
+ context_limit: 33000
197
+ modalities:
198
+ - image
199
+ - text
200
+ cost: {input: 0.3, output: 2.5, cache_read: 0.03, image: 30}
181
201
  - provider_name: bedrock
182
202
  protocol: bedrock
183
203
  aws_access_key: ${AWS_ACCESS_KEY_ID}
@@ -222,7 +242,6 @@ provider_list:
222
242
  - model_name: opus
223
243
  model_id: claude-opus-4-5-20251101
224
244
  context_limit: 200000
225
- verbosity: high
226
245
  thinking:
227
246
  type: enabled
228
247
  budget_tokens: 2048
@@ -332,11 +332,12 @@ class Config(BaseModel):
332
332
 
333
333
  raise ValueError(f"Unknown model: {model_name}")
334
334
 
335
- def iter_model_entries(self, only_available: bool = False) -> list[ModelEntry]:
335
+ def iter_model_entries(self, only_available: bool = False, include_disabled: bool = True) -> list[ModelEntry]:
336
336
  """Return all model entries with their provider names.
337
337
 
338
338
  Args:
339
339
  only_available: If True, only return models from providers with valid API keys.
340
+ include_disabled: If False, exclude models with disabled=True.
340
341
  """
341
342
  return [
342
343
  ModelEntry(
@@ -347,25 +348,26 @@ class Config(BaseModel):
347
348
  for provider in self.provider_list
348
349
  if not only_available or not provider.is_api_key_missing()
349
350
  for model in provider.model_list
351
+ if include_disabled or not model.disabled
350
352
  ]
351
353
 
352
354
  def has_available_image_model(self) -> bool:
353
355
  """Check if any image generation model is available."""
354
- for entry in self.iter_model_entries(only_available=True):
356
+ for entry in self.iter_model_entries(only_available=True, include_disabled=False):
355
357
  if entry.modalities and "image" in entry.modalities:
356
358
  return True
357
359
  return False
358
360
 
359
361
  def get_first_available_nano_banana_model(self) -> str | None:
360
362
  """Get the first available nano-banana model, or None."""
361
- for entry in self.iter_model_entries(only_available=True):
363
+ for entry in self.iter_model_entries(only_available=True, include_disabled=False):
362
364
  if "nano-banana" in entry.model_name:
363
365
  return entry.model_name
364
366
  return None
365
367
 
366
368
  def get_first_available_image_model(self) -> str | None:
367
369
  """Get the first available image generation model, or None."""
368
- for entry in self.iter_model_entries(only_available=True):
370
+ for entry in self.iter_model_entries(only_available=True, include_disabled=False):
369
371
  if entry.modalities and "image" in entry.modalities:
370
372
  return entry.model_name
371
373
  return None
@@ -435,11 +437,26 @@ def _get_builtin_config() -> Config:
435
437
  return Config(provider_list=providers, sub_agent_models=sub_agent_models)
436
438
 
437
439
 
440
+ def _merge_model(builtin: ModelConfig, user: ModelConfig) -> ModelConfig:
441
+ """Merge user model config with builtin model config.
442
+
443
+ Strategy: user values take precedence if explicitly set (not default).
444
+ This allows users to override specific fields (e.g., disabled=true)
445
+ without losing other builtin settings (e.g., model_id, max_tokens).
446
+ """
447
+ merged_data = builtin.model_dump()
448
+ user_data = user.model_dump(exclude_defaults=True, exclude={"model_name"})
449
+ for key, value in user_data.items():
450
+ if value is not None:
451
+ merged_data[key] = value
452
+ return ModelConfig.model_validate(merged_data)
453
+
454
+
438
455
  def _merge_provider(builtin: ProviderConfig, user: UserProviderConfig) -> ProviderConfig:
439
456
  """Merge user provider config with builtin provider config.
440
457
 
441
458
  Strategy:
442
- - model_list: merge by model_name, user models override builtin models with same name
459
+ - model_list: merge by model_name, user model fields override builtin fields
443
460
  - Other fields (api_key, base_url, etc.): user config takes precedence if set
444
461
  """
445
462
  # Merge model_list: builtin first, then user overrides/appends
@@ -447,7 +464,12 @@ def _merge_provider(builtin: ProviderConfig, user: UserProviderConfig) -> Provid
447
464
  for m in builtin.model_list:
448
465
  merged_models[m.model_name] = m
449
466
  for m in user.model_list:
450
- merged_models[m.model_name] = m
467
+ if m.model_name in merged_models:
468
+ # Merge with builtin model
469
+ merged_models[m.model_name] = _merge_model(merged_models[m.model_name], m)
470
+ else:
471
+ # New model from user
472
+ merged_models[m.model_name] = m
451
473
 
452
474
  # For other fields, use user values if explicitly set, otherwise use builtin
453
475
  # We check if user explicitly provided a value by comparing to defaults
@@ -578,7 +600,8 @@ def _load_config_cached() -> Config:
578
600
  def load_config() -> Config:
579
601
  """Load config from disk (builtin + user merged).
580
602
 
581
- Always returns a valid Config. Use config.iter_model_entries(only_available=True)
603
+ Always returns a valid Config. Use
604
+ ``config.iter_model_entries(only_available=True, include_disabled=False)``
582
605
  to check if any models are actually usable.
583
606
  """
584
607
  try:
@@ -48,9 +48,9 @@ def match_model_from_config(preferred: str | None = None) -> ModelMatchResult:
48
48
  """
49
49
  config = load_config()
50
50
 
51
- # Only show models from providers with valid API keys
51
+ # Only show models from providers with valid API keys, exclude disabled models
52
52
  models: list[ModelEntry] = sorted(
53
- config.iter_model_entries(only_available=True),
53
+ config.iter_model_entries(only_available=True, include_disabled=False),
54
54
  key=lambda m: (m.provider.lower(), m.model_name.lower()),
55
55
  )
56
56
 
@@ -102,6 +102,7 @@ def match_model_from_config(preferred: str | None = None) -> ModelMatchResult:
102
102
  )
103
103
 
104
104
  # Normalized matching (e.g. gpt52 == gpt-5.2, gpt52 in gpt-5.2-2025-...)
105
+ # Only match selector/model_name exactly; model_id is checked via substring match below
105
106
  preferred_norm = _normalize_model_key(preferred)
106
107
  normalized_matches: list[ModelEntry] = []
107
108
  if preferred_norm:
@@ -110,7 +111,6 @@ def match_model_from_config(preferred: str | None = None) -> ModelMatchResult:
110
111
  for m in models
111
112
  if preferred_norm == _normalize_model_key(m.selector)
112
113
  or preferred_norm == _normalize_model_key(m.model_name)
113
- or preferred_norm == _normalize_model_key(m.model_id or "")
114
114
  ]
115
115
  if len(normalized_matches) == 1:
116
116
  return ModelMatchResult(
@@ -180,7 +180,7 @@ class SubAgentModelHelper:
180
180
  - Returns all available models
181
181
  """
182
182
  profile = get_sub_agent_profile(sub_agent_type)
183
- all_models = self._config.iter_model_entries(only_available=True)
183
+ all_models = self._config.iter_model_entries(only_available=True, include_disabled=False)
184
184
 
185
185
  if profile.availability_requirement == AVAILABILITY_IMAGE_MODEL:
186
186
  return [m for m in all_models if m.modalities and "image" in m.modalities]
klaude_code/const.py CHANGED
@@ -27,6 +27,7 @@ def _get_int_env(name: str, default: int) -> int:
27
27
  # =============================================================================
28
28
 
29
29
  MAX_FAILED_TURN_RETRIES = 10 # Maximum retry attempts for failed turns
30
+ RETRY_PRESERVE_PARTIAL_MESSAGE = True # Preserve partial message on stream error for retry prefill
30
31
  LLM_HTTP_TIMEOUT_TOTAL = 300.0 # HTTP timeout for LLM API requests (seconds)
31
32
  LLM_HTTP_TIMEOUT_CONNECT = 15.0 # HTTP connect timeout (seconds)
32
33
  LLM_HTTP_TIMEOUT_READ = 285.0 # HTTP read timeout (seconds)
@@ -157,7 +158,7 @@ MARKDOWN_RIGHT_MARGIN = 2 # Right margin (columns) for markdown rendering
157
158
  STATUS_HINT_TEXT = " (esc to interrupt)" # Status hint text shown after spinner
158
159
 
159
160
  # Spinner status texts
160
- STATUS_WAITING_TEXT = "Connecting …"
161
+ STATUS_WAITING_TEXT = "Loading …"
161
162
  STATUS_THINKING_TEXT = "Thinking …"
162
163
  STATUS_COMPOSING_TEXT = "Composing"
163
164
 
@@ -48,6 +48,7 @@ COMMAND_DESCRIPTIONS: dict[str, str] = {
48
48
  "fd": "simple and fast alternative to find",
49
49
  "tree": "directory listing as a tree",
50
50
  "sg": "ast-grep - AST-aware code search",
51
+ "jq": "command-line JSON processor",
51
52
  "jj": "jujutsu - Git-compatible version control system",
52
53
  }
53
54
 
@@ -18,6 +18,7 @@ from klaude_code.config import load_config
18
18
  from klaude_code.config.sub_agent_model_helper import SubAgentModelHelper
19
19
  from klaude_code.core.agent import Agent
20
20
  from klaude_code.core.agent_profile import DefaultModelProfileProvider, ModelProfileProvider
21
+ from klaude_code.core.loaded_skills import get_loaded_skill_names_by_location
21
22
  from klaude_code.core.manager import LLMClients, SubAgentManager
22
23
  from klaude_code.llm.registry import create_llm_client
23
24
  from klaude_code.log import DebugType, log_debug
@@ -136,6 +137,7 @@ class AgentRuntime:
136
137
  session_id=session.id,
137
138
  work_dir=str(session.work_dir),
138
139
  llm_config=self._llm_clients.main.get_llm_config(),
140
+ loaded_skills=get_loaded_skill_names_by_location(),
139
141
  )
140
142
  )
141
143
 
@@ -192,6 +194,7 @@ class AgentRuntime:
192
194
  session_id=agent.session.id,
193
195
  work_dir=str(agent.session.work_dir),
194
196
  llm_config=self._llm_clients.main.get_llm_config(),
197
+ loaded_skills=get_loaded_skill_names_by_location(),
195
198
  )
196
199
  )
197
200
 
@@ -215,6 +218,7 @@ class AgentRuntime:
215
218
  session_id=target_session.id,
216
219
  work_dir=str(target_session.work_dir),
217
220
  llm_config=self._llm_clients.main.get_llm_config(),
221
+ loaded_skills=get_loaded_skill_names_by_location(),
218
222
  )
219
223
  )
220
224
 
@@ -0,0 +1,36 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ def get_loaded_skill_names_by_location() -> dict[str, list[str]]:
5
+ """Return loaded skill names grouped by location.
6
+
7
+ The UI should not import the skill system directly. Core can expose a
8
+ lightweight summary suitable for WelcomeEvent rendering.
9
+ """
10
+
11
+ try:
12
+ # Import lazily to keep startup overhead minimal and avoid unnecessary
13
+ # coupling at module import time.
14
+ from klaude_code.skill.manager import get_available_skills
15
+ except Exception:
16
+ return {}
17
+
18
+ result: dict[str, list[str]] = {"user": [], "project": [], "system": []}
19
+ try:
20
+ for name, _desc, location in get_available_skills():
21
+ if location == "user":
22
+ result["user"].append(name)
23
+ elif location == "project":
24
+ result["project"].append(name)
25
+ elif location == "system":
26
+ result["system"].append(name)
27
+ except Exception:
28
+ return {}
29
+
30
+ if not result["user"] and not result["project"] and not result["system"]:
31
+ return {}
32
+
33
+ result["user"].sort()
34
+ result["project"].sort()
35
+ result["system"].sort()
36
+ return result
@@ -89,7 +89,5 @@ class ToolContext:
89
89
  def with_record_sub_agent_session_id(self, callback: Callable[[str], None] | None) -> ToolContext:
90
90
  return replace(self, record_sub_agent_session_id=callback)
91
91
 
92
- def with_register_sub_agent_metadata_getter(
93
- self, callback: Callable[[GetMetadataFn], None] | None
94
- ) -> ToolContext:
92
+ def with_register_sub_agent_metadata_getter(self, callback: Callable[[GetMetadataFn], None] | None) -> ToolContext:
95
93
  return replace(self, register_sub_agent_metadata_getter=callback)
@@ -98,7 +98,7 @@ class EditTool(ToolABC):
98
98
  if is_directory(file_path):
99
99
  return message.ToolResultMessage(
100
100
  status="error",
101
- output_text="<tool_use_error>Illegal operation on a directory. edit</tool_use_error>",
101
+ output_text="<tool_use_error>Illegal operation on a directory: edit</tool_use_error>",
102
102
  )
103
103
 
104
104
  if args.old_string == "":
@@ -210,7 +210,7 @@ class ReadTool(ToolABC):
210
210
  if is_directory(file_path):
211
211
  return message.ToolResultMessage(
212
212
  status="error",
213
- output_text="<tool_use_error>Illegal operation on a directory. read</tool_use_error>",
213
+ output_text="<tool_use_error>Illegal operation on a directory: read</tool_use_error>",
214
214
  )
215
215
  if not file_exists(file_path):
216
216
  return message.ToolResultMessage(
@@ -308,7 +308,7 @@ class ReadTool(ToolABC):
308
308
  except IsADirectoryError:
309
309
  return message.ToolResultMessage(
310
310
  status="error",
311
- output_text="<tool_use_error>Illegal operation on a directory. read</tool_use_error>",
311
+ output_text="<tool_use_error>Illegal operation on a directory: read</tool_use_error>",
312
312
  )
313
313
 
314
314
  if offset > max(read_result.total_lines, 0):
@@ -57,7 +57,7 @@ class WriteTool(ToolABC):
57
57
  if is_directory(file_path):
58
58
  return message.ToolResultMessage(
59
59
  status="error",
60
- output_text="<tool_use_error>Illegal operation on a directory. write</tool_use_error>",
60
+ output_text="<tool_use_error>Illegal operation on a directory: write</tool_use_error>",
61
61
  )
62
62
 
63
63
  file_tracker = context.file_tracker
klaude_code/core/turn.py CHANGED
@@ -4,7 +4,7 @@ from collections.abc import AsyncGenerator
4
4
  from dataclasses import dataclass, field
5
5
  from typing import TYPE_CHECKING
6
6
 
7
- from klaude_code.const import SUPPORTED_IMAGE_SIZES
7
+ from klaude_code.const import RETRY_PRESERVE_PARTIAL_MESSAGE, SUPPORTED_IMAGE_SIZES
8
8
  from klaude_code.core.tool import ToolABC
9
9
  from klaude_code.core.tool.context import SubAgentResumeClaims, ToolContext
10
10
 
@@ -24,6 +24,12 @@ from klaude_code.llm.client import LLMStreamABC
24
24
  from klaude_code.log import DebugType, log_debug
25
25
  from klaude_code.protocol import events, llm_param, message, model, tools
26
26
 
27
+ # Protocols that support prefill (continuing from partial assistant message)
28
+ _PREFILL_SUPPORTED_PROTOCOLS = frozenset({
29
+ "anthropic",
30
+ "claude_oauth",
31
+ })
32
+
27
33
 
28
34
  class TurnError(Exception):
29
35
  """Raised when a turn fails and should be retried."""
@@ -176,6 +182,18 @@ class TurnExecutor:
176
182
  yield event
177
183
 
178
184
  if self._turn_result.stream_error is not None:
185
+ # Save accumulated content for potential prefill on retry (only for supported protocols)
186
+ protocol = ctx.llm_client.get_llm_config().protocol
187
+ supports_prefill = protocol.value in _PREFILL_SUPPORTED_PROTOCOLS
188
+ if (
189
+ RETRY_PRESERVE_PARTIAL_MESSAGE
190
+ and supports_prefill
191
+ and self._turn_result.assistant_message is not None
192
+ and self._turn_result.assistant_message.parts
193
+ ):
194
+ session_ctx.append_history([self._turn_result.assistant_message])
195
+ # Add continuation prompt to avoid Anthropic thinking block requirement
196
+ session_ctx.append_history([message.UserMessage(parts=[message.TextPart(text="continue")])])
179
197
  session_ctx.append_history([self._turn_result.stream_error])
180
198
  yield events.TurnEndEvent(session_id=session_ctx.session_id)
181
199
  raise TurnError(self._turn_result.stream_error.error)
@@ -339,12 +357,6 @@ class TurnExecutor:
339
357
  )
340
358
  case message.StreamErrorItem() as msg:
341
359
  turn_result.stream_error = msg
342
- log_debug(
343
- "[StreamError]",
344
- msg.error,
345
- style="red",
346
- debug_type=DebugType.RESPONSE,
347
- )
348
360
  case message.ToolCallStartDelta() as msg:
349
361
  if thinking_active:
350
362
  thinking_active = False