klaude-code 1.2.6__py3-none-any.whl → 1.8.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 (205) hide show
  1. klaude_code/auth/__init__.py +24 -0
  2. klaude_code/auth/codex/__init__.py +20 -0
  3. klaude_code/auth/codex/exceptions.py +17 -0
  4. klaude_code/auth/codex/jwt_utils.py +45 -0
  5. klaude_code/auth/codex/oauth.py +229 -0
  6. klaude_code/auth/codex/token_manager.py +84 -0
  7. klaude_code/cli/auth_cmd.py +73 -0
  8. klaude_code/cli/config_cmd.py +91 -0
  9. klaude_code/cli/cost_cmd.py +338 -0
  10. klaude_code/cli/debug.py +78 -0
  11. klaude_code/cli/list_model.py +307 -0
  12. klaude_code/cli/main.py +233 -134
  13. klaude_code/cli/runtime.py +309 -117
  14. klaude_code/{version.py → cli/self_update.py} +114 -5
  15. klaude_code/cli/session_cmd.py +37 -21
  16. klaude_code/command/__init__.py +88 -27
  17. klaude_code/command/clear_cmd.py +8 -7
  18. klaude_code/command/command_abc.py +31 -31
  19. klaude_code/command/debug_cmd.py +79 -0
  20. klaude_code/command/export_cmd.py +19 -53
  21. klaude_code/command/export_online_cmd.py +154 -0
  22. klaude_code/command/fork_session_cmd.py +267 -0
  23. klaude_code/command/help_cmd.py +7 -8
  24. klaude_code/command/model_cmd.py +60 -10
  25. klaude_code/command/model_select.py +84 -0
  26. klaude_code/command/prompt-jj-describe.md +32 -0
  27. klaude_code/command/prompt_command.py +19 -11
  28. klaude_code/command/refresh_cmd.py +8 -10
  29. klaude_code/command/registry.py +139 -40
  30. klaude_code/command/release_notes_cmd.py +84 -0
  31. klaude_code/command/resume_cmd.py +111 -0
  32. klaude_code/command/status_cmd.py +104 -60
  33. klaude_code/command/terminal_setup_cmd.py +7 -9
  34. klaude_code/command/thinking_cmd.py +98 -0
  35. klaude_code/config/__init__.py +14 -6
  36. klaude_code/config/assets/__init__.py +1 -0
  37. klaude_code/config/assets/builtin_config.yaml +303 -0
  38. klaude_code/config/builtin_config.py +38 -0
  39. klaude_code/config/config.py +378 -109
  40. klaude_code/config/select_model.py +117 -53
  41. klaude_code/config/thinking.py +269 -0
  42. klaude_code/{const/__init__.py → const.py} +50 -19
  43. klaude_code/core/agent.py +20 -28
  44. klaude_code/core/executor.py +327 -112
  45. klaude_code/core/manager/__init__.py +2 -4
  46. klaude_code/core/manager/llm_clients.py +1 -15
  47. klaude_code/core/manager/llm_clients_builder.py +10 -11
  48. klaude_code/core/manager/sub_agent_manager.py +37 -6
  49. klaude_code/core/prompt.py +63 -44
  50. klaude_code/core/prompts/prompt-claude-code.md +2 -13
  51. klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
  52. klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
  53. klaude_code/core/prompts/prompt-codex.md +9 -42
  54. klaude_code/core/prompts/prompt-minimal.md +12 -0
  55. klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
  56. klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
  57. klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
  58. klaude_code/core/reminders.py +283 -95
  59. klaude_code/core/task.py +113 -75
  60. klaude_code/core/tool/__init__.py +24 -31
  61. klaude_code/core/tool/file/_utils.py +36 -0
  62. klaude_code/core/tool/file/apply_patch.py +17 -25
  63. klaude_code/core/tool/file/apply_patch_tool.py +57 -77
  64. klaude_code/core/tool/file/diff_builder.py +151 -0
  65. klaude_code/core/tool/file/edit_tool.py +50 -63
  66. klaude_code/core/tool/file/move_tool.md +41 -0
  67. klaude_code/core/tool/file/move_tool.py +435 -0
  68. klaude_code/core/tool/file/read_tool.md +1 -1
  69. klaude_code/core/tool/file/read_tool.py +86 -86
  70. klaude_code/core/tool/file/write_tool.py +59 -69
  71. klaude_code/core/tool/report_back_tool.py +84 -0
  72. klaude_code/core/tool/shell/bash_tool.py +265 -22
  73. klaude_code/core/tool/shell/command_safety.py +3 -6
  74. klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
  75. klaude_code/core/tool/sub_agent_tool.py +13 -2
  76. klaude_code/core/tool/todo/todo_write_tool.md +0 -157
  77. klaude_code/core/tool/todo/todo_write_tool.py +1 -1
  78. klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
  79. klaude_code/core/tool/todo/update_plan_tool.py +1 -1
  80. klaude_code/core/tool/tool_abc.py +18 -0
  81. klaude_code/core/tool/tool_context.py +27 -12
  82. klaude_code/core/tool/tool_registry.py +7 -7
  83. klaude_code/core/tool/tool_runner.py +44 -36
  84. klaude_code/core/tool/truncation.py +29 -14
  85. klaude_code/core/tool/web/mermaid_tool.md +43 -0
  86. klaude_code/core/tool/web/mermaid_tool.py +2 -5
  87. klaude_code/core/tool/web/web_fetch_tool.md +1 -1
  88. klaude_code/core/tool/web/web_fetch_tool.py +112 -22
  89. klaude_code/core/tool/web/web_search_tool.md +23 -0
  90. klaude_code/core/tool/web/web_search_tool.py +130 -0
  91. klaude_code/core/turn.py +168 -66
  92. klaude_code/llm/__init__.py +2 -10
  93. klaude_code/llm/anthropic/client.py +190 -178
  94. klaude_code/llm/anthropic/input.py +39 -15
  95. klaude_code/llm/bedrock/__init__.py +3 -0
  96. klaude_code/llm/bedrock/client.py +60 -0
  97. klaude_code/llm/client.py +7 -21
  98. klaude_code/llm/codex/__init__.py +5 -0
  99. klaude_code/llm/codex/client.py +149 -0
  100. klaude_code/llm/google/__init__.py +3 -0
  101. klaude_code/llm/google/client.py +309 -0
  102. klaude_code/llm/google/input.py +215 -0
  103. klaude_code/llm/input_common.py +3 -9
  104. klaude_code/llm/openai_compatible/client.py +72 -164
  105. klaude_code/llm/openai_compatible/input.py +6 -4
  106. klaude_code/llm/openai_compatible/stream.py +273 -0
  107. klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
  108. klaude_code/llm/openrouter/client.py +89 -160
  109. klaude_code/llm/openrouter/input.py +18 -30
  110. klaude_code/llm/openrouter/reasoning.py +118 -0
  111. klaude_code/llm/registry.py +39 -7
  112. klaude_code/llm/responses/client.py +184 -171
  113. klaude_code/llm/responses/input.py +20 -1
  114. klaude_code/llm/usage.py +17 -12
  115. klaude_code/protocol/commands.py +17 -1
  116. klaude_code/protocol/events.py +31 -4
  117. klaude_code/protocol/llm_param.py +13 -10
  118. klaude_code/protocol/model.py +232 -29
  119. klaude_code/protocol/op.py +90 -1
  120. klaude_code/protocol/op_handler.py +35 -1
  121. klaude_code/protocol/sub_agent/__init__.py +117 -0
  122. klaude_code/protocol/sub_agent/explore.py +63 -0
  123. klaude_code/protocol/sub_agent/oracle.py +91 -0
  124. klaude_code/protocol/sub_agent/task.py +61 -0
  125. klaude_code/protocol/sub_agent/web.py +79 -0
  126. klaude_code/protocol/tools.py +4 -2
  127. klaude_code/session/__init__.py +2 -2
  128. klaude_code/session/codec.py +71 -0
  129. klaude_code/session/export.py +293 -86
  130. klaude_code/session/selector.py +89 -67
  131. klaude_code/session/session.py +320 -309
  132. klaude_code/session/store.py +220 -0
  133. klaude_code/session/templates/export_session.html +595 -83
  134. klaude_code/session/templates/mermaid_viewer.html +926 -0
  135. klaude_code/skill/__init__.py +27 -0
  136. klaude_code/skill/assets/deslop/SKILL.md +17 -0
  137. klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
  138. klaude_code/skill/assets/handoff/SKILL.md +39 -0
  139. klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
  140. klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
  141. klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
  142. klaude_code/skill/manager.py +70 -0
  143. klaude_code/skill/system_skills.py +192 -0
  144. klaude_code/trace/__init__.py +20 -2
  145. klaude_code/trace/log.py +150 -5
  146. klaude_code/ui/__init__.py +4 -9
  147. klaude_code/ui/core/input.py +1 -1
  148. klaude_code/ui/core/stage_manager.py +7 -7
  149. klaude_code/ui/modes/debug/display.py +2 -1
  150. klaude_code/ui/modes/repl/__init__.py +3 -48
  151. klaude_code/ui/modes/repl/clipboard.py +5 -5
  152. klaude_code/ui/modes/repl/completers.py +487 -123
  153. klaude_code/ui/modes/repl/display.py +5 -4
  154. klaude_code/ui/modes/repl/event_handler.py +370 -117
  155. klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
  156. klaude_code/ui/modes/repl/key_bindings.py +146 -23
  157. klaude_code/ui/modes/repl/renderer.py +189 -99
  158. klaude_code/ui/renderers/assistant.py +9 -2
  159. klaude_code/ui/renderers/bash_syntax.py +178 -0
  160. klaude_code/ui/renderers/common.py +78 -0
  161. klaude_code/ui/renderers/developer.py +104 -48
  162. klaude_code/ui/renderers/diffs.py +87 -6
  163. klaude_code/ui/renderers/errors.py +11 -6
  164. klaude_code/ui/renderers/mermaid_viewer.py +57 -0
  165. klaude_code/ui/renderers/metadata.py +112 -76
  166. klaude_code/ui/renderers/sub_agent.py +92 -7
  167. klaude_code/ui/renderers/thinking.py +40 -18
  168. klaude_code/ui/renderers/tools.py +405 -227
  169. klaude_code/ui/renderers/user_input.py +73 -13
  170. klaude_code/ui/rich/__init__.py +10 -1
  171. klaude_code/ui/rich/cjk_wrap.py +228 -0
  172. klaude_code/ui/rich/code_panel.py +131 -0
  173. klaude_code/ui/rich/live.py +17 -0
  174. klaude_code/ui/rich/markdown.py +305 -170
  175. klaude_code/ui/rich/searchable_text.py +10 -13
  176. klaude_code/ui/rich/status.py +190 -49
  177. klaude_code/ui/rich/theme.py +135 -39
  178. klaude_code/ui/terminal/__init__.py +55 -0
  179. klaude_code/ui/terminal/color.py +1 -1
  180. klaude_code/ui/terminal/control.py +13 -22
  181. klaude_code/ui/terminal/notifier.py +44 -4
  182. klaude_code/ui/terminal/selector.py +658 -0
  183. klaude_code/ui/utils/common.py +0 -18
  184. klaude_code-1.8.0.dist-info/METADATA +377 -0
  185. klaude_code-1.8.0.dist-info/RECORD +219 -0
  186. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
  187. klaude_code/command/diff_cmd.py +0 -138
  188. klaude_code/command/prompt-dev-docs-update.md +0 -56
  189. klaude_code/command/prompt-dev-docs.md +0 -46
  190. klaude_code/config/list_model.py +0 -162
  191. klaude_code/core/manager/agent_manager.py +0 -127
  192. klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
  193. klaude_code/core/tool/file/multi_edit_tool.md +0 -42
  194. klaude_code/core/tool/file/multi_edit_tool.py +0 -199
  195. klaude_code/core/tool/memory/memory_tool.md +0 -16
  196. klaude_code/core/tool/memory/memory_tool.py +0 -462
  197. klaude_code/llm/openrouter/reasoning_handler.py +0 -209
  198. klaude_code/protocol/sub_agent.py +0 -348
  199. klaude_code/ui/utils/debouncer.py +0 -42
  200. klaude_code-1.2.6.dist-info/METADATA +0 -178
  201. klaude_code-1.2.6.dist-info/RECORD +0 -167
  202. /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
  203. /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
  204. /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
  205. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,192 @@
1
+ """System skills management - install built-in skills to user directory.
2
+
3
+ This module handles extracting bundled skills from the package to ~/.klaude/skills/.system/
4
+ on application startup. It uses a fingerprint mechanism to avoid unnecessary re-extraction.
5
+ """
6
+
7
+ import hashlib
8
+ import shutil
9
+ from collections.abc import Iterator
10
+ from contextlib import contextmanager
11
+ from importlib import resources
12
+ from pathlib import Path
13
+
14
+ from klaude_code.trace import log_debug
15
+
16
+ # Marker file name for tracking installed skills version
17
+ SYSTEM_SKILLS_MARKER_FILENAME = ".klaude-system-skills.marker"
18
+
19
+ # Salt for fingerprint calculation (increment to force re-extraction)
20
+ SYSTEM_SKILLS_MARKER_SALT = "v1"
21
+
22
+
23
+ def get_system_skills_dir() -> Path:
24
+ """Get the system skills installation directory.
25
+
26
+ Returns:
27
+ Path to ~/.klaude/skills/.system/
28
+ """
29
+ return Path.home() / ".klaude" / "skills" / ".system"
30
+
31
+
32
+ def _calculate_fingerprint(assets_dir: Path) -> str:
33
+ """Calculate a fingerprint hash for the embedded skills assets.
34
+
35
+ The fingerprint is based on all file paths and their contents.
36
+
37
+ Args:
38
+ assets_dir: Path to the assets directory
39
+
40
+ Returns:
41
+ Hex string of the hash
42
+ """
43
+ hasher = hashlib.sha256()
44
+ hasher.update(SYSTEM_SKILLS_MARKER_SALT.encode())
45
+
46
+ if not assets_dir.exists():
47
+ return hasher.hexdigest()
48
+
49
+ # Sort entries for consistent ordering
50
+ for entry in sorted(assets_dir.rglob("*")):
51
+ if entry.is_file():
52
+ # Hash the relative path
53
+ rel_path = entry.relative_to(assets_dir)
54
+ hasher.update(str(rel_path).encode())
55
+ # Hash the file contents
56
+ hasher.update(entry.read_bytes())
57
+
58
+ return hasher.hexdigest()
59
+
60
+
61
+ def _read_marker(marker_path: Path) -> str | None:
62
+ """Read the fingerprint from the marker file.
63
+
64
+ Args:
65
+ marker_path: Path to the marker file
66
+
67
+ Returns:
68
+ The stored fingerprint, or None if the file doesn't exist or is invalid
69
+ """
70
+ try:
71
+ if marker_path.exists():
72
+ return marker_path.read_text(encoding="utf-8").strip()
73
+ except OSError:
74
+ pass
75
+ return None
76
+
77
+
78
+ def _write_marker(marker_path: Path, fingerprint: str) -> None:
79
+ """Write the fingerprint to the marker file.
80
+
81
+ Args:
82
+ marker_path: Path to the marker file
83
+ fingerprint: The fingerprint to store
84
+ """
85
+ marker_path.write_text(f"{fingerprint}\n", encoding="utf-8")
86
+
87
+
88
+ @contextmanager
89
+ def _with_embedded_assets_dir() -> Iterator[Path | None]:
90
+ """Resolve the embedded assets directory as a real filesystem path.
91
+
92
+ Uses `importlib.resources.as_file()` so it works for both normal installs
93
+ and zipimport-style environments.
94
+ """
95
+ try:
96
+ assets_ref = resources.files("klaude_code.skill").joinpath("assets")
97
+ with resources.as_file(assets_ref) as assets_path:
98
+ p = Path(assets_path)
99
+ yield p if p.exists() else None
100
+ return
101
+ except (TypeError, AttributeError, ImportError, FileNotFoundError, OSError):
102
+ pass
103
+
104
+ try:
105
+ module_dir = Path(__file__).parent
106
+ assets_path = module_dir / "assets"
107
+ yield assets_path if assets_path.exists() else None
108
+ except (TypeError, NameError, OSError):
109
+ yield None
110
+
111
+
112
+ def install_system_skills() -> bool:
113
+ """Install system skills from the embedded assets to the user directory.
114
+
115
+ This function:
116
+ 1. Calculates a fingerprint of the embedded assets
117
+ 2. Checks if the installed skills match (via marker file)
118
+ 3. If they don't match, clears and re-extracts the skills
119
+
120
+ Returns:
121
+ True if skills were installed/updated, False if already up-to-date
122
+ """
123
+ dest_dir = get_system_skills_dir()
124
+ marker_path = dest_dir / SYSTEM_SKILLS_MARKER_FILENAME
125
+
126
+ with _with_embedded_assets_dir() as assets_path:
127
+ if assets_path is None or not assets_path.exists():
128
+ log_debug("No embedded system skills found")
129
+ return False
130
+
131
+ # Calculate fingerprint of embedded assets
132
+ expected_fingerprint = _calculate_fingerprint(assets_path)
133
+
134
+ # Check if already installed with matching fingerprint
135
+ current_fingerprint = _read_marker(marker_path)
136
+ if current_fingerprint == expected_fingerprint and dest_dir.exists():
137
+ log_debug("System skills already up-to-date")
138
+ return False
139
+
140
+ log_debug(f"Installing system skills to {dest_dir}")
141
+
142
+ # Clear existing installation
143
+ if dest_dir.exists():
144
+ try:
145
+ shutil.rmtree(dest_dir)
146
+ except OSError as e:
147
+ log_debug(f"Failed to clear existing system skills: {e}")
148
+ return False
149
+
150
+ # Create destination directory
151
+ dest_dir.mkdir(parents=True, exist_ok=True)
152
+
153
+ # Copy all skill directories from assets
154
+ try:
155
+ for item in assets_path.iterdir():
156
+ if item.is_dir() and not item.name.startswith("."):
157
+ dest_skill_dir = dest_dir / item.name
158
+ shutil.copytree(item, dest_skill_dir)
159
+ log_debug(f"Installed system skill: {item.name}")
160
+ except OSError as e:
161
+ log_debug(f"Failed to copy system skills: {e}")
162
+ return False
163
+
164
+ # Write marker file
165
+ try:
166
+ _write_marker(marker_path, expected_fingerprint)
167
+ except OSError as e:
168
+ log_debug(f"Failed to write marker file: {e}")
169
+ # Installation succeeded, just marker failed
170
+
171
+ log_debug("System skills installation complete")
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
@@ -1,3 +1,21 @@
1
- from .log import DebugType, is_debug_enabled, log, log_debug, logger, set_debug_logging
1
+ from .log import (
2
+ DebugType,
3
+ get_current_log_file,
4
+ is_debug_enabled,
5
+ log,
6
+ log_debug,
7
+ logger,
8
+ prepare_debug_log_file,
9
+ set_debug_logging,
10
+ )
2
11
 
3
- __all__ = ["log", "log_debug", "logger", "set_debug_logging", "DebugType", "is_debug_enabled"]
12
+ __all__ = [
13
+ "DebugType",
14
+ "get_current_log_file",
15
+ "is_debug_enabled",
16
+ "log",
17
+ "log_debug",
18
+ "logger",
19
+ "prepare_debug_log_file",
20
+ "set_debug_logging",
21
+ ]
klaude_code/trace/log.py CHANGED
@@ -1,7 +1,13 @@
1
+ import gzip
1
2
  import logging
3
+ import os
4
+ import shutil
5
+ import subprocess
6
+ from collections.abc import Iterable
7
+ from datetime import datetime, timedelta
2
8
  from enum import Enum
3
9
  from logging.handlers import RotatingFileHandler
4
- from typing import Iterable
10
+ from pathlib import Path
5
11
 
6
12
  from rich.console import Console
7
13
  from rich.logging import RichHandler
@@ -49,6 +55,26 @@ _file_handler: RotatingFileHandler | None = None
49
55
  _console_handler: RichHandler | None = None
50
56
  _debug_filter: DebugTypeFilter | None = None
51
57
  _debug_enabled = False
58
+ _current_log_file: Path | None = None
59
+
60
+ LOG_RETENTION_DAYS = 3
61
+ LOG_MAX_TOTAL_BYTES = 200 * 1024 * 1024
62
+
63
+
64
+ class GzipRotatingFileHandler(RotatingFileHandler):
65
+ """Rotating file handler that gzips rolled files."""
66
+
67
+ def rotation_filename(self, default_name: str) -> str:
68
+ """Append .gz to rotation targets."""
69
+
70
+ return f"{default_name}.gz"
71
+
72
+ def rotate(self, source: str, dest: str) -> None:
73
+ """Compress the rotated file and remove the original."""
74
+
75
+ with open(source, "rb") as source_file, gzip.open(dest, "wb") as dest_file:
76
+ shutil.copyfileobj(source_file, dest_file)
77
+ Path(source).unlink(missing_ok=True)
52
78
 
53
79
 
54
80
  def set_debug_logging(
@@ -66,7 +92,7 @@ def set_debug_logging(
66
92
  log_file: Path to the log file (default: debug.log)
67
93
  filters: Set of DebugType to include; None means all types
68
94
  """
69
- global _file_handler, _console_handler, _debug_filter, _debug_enabled
95
+ global _file_handler, _console_handler, _debug_filter, _debug_enabled, _current_log_file
70
96
 
71
97
  _debug_enabled = enabled
72
98
 
@@ -80,6 +106,7 @@ def set_debug_logging(
80
106
  _console_handler = None
81
107
 
82
108
  if not enabled:
109
+ _current_log_file = None
83
110
  return
84
111
 
85
112
  # Create filter
@@ -87,10 +114,19 @@ def set_debug_logging(
87
114
 
88
115
  # Determine output mode
89
116
  use_file = write_to_file if write_to_file is not None else True
90
- file_path = log_file if log_file is not None else const.DEFAULT_DEBUG_LOG_FILE
91
-
92
117
  if use_file:
93
- _file_handler = RotatingFileHandler(
118
+ if _current_log_file is None:
119
+ _current_log_file = _resolve_log_file(log_file)
120
+ file_path = _current_log_file
121
+ else:
122
+ _current_log_file = None
123
+ file_path = None
124
+
125
+ if use_file and file_path is not None:
126
+ _prune_old_logs(const.DEFAULT_DEBUG_LOG_DIR, LOG_RETENTION_DAYS, LOG_MAX_TOTAL_BYTES)
127
+
128
+ if use_file and file_path is not None:
129
+ _file_handler = GzipRotatingFileHandler(
94
130
  file_path,
95
131
  maxBytes=const.LOG_MAX_BYTES,
96
132
  backupCount=const.LOG_BACKUP_COUNT,
@@ -166,3 +202,112 @@ def _build_message(objects: Iterable[str | tuple[str, str]]) -> str:
166
202
  def is_debug_enabled() -> bool:
167
203
  """Check if debug logging is currently enabled."""
168
204
  return _debug_enabled
205
+
206
+
207
+ def prepare_debug_log_file(log_file: str | os.PathLike[str] | None = None) -> Path:
208
+ """Prepare and remember the log file path for this session."""
209
+
210
+ global _current_log_file
211
+ _current_log_file = _resolve_log_file(log_file)
212
+ return _current_log_file
213
+
214
+
215
+ def get_current_log_file() -> Path | None:
216
+ """Return the currently active log file path, if any."""
217
+
218
+ return _current_log_file
219
+
220
+
221
+ def _resolve_log_file(log_file: str | os.PathLike[str] | None) -> Path:
222
+ """Resolve the log file path and ensure directories exist."""
223
+
224
+ if log_file:
225
+ path = Path(log_file).expanduser()
226
+ path.parent.mkdir(parents=True, exist_ok=True)
227
+ return path
228
+ else:
229
+ path = _build_default_log_file_path()
230
+
231
+ path.parent.mkdir(parents=True, exist_ok=True)
232
+ path.touch(exist_ok=True)
233
+ _refresh_latest_symlink(path)
234
+ return path
235
+
236
+
237
+ def _build_default_log_file_path() -> Path:
238
+ """Build a per-session log path under the default log directory."""
239
+
240
+ now = datetime.now()
241
+ session_dir = const.DEFAULT_DEBUG_LOG_DIR / now.strftime("%Y-%m-%d")
242
+ session_dir.mkdir(parents=True, exist_ok=True)
243
+ filename = f"{now.strftime('%H%M%S')}-{os.getpid()}.log"
244
+ return session_dir / filename
245
+
246
+
247
+ def _refresh_latest_symlink(target: Path) -> None:
248
+ """Point the debug.log symlink at the latest session file."""
249
+
250
+ latest = const.DEFAULT_DEBUG_LOG_FILE
251
+ try:
252
+ latest.unlink(missing_ok=True)
253
+ latest.symlink_to(target)
254
+ except OSError:
255
+ # Non-blocking best-effort; logging should still proceed
256
+ return
257
+
258
+
259
+ def _prune_old_logs(log_root: Path, keep_days: int, max_total_bytes: int) -> None:
260
+ """Remove logs older than keep_days or when exceeding max_total_bytes."""
261
+
262
+ if not log_root.exists():
263
+ return
264
+
265
+ cutoff = datetime.now() - timedelta(days=keep_days)
266
+ files: list[Path] = [p for p in log_root.rglob("*") if p.is_file() and not p.is_symlink()]
267
+
268
+ # Remove by age
269
+ for path in files:
270
+ try:
271
+ mtime = datetime.fromtimestamp(path.stat().st_mtime)
272
+ except OSError:
273
+ continue
274
+ if mtime < cutoff:
275
+ _trash_path(path)
276
+
277
+ # Recompute remaining files and sizes
278
+ remaining: list[tuple[Path, float, int]] = []
279
+ total_size = 0
280
+ for path in log_root.rglob("*"):
281
+ if not path.is_file() or path.is_symlink():
282
+ continue
283
+ try:
284
+ stat = path.stat()
285
+ except OSError:
286
+ continue
287
+ remaining.append((path, stat.st_mtime, stat.st_size))
288
+ total_size += stat.st_size
289
+
290
+ if total_size <= max_total_bytes:
291
+ return
292
+
293
+ remaining.sort(key=lambda item: item[1])
294
+ for path, _, size in remaining:
295
+ _trash_path(path)
296
+ total_size -= size
297
+ if total_size <= max_total_bytes:
298
+ break
299
+
300
+
301
+ def _trash_path(path: Path) -> None:
302
+ """Send a path to trash, falling back to unlink if trash is unavailable."""
303
+
304
+ try:
305
+ subprocess.run(
306
+ ["trash", str(path)],
307
+ stdin=subprocess.DEVNULL,
308
+ stdout=subprocess.DEVNULL,
309
+ stderr=subprocess.DEVNULL,
310
+ check=False,
311
+ )
312
+ except FileNotFoundError:
313
+ path.unlink(missing_ok=True)
@@ -73,19 +73,14 @@ def create_exec_display(debug: bool = False, stream_json: bool = False) -> Displ
73
73
 
74
74
 
75
75
  __all__ = [
76
- # Abstract interfaces
76
+ "DebugEventDisplay",
77
77
  "DisplayABC",
78
+ "ExecDisplay",
78
79
  "InputProviderABC",
79
- # Display mode implementations
80
+ "PromptToolkitInput",
80
81
  "REPLDisplay",
81
- "ExecDisplay",
82
82
  "StreamJsonDisplay",
83
- "DebugEventDisplay",
84
- # Input implementations
85
- "PromptToolkitInput",
86
- # Factory functions
83
+ "TerminalNotifier",
87
84
  "create_default_display",
88
85
  "create_exec_display",
89
- # Supporting types
90
- "TerminalNotifier",
91
86
  ]
@@ -68,4 +68,4 @@ class InputProviderABC(ABC):
68
68
  UserInputPayload with text and optional images.
69
69
  """
70
70
  raise NotImplementedError
71
- yield UserInputPayload(text="") # pyright: ignore[reportUnreachable]
71
+ yield UserInputPayload(text="")
@@ -1,7 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Awaitable, Callable
3
4
  from enum import Enum
4
- from typing import Awaitable, Callable
5
5
 
6
6
 
7
7
  class Stage(Enum):
@@ -19,11 +19,11 @@ class StageManager:
19
19
  self,
20
20
  *,
21
21
  finish_assistant: Callable[[], Awaitable[None]],
22
- on_enter_thinking: Callable[[], None],
22
+ finish_thinking: Callable[[], Awaitable[None]],
23
23
  ):
24
24
  self._stage = Stage.WAITING
25
25
  self._finish_assistant = finish_assistant
26
- self._on_enter_thinking = on_enter_thinking
26
+ self._finish_thinking = finish_thinking
27
27
 
28
28
  @property
29
29
  def current_stage(self) -> Stage:
@@ -39,7 +39,6 @@ class StageManager:
39
39
  if self._stage == Stage.THINKING:
40
40
  return
41
41
  await self.transition_to(Stage.THINKING)
42
- self._on_enter_thinking()
43
42
 
44
43
  async def finish_assistant(self) -> None:
45
44
  if self._stage != Stage.ASSISTANT:
@@ -49,7 +48,8 @@ class StageManager:
49
48
  self._stage = Stage.WAITING
50
49
 
51
50
  async def _leave_current_stage(self) -> None:
52
- if self._stage == Stage.ASSISTANT:
51
+ if self._stage == Stage.THINKING:
52
+ await self._finish_thinking()
53
+ elif self._stage == Stage.ASSISTANT:
53
54
  await self.finish_assistant()
54
- elif self._stage != Stage.WAITING:
55
- self._stage = Stage.WAITING
55
+ self._stage = Stage.WAITING
@@ -1,3 +1,4 @@
1
+ import os
1
2
  from typing import override
2
3
 
3
4
  from klaude_code import const
@@ -10,7 +11,7 @@ class DebugEventDisplay(DisplayABC):
10
11
  def __init__(
11
12
  self,
12
13
  wrapped_display: DisplayABC | None = None,
13
- log_file: str = const.DEFAULT_DEBUG_LOG_FILE,
14
+ log_file: str | os.PathLike[str] = const.DEFAULT_DEBUG_LOG_FILE,
14
15
  ):
15
16
  self.wrapped_display = wrapped_display
16
17
  self.log_file = log_file
@@ -1,51 +1,6 @@
1
- from __future__ import annotations
2
-
3
- from typing import TYPE_CHECKING
4
-
5
- from klaude_code.protocol import model
6
1
  from klaude_code.ui.modes.repl.input_prompt_toolkit import REPLStatusSnapshot
7
2
 
8
- if TYPE_CHECKING:
9
- from klaude_code.core.agent import Agent
10
-
11
-
12
- def build_repl_status_snapshot(agent: "Agent | None", update_message: str | None) -> REPLStatusSnapshot:
13
- """Build a status snapshot for the REPL bottom toolbar.
14
-
15
- Aggregates model name, context usage, and basic call counts from the
16
- provided agent's session history.
17
- """
18
-
19
- model_name = ""
20
- context_usage_percent: float | None = None
21
- llm_calls = 0
22
- tool_calls = 0
23
-
24
- if agent is not None:
25
- profile = agent.profile
26
- if profile is not None:
27
- model_name = profile.llm_client.model_name or ""
28
- else:
29
- model_name = "N/A"
30
-
31
- history = agent.session.conversation_history
32
- for item in history:
33
- if isinstance(item, model.AssistantMessageItem):
34
- llm_calls += 1
35
- elif isinstance(item, model.ToolCallItem):
36
- tool_calls += 1
37
-
38
- for item in reversed(history):
39
- if isinstance(item, model.ResponseMetadataItem):
40
- usage = item.usage
41
- if usage is not None and hasattr(usage, "context_usage_percent"):
42
- context_usage_percent = usage.context_usage_percent
43
- break
44
3
 
45
- return REPLStatusSnapshot(
46
- model_name=model_name,
47
- context_usage_percent=context_usage_percent,
48
- llm_calls=llm_calls,
49
- tool_calls=tool_calls,
50
- update_message=update_message,
51
- )
4
+ def build_repl_status_snapshot(update_message: str | None) -> REPLStatusSnapshot:
5
+ """Build a status snapshot for the REPL bottom toolbar."""
6
+ return REPLStatusSnapshot(update_message=update_message)
@@ -40,19 +40,19 @@ class ClipboardCaptureState:
40
40
  """Capture image from clipboard, save to disk, and return a tag like [Image #N]."""
41
41
  try:
42
42
  clipboard_data = ImageGrab.grabclipboard()
43
- except Exception:
43
+ except OSError:
44
44
  return None
45
45
  if not isinstance(clipboard_data, Image.Image):
46
46
  return None
47
47
  try:
48
48
  self._images_dir.mkdir(parents=True, exist_ok=True)
49
- except Exception:
49
+ except OSError:
50
50
  return None
51
51
  filename = f"clipboard_{uuid.uuid4().hex[:8]}.png"
52
52
  path = self._images_dir / filename
53
53
  try:
54
54
  clipboard_data.save(path, "PNG")
55
- except Exception:
55
+ except OSError:
56
56
  return None
57
57
  tag = f"[Image #{self._counter}]"
58
58
  self._counter += 1
@@ -123,7 +123,7 @@ def _encode_image_file(file_path: str) -> ImageURLPart | None:
123
123
  # Clipboard images are always saved as PNG
124
124
  data_url = f"data:image/png;base64,{encoded}"
125
125
  return ImageURLPart(image_url=ImageURLPart.ImageURL(url=data_url, id=None))
126
- except Exception:
126
+ except OSError:
127
127
  return None
128
128
 
129
129
 
@@ -148,5 +148,5 @@ def copy_to_clipboard(text: str) -> None:
148
148
  input=text.encode("utf-8"),
149
149
  check=True,
150
150
  )
151
- except Exception:
151
+ except (OSError, subprocess.SubprocessError):
152
152
  pass