klaude-code 2.8.0__py3-none-any.whl → 2.9.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 (100) hide show
  1. klaude_code/app/runtime.py +2 -1
  2. klaude_code/auth/antigravity/oauth.py +0 -9
  3. klaude_code/auth/antigravity/token_manager.py +0 -18
  4. klaude_code/auth/base.py +53 -0
  5. klaude_code/auth/codex/exceptions.py +0 -4
  6. klaude_code/auth/codex/oauth.py +32 -28
  7. klaude_code/auth/codex/token_manager.py +0 -18
  8. klaude_code/cli/cost_cmd.py +128 -39
  9. klaude_code/cli/list_model.py +27 -10
  10. klaude_code/cli/main.py +15 -4
  11. klaude_code/config/assets/builtin_config.yaml +8 -24
  12. klaude_code/config/config.py +47 -25
  13. klaude_code/config/sub_agent_model_helper.py +18 -13
  14. klaude_code/config/thinking.py +0 -8
  15. klaude_code/const.py +2 -2
  16. klaude_code/core/agent_profile.py +11 -53
  17. klaude_code/core/compaction/compaction.py +4 -6
  18. klaude_code/core/compaction/overflow.py +0 -4
  19. klaude_code/core/executor.py +51 -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 +0 -4
  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/edit_tool.py +1 -2
  27. klaude_code/core/tool/file/read_tool.md +3 -2
  28. klaude_code/core/tool/file/read_tool.py +15 -2
  29. klaude_code/core/tool/offload.py +0 -35
  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/codex/client.py +22 -0
  40. klaude_code/llm/codex/prompt_sync.py +237 -0
  41. klaude_code/llm/google/client.py +8 -6
  42. klaude_code/llm/google/input.py +20 -12
  43. klaude_code/llm/image.py +18 -11
  44. klaude_code/llm/input_common.py +14 -6
  45. klaude_code/llm/json_stable.py +37 -0
  46. klaude_code/llm/openai_compatible/input.py +0 -10
  47. klaude_code/llm/openai_compatible/stream.py +16 -1
  48. klaude_code/llm/registry.py +0 -5
  49. klaude_code/llm/responses/input.py +15 -5
  50. klaude_code/llm/usage.py +0 -8
  51. klaude_code/protocol/commands.py +1 -0
  52. klaude_code/protocol/events.py +2 -1
  53. klaude_code/protocol/message.py +2 -2
  54. klaude_code/protocol/model.py +20 -1
  55. klaude_code/protocol/op.py +27 -0
  56. klaude_code/protocol/op_handler.py +10 -0
  57. klaude_code/protocol/sub_agent/AGENTS.md +5 -5
  58. klaude_code/protocol/sub_agent/__init__.py +13 -34
  59. klaude_code/protocol/sub_agent/explore.py +7 -34
  60. klaude_code/protocol/sub_agent/image_gen.py +3 -74
  61. klaude_code/protocol/sub_agent/task.py +3 -47
  62. klaude_code/protocol/sub_agent/web.py +8 -52
  63. klaude_code/protocol/tools.py +2 -0
  64. klaude_code/session/export.py +308 -299
  65. klaude_code/session/session.py +58 -21
  66. klaude_code/session/store.py +0 -4
  67. klaude_code/session/templates/export_session.html +430 -134
  68. klaude_code/skill/assets/deslop/SKILL.md +9 -0
  69. klaude_code/skill/system_skills.py +0 -20
  70. klaude_code/tui/command/__init__.py +3 -0
  71. klaude_code/tui/command/continue_cmd.py +34 -0
  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/command_output.py +3 -1
  77. klaude_code/tui/components/developer.py +3 -0
  78. klaude_code/tui/components/diffs.py +2 -208
  79. klaude_code/tui/components/errors.py +4 -0
  80. klaude_code/tui/components/mermaid_viewer.py +2 -2
  81. klaude_code/tui/components/rich/markdown.py +60 -63
  82. klaude_code/tui/components/rich/theme.py +2 -0
  83. klaude_code/tui/components/sub_agent.py +2 -46
  84. klaude_code/tui/components/thinking.py +0 -33
  85. klaude_code/tui/components/tools.py +43 -21
  86. klaude_code/tui/input/images.py +21 -18
  87. klaude_code/tui/input/key_bindings.py +2 -2
  88. klaude_code/tui/input/prompt_toolkit.py +49 -49
  89. klaude_code/tui/machine.py +15 -11
  90. klaude_code/tui/renderer.py +12 -20
  91. klaude_code/tui/runner.py +2 -1
  92. klaude_code/tui/terminal/image.py +6 -34
  93. klaude_code/ui/common.py +0 -70
  94. {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/METADATA +3 -6
  95. {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/RECORD +97 -92
  96. klaude_code/core/tool/sub_agent_tool.py +0 -126
  97. klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -108
  98. klaude_code/tui/components/rich/searchable_text.py +0 -68
  99. {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/WHEEL +0 -0
  100. {klaude_code-2.8.0.dist-info → klaude_code-2.9.0.dist-info}/entry_points.txt +0 -0
@@ -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
@@ -31,6 +31,7 @@ def ensure_commands_loaded() -> None:
31
31
  # Import and register commands in display order
32
32
  from .clear_cmd import ClearCommand
33
33
  from .compact_cmd import CompactCommand
34
+ from .continue_cmd import ContinueCommand
34
35
  from .copy_cmd import CopyCommand
35
36
  from .debug_cmd import DebugCommand
36
37
  from .export_cmd import ExportCommand
@@ -47,6 +48,7 @@ def ensure_commands_loaded() -> None:
47
48
  register(CopyCommand())
48
49
  register(ExportCommand())
49
50
  register(CompactCommand())
51
+ register(ContinueCommand())
50
52
  register(RefreshTerminalCommand())
51
53
  register(ModelCommand())
52
54
  register(SubAgentModelCommand())
@@ -67,6 +69,7 @@ def __getattr__(name: str) -> object:
67
69
  _commands_map = {
68
70
  "ClearCommand": "clear_cmd",
69
71
  "CompactCommand": "compact_cmd",
72
+ "ContinueCommand": "continue_cmd",
70
73
  "CopyCommand": "copy_cmd",
71
74
  "DebugCommand": "debug_cmd",
72
75
  "ExportCommand": "export_cmd",
@@ -0,0 +1,34 @@
1
+ from klaude_code.protocol import commands, events, message, op
2
+
3
+ from .command_abc import Agent, CommandABC, CommandResult
4
+
5
+
6
+ class ContinueCommand(CommandABC):
7
+ """Continue agent execution without adding a new user message."""
8
+
9
+ @property
10
+ def name(self) -> commands.CommandName:
11
+ return commands.CommandName.CONTINUE
12
+
13
+ @property
14
+ def summary(self) -> str:
15
+ return "Continue agent execution (for recovery after interruptions)"
16
+
17
+ async def run(self, agent: Agent, user_input: message.UserInputPayload) -> CommandResult:
18
+ del user_input # unused
19
+
20
+ if agent.session.messages_count == 0:
21
+ return CommandResult(
22
+ events=[
23
+ events.CommandOutputEvent(
24
+ session_id=agent.session.id,
25
+ command_name=self.name,
26
+ content="Cannot continue: no conversation history. Start a conversation first.",
27
+ is_error=True,
28
+ )
29
+ ]
30
+ )
31
+
32
+ return CommandResult(
33
+ operations=[op.ContinueAgentOperation(session_id=agent.session.id)],
34
+ )
@@ -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
@@ -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("")
@@ -1,185 +1,12 @@
1
- from rich import box
2
- from rich.console import Group, RenderableType
3
- from rich.padding import Padding
4
- from rich.panel import Panel
1
+ from rich.console import RenderableType
5
2
  from rich.text import Text
6
3
 
7
- from klaude_code.const import DIFF_PREFIX_WIDTH, MAX_DIFF_LINES
4
+ from klaude_code.const import DIFF_PREFIX_WIDTH
8
5
  from klaude_code.protocol import model
9
6
  from klaude_code.tui.components.common import create_grid
10
7
  from klaude_code.tui.components.rich.theme import ThemeKey
11
8
 
12
9
 
13
- def _make_diff_prefix(line: str, new_ln: int | None, width: int) -> tuple[str, int | None]:
14
- kind = line[0]
15
-
16
- number = " " * width
17
- if kind in {"+", " "} and new_ln is not None:
18
- number = f"{new_ln:>{width}}"
19
- new_ln += 1
20
-
21
- if kind == "-":
22
- marker = "-"
23
- elif kind == "+":
24
- marker = "+"
25
- else:
26
- marker = " "
27
-
28
- prefix = f"{number} {marker}"
29
- return prefix, new_ln
30
-
31
-
32
- def render_diff(diff_text: str, show_file_name: bool = False) -> RenderableType:
33
- if diff_text == "":
34
- return Text("")
35
-
36
- lines = diff_text.split("\n")
37
- grid = create_grid()
38
- grid.padding = (0, 0)
39
-
40
- # Track line numbers based on hunk headers
41
- new_ln: int | None = None
42
- # Track if we're in untracked files section
43
- in_untracked_section = False
44
- # Track whether we've already rendered a file header
45
- has_rendered_file_header = False
46
- # Track whether we have rendered actual diff content for the current file
47
- has_rendered_diff_content = False
48
- # Track the "from" file name from --- line (used for deleted files)
49
- from_file_name: str | None = None
50
-
51
- for i, line in enumerate(lines):
52
- # Check for untracked files section header
53
- if line == "git ls-files --others --exclude-standard":
54
- in_untracked_section = True
55
- grid.add_row("", "")
56
- grid.add_row("", Text("Untracked files:", style=ThemeKey.TOOL_MARK))
57
- grid.add_row("", "")
58
- continue
59
-
60
- # Handle untracked files
61
- if in_untracked_section:
62
- # If we hit a new section or empty line, we're done with untracked files
63
- if line.startswith("diff --git") or line.strip() == "":
64
- in_untracked_section = False
65
- elif line.strip(): # Non-empty line in untracked section
66
- file_text = Text(line.strip(), style=ThemeKey.TOOL_PARAM_BOLD)
67
- grid.add_row(
68
- Text(f"{'+':>{DIFF_PREFIX_WIDTH}}", style=ThemeKey.TOOL_PARAM_BOLD),
69
- file_text,
70
- )
71
- continue
72
-
73
- # Capture "from" file name from --- line (needed for deleted files)
74
- if line.startswith("--- "):
75
- raw = line[4:].strip()
76
- if raw != "/dev/null":
77
- from_file_name = raw[2:] if raw.startswith(("a/", "b/")) else raw
78
- continue
79
-
80
- # Parse file name from diff headers
81
- if show_file_name and line.startswith("+++ "):
82
- # Extract file name from +++ header with proper handling of /dev/null
83
- raw = line[4:].strip()
84
- if raw == "/dev/null":
85
- # File was deleted, use the "from" file name
86
- file_name = from_file_name or raw
87
- elif raw.startswith(("a/", "b/")):
88
- file_name = raw[2:]
89
- else:
90
- file_name = raw
91
-
92
- file_text = Text(file_name, style=ThemeKey.DIFF_FILE_NAME)
93
-
94
- # Count actual +/- lines for this file from i+1 onwards
95
- file_additions = 0
96
- file_deletions = 0
97
- for remaining_line in lines[i + 1 :]:
98
- if remaining_line.startswith("diff --git"):
99
- break
100
- elif remaining_line.startswith("+") and not remaining_line.startswith("+++"):
101
- file_additions += 1
102
- elif remaining_line.startswith("-") and not remaining_line.startswith("---"):
103
- file_deletions += 1
104
-
105
- # Create stats text
106
- stats_text = Text()
107
- if file_additions > 0:
108
- stats_text.append(f"+{file_additions}", style=ThemeKey.DIFF_STATS_ADD)
109
- if file_deletions > 0:
110
- if file_additions > 0:
111
- stats_text.append(" ")
112
- stats_text.append(f"-{file_deletions}", style=ThemeKey.DIFF_STATS_REMOVE)
113
-
114
- # Combine file name and stats
115
- file_line = Text(style=ThemeKey.DIFF_FILE_NAME)
116
- file_line.append_text(file_text)
117
- if stats_text.plain:
118
- file_line.append(" (")
119
- file_line.append_text(stats_text)
120
- file_line.append(")")
121
-
122
- if has_rendered_file_header:
123
- grid.add_row("", "")
124
-
125
- if file_additions > 0 and file_deletions == 0:
126
- file_mark = "+"
127
- elif file_deletions > 0 and file_additions == 0:
128
- file_mark = "-"
129
- else:
130
- file_mark = "±"
131
-
132
- grid.add_row(
133
- Text(f"{file_mark:>{DIFF_PREFIX_WIDTH}} ", style=ThemeKey.DIFF_FILE_NAME),
134
- file_line,
135
- )
136
- has_rendered_file_header = True
137
- has_rendered_diff_content = False
138
- continue
139
-
140
- if line.startswith("diff --git"):
141
- has_rendered_diff_content = False
142
- continue
143
-
144
- # Parse hunk headers to reset counters: @@ -l,s +l,s @@
145
- if line.startswith("@@"):
146
- try:
147
- parts = line.split()
148
- plus = parts[2] # like '+12,4'
149
- new_start = int(plus[1:].split(",")[0])
150
- new_ln = new_start
151
- except (IndexError, ValueError):
152
- new_ln = None
153
- if has_rendered_diff_content:
154
- grid.add_row(Text(f"{'⋮':>{DIFF_PREFIX_WIDTH}}", style=ThemeKey.TOOL_RESULT), "")
155
- continue
156
-
157
- # Skip +++ lines (already handled above)
158
- if line.startswith("+++ "):
159
- continue
160
-
161
- # Only handle unified diff hunk lines; ignore other metadata like
162
- # "diff --git" or "index …" which would otherwise skew counters.
163
- if not line or line[:1] not in {" ", "+", "-"}:
164
- continue
165
-
166
- # Compute line number prefix and style diff content
167
- prefix, new_ln = _make_diff_prefix(line, new_ln, DIFF_PREFIX_WIDTH)
168
-
169
- if line.startswith("-"):
170
- text = Text(line[1:])
171
- text.stylize(ThemeKey.DIFF_REMOVE)
172
- elif line.startswith("+"):
173
- text = Text(line[1:])
174
- text.stylize(ThemeKey.DIFF_ADD)
175
- else:
176
- text = Text(line, style=ThemeKey.TOOL_RESULT)
177
- grid.add_row(Text(prefix, ThemeKey.TOOL_RESULT), text)
178
- has_rendered_diff_content = True
179
-
180
- return grid
181
-
182
-
183
10
  def render_structured_diff(ui_extra: model.DiffUIExtra, show_file_name: bool = False) -> RenderableType:
184
11
  files = ui_extra.files
185
12
  if not files:
@@ -204,39 +31,6 @@ def render_structured_diff(ui_extra: model.DiffUIExtra, show_file_name: bool = F
204
31
  return grid
205
32
 
206
33
 
207
- def render_diff_panel(
208
- diff_text: str,
209
- *,
210
- show_file_name: bool = True,
211
- heading: str = "DIFF",
212
- indent: int = 2,
213
- ) -> RenderableType:
214
- lines = diff_text.splitlines()
215
- truncated_notice: Text | None = None
216
- if len(lines) > MAX_DIFF_LINES:
217
- truncated_lines = len(lines) - MAX_DIFF_LINES
218
- diff_text = "\n".join(lines[:MAX_DIFF_LINES])
219
- truncated_notice = Text(f"… truncated {truncated_lines} lines", style=ThemeKey.TOOL_MARK)
220
-
221
- diff_body = render_diff(diff_text, show_file_name=show_file_name)
222
- renderables: list[RenderableType] = [
223
- Text(f" {heading} ", style="bold reverse"),
224
- diff_body,
225
- ]
226
- if truncated_notice is not None:
227
- renderables.extend([Text(""), truncated_notice])
228
-
229
- panel = Panel.fit(
230
- Group(*renderables),
231
- border_style=ThemeKey.LINES,
232
- title_align="center",
233
- box=box.ROUNDED,
234
- )
235
- if indent <= 0:
236
- return panel
237
- return Padding.indent(panel, level=indent)
238
-
239
-
240
34
  def _render_file_header(file_diff: model.DiffFileDiff) -> tuple[Text, Text]:
241
35
  file_text = Text(file_diff.file_path, style=ThemeKey.DIFF_FILE_NAME)
242
36
  stats_text = Text()
@@ -9,6 +9,8 @@ def render_error(error_msg: Text) -> RenderableType:
9
9
  """Render error with X mark for error events."""
10
10
  grid = create_grid()
11
11
  error_msg.style = ThemeKey.ERROR
12
+ error_msg.overflow = "ellipsis"
13
+ error_msg.no_wrap = True
12
14
  grid.add_row(Text("✘", style=ThemeKey.ERROR_BOLD), error_msg)
13
15
  return grid
14
16
 
@@ -17,5 +19,7 @@ def render_tool_error(error_msg: Text) -> RenderableType:
17
19
  """Render error with indent for tool results."""
18
20
  grid = create_grid()
19
21
  error_msg.style = ThemeKey.ERROR
22
+ error_msg.overflow = "ellipsis"
23
+ error_msg.no_wrap = True
20
24
  grid.add_row(Text(" "), error_msg)
21
25
  return grid
@@ -16,7 +16,7 @@ _MERMAID_DEFAULT_PNG_SCALE = 2
16
16
 
17
17
 
18
18
  def artifacts_dir() -> Path:
19
- return Path(TOOL_OUTPUT_TRUNCATION_DIR) / "mermaid"
19
+ return Path(TOOL_OUTPUT_TRUNCATION_DIR)
20
20
 
21
21
 
22
22
  def _extract_pako_from_link(link: str) -> str | None:
@@ -72,7 +72,7 @@ def ensure_viewer_file(*, code: str, link: str, tool_call_id: str) -> Path | Non
72
72
  return None
73
73
 
74
74
  safe_id = tool_call_id.replace("/", "_")
75
- path = artifacts_dir() / f"mermaid-viewer-{safe_id}.html"
75
+ path = artifacts_dir() / f"klaude-mermaid-{safe_id}.html"
76
76
  if path.exists():
77
77
  return path
78
78