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.
- klaude_code/__init__.py +0 -0
- klaude_code/cli/__init__.py +1 -0
- klaude_code/cli/main.py +298 -0
- klaude_code/cli/runtime.py +331 -0
- klaude_code/cli/session_cmd.py +80 -0
- klaude_code/command/__init__.py +43 -0
- klaude_code/command/clear_cmd.py +20 -0
- klaude_code/command/command_abc.py +92 -0
- klaude_code/command/diff_cmd.py +138 -0
- klaude_code/command/export_cmd.py +86 -0
- klaude_code/command/help_cmd.py +51 -0
- klaude_code/command/model_cmd.py +43 -0
- klaude_code/command/prompt-dev-docs-update.md +56 -0
- klaude_code/command/prompt-dev-docs.md +46 -0
- klaude_code/command/prompt-init.md +45 -0
- klaude_code/command/prompt_command.py +69 -0
- klaude_code/command/refresh_cmd.py +43 -0
- klaude_code/command/registry.py +110 -0
- klaude_code/command/status_cmd.py +111 -0
- klaude_code/command/terminal_setup_cmd.py +252 -0
- klaude_code/config/__init__.py +11 -0
- klaude_code/config/config.py +177 -0
- klaude_code/config/list_model.py +162 -0
- klaude_code/config/select_model.py +67 -0
- klaude_code/const/__init__.py +133 -0
- klaude_code/core/__init__.py +0 -0
- klaude_code/core/agent.py +165 -0
- klaude_code/core/executor.py +485 -0
- klaude_code/core/manager/__init__.py +19 -0
- klaude_code/core/manager/agent_manager.py +127 -0
- klaude_code/core/manager/llm_clients.py +42 -0
- klaude_code/core/manager/llm_clients_builder.py +49 -0
- klaude_code/core/manager/sub_agent_manager.py +86 -0
- klaude_code/core/prompt.py +89 -0
- klaude_code/core/prompts/prompt-claude-code.md +98 -0
- klaude_code/core/prompts/prompt-codex.md +331 -0
- klaude_code/core/prompts/prompt-gemini.md +43 -0
- klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
- klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
- klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
- klaude_code/core/prompts/prompt-subagent.md +8 -0
- klaude_code/core/reminders.py +445 -0
- klaude_code/core/task.py +237 -0
- klaude_code/core/tool/__init__.py +75 -0
- klaude_code/core/tool/file/__init__.py +0 -0
- klaude_code/core/tool/file/apply_patch.py +492 -0
- klaude_code/core/tool/file/apply_patch_tool.md +1 -0
- klaude_code/core/tool/file/apply_patch_tool.py +204 -0
- klaude_code/core/tool/file/edit_tool.md +9 -0
- klaude_code/core/tool/file/edit_tool.py +274 -0
- klaude_code/core/tool/file/multi_edit_tool.md +42 -0
- klaude_code/core/tool/file/multi_edit_tool.py +199 -0
- klaude_code/core/tool/file/read_tool.md +14 -0
- klaude_code/core/tool/file/read_tool.py +326 -0
- klaude_code/core/tool/file/write_tool.md +8 -0
- klaude_code/core/tool/file/write_tool.py +146 -0
- klaude_code/core/tool/memory/__init__.py +0 -0
- klaude_code/core/tool/memory/memory_tool.md +16 -0
- klaude_code/core/tool/memory/memory_tool.py +462 -0
- klaude_code/core/tool/memory/skill_loader.py +245 -0
- klaude_code/core/tool/memory/skill_tool.md +24 -0
- klaude_code/core/tool/memory/skill_tool.py +97 -0
- klaude_code/core/tool/shell/__init__.py +0 -0
- klaude_code/core/tool/shell/bash_tool.md +43 -0
- klaude_code/core/tool/shell/bash_tool.py +123 -0
- klaude_code/core/tool/shell/command_safety.py +363 -0
- klaude_code/core/tool/sub_agent_tool.py +83 -0
- klaude_code/core/tool/todo/__init__.py +0 -0
- klaude_code/core/tool/todo/todo_write_tool.md +182 -0
- klaude_code/core/tool/todo/todo_write_tool.py +121 -0
- klaude_code/core/tool/todo/update_plan_tool.md +3 -0
- klaude_code/core/tool/todo/update_plan_tool.py +104 -0
- klaude_code/core/tool/tool_abc.py +25 -0
- klaude_code/core/tool/tool_context.py +106 -0
- klaude_code/core/tool/tool_registry.py +78 -0
- klaude_code/core/tool/tool_runner.py +252 -0
- klaude_code/core/tool/truncation.py +170 -0
- klaude_code/core/tool/web/__init__.py +0 -0
- klaude_code/core/tool/web/mermaid_tool.md +21 -0
- klaude_code/core/tool/web/mermaid_tool.py +76 -0
- klaude_code/core/tool/web/web_fetch_tool.md +8 -0
- klaude_code/core/tool/web/web_fetch_tool.py +159 -0
- klaude_code/core/turn.py +220 -0
- klaude_code/llm/__init__.py +21 -0
- klaude_code/llm/anthropic/__init__.py +3 -0
- klaude_code/llm/anthropic/client.py +221 -0
- klaude_code/llm/anthropic/input.py +200 -0
- klaude_code/llm/client.py +49 -0
- klaude_code/llm/input_common.py +239 -0
- klaude_code/llm/openai_compatible/__init__.py +3 -0
- klaude_code/llm/openai_compatible/client.py +211 -0
- klaude_code/llm/openai_compatible/input.py +109 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
- klaude_code/llm/openrouter/__init__.py +3 -0
- klaude_code/llm/openrouter/client.py +200 -0
- klaude_code/llm/openrouter/input.py +160 -0
- klaude_code/llm/openrouter/reasoning_handler.py +209 -0
- klaude_code/llm/registry.py +22 -0
- klaude_code/llm/responses/__init__.py +3 -0
- klaude_code/llm/responses/client.py +216 -0
- klaude_code/llm/responses/input.py +167 -0
- klaude_code/llm/usage.py +109 -0
- klaude_code/protocol/__init__.py +4 -0
- klaude_code/protocol/commands.py +21 -0
- klaude_code/protocol/events.py +163 -0
- klaude_code/protocol/llm_param.py +147 -0
- klaude_code/protocol/model.py +287 -0
- klaude_code/protocol/op.py +89 -0
- klaude_code/protocol/op_handler.py +28 -0
- klaude_code/protocol/sub_agent.py +348 -0
- klaude_code/protocol/tools.py +15 -0
- klaude_code/session/__init__.py +4 -0
- klaude_code/session/export.py +624 -0
- klaude_code/session/selector.py +76 -0
- klaude_code/session/session.py +474 -0
- klaude_code/session/templates/export_session.html +1434 -0
- klaude_code/trace/__init__.py +3 -0
- klaude_code/trace/log.py +168 -0
- klaude_code/ui/__init__.py +91 -0
- klaude_code/ui/core/__init__.py +1 -0
- klaude_code/ui/core/display.py +103 -0
- klaude_code/ui/core/input.py +71 -0
- klaude_code/ui/core/stage_manager.py +55 -0
- klaude_code/ui/modes/__init__.py +1 -0
- klaude_code/ui/modes/debug/__init__.py +1 -0
- klaude_code/ui/modes/debug/display.py +36 -0
- klaude_code/ui/modes/exec/__init__.py +1 -0
- klaude_code/ui/modes/exec/display.py +63 -0
- klaude_code/ui/modes/repl/__init__.py +51 -0
- klaude_code/ui/modes/repl/clipboard.py +152 -0
- klaude_code/ui/modes/repl/completers.py +429 -0
- klaude_code/ui/modes/repl/display.py +60 -0
- klaude_code/ui/modes/repl/event_handler.py +375 -0
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
- klaude_code/ui/modes/repl/key_bindings.py +170 -0
- klaude_code/ui/modes/repl/renderer.py +281 -0
- klaude_code/ui/renderers/__init__.py +0 -0
- klaude_code/ui/renderers/assistant.py +21 -0
- klaude_code/ui/renderers/common.py +8 -0
- klaude_code/ui/renderers/developer.py +158 -0
- klaude_code/ui/renderers/diffs.py +215 -0
- klaude_code/ui/renderers/errors.py +16 -0
- klaude_code/ui/renderers/metadata.py +190 -0
- klaude_code/ui/renderers/sub_agent.py +71 -0
- klaude_code/ui/renderers/thinking.py +39 -0
- klaude_code/ui/renderers/tools.py +551 -0
- klaude_code/ui/renderers/user_input.py +65 -0
- klaude_code/ui/rich/__init__.py +1 -0
- klaude_code/ui/rich/live.py +65 -0
- klaude_code/ui/rich/markdown.py +308 -0
- klaude_code/ui/rich/quote.py +34 -0
- klaude_code/ui/rich/searchable_text.py +71 -0
- klaude_code/ui/rich/status.py +240 -0
- klaude_code/ui/rich/theme.py +274 -0
- klaude_code/ui/terminal/__init__.py +1 -0
- klaude_code/ui/terminal/color.py +244 -0
- klaude_code/ui/terminal/control.py +147 -0
- klaude_code/ui/terminal/notifier.py +107 -0
- klaude_code/ui/terminal/progress_bar.py +87 -0
- klaude_code/ui/utils/__init__.py +1 -0
- klaude_code/ui/utils/common.py +108 -0
- klaude_code/ui/utils/debouncer.py +42 -0
- klaude_code/version.py +163 -0
- klaude_code-1.2.6.dist-info/METADATA +178 -0
- klaude_code-1.2.6.dist-info/RECORD +167 -0
- klaude_code-1.2.6.dist-info/WHEEL +4 -0
- 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
|