klaude-code 2.8.1__py3-none-any.whl → 2.9.1__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 (107) hide show
  1. klaude_code/app/runtime.py +2 -1
  2. klaude_code/auth/antigravity/oauth.py +33 -38
  3. klaude_code/auth/antigravity/token_manager.py +0 -18
  4. klaude_code/auth/base.py +53 -0
  5. klaude_code/auth/claude/oauth.py +34 -49
  6. klaude_code/auth/codex/exceptions.py +0 -4
  7. klaude_code/auth/codex/oauth.py +32 -28
  8. klaude_code/auth/codex/token_manager.py +0 -18
  9. klaude_code/cli/cost_cmd.py +128 -39
  10. klaude_code/cli/list_model.py +27 -10
  11. klaude_code/cli/main.py +14 -3
  12. klaude_code/config/assets/builtin_config.yaml +25 -24
  13. klaude_code/config/config.py +47 -25
  14. klaude_code/config/sub_agent_model_helper.py +18 -13
  15. klaude_code/config/thinking.py +0 -8
  16. klaude_code/const.py +1 -1
  17. klaude_code/core/agent_profile.py +11 -56
  18. klaude_code/core/compaction/overflow.py +0 -4
  19. klaude_code/core/executor.py +33 -5
  20. klaude_code/core/manager/llm_clients.py +9 -1
  21. klaude_code/core/prompts/prompt-claude-code.md +4 -4
  22. klaude_code/core/reminders.py +21 -23
  23. klaude_code/core/task.py +1 -5
  24. klaude_code/core/tool/__init__.py +3 -2
  25. klaude_code/core/tool/file/apply_patch.py +0 -27
  26. klaude_code/core/tool/file/read_tool.md +3 -2
  27. klaude_code/core/tool/file/read_tool.py +27 -3
  28. klaude_code/core/tool/offload.py +0 -35
  29. klaude_code/core/tool/shell/bash_tool.py +1 -1
  30. klaude_code/core/tool/sub_agent/__init__.py +6 -0
  31. klaude_code/core/tool/sub_agent/image_gen.md +16 -0
  32. klaude_code/core/tool/sub_agent/image_gen.py +146 -0
  33. klaude_code/core/tool/sub_agent/task.md +20 -0
  34. klaude_code/core/tool/sub_agent/task.py +205 -0
  35. klaude_code/core/tool/tool_registry.py +0 -16
  36. klaude_code/core/turn.py +1 -1
  37. klaude_code/llm/anthropic/input.py +6 -5
  38. klaude_code/llm/antigravity/input.py +14 -7
  39. klaude_code/llm/bedrock_anthropic/__init__.py +3 -0
  40. klaude_code/llm/google/client.py +8 -6
  41. klaude_code/llm/google/input.py +20 -12
  42. klaude_code/llm/image.py +18 -11
  43. klaude_code/llm/input_common.py +32 -6
  44. klaude_code/llm/json_stable.py +37 -0
  45. klaude_code/llm/{codex → openai_codex}/__init__.py +1 -1
  46. klaude_code/llm/{codex → openai_codex}/client.py +24 -2
  47. klaude_code/llm/openai_codex/prompt_sync.py +237 -0
  48. klaude_code/llm/openai_compatible/client.py +3 -1
  49. klaude_code/llm/openai_compatible/input.py +0 -10
  50. klaude_code/llm/openai_compatible/stream.py +35 -10
  51. klaude_code/llm/{responses → openai_responses}/client.py +1 -1
  52. klaude_code/llm/{responses → openai_responses}/input.py +15 -5
  53. klaude_code/llm/registry.py +3 -8
  54. klaude_code/llm/stream_parts.py +3 -1
  55. klaude_code/llm/usage.py +1 -9
  56. klaude_code/protocol/events.py +2 -2
  57. klaude_code/protocol/message.py +3 -2
  58. klaude_code/protocol/model.py +34 -2
  59. klaude_code/protocol/op.py +13 -0
  60. klaude_code/protocol/op_handler.py +5 -0
  61. klaude_code/protocol/sub_agent/AGENTS.md +5 -5
  62. klaude_code/protocol/sub_agent/__init__.py +13 -34
  63. klaude_code/protocol/sub_agent/explore.py +7 -34
  64. klaude_code/protocol/sub_agent/image_gen.py +3 -74
  65. klaude_code/protocol/sub_agent/task.py +3 -47
  66. klaude_code/protocol/sub_agent/web.py +8 -52
  67. klaude_code/protocol/tools.py +2 -0
  68. klaude_code/session/session.py +80 -22
  69. klaude_code/session/store.py +0 -4
  70. klaude_code/skill/assets/deslop/SKILL.md +9 -0
  71. klaude_code/skill/system_skills.py +0 -20
  72. klaude_code/tui/command/fork_session_cmd.py +5 -2
  73. klaude_code/tui/command/resume_cmd.py +9 -2
  74. klaude_code/tui/command/sub_agent_model_cmd.py +85 -18
  75. klaude_code/tui/components/assistant.py +0 -26
  76. klaude_code/tui/components/bash_syntax.py +4 -0
  77. klaude_code/tui/components/command_output.py +3 -1
  78. klaude_code/tui/components/developer.py +3 -0
  79. klaude_code/tui/components/diffs.py +4 -209
  80. klaude_code/tui/components/errors.py +4 -0
  81. klaude_code/tui/components/mermaid_viewer.py +2 -2
  82. klaude_code/tui/components/metadata.py +0 -3
  83. klaude_code/tui/components/rich/markdown.py +120 -87
  84. klaude_code/tui/components/rich/status.py +2 -2
  85. klaude_code/tui/components/rich/theme.py +11 -6
  86. klaude_code/tui/components/sub_agent.py +2 -46
  87. klaude_code/tui/components/thinking.py +0 -33
  88. klaude_code/tui/components/tools.py +65 -21
  89. klaude_code/tui/components/user_input.py +2 -0
  90. klaude_code/tui/input/images.py +21 -18
  91. klaude_code/tui/input/key_bindings.py +2 -2
  92. klaude_code/tui/input/prompt_toolkit.py +49 -49
  93. klaude_code/tui/machine.py +29 -47
  94. klaude_code/tui/renderer.py +48 -33
  95. klaude_code/tui/runner.py +2 -1
  96. klaude_code/tui/terminal/image.py +27 -34
  97. klaude_code/ui/common.py +0 -70
  98. {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/METADATA +3 -6
  99. {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/RECORD +103 -99
  100. klaude_code/core/tool/sub_agent_tool.py +0 -126
  101. klaude_code/llm/bedrock/__init__.py +0 -3
  102. klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -108
  103. klaude_code/tui/components/rich/searchable_text.py +0 -68
  104. /klaude_code/llm/{bedrock → bedrock_anthropic}/client.py +0 -0
  105. /klaude_code/llm/{responses → openai_responses}/__init__.py +0 -0
  106. {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/WHEEL +0 -0
  107. {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/entry_points.txt +0 -0
@@ -92,10 +92,6 @@ class Session(BaseModel):
92
92
  ]
93
93
  return self._user_messages_cache
94
94
 
95
- @staticmethod
96
- def _project_key() -> str:
97
- return project_key_from_cwd()
98
-
99
95
  @classmethod
100
96
  def paths(cls) -> ProjectPaths:
101
97
  return get_default_store().paths
@@ -320,10 +316,15 @@ class Session(BaseModel):
320
316
  prev_item: message.HistoryEvent | None = None
321
317
  last_assistant_content: str = ""
322
318
  report_back_result: str | None = None
319
+ pending_tool_calls: dict[str, events.ToolCallEvent] = {}
323
320
  history = self.conversation_history
324
321
  history_len = len(history)
325
322
  yield events.TaskStartEvent(session_id=self.id, sub_agent_state=self.sub_agent_state)
326
323
  for idx, it in enumerate(history):
324
+ # Flush pending tool calls if current item won't consume them
325
+ if pending_tool_calls and not isinstance(it, message.ToolResultMessage):
326
+ yield from pending_tool_calls.values()
327
+ pending_tool_calls.clear()
327
328
  if self.need_turn_start(prev_item, it):
328
329
  yield events.TurnStartEvent(session_id=self.id)
329
330
  match it:
@@ -335,6 +336,7 @@ class Session(BaseModel):
335
336
  # Reconstruct streaming boundaries from saved parts.
336
337
  # This allows replay to reuse the same TUI state machine as live events.
337
338
  thinking_open = False
339
+ thinking_had_content = False
338
340
  assistant_open = False
339
341
 
340
342
  for part in am.parts:
@@ -346,15 +348,23 @@ class Session(BaseModel):
346
348
  thinking_open = True
347
349
  yield events.ThinkingStartEvent(response_id=am.response_id, session_id=self.id)
348
350
  if part.text:
351
+ if thinking_had_content:
352
+ yield events.ThinkingDeltaEvent(
353
+ content=" \n \n",
354
+ response_id=am.response_id,
355
+ session_id=self.id,
356
+ )
349
357
  yield events.ThinkingDeltaEvent(
350
358
  content=part.text,
351
359
  response_id=am.response_id,
352
360
  session_id=self.id,
353
361
  )
362
+ thinking_had_content = True
354
363
  continue
355
364
 
356
365
  if thinking_open:
357
366
  thinking_open = False
367
+ thinking_had_content = False
358
368
  yield events.ThinkingEndEvent(response_id=am.response_id, session_id=self.id)
359
369
 
360
370
  if isinstance(part, message.TextPart):
@@ -384,7 +394,7 @@ class Session(BaseModel):
384
394
  continue
385
395
  if part.tool_name == tools.REPORT_BACK:
386
396
  report_back_result = part.arguments_json
387
- yield events.ToolCallEvent(
397
+ pending_tool_calls[part.call_id] = events.ToolCallEvent(
388
398
  tool_call_id=part.call_id,
389
399
  tool_name=part.tool_name,
390
400
  arguments=part.arguments_json,
@@ -394,6 +404,8 @@ class Session(BaseModel):
394
404
  if am.stop_reason == "aborted":
395
405
  yield events.InterruptEvent(session_id=self.id)
396
406
  case message.ToolResultMessage() as tr:
407
+ if tr.call_id in pending_tool_calls:
408
+ yield pending_tool_calls.pop(tr.call_id)
397
409
  status = "success" if tr.status == "success" else "error"
398
410
  # Check if this is the last tool result in the current turn
399
411
  next_item = history[idx + 1] if idx + 1 < history_len else None
@@ -410,7 +422,9 @@ class Session(BaseModel):
410
422
  )
411
423
  yield from self._iter_sub_agent_history(tr, seen_sub_agent_sessions)
412
424
  case message.UserMessage() as um:
413
- images = [part for part in um.parts if isinstance(part, message.ImageURLPart)]
425
+ images = [
426
+ part for part in um.parts if isinstance(part, (message.ImageURLPart, message.ImageFilePart))
427
+ ]
414
428
  yield events.UserMessageEvent(
415
429
  content=message.join_text_parts(um.parts),
416
430
  session_id=self.id,
@@ -439,6 +453,11 @@ class Session(BaseModel):
439
453
  pass
440
454
  prev_item = it
441
455
 
456
+ # Flush any remaining pending tool calls (e.g., from aborted or incomplete sessions)
457
+ if pending_tool_calls:
458
+ yield from pending_tool_calls.values()
459
+ pending_tool_calls.clear()
460
+
442
461
  has_structured_output = report_back_result is not None
443
462
  task_result = report_back_result if has_structured_output else last_assistant_content
444
463
 
@@ -608,27 +627,66 @@ class Session(BaseModel):
608
627
  return resolved[0]
609
628
 
610
629
  @classmethod
611
- def clean_small_sessions(cls, min_messages: int = 5) -> int:
612
- sessions = cls.list_sessions()
613
- deleted_count = 0
630
+ def find_sessions_by_prefix(cls, prefix: str) -> list[str]:
631
+ """Find main session IDs matching a prefix.
632
+
633
+ Args:
634
+ prefix: Session ID prefix to match.
635
+
636
+ Returns:
637
+ List of matching session IDs, sorted alphabetically.
638
+ """
639
+ prefix = (prefix or "").strip().lower()
640
+ if not prefix:
641
+ return []
642
+
614
643
  store = get_default_store()
615
- for session_meta in sessions:
616
- if session_meta.messages_count < 0:
644
+ matches: set[str] = set()
645
+
646
+ for meta_path in store.iter_meta_files():
647
+ data = _read_json_dict(meta_path)
648
+ if data is None:
617
649
  continue
618
- if session_meta.messages_count < min_messages:
619
- store.delete_session(session_meta.id)
620
- deleted_count += 1
621
- return deleted_count
650
+ # Exclude sub-agent sessions.
651
+ if data.get("sub_agent_state") is not None:
652
+ continue
653
+ sid = str(data.get("id", meta_path.parent.name)).strip()
654
+ if sid.lower().startswith(prefix):
655
+ matches.add(sid)
656
+
657
+ return sorted(matches)
622
658
 
623
659
  @classmethod
624
- def clean_all_sessions(cls) -> int:
625
- sessions = cls.list_sessions()
626
- deleted_count = 0
660
+ def shortest_unique_prefix(cls, session_id: str, min_length: int = 4) -> str:
661
+ """Find the shortest unique prefix for a session ID.
662
+
663
+ Args:
664
+ session_id: The session ID to find prefix for.
665
+ min_length: Minimum prefix length (default 4).
666
+
667
+ Returns:
668
+ The shortest prefix that uniquely identifies this session.
669
+ """
627
670
  store = get_default_store()
628
- for session_meta in sessions:
629
- store.delete_session(session_meta.id)
630
- deleted_count += 1
631
- return deleted_count
671
+ other_ids: list[str] = []
672
+
673
+ for meta_path in store.iter_meta_files():
674
+ data = _read_json_dict(meta_path)
675
+ if data is None:
676
+ continue
677
+ if data.get("sub_agent_state") is not None:
678
+ continue
679
+ sid = str(data.get("id", meta_path.parent.name)).strip()
680
+ if sid != session_id:
681
+ other_ids.append(sid.lower())
682
+
683
+ session_lower = session_id.lower()
684
+ for length in range(min_length, len(session_id) + 1):
685
+ prefix = session_lower[:length]
686
+ if not any(other.startswith(prefix) for other in other_ids):
687
+ return session_id[:length]
688
+
689
+ return session_id
632
690
 
633
691
  @classmethod
634
692
  def exports_dir(cls) -> Path:
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import json
5
- import shutil
6
5
  from collections.abc import Iterable, Sequence
7
6
  from dataclasses import dataclass
8
7
  from pathlib import Path
@@ -152,9 +151,6 @@ class JsonlSessionStore:
152
151
  return []
153
152
  return sessions_dir.glob("*/meta.json")
154
153
 
155
- def delete_session(self, session_id: str) -> None:
156
- shutil.rmtree(self._paths.session_dir(session_id), ignore_errors=True)
157
-
158
154
  async def aclose(self) -> None:
159
155
  await self._writer.aclose()
160
156
 
@@ -12,6 +12,15 @@ Remove AI-generated slop from code. Check the specified files or diff and remove
12
12
  - Extra comments that a human wouldn't add or are inconsistent with the rest of the file
13
13
  - Extra defensive checks or try/catch blocks that are abnormal for that area of the codebase (especially if called by trusted/validated codepaths)
14
14
  - Casts to `any` or `# type: ignore` to get around type issues
15
+ - Unnecessary complexity and nesting that reduces readability
16
+ - Redundant abstractions or over-engineered solutions
15
17
  - Any other style that is inconsistent with the file
16
18
 
19
+ ## Principles
20
+
21
+ 1. **Preserve functionality**: Never change what the code does - only how it does it
22
+ 2. **Prefer clarity over brevity**: Explicit readable code is better than overly compact solutions
23
+ 3. **Avoid over-simplification**: Don't create overly clever solutions that are hard to understand or debug
24
+ 4. **Focus scope**: Only refine the specified files or recently modified code, unless instructed otherwise
25
+
17
26
  Report at the end with only a 1-3 sentence summary of what you changed.
@@ -170,23 +170,3 @@ def install_system_skills() -> bool:
170
170
 
171
171
  log_debug("System skills installation complete")
172
172
  return True
173
-
174
-
175
- def get_installed_system_skills() -> list[Path]:
176
- """Get list of installed system skill directories.
177
-
178
- Returns:
179
- List of paths to installed skill directories
180
- """
181
- dest_dir = get_system_skills_dir()
182
- if not dest_dir.exists():
183
- return []
184
-
185
- skills: list[Path] = []
186
- for item in dest_dir.iterdir():
187
- if item.is_dir() and not item.name.startswith("."):
188
- skill_file = item / "SKILL.md"
189
- if skill_file.exists():
190
- skills.append(item)
191
-
192
- return skills
@@ -6,6 +6,7 @@ from typing import Literal
6
6
  from prompt_toolkit.styles import Style, merge_styles
7
7
 
8
8
  from klaude_code.protocol import commands, events, message, model
9
+ from klaude_code.session import Session
9
10
  from klaude_code.tui.input.key_bindings import copy_to_clipboard
10
11
  from klaude_code.tui.terminal.selector import DEFAULT_PICKER_STYLE, SelectItem, select_one
11
12
 
@@ -312,7 +313,8 @@ class ForkSessionCommand(CommandABC):
312
313
  new_session = agent.session.fork()
313
314
  await new_session.wait_for_flush()
314
315
 
315
- resume_cmd = f"klaude --resume {new_session.id}"
316
+ short_id = Session.shortest_unique_prefix(new_session.id)
317
+ resume_cmd = f"klaude -r {short_id}"
316
318
  copy_to_clipboard(resume_cmd)
317
319
 
318
320
  event = events.CommandOutputEvent(
@@ -345,7 +347,8 @@ class ForkSessionCommand(CommandABC):
345
347
  else:
346
348
  fork_description = "entire conversation" if selected == -1 else f"up to message index {selected}"
347
349
 
348
- resume_cmd = f"klaude --resume {new_session.id}"
350
+ short_id = Session.shortest_unique_prefix(new_session.id)
351
+ resume_cmd = f"klaude -r {short_id}"
349
352
  copy_to_clipboard(resume_cmd)
350
353
 
351
354
  event = events.CommandOutputEvent(
@@ -8,9 +8,16 @@ from klaude_code.tui.terminal.selector import DEFAULT_PICKER_STYLE, SelectItem,
8
8
  from .command_abc import Agent, CommandABC, CommandResult
9
9
 
10
10
 
11
- def select_session_sync() -> str | None:
12
- """Interactive session selection (sync version for asyncio.to_thread)."""
11
+ def select_session_sync(session_ids: list[str] | None = None) -> str | None:
12
+ """Interactive session selection (sync version for asyncio.to_thread).
13
+
14
+ Args:
15
+ session_ids: Optional list of session IDs to filter. If provided, only show these sessions.
16
+ """
13
17
  options = build_session_select_options()
18
+ if session_ids is not None:
19
+ session_id_set = set(session_ids)
20
+ options = [opt for opt in options if opt.session_id in session_id_set]
14
21
  if not options:
15
22
  log("No sessions found for this project.")
16
23
  return None
@@ -1,10 +1,10 @@
1
- """Command for changing sub-agent models."""
1
+ """Command for changing sub-agent models and compact model."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
5
  import asyncio
6
6
 
7
- from klaude_code.config.config import load_config
7
+ from klaude_code.config.config import Config, load_config
8
8
  from klaude_code.config.sub_agent_model_helper import SubAgentModelHelper, SubAgentModelInfo
9
9
  from klaude_code.protocol import commands, events, message, op
10
10
  from klaude_code.tui.terminal.selector import DEFAULT_PICKER_STYLE, SelectItem, build_model_select_items, select_one
@@ -12,16 +12,35 @@ from klaude_code.tui.terminal.selector import DEFAULT_PICKER_STYLE, SelectItem,
12
12
  from .command_abc import Agent, CommandABC, CommandResult
13
13
 
14
14
  USE_DEFAULT_BEHAVIOR = "__default__"
15
+ COMPACT_MODEL_OPTION = "__compact__"
16
+
17
+
18
+ def _build_compact_model_item(config: Config, max_name_len: int, main_model_name: str) -> SelectItem[str]:
19
+ """Build SelectItem for compact model configuration."""
20
+ name = "Compact"
21
+ model_display = config.compact_model or f"(inherit from main agent: {main_model_name})"
22
+
23
+ title = [
24
+ ("class:msg", f"{name:<{max_name_len}}"),
25
+ ("class:meta", f" current: {model_display}\n"),
26
+ ]
27
+ return SelectItem(title=title, value=COMPACT_MODEL_OPTION, search_text="compact")
15
28
 
16
29
 
17
30
  def _build_sub_agent_select_items(
18
31
  sub_agents: list[SubAgentModelInfo],
19
32
  helper: SubAgentModelHelper,
20
33
  main_model_name: str,
34
+ config: Config,
21
35
  ) -> list[SelectItem[str]]:
22
- """Build SelectItem list for sub-agent selection."""
36
+ """Build SelectItem list for sub-agent selection (including compact model)."""
23
37
  items: list[SelectItem[str]] = []
38
+ # Include "Compact" in max_name_len calculation
24
39
  max_name_len = max(len(sa.profile.name) for sa in sub_agents) if sub_agents else 0
40
+ max_name_len = max(max_name_len, len("Compact"))
41
+
42
+ # Add compact model option first
43
+ items.append(_build_compact_model_item(config, max_name_len, main_model_name))
25
44
 
26
45
  for sa in sub_agents:
27
46
  name = sa.profile.name
@@ -45,9 +64,10 @@ def _select_sub_agent_sync(
45
64
  sub_agents: list[SubAgentModelInfo],
46
65
  helper: SubAgentModelHelper,
47
66
  main_model_name: str,
67
+ config: Config,
48
68
  ) -> str | None:
49
- """Synchronous sub-agent type selection."""
50
- items = _build_sub_agent_select_items(sub_agents, helper, main_model_name)
69
+ """Synchronous sub-agent type selection (including compact model)."""
70
+ items = _build_sub_agent_select_items(sub_agents, helper, main_model_name, config)
51
71
  if not items:
52
72
  return None
53
73
 
@@ -98,8 +118,39 @@ def _select_model_for_sub_agent_sync(
98
118
  return None
99
119
 
100
120
 
121
+ def _select_model_for_compact_sync(
122
+ config: Config,
123
+ main_model_name: str,
124
+ ) -> str | None:
125
+ """Synchronous model selection for compact model."""
126
+ models = config.iter_model_entries(only_available=True, include_disabled=False)
127
+
128
+ inherit_item = SelectItem[str](
129
+ title=[
130
+ ("class:msg", "(Use default behavior)"),
131
+ ("class:meta", f" -> inherit from main agent: {main_model_name}\n"),
132
+ ],
133
+ value=USE_DEFAULT_BEHAVIOR,
134
+ search_text="default unset",
135
+ )
136
+ model_items = build_model_select_items(models)
137
+ all_items = [inherit_item, *model_items]
138
+
139
+ try:
140
+ result = select_one(
141
+ message="Select model for Compact:",
142
+ items=all_items,
143
+ pointer="→",
144
+ style=DEFAULT_PICKER_STYLE,
145
+ use_search_filter=True,
146
+ )
147
+ return result if isinstance(result, str) else None
148
+ except KeyboardInterrupt:
149
+ return None
150
+
151
+
101
152
  class SubAgentModelCommand(CommandABC):
102
- """Configure models for sub-agents (Task, Explore, WebAgent, ImageGen)."""
153
+ """Configure models for sub-agents (Task, Explore, Web, ImageGen) and compact model."""
103
154
 
104
155
  @property
105
156
  def name(self) -> commands.CommandName:
@@ -119,32 +170,48 @@ class SubAgentModelCommand(CommandABC):
119
170
  main_model_name = agent.get_llm_client().model_name
120
171
 
121
172
  sub_agents = helper.get_available_sub_agents()
122
- if not sub_agents:
173
+
174
+ selected_option = await asyncio.to_thread(_select_sub_agent_sync, sub_agents, helper, main_model_name, config)
175
+ if selected_option is None:
123
176
  return CommandResult(
124
177
  events=[
125
178
  events.CommandOutputEvent(
126
179
  session_id=agent.session.id,
127
180
  command_name=self.name,
128
- content="No sub-agents available",
129
- is_error=True,
181
+ content="(cancelled)",
130
182
  )
131
183
  ]
132
184
  )
133
185
 
134
- selected_sub_agent = await asyncio.to_thread(_select_sub_agent_sync, sub_agents, helper, main_model_name)
135
- if selected_sub_agent is None:
186
+ # Handle compact model selection
187
+ if selected_option == COMPACT_MODEL_OPTION:
188
+ selected_model = await asyncio.to_thread(_select_model_for_compact_sync, config, main_model_name)
189
+ if selected_model is None:
190
+ return CommandResult(
191
+ events=[
192
+ events.CommandOutputEvent(
193
+ session_id=agent.session.id,
194
+ command_name=self.name,
195
+ content="(cancelled)",
196
+ )
197
+ ]
198
+ )
199
+
200
+ model_name: str | None = None if selected_model == USE_DEFAULT_BEHAVIOR else selected_model
201
+
136
202
  return CommandResult(
137
- events=[
138
- events.CommandOutputEvent(
203
+ operations=[
204
+ op.ChangeCompactModelOperation(
139
205
  session_id=agent.session.id,
140
- command_name=self.name,
141
- content="(cancelled)",
206
+ model_name=model_name,
207
+ save_as_default=True,
142
208
  )
143
209
  ]
144
210
  )
145
211
 
212
+ # Handle sub-agent model selection
146
213
  selected_model = await asyncio.to_thread(
147
- _select_model_for_sub_agent_sync, helper, selected_sub_agent, main_model_name
214
+ _select_model_for_sub_agent_sync, helper, selected_option, main_model_name
148
215
  )
149
216
  if selected_model is None:
150
217
  return CommandResult(
@@ -157,13 +224,13 @@ class SubAgentModelCommand(CommandABC):
157
224
  ]
158
225
  )
159
226
 
160
- model_name: str | None = None if selected_model == USE_DEFAULT_BEHAVIOR else selected_model
227
+ model_name = None if selected_model == USE_DEFAULT_BEHAVIOR else selected_model
161
228
 
162
229
  return CommandResult(
163
230
  operations=[
164
231
  op.ChangeSubAgentModelOperation(
165
232
  session_id=agent.session.id,
166
- sub_agent_type=selected_sub_agent,
233
+ sub_agent_type=selected_option,
167
234
  model_name=model_name,
168
235
  save_as_default=True,
169
236
  )
@@ -1,28 +1,2 @@
1
- from rich.console import RenderableType
2
- from rich.padding import Padding
3
- from rich.text import Text
4
-
5
- from klaude_code.const import MARKDOWN_RIGHT_MARGIN
6
- from klaude_code.tui.components.common import create_grid
7
- from klaude_code.tui.components.rich.markdown import NoInsetMarkdown
8
- from klaude_code.tui.components.rich.theme import ThemeKey
9
-
10
1
  # UI markers
11
2
  ASSISTANT_MESSAGE_MARK = "•"
12
-
13
-
14
- def render_assistant_message(content: str, *, code_theme: str) -> RenderableType | None:
15
- """Render assistant message for replay history display.
16
-
17
- Returns None if content is empty.
18
- """
19
- stripped = content.strip()
20
- if len(stripped) == 0:
21
- return None
22
-
23
- grid = create_grid()
24
- grid.add_row(
25
- Text(ASSISTANT_MESSAGE_MARK, style=ThemeKey.ASSISTANT_MESSAGE_MARK),
26
- Padding(NoInsetMarkdown(stripped, code_theme=code_theme), (0, MARKDOWN_RIGHT_MARGIN, 0, 0)),
27
- )
28
- return grid
@@ -187,6 +187,10 @@ def highlight_bash_command(command: str) -> Text:
187
187
  expect_subcommand = False
188
188
  elif token_type in (Token.Text.Whitespace,):
189
189
  result.append(token_value)
190
+ # Newline starts a new command context (like ; or &&)
191
+ if "\n" in token_value:
192
+ expect_command = True
193
+ expect_subcommand = False
190
194
  elif token_type == Token.Name.Builtin:
191
195
  # Built-in commands are always commands
192
196
  result.append(token_value, style=ThemeKey.BASH_COMMAND)
@@ -4,6 +4,7 @@ from rich.table import Table
4
4
  from rich.text import Text
5
5
 
6
6
  from klaude_code.protocol import events, model
7
+ from klaude_code.session import Session
7
8
  from klaude_code.tui.components.common import truncate_middle
8
9
  from klaude_code.tui.components.rich.theme import ThemeKey
9
10
 
@@ -47,10 +48,11 @@ def _render_fork_session_output(e: events.CommandOutputEvent) -> RenderableType:
47
48
 
48
49
  grid = Table.grid(padding=(0, 1))
49
50
  session_id = e.ui_extra.session_id
51
+ short_id = Session.shortest_unique_prefix(session_id)
50
52
  grid.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
51
53
 
52
54
  grid.add_row(Text("Session forked. Resume command copied to clipboard:", style=ThemeKey.TOOL_RESULT))
53
- grid.add_row(Text(f" klaude --resume {session_id}", style=ThemeKey.TOOL_RESULT_BOLD))
55
+ grid.add_row(Text(f" klaude -r {short_id}", style=ThemeKey.TOOL_RESULT_BOLD))
54
56
 
55
57
  return Padding.indent(grid, level=2)
56
58
 
@@ -115,5 +115,8 @@ def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
115
115
  ),
116
116
  )
117
117
  parts.append(grid)
118
+ case model.AtFileImagesUIItem():
119
+ # Image display is handled by renderer.display_developer_message
120
+ pass
118
121
 
119
122
  return Group(*parts) if parts else Text("")