klaude-code 1.2.6__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 (167) hide show
  1. klaude_code/__init__.py +0 -0
  2. klaude_code/cli/__init__.py +1 -0
  3. klaude_code/cli/main.py +298 -0
  4. klaude_code/cli/runtime.py +331 -0
  5. klaude_code/cli/session_cmd.py +80 -0
  6. klaude_code/command/__init__.py +43 -0
  7. klaude_code/command/clear_cmd.py +20 -0
  8. klaude_code/command/command_abc.py +92 -0
  9. klaude_code/command/diff_cmd.py +138 -0
  10. klaude_code/command/export_cmd.py +86 -0
  11. klaude_code/command/help_cmd.py +51 -0
  12. klaude_code/command/model_cmd.py +43 -0
  13. klaude_code/command/prompt-dev-docs-update.md +56 -0
  14. klaude_code/command/prompt-dev-docs.md +46 -0
  15. klaude_code/command/prompt-init.md +45 -0
  16. klaude_code/command/prompt_command.py +69 -0
  17. klaude_code/command/refresh_cmd.py +43 -0
  18. klaude_code/command/registry.py +110 -0
  19. klaude_code/command/status_cmd.py +111 -0
  20. klaude_code/command/terminal_setup_cmd.py +252 -0
  21. klaude_code/config/__init__.py +11 -0
  22. klaude_code/config/config.py +177 -0
  23. klaude_code/config/list_model.py +162 -0
  24. klaude_code/config/select_model.py +67 -0
  25. klaude_code/const/__init__.py +133 -0
  26. klaude_code/core/__init__.py +0 -0
  27. klaude_code/core/agent.py +165 -0
  28. klaude_code/core/executor.py +485 -0
  29. klaude_code/core/manager/__init__.py +19 -0
  30. klaude_code/core/manager/agent_manager.py +127 -0
  31. klaude_code/core/manager/llm_clients.py +42 -0
  32. klaude_code/core/manager/llm_clients_builder.py +49 -0
  33. klaude_code/core/manager/sub_agent_manager.py +86 -0
  34. klaude_code/core/prompt.py +89 -0
  35. klaude_code/core/prompts/prompt-claude-code.md +98 -0
  36. klaude_code/core/prompts/prompt-codex.md +331 -0
  37. klaude_code/core/prompts/prompt-gemini.md +43 -0
  38. klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
  39. klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
  40. klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
  41. klaude_code/core/prompts/prompt-subagent.md +8 -0
  42. klaude_code/core/reminders.py +445 -0
  43. klaude_code/core/task.py +237 -0
  44. klaude_code/core/tool/__init__.py +75 -0
  45. klaude_code/core/tool/file/__init__.py +0 -0
  46. klaude_code/core/tool/file/apply_patch.py +492 -0
  47. klaude_code/core/tool/file/apply_patch_tool.md +1 -0
  48. klaude_code/core/tool/file/apply_patch_tool.py +204 -0
  49. klaude_code/core/tool/file/edit_tool.md +9 -0
  50. klaude_code/core/tool/file/edit_tool.py +274 -0
  51. klaude_code/core/tool/file/multi_edit_tool.md +42 -0
  52. klaude_code/core/tool/file/multi_edit_tool.py +199 -0
  53. klaude_code/core/tool/file/read_tool.md +14 -0
  54. klaude_code/core/tool/file/read_tool.py +326 -0
  55. klaude_code/core/tool/file/write_tool.md +8 -0
  56. klaude_code/core/tool/file/write_tool.py +146 -0
  57. klaude_code/core/tool/memory/__init__.py +0 -0
  58. klaude_code/core/tool/memory/memory_tool.md +16 -0
  59. klaude_code/core/tool/memory/memory_tool.py +462 -0
  60. klaude_code/core/tool/memory/skill_loader.py +245 -0
  61. klaude_code/core/tool/memory/skill_tool.md +24 -0
  62. klaude_code/core/tool/memory/skill_tool.py +97 -0
  63. klaude_code/core/tool/shell/__init__.py +0 -0
  64. klaude_code/core/tool/shell/bash_tool.md +43 -0
  65. klaude_code/core/tool/shell/bash_tool.py +123 -0
  66. klaude_code/core/tool/shell/command_safety.py +363 -0
  67. klaude_code/core/tool/sub_agent_tool.py +83 -0
  68. klaude_code/core/tool/todo/__init__.py +0 -0
  69. klaude_code/core/tool/todo/todo_write_tool.md +182 -0
  70. klaude_code/core/tool/todo/todo_write_tool.py +121 -0
  71. klaude_code/core/tool/todo/update_plan_tool.md +3 -0
  72. klaude_code/core/tool/todo/update_plan_tool.py +104 -0
  73. klaude_code/core/tool/tool_abc.py +25 -0
  74. klaude_code/core/tool/tool_context.py +106 -0
  75. klaude_code/core/tool/tool_registry.py +78 -0
  76. klaude_code/core/tool/tool_runner.py +252 -0
  77. klaude_code/core/tool/truncation.py +170 -0
  78. klaude_code/core/tool/web/__init__.py +0 -0
  79. klaude_code/core/tool/web/mermaid_tool.md +21 -0
  80. klaude_code/core/tool/web/mermaid_tool.py +76 -0
  81. klaude_code/core/tool/web/web_fetch_tool.md +8 -0
  82. klaude_code/core/tool/web/web_fetch_tool.py +159 -0
  83. klaude_code/core/turn.py +220 -0
  84. klaude_code/llm/__init__.py +21 -0
  85. klaude_code/llm/anthropic/__init__.py +3 -0
  86. klaude_code/llm/anthropic/client.py +221 -0
  87. klaude_code/llm/anthropic/input.py +200 -0
  88. klaude_code/llm/client.py +49 -0
  89. klaude_code/llm/input_common.py +239 -0
  90. klaude_code/llm/openai_compatible/__init__.py +3 -0
  91. klaude_code/llm/openai_compatible/client.py +211 -0
  92. klaude_code/llm/openai_compatible/input.py +109 -0
  93. klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
  94. klaude_code/llm/openrouter/__init__.py +3 -0
  95. klaude_code/llm/openrouter/client.py +200 -0
  96. klaude_code/llm/openrouter/input.py +160 -0
  97. klaude_code/llm/openrouter/reasoning_handler.py +209 -0
  98. klaude_code/llm/registry.py +22 -0
  99. klaude_code/llm/responses/__init__.py +3 -0
  100. klaude_code/llm/responses/client.py +216 -0
  101. klaude_code/llm/responses/input.py +167 -0
  102. klaude_code/llm/usage.py +109 -0
  103. klaude_code/protocol/__init__.py +4 -0
  104. klaude_code/protocol/commands.py +21 -0
  105. klaude_code/protocol/events.py +163 -0
  106. klaude_code/protocol/llm_param.py +147 -0
  107. klaude_code/protocol/model.py +287 -0
  108. klaude_code/protocol/op.py +89 -0
  109. klaude_code/protocol/op_handler.py +28 -0
  110. klaude_code/protocol/sub_agent.py +348 -0
  111. klaude_code/protocol/tools.py +15 -0
  112. klaude_code/session/__init__.py +4 -0
  113. klaude_code/session/export.py +624 -0
  114. klaude_code/session/selector.py +76 -0
  115. klaude_code/session/session.py +474 -0
  116. klaude_code/session/templates/export_session.html +1434 -0
  117. klaude_code/trace/__init__.py +3 -0
  118. klaude_code/trace/log.py +168 -0
  119. klaude_code/ui/__init__.py +91 -0
  120. klaude_code/ui/core/__init__.py +1 -0
  121. klaude_code/ui/core/display.py +103 -0
  122. klaude_code/ui/core/input.py +71 -0
  123. klaude_code/ui/core/stage_manager.py +55 -0
  124. klaude_code/ui/modes/__init__.py +1 -0
  125. klaude_code/ui/modes/debug/__init__.py +1 -0
  126. klaude_code/ui/modes/debug/display.py +36 -0
  127. klaude_code/ui/modes/exec/__init__.py +1 -0
  128. klaude_code/ui/modes/exec/display.py +63 -0
  129. klaude_code/ui/modes/repl/__init__.py +51 -0
  130. klaude_code/ui/modes/repl/clipboard.py +152 -0
  131. klaude_code/ui/modes/repl/completers.py +429 -0
  132. klaude_code/ui/modes/repl/display.py +60 -0
  133. klaude_code/ui/modes/repl/event_handler.py +375 -0
  134. klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
  135. klaude_code/ui/modes/repl/key_bindings.py +170 -0
  136. klaude_code/ui/modes/repl/renderer.py +281 -0
  137. klaude_code/ui/renderers/__init__.py +0 -0
  138. klaude_code/ui/renderers/assistant.py +21 -0
  139. klaude_code/ui/renderers/common.py +8 -0
  140. klaude_code/ui/renderers/developer.py +158 -0
  141. klaude_code/ui/renderers/diffs.py +215 -0
  142. klaude_code/ui/renderers/errors.py +16 -0
  143. klaude_code/ui/renderers/metadata.py +190 -0
  144. klaude_code/ui/renderers/sub_agent.py +71 -0
  145. klaude_code/ui/renderers/thinking.py +39 -0
  146. klaude_code/ui/renderers/tools.py +551 -0
  147. klaude_code/ui/renderers/user_input.py +65 -0
  148. klaude_code/ui/rich/__init__.py +1 -0
  149. klaude_code/ui/rich/live.py +65 -0
  150. klaude_code/ui/rich/markdown.py +308 -0
  151. klaude_code/ui/rich/quote.py +34 -0
  152. klaude_code/ui/rich/searchable_text.py +71 -0
  153. klaude_code/ui/rich/status.py +240 -0
  154. klaude_code/ui/rich/theme.py +274 -0
  155. klaude_code/ui/terminal/__init__.py +1 -0
  156. klaude_code/ui/terminal/color.py +244 -0
  157. klaude_code/ui/terminal/control.py +147 -0
  158. klaude_code/ui/terminal/notifier.py +107 -0
  159. klaude_code/ui/terminal/progress_bar.py +87 -0
  160. klaude_code/ui/utils/__init__.py +1 -0
  161. klaude_code/ui/utils/common.py +108 -0
  162. klaude_code/ui/utils/debouncer.py +42 -0
  163. klaude_code/version.py +163 -0
  164. klaude_code-1.2.6.dist-info/METADATA +178 -0
  165. klaude_code-1.2.6.dist-info/RECORD +167 -0
  166. klaude_code-1.2.6.dist-info/WHEEL +4 -0
  167. klaude_code-1.2.6.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,274 @@
1
+ from dataclasses import dataclass
2
+ from enum import Enum
3
+
4
+ from rich.style import Style
5
+ from rich.theme import Theme
6
+
7
+
8
+ @dataclass
9
+ class Palette:
10
+ red: str
11
+ yellow: str
12
+ green: str
13
+ cyan: str
14
+ blue: str
15
+ orange: str
16
+ magenta: str
17
+ grey_blue: str
18
+ grey1: str
19
+ grey2: str
20
+ grey3: str
21
+ grey_green: str
22
+ purple: str
23
+ diff_add: str
24
+ diff_remove: str
25
+ code_theme: str
26
+ text_background: str
27
+
28
+
29
+ LIGHT_PALETTE = Palette(
30
+ red="red",
31
+ yellow="yellow",
32
+ green="spring_green4",
33
+ cyan="cyan",
34
+ blue="#3678b7",
35
+ orange="#d77757",
36
+ magenta="magenta",
37
+ grey_blue="steel_blue",
38
+ grey1="#667e90",
39
+ grey2="#93a4b1",
40
+ grey3="#c4ced4",
41
+ grey_green="#96a696",
42
+ purple="slate_blue3",
43
+ diff_add="#2e5a32 on #e8f5e9",
44
+ diff_remove="#5a2e32 on #ffebee",
45
+ code_theme="ansi_light",
46
+ text_background="#f0f0f0",
47
+ )
48
+
49
+ DARK_PALETTE = Palette(
50
+ red="indian_red",
51
+ yellow="yellow",
52
+ green="sea_green3",
53
+ cyan="cyan",
54
+ blue="deep_sky_blue1",
55
+ orange="#e6704e",
56
+ magenta="magenta",
57
+ grey_blue="steel_blue",
58
+ grey1="#99aabb",
59
+ grey2="#778899",
60
+ grey3="#646464",
61
+ grey_green="#6d8672",
62
+ purple="#afbafe",
63
+ diff_add="#c8e6c9 on #2e4a32",
64
+ diff_remove="#ffcdd2 on #4a2e32",
65
+ code_theme="ansi_dark",
66
+ text_background="#2f3440",
67
+ )
68
+
69
+
70
+ class ThemeKey(str, Enum):
71
+ LINES = "lines"
72
+ # DIFF
73
+ DIFF_FILE_NAME = "diff.file_name"
74
+ DIFF_REMOVE = "diff.remove"
75
+ DIFF_ADD = "diff.add"
76
+ DIFF_STATS_ADD = "diff.stats.add"
77
+ DIFF_STATS_REMOVE = "diff.stats.remove"
78
+ # ERROR
79
+ ERROR = "error"
80
+ ERROR_BOLD = "error.bold"
81
+ INTERRUPT = "interrupt"
82
+ # METADATA
83
+ METADATA = "metadata"
84
+ METADATA_DIM = "metadata.dim"
85
+ METADATA_BOLD = "metadata.bold"
86
+ # SPINNER_STATUS
87
+ SPINNER_STATUS = "spinner.status"
88
+ SPINNER_STATUS_TEXT = "spinner.status.text"
89
+ # STATUS
90
+ STATUS_HINT = "status.hint"
91
+ # USER_INPUT
92
+ USER_INPUT = "user.input"
93
+ USER_INPUT_PROMPT = "user.input.prompt"
94
+ USER_INPUT_AT_PATTERN = "user.at_pattern"
95
+ USER_INPUT_SLASH_COMMAND = "user.slash_command"
96
+ # REMINDER
97
+ REMINDER = "reminder"
98
+ REMINDER_BOLD = "reminder.bold"
99
+ # TOOL
100
+ INVALID_TOOL_CALL_ARGS = "tool.invalid_tool_call_args"
101
+ TOOL_NAME = "tool.name"
102
+ TOOL_PARAM_FILE_PATH = "tool.param.file_path"
103
+ TOOL_PARAM = "tool.param"
104
+ TOOL_PARAM_BOLD = "tool.param.bold"
105
+ TOOL_RESULT = "tool.result"
106
+ TOOL_RESULT_BOLD = "tool.result.bold"
107
+ TOOL_MARK = "tool.mark"
108
+ TOOL_APPROVED = "tool.approved"
109
+ TOOL_REJECTED = "tool.rejected"
110
+ # THINKING
111
+ THINKING = "thinking"
112
+ THINKING_BOLD = "thinking.bold"
113
+ # TODO_ITEM
114
+ TODO_EXPLANATION = "todo.explanation"
115
+ TODO_PENDING_MARK = "todo.pending.mark"
116
+ TODO_COMPLETED_MARK = "todo.completed.mark"
117
+ TODO_IN_PROGRESS_MARK = "todo.in_progress.mark"
118
+ TODO_NEW_COMPLETED_MARK = "todo.new_completed.mark"
119
+ TODO_PENDING = "todo.pending"
120
+ TODO_COMPLETED = "todo.completed"
121
+ TODO_IN_PROGRESS = "todo.in_progress"
122
+ TODO_NEW_COMPLETED = "todo.new_completed"
123
+ # WELCOME
124
+ WELCOME_HIGHLIGHT_BOLD = "welcome.highlight.bold"
125
+ WELCOME_HIGHLIGHT = "welcome.highlight"
126
+ WELCOME_INFO = "welcome.info"
127
+ # WELCOME DEBUG
128
+ WELCOME_DEBUG_TITLE = "welcome.debug.title"
129
+ WELCOME_DEBUG_BORDER = "welcome.debug.border"
130
+ # RESUME
131
+ RESUME_FLAG = "resume.flag"
132
+ RESUME_INFO = "resume.info"
133
+ # CONFIGURATION DISPLAY
134
+ CONFIG_TABLE_HEADER = "config.table.header"
135
+ CONFIG_STATUS_OK = "config.status.ok"
136
+ CONFIG_STATUS_PRIMARY = "config.status.primary"
137
+ CONFIG_ITEM_NAME = "config.item.name"
138
+ CONFIG_PARAM_LABEL = "config.param.label"
139
+ CONFIG_PANEL_BORDER = "config.panel.border"
140
+
141
+ def __str__(self) -> str:
142
+ return self.value
143
+
144
+
145
+ @dataclass
146
+ class Themes:
147
+ app_theme: Theme
148
+ markdown_theme: Theme
149
+ thinking_markdown_theme: Theme
150
+ code_theme: str
151
+ sub_agent_colors: list[Style]
152
+
153
+
154
+ def get_theme(theme: str | None = None) -> Themes:
155
+ if theme == "light":
156
+ palette = LIGHT_PALETTE
157
+ else:
158
+ palette = DARK_PALETTE
159
+ return Themes(
160
+ app_theme=Theme(
161
+ styles={
162
+ ThemeKey.LINES.value: palette.grey3,
163
+ # DIFF
164
+ ThemeKey.DIFF_FILE_NAME.value: palette.blue,
165
+ ThemeKey.DIFF_REMOVE.value: palette.diff_remove,
166
+ ThemeKey.DIFF_ADD.value: palette.diff_add,
167
+ ThemeKey.DIFF_STATS_ADD.value: palette.green,
168
+ ThemeKey.DIFF_STATS_REMOVE.value: palette.red,
169
+ # ERROR
170
+ ThemeKey.ERROR.value: palette.red,
171
+ ThemeKey.ERROR_BOLD.value: "bold " + palette.red,
172
+ ThemeKey.INTERRUPT.value: "reverse bold " + palette.red,
173
+ # USER_INPUT
174
+ ThemeKey.USER_INPUT.value: palette.magenta,
175
+ ThemeKey.USER_INPUT_PROMPT.value: palette.magenta,
176
+ ThemeKey.USER_INPUT_AT_PATTERN.value: palette.purple,
177
+ ThemeKey.USER_INPUT_SLASH_COMMAND.value: "bold reverse " + palette.blue,
178
+ # METADATA
179
+ ThemeKey.METADATA.value: palette.grey_blue,
180
+ ThemeKey.METADATA_DIM.value: "dim " + palette.grey_blue,
181
+ ThemeKey.METADATA_BOLD.value: "bold " + palette.grey_blue,
182
+ # SPINNER_STATUS
183
+ ThemeKey.SPINNER_STATUS.value: palette.blue,
184
+ ThemeKey.SPINNER_STATUS_TEXT.value: palette.blue,
185
+ # STATUS
186
+ ThemeKey.STATUS_HINT.value: palette.grey2,
187
+ # REMINDER
188
+ ThemeKey.REMINDER.value: palette.grey1,
189
+ ThemeKey.REMINDER_BOLD.value: "bold " + palette.grey1,
190
+ # TOOL
191
+ ThemeKey.INVALID_TOOL_CALL_ARGS.value: palette.yellow,
192
+ ThemeKey.TOOL_NAME.value: "bold",
193
+ ThemeKey.TOOL_PARAM_FILE_PATH.value: palette.green,
194
+ ThemeKey.TOOL_PARAM.value: palette.green,
195
+ ThemeKey.TOOL_PARAM_BOLD.value: "bold " + palette.green,
196
+ ThemeKey.TOOL_RESULT.value: palette.grey_green,
197
+ ThemeKey.TOOL_RESULT_BOLD.value: "bold " + palette.grey_green,
198
+ ThemeKey.TOOL_MARK.value: "bold",
199
+ ThemeKey.TOOL_APPROVED.value: palette.green + " bold reverse",
200
+ ThemeKey.TOOL_REJECTED.value: palette.red + " bold reverse",
201
+ # THINKING
202
+ ThemeKey.THINKING.value: "italic " + palette.grey2,
203
+ ThemeKey.THINKING_BOLD.value: "bold italic " + palette.grey1,
204
+ # TODO_ITEM
205
+ ThemeKey.TODO_EXPLANATION.value: palette.grey1 + " italic",
206
+ ThemeKey.TODO_PENDING_MARK.value: "bold " + palette.grey1,
207
+ ThemeKey.TODO_COMPLETED_MARK.value: "bold " + palette.grey3,
208
+ ThemeKey.TODO_IN_PROGRESS_MARK.value: "bold " + palette.blue,
209
+ ThemeKey.TODO_NEW_COMPLETED_MARK.value: "bold " + palette.green,
210
+ ThemeKey.TODO_PENDING.value: palette.grey1,
211
+ ThemeKey.TODO_COMPLETED.value: palette.grey3 + " strike",
212
+ ThemeKey.TODO_IN_PROGRESS.value: "bold " + palette.blue,
213
+ ThemeKey.TODO_NEW_COMPLETED.value: "bold strike " + palette.green,
214
+ # WELCOME
215
+ ThemeKey.WELCOME_HIGHLIGHT_BOLD.value: "bold",
216
+ ThemeKey.WELCOME_HIGHLIGHT.value: palette.blue,
217
+ ThemeKey.WELCOME_INFO.value: palette.grey1,
218
+ # WELCOME DEBUG
219
+ ThemeKey.WELCOME_DEBUG_TITLE.value: "bold " + palette.red,
220
+ ThemeKey.WELCOME_DEBUG_BORDER.value: palette.red,
221
+ # RESUME
222
+ ThemeKey.RESUME_FLAG.value: "bold reverse " + palette.green,
223
+ ThemeKey.RESUME_INFO.value: palette.green,
224
+ # CONFIGURATION DISPLAY
225
+ ThemeKey.CONFIG_TABLE_HEADER.value: palette.green,
226
+ ThemeKey.CONFIG_STATUS_OK.value: palette.green,
227
+ ThemeKey.CONFIG_STATUS_PRIMARY.value: palette.yellow,
228
+ ThemeKey.CONFIG_ITEM_NAME.value: palette.cyan,
229
+ ThemeKey.CONFIG_PARAM_LABEL.value: palette.grey1,
230
+ ThemeKey.CONFIG_PANEL_BORDER.value: palette.grey3,
231
+ }
232
+ ),
233
+ markdown_theme=Theme(
234
+ styles={
235
+ "markdown.code": palette.purple,
236
+ "markdown.code.panel": palette.grey3,
237
+ "markdown.h1": "bold reverse",
238
+ "markdown.h1.border": palette.grey3,
239
+ "markdown.h2.border": palette.grey3,
240
+ "markdown.h3": "bold " + palette.grey1,
241
+ "markdown.h4": "bold " + palette.grey2,
242
+ "markdown.hr": palette.grey3,
243
+ "markdown.item.bullet": palette.grey2,
244
+ "markdown.item.number": palette.grey2,
245
+ }
246
+ ),
247
+ thinking_markdown_theme=Theme(
248
+ styles={
249
+ "markdown.code": palette.grey1 + " on " + palette.text_background,
250
+ "markdown.code.panel": palette.grey3,
251
+ "markdown.h1": "bold reverse",
252
+ "markdown.h1.border": palette.grey3,
253
+ "markdown.h2.border": palette.grey3,
254
+ "markdown.h3": "bold " + palette.grey1,
255
+ "markdown.h4": "bold " + palette.grey2,
256
+ "markdown.hr": palette.grey3,
257
+ "markdown.item.bullet": palette.grey2,
258
+ "markdown.item.number": palette.grey2,
259
+ "markdown.strong": "bold italic " + palette.grey1,
260
+ }
261
+ ),
262
+ code_theme=palette.code_theme,
263
+ sub_agent_colors=[
264
+ Style(color=palette.cyan),
265
+ Style(color=palette.green),
266
+ Style(color=palette.blue),
267
+ Style(color=palette.purple),
268
+ Style(color=palette.orange),
269
+ Style(color=palette.grey_blue),
270
+ Style(color=palette.red),
271
+ Style(color=palette.grey1),
272
+ Style(color=palette.yellow),
273
+ ],
274
+ )
@@ -0,0 +1 @@
1
+ # Terminal utilities
@@ -0,0 +1,244 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import re
5
+ import select
6
+ import sys
7
+ import termios
8
+ import time
9
+ import tty
10
+ from typing import BinaryIO, Final
11
+
12
+ from klaude_code.trace import DebugType, log_debug
13
+
14
+ ST: Final[bytes] = b"\x1b\\" # ESC \
15
+ BEL: Final[int] = 7
16
+
17
+ # Match OSC 11 response like: ESC ] 11 ; <payload> BEL/ST
18
+ _OSC_BG_REGEX = re.compile(r"\x1b]11;([^\x07\x1b\\]*)")
19
+
20
+ # Cache for the last successfully detected terminal background RGB.
21
+ _last_bg_rgb: tuple[int, int, int] | None = None
22
+
23
+
24
+ def is_light_terminal_background(timeout: float = 0.5) -> bool | None:
25
+ """Detect whether the current terminal background is light.
26
+
27
+ Returns True for light background, False for dark, and None if detection fails.
28
+ """
29
+
30
+ rgb = _query_color_slot(slot=11, timeout=timeout)
31
+ if rgb is None:
32
+ return None
33
+
34
+ global _last_bg_rgb
35
+ _last_bg_rgb = rgb
36
+
37
+ r, g, b = rgb
38
+ # Same luminance formula as codex-rs: 0.299*r + 0.587*g + 0.114*b > 128.0
39
+ y = 0.299 * float(r) + 0.587 * float(g) + 0.114 * float(b)
40
+ return y > 128.0
41
+
42
+
43
+ def get_last_terminal_background_rgb() -> tuple[int, int, int] | None:
44
+ """Return the last detected terminal background RGB, if available.
45
+
46
+ The value is populated as a side effect of ``is_light_terminal_background``
47
+ (which queries OSC 11). If detection has not run or failed, this returns
48
+ None.
49
+ """
50
+
51
+ return _last_bg_rgb
52
+
53
+
54
+ def _query_color_slot(slot: int, timeout: float) -> tuple[int, int, int] | None:
55
+ """Query an OSC color slot (10=fg, 11=bg) and return RGB if possible.
56
+
57
+ This sends OSC `ESC ] slot ; ? ESC \\` to the controlling TTY and then
58
+ reads back the response directly from `/dev/tty`, consuming the bytes so
59
+ they do not leak into the next shell prompt.
60
+ """
61
+
62
+ if sys.platform == "win32":
63
+ return None
64
+
65
+ term = os.getenv("TERM", "").lower()
66
+ if term in {"", "dumb"}:
67
+ return None
68
+
69
+ try:
70
+ with open("/dev/tty", "r+b", buffering=0) as tty_fp:
71
+ fd = tty_fp.fileno()
72
+ if not os.isatty(fd):
73
+ return None
74
+
75
+ try:
76
+ old_attrs = termios.tcgetattr(fd)
77
+ except Exception as exc: # termios.error and others
78
+ log_debug(
79
+ f"Failed to get termios attributes for /dev/tty: {exc}",
80
+ debug_type=DebugType.TERMINAL,
81
+ )
82
+ old_attrs = None
83
+
84
+ try:
85
+ if old_attrs is not None:
86
+ # Put tty into cbreak mode so we can read the OSC response bytes immediately.
87
+ tty.setcbreak(fd)
88
+
89
+ _send_osc_query(tty_fp, slot)
90
+ raw = _read_osc_response(fd, timeout=timeout)
91
+ finally:
92
+ if old_attrs is not None:
93
+ try:
94
+ termios.tcsetattr(fd, termios.TCSADRAIN, old_attrs)
95
+ except Exception as exc: # best-effort restore
96
+ log_debug(
97
+ f"Failed to restore termios attributes for /dev/tty: {exc}",
98
+ debug_type=DebugType.TERMINAL,
99
+ )
100
+
101
+ except OSError as exc:
102
+ log_debug(
103
+ f"Failed to open /dev/tty for OSC color query: {exc}",
104
+ debug_type=DebugType.TERMINAL,
105
+ )
106
+ return None
107
+
108
+ if raw is None or not raw:
109
+ return None
110
+
111
+ return _parse_osc_color_response(raw)
112
+
113
+
114
+ def _send_osc_query(tty_fp: BinaryIO, slot: int) -> None:
115
+ """Send OSC color query for the given slot to the TTY."""
116
+
117
+ seq = f"\x1b]{slot};?\x1b\\".encode("ascii", errors="ignore")
118
+ try:
119
+ tty_fp.write(seq)
120
+ tty_fp.flush()
121
+ except Exception as exc:
122
+ log_debug(
123
+ f"Failed to write OSC color query to /dev/tty: {exc}",
124
+ debug_type=DebugType.TERMINAL,
125
+ )
126
+
127
+
128
+ def _read_osc_response(fd: int, timeout: float) -> bytes | None:
129
+ """Read a single OSC response terminated by BEL or ST from the TTY.
130
+
131
+ The bytes are consumed from `/dev/tty` so that the terminal's reply does
132
+ not become visible as part of the next shell prompt.
133
+ """
134
+
135
+ deadline = time.monotonic() + max(timeout, 0.0)
136
+ buf = bytearray()
137
+
138
+ while True:
139
+ remaining = deadline - time.monotonic()
140
+ if remaining <= 0:
141
+ break
142
+
143
+ readable, _, _ = select.select([fd], [], [], remaining)
144
+ if not readable:
145
+ continue
146
+
147
+ try:
148
+ chunk = os.read(fd, 1024)
149
+ except Exception as exc:
150
+ log_debug(
151
+ f"Failed to read OSC color response from /dev/tty: {exc}",
152
+ debug_type=DebugType.TERMINAL,
153
+ )
154
+ break
155
+
156
+ if not chunk:
157
+ break
158
+
159
+ buf.extend(chunk)
160
+
161
+ # BEL terminator
162
+ if BEL in buf:
163
+ idx = buf.index(BEL)
164
+ return bytes(buf[: idx + 1])
165
+
166
+ # ST terminator (ESC \), may span chunks so search the whole buffer
167
+ st_index = buf.find(ST)
168
+ if st_index != -1:
169
+ return bytes(buf[: st_index + len(ST)])
170
+
171
+ if buf:
172
+ return bytes(buf)
173
+ return None
174
+
175
+
176
+ def _parse_osc_color_response(data: bytes) -> tuple[int, int, int] | None:
177
+ """Extract an RGB triple from an OSC 11 response payload.
178
+
179
+ Supports typical xterm-style responses like `ESC ] 11 ; rgb:rrrr/gggg/bbbb BEL` or
180
+ `ESC ] 11 ; #rrggbb BEL`.
181
+ """
182
+
183
+ try:
184
+ text = data.decode("ascii", errors="ignore")
185
+ except Exception:
186
+ return None
187
+
188
+ match = _OSC_BG_REGEX.search(text)
189
+ if not match:
190
+ return None
191
+
192
+ payload = match.group(1).strip()
193
+ # In case the terminal adds extra metadata separated by ';', only use the first field.
194
+ payload = payload.split(";", 1)[0].strip()
195
+
196
+ rgb = _parse_rgb_spec(payload)
197
+ return rgb
198
+
199
+
200
+ def _parse_rgb_spec(spec: str) -> tuple[int, int, int] | None:
201
+ """Parse a color specification like `rgb:rrrr/gggg/bbbb` or `#rrggbb`."""
202
+
203
+ spec = spec.strip()
204
+
205
+ # xterm-style rgb:rrrr/gggg/bbbb where each component is 1-4 hex digits
206
+ if spec.lower().startswith("rgb:"):
207
+ body = spec[4:]
208
+ parts = body.split("/")
209
+ if len(parts) != 3:
210
+ return None
211
+ try:
212
+ r = _scale_hex_component(parts[0])
213
+ g = _scale_hex_component(parts[1])
214
+ b = _scale_hex_component(parts[2])
215
+ except ValueError:
216
+ return None
217
+ return r, g, b
218
+
219
+ # Simple #rrggbb response
220
+ if spec.startswith("#") and len(spec) == 7:
221
+ try:
222
+ r = int(spec[1:3], 16)
223
+ g = int(spec[3:5], 16)
224
+ b = int(spec[5:7], 16)
225
+ except ValueError:
226
+ return None
227
+ return r, g, b
228
+
229
+ return None
230
+
231
+
232
+ def _scale_hex_component(component: str) -> int:
233
+ """Scale 1-4 digit hex component to 0-255 range."""
234
+
235
+ if not component:
236
+ raise ValueError("empty component")
237
+
238
+ value = int(component, 16)
239
+ max_value = (16 ** len(component)) - 1
240
+ if max_value <= 0:
241
+ raise ValueError("invalid component width")
242
+
243
+ scaled = round((value / float(max_value)) * 255.0)
244
+ return max(0, min(255, int(scaled)))
@@ -0,0 +1,147 @@
1
+ import asyncio
2
+ import os
3
+ import select
4
+ import signal
5
+ import sys
6
+ import termios
7
+ import threading
8
+ import time
9
+ import tty
10
+ from collections.abc import Callable, Coroutine
11
+ from types import FrameType
12
+ from typing import Any
13
+
14
+ from klaude_code.trace import log
15
+
16
+
17
+ def start_esc_interrupt_monitor(
18
+ on_interrupt: Callable[[], Coroutine[Any, Any, None]],
19
+ ) -> tuple[threading.Event, asyncio.Task[None]]:
20
+ """Start a background monitor that triggers a callback on bare ESC.
21
+
22
+ This utility watches stdin for a *single* ESC key press (not part of an escape
23
+ sequence like arrow keys). When detected, it schedules the provided
24
+ ``on_interrupt`` coroutine on the current event loop.
25
+
26
+ Returns a tuple of ``(stop_event, esc_task)``:
27
+ - ``stop_event`` can be set to request the monitor to stop.
28
+ - ``esc_task`` is the asyncio task running the monitor thread; callers should
29
+ ``await`` it during shutdown to restore TTY state safely.
30
+
31
+ If stdin is not a TTY or the platform does not support ``termios`` semantics,
32
+ a no-op task is returned so callers can use the same shutdown code path.
33
+ """
34
+
35
+ stop_event = threading.Event()
36
+ loop = asyncio.get_running_loop()
37
+
38
+ # Fallback for non-interactive or non-POSIX environments.
39
+ if not sys.stdin.isatty() or os.name != "posix":
40
+
41
+ async def _noop() -> None: # type: ignore[return-type]
42
+ return None
43
+
44
+ return stop_event, asyncio.create_task(_noop())
45
+
46
+ def _esc_monitor(stop: threading.Event) -> None:
47
+ try:
48
+ fd = sys.stdin.fileno()
49
+ old = termios.tcgetattr(fd)
50
+ except Exception as exc: # pragma: no cover - environment dependent
51
+ log((f"esc monitor init error: {exc}", "r red"))
52
+ return
53
+
54
+ try:
55
+ tty.setcbreak(fd)
56
+ while not stop.is_set():
57
+ rlist, _, _ = select.select([sys.stdin], [], [], 0.05)
58
+ if not rlist:
59
+ continue
60
+ try:
61
+ ch = os.read(fd, 1).decode(errors="ignore")
62
+ except Exception:
63
+ continue
64
+ if ch != "\x1b":
65
+ continue
66
+
67
+ # Peek following characters to distinguish bare ESC from sequences.
68
+ seq = ""
69
+ r2, _, _ = select.select([sys.stdin], [], [], 0.005)
70
+ while r2:
71
+ try:
72
+ seq += os.read(fd, 1).decode(errors="ignore")
73
+ except Exception:
74
+ break
75
+ r2, _, _ = select.select([sys.stdin], [], [], 0.0)
76
+
77
+ if seq == "":
78
+ try:
79
+ asyncio.run_coroutine_threadsafe(on_interrupt(), loop)
80
+ except Exception:
81
+ # Best-effort only; failures here should not crash the UI.
82
+ pass
83
+ stop.set()
84
+ except Exception as exc: # pragma: no cover - environment dependent
85
+ log((f"esc monitor error: {exc}", "r red"))
86
+ finally:
87
+ try:
88
+ termios.tcsetattr(fd, termios.TCSADRAIN, old) # type: ignore[name-defined]
89
+ except Exception:
90
+ pass
91
+
92
+ esc_task: asyncio.Task[None] = asyncio.create_task(asyncio.to_thread(_esc_monitor, stop_event))
93
+ return stop_event, esc_task
94
+
95
+
96
+ def install_sigint_double_press_exit(
97
+ show_toast: Callable[[], None],
98
+ hide_progress: Callable[[], None],
99
+ *,
100
+ window_seconds: float = 2.0,
101
+ ) -> Callable[[], None]:
102
+ """Install a SIGINT handler that requires a double press to exit.
103
+
104
+ Behavior:
105
+ - First Ctrl+C within ``window_seconds``: calls ``show_toast`` to inform the
106
+ user that a second press will exit.
107
+ - Second Ctrl+C within the time window: calls ``hide_progress`` and raises
108
+ ``KeyboardInterrupt`` to unwind the current asyncio loop.
109
+
110
+ Returns a ``restore()`` function that should be called during shutdown to
111
+ restore the original SIGINT handler.
112
+ """
113
+
114
+ last_sigint_time: float = 0.0
115
+ original_handler = signal.getsignal(signal.SIGINT)
116
+
117
+ def _handler(signum: int, frame: FrameType | None) -> None:
118
+ nonlocal last_sigint_time
119
+ now = time.monotonic()
120
+ if now - last_sigint_time <= window_seconds:
121
+ # Second press within window: hide progress UI and exit.
122
+ try:
123
+ hide_progress()
124
+ except Exception:
125
+ pass
126
+ raise KeyboardInterrupt
127
+
128
+ # First press: remember timestamp and show toast.
129
+ last_sigint_time = now
130
+ try:
131
+ show_toast()
132
+ except Exception:
133
+ pass
134
+
135
+ try:
136
+ signal.signal(signal.SIGINT, _handler)
137
+ except Exception: # pragma: no cover - platform dependent
138
+ # If installing the handler fails, restore() will be a no-op.
139
+ return lambda: None
140
+
141
+ def restore() -> None:
142
+ try:
143
+ signal.signal(signal.SIGINT, original_handler)
144
+ except Exception:
145
+ pass
146
+
147
+ return restore