klaude-code 2.0.1__py3-none-any.whl → 2.1.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 (160) hide show
  1. klaude_code/app/__init__.py +12 -0
  2. klaude_code/app/runtime.py +215 -0
  3. klaude_code/cli/auth_cmd.py +2 -2
  4. klaude_code/cli/config_cmd.py +2 -2
  5. klaude_code/cli/cost_cmd.py +1 -1
  6. klaude_code/cli/debug.py +12 -36
  7. klaude_code/cli/list_model.py +3 -3
  8. klaude_code/cli/main.py +17 -60
  9. klaude_code/cli/self_update.py +2 -187
  10. klaude_code/cli/session_cmd.py +2 -2
  11. klaude_code/config/config.py +1 -1
  12. klaude_code/config/select_model.py +1 -1
  13. klaude_code/const.py +10 -1
  14. klaude_code/core/agent.py +9 -62
  15. klaude_code/core/agent_profile.py +284 -0
  16. klaude_code/core/executor.py +343 -230
  17. klaude_code/core/manager/llm_clients_builder.py +1 -1
  18. klaude_code/core/manager/sub_agent_manager.py +16 -29
  19. klaude_code/core/reminders.py +107 -155
  20. klaude_code/core/task.py +12 -20
  21. klaude_code/core/tool/__init__.py +5 -19
  22. klaude_code/core/tool/context.py +84 -0
  23. klaude_code/core/tool/file/apply_patch_tool.py +18 -21
  24. klaude_code/core/tool/file/edit_tool.py +42 -44
  25. klaude_code/core/tool/file/read_tool.py +14 -9
  26. klaude_code/core/tool/file/write_tool.py +12 -13
  27. klaude_code/core/tool/report_back_tool.py +4 -1
  28. klaude_code/core/tool/shell/bash_tool.py +6 -11
  29. klaude_code/core/tool/skill/skill_tool.py +3 -1
  30. klaude_code/core/tool/sub_agent_tool.py +8 -7
  31. klaude_code/core/tool/todo/todo_write_tool.py +3 -9
  32. klaude_code/core/tool/todo/update_plan_tool.py +3 -5
  33. klaude_code/core/tool/tool_abc.py +2 -1
  34. klaude_code/core/tool/tool_registry.py +2 -33
  35. klaude_code/core/tool/tool_runner.py +13 -10
  36. klaude_code/core/tool/web/mermaid_tool.py +3 -1
  37. klaude_code/core/tool/web/web_fetch_tool.py +5 -3
  38. klaude_code/core/tool/web/web_search_tool.py +5 -3
  39. klaude_code/core/turn.py +86 -26
  40. klaude_code/llm/anthropic/client.py +1 -1
  41. klaude_code/llm/bedrock/client.py +1 -1
  42. klaude_code/llm/claude/client.py +1 -1
  43. klaude_code/llm/codex/client.py +1 -1
  44. klaude_code/llm/google/client.py +1 -1
  45. klaude_code/llm/openai_compatible/client.py +1 -1
  46. klaude_code/llm/openai_compatible/tool_call_accumulator.py +1 -1
  47. klaude_code/llm/openrouter/client.py +1 -1
  48. klaude_code/llm/openrouter/reasoning.py +1 -1
  49. klaude_code/llm/responses/client.py +1 -1
  50. klaude_code/protocol/events/__init__.py +57 -0
  51. klaude_code/protocol/events/base.py +18 -0
  52. klaude_code/protocol/events/chat.py +20 -0
  53. klaude_code/protocol/events/lifecycle.py +22 -0
  54. klaude_code/protocol/events/metadata.py +15 -0
  55. klaude_code/protocol/events/streaming.py +43 -0
  56. klaude_code/protocol/events/system.py +53 -0
  57. klaude_code/protocol/events/tools.py +23 -0
  58. klaude_code/protocol/message.py +3 -11
  59. klaude_code/protocol/model.py +78 -9
  60. klaude_code/protocol/op.py +5 -0
  61. klaude_code/protocol/sub_agent/explore.py +0 -15
  62. klaude_code/protocol/sub_agent/task.py +1 -1
  63. klaude_code/protocol/sub_agent/web.py +1 -17
  64. klaude_code/protocol/tools.py +0 -1
  65. klaude_code/session/session.py +6 -5
  66. klaude_code/skill/assets/create-plan/SKILL.md +76 -0
  67. klaude_code/skill/loader.py +1 -1
  68. klaude_code/skill/system_skills.py +1 -1
  69. klaude_code/tui/__init__.py +8 -0
  70. klaude_code/{command → tui/command}/clear_cmd.py +2 -1
  71. klaude_code/{command → tui/command}/debug_cmd.py +4 -3
  72. klaude_code/{command → tui/command}/export_cmd.py +2 -1
  73. klaude_code/{command → tui/command}/export_online_cmd.py +6 -5
  74. klaude_code/{command → tui/command}/fork_session_cmd.py +10 -9
  75. klaude_code/{command → tui/command}/help_cmd.py +3 -2
  76. klaude_code/{command → tui/command}/model_cmd.py +5 -4
  77. klaude_code/{command → tui/command}/model_select.py +2 -2
  78. klaude_code/{command → tui/command}/prompt_command.py +4 -3
  79. klaude_code/{command → tui/command}/refresh_cmd.py +3 -1
  80. klaude_code/{command → tui/command}/registry.py +16 -6
  81. klaude_code/{command → tui/command}/release_notes_cmd.py +3 -2
  82. klaude_code/{command → tui/command}/resume_cmd.py +6 -5
  83. klaude_code/{command → tui/command}/status_cmd.py +4 -3
  84. klaude_code/{command → tui/command}/terminal_setup_cmd.py +4 -3
  85. klaude_code/{command → tui/command}/thinking_cmd.py +4 -3
  86. klaude_code/tui/commands.py +164 -0
  87. klaude_code/{ui/renderers → tui/components}/assistant.py +3 -3
  88. klaude_code/{ui/renderers → tui/components}/bash_syntax.py +2 -2
  89. klaude_code/{ui/renderers → tui/components}/common.py +1 -1
  90. klaude_code/tui/components/developer.py +231 -0
  91. klaude_code/{ui/renderers → tui/components}/diffs.py +2 -2
  92. klaude_code/{ui/renderers → tui/components}/errors.py +2 -2
  93. klaude_code/{ui/renderers → tui/components}/metadata.py +34 -21
  94. klaude_code/{ui → tui/components}/rich/markdown.py +78 -34
  95. klaude_code/{ui → tui/components}/rich/status.py +2 -2
  96. klaude_code/{ui → tui/components}/rich/theme.py +12 -5
  97. klaude_code/{ui/renderers → tui/components}/sub_agent.py +23 -43
  98. klaude_code/{ui/renderers → tui/components}/thinking.py +3 -3
  99. klaude_code/{ui/renderers → tui/components}/tools.py +11 -48
  100. klaude_code/{ui/renderers → tui/components}/user_input.py +3 -20
  101. klaude_code/tui/display.py +85 -0
  102. klaude_code/{ui/modes/repl → tui/input}/__init__.py +1 -1
  103. klaude_code/{ui/modes/repl → tui/input}/completers.py +1 -1
  104. klaude_code/{ui/modes/repl/input_prompt_toolkit.py → tui/input/prompt_toolkit.py} +11 -7
  105. klaude_code/tui/machine.py +606 -0
  106. klaude_code/tui/renderer.py +707 -0
  107. klaude_code/tui/runner.py +321 -0
  108. klaude_code/tui/terminal/__init__.py +56 -0
  109. klaude_code/{ui → tui}/terminal/color.py +1 -1
  110. klaude_code/{ui → tui}/terminal/control.py +1 -1
  111. klaude_code/{ui → tui}/terminal/notifier.py +1 -1
  112. klaude_code/{ui → tui}/terminal/selector.py +36 -17
  113. klaude_code/ui/__init__.py +6 -50
  114. klaude_code/ui/core/display.py +3 -3
  115. klaude_code/ui/core/input.py +2 -1
  116. klaude_code/ui/{modes/debug/display.py → debug_mode.py} +1 -1
  117. klaude_code/ui/{modes/exec/display.py → exec_mode.py} +1 -4
  118. klaude_code/ui/terminal/__init__.py +6 -54
  119. klaude_code/ui/terminal/title.py +31 -0
  120. klaude_code/update.py +163 -0
  121. {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/METADATA +1 -1
  122. klaude_code-2.1.0.dist-info/RECORD +235 -0
  123. klaude_code/cli/runtime.py +0 -525
  124. klaude_code/core/prompt.py +0 -108
  125. klaude_code/core/tool/file/move_tool.md +0 -41
  126. klaude_code/core/tool/file/move_tool.py +0 -435
  127. klaude_code/core/tool/tool_context.py +0 -148
  128. klaude_code/protocol/events.py +0 -194
  129. klaude_code/skill/assets/dev-docs/SKILL.md +0 -108
  130. klaude_code/trace/__init__.py +0 -21
  131. klaude_code/ui/core/stage_manager.py +0 -48
  132. klaude_code/ui/modes/__init__.py +0 -1
  133. klaude_code/ui/modes/debug/__init__.py +0 -1
  134. klaude_code/ui/modes/exec/__init__.py +0 -1
  135. klaude_code/ui/modes/repl/display.py +0 -61
  136. klaude_code/ui/modes/repl/event_handler.py +0 -634
  137. klaude_code/ui/modes/repl/renderer.py +0 -463
  138. klaude_code/ui/renderers/developer.py +0 -215
  139. klaude_code/ui/utils/__init__.py +0 -1
  140. klaude_code-2.0.1.dist-info/RECORD +0 -229
  141. /klaude_code/{trace/log.py → log.py} +0 -0
  142. /klaude_code/{command → tui/command}/__init__.py +0 -0
  143. /klaude_code/{command → tui/command}/command_abc.py +0 -0
  144. /klaude_code/{command → tui/command}/prompt-commit.md +0 -0
  145. /klaude_code/{command → tui/command}/prompt-init.md +0 -0
  146. /klaude_code/{ui/renderers → tui/components}/__init__.py +0 -0
  147. /klaude_code/{ui/renderers → tui/components}/mermaid_viewer.py +0 -0
  148. /klaude_code/{ui → tui/components}/rich/__init__.py +0 -0
  149. /klaude_code/{ui → tui/components}/rich/cjk_wrap.py +0 -0
  150. /klaude_code/{ui → tui/components}/rich/code_panel.py +0 -0
  151. /klaude_code/{ui → tui/components}/rich/live.py +0 -0
  152. /klaude_code/{ui → tui/components}/rich/quote.py +0 -0
  153. /klaude_code/{ui → tui/components}/rich/searchable_text.py +0 -0
  154. /klaude_code/{ui/modes/repl → tui/input}/clipboard.py +0 -0
  155. /klaude_code/{ui/modes/repl → tui/input}/key_bindings.py +0 -0
  156. /klaude_code/{ui → tui}/terminal/image.py +0 -0
  157. /klaude_code/{ui → tui}/terminal/progress_bar.py +0 -0
  158. /klaude_code/ui/{utils/common.py → common.py} +0 -0
  159. {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/WHEEL +0 -0
  160. {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,321 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import contextlib
5
+ import sys
6
+ from uuid import uuid4
7
+
8
+ from klaude_code import ui
9
+ from klaude_code.app.runtime import (
10
+ AppInitConfig,
11
+ backfill_session_model_config,
12
+ cleanup_app_components,
13
+ handle_keyboard_interrupt,
14
+ initialize_app_components,
15
+ initialize_session,
16
+ )
17
+ from klaude_code.config import load_config
18
+ from klaude_code.const import SIGINT_DOUBLE_PRESS_EXIT_TEXT
19
+ from klaude_code.core.executor import Executor
20
+ from klaude_code.log import log
21
+ from klaude_code.protocol import events, llm_param, op
22
+ from klaude_code.protocol.message import UserInputPayload
23
+ from klaude_code.session.session import Session
24
+ from klaude_code.tui.command import (
25
+ dispatch_command,
26
+ get_command_info_list,
27
+ has_interactive_command,
28
+ )
29
+ from klaude_code.tui.display import TUIDisplay
30
+ from klaude_code.tui.input import build_repl_status_snapshot
31
+ from klaude_code.tui.input.prompt_toolkit import PromptToolkitInput, REPLStatusSnapshot
32
+ from klaude_code.tui.terminal.color import is_light_terminal_background
33
+ from klaude_code.tui.terminal.control import install_sigint_double_press_exit, start_esc_interrupt_monitor
34
+ from klaude_code.ui.terminal.title import update_terminal_title
35
+ from klaude_code.update import get_update_message
36
+
37
+
38
+ async def submit_user_input_payload(
39
+ *,
40
+ executor: Executor,
41
+ event_queue: asyncio.Queue[events.Event],
42
+ user_input: UserInputPayload,
43
+ session_id: str | None,
44
+ ) -> str | None:
45
+ """Parse/dispatch a user input payload (TUI commands) and submit operations.
46
+
47
+ This function is TUI-only: it supports slash command parsing and interactive prompts.
48
+
49
+ Returns a submission id that should be awaited, or None if there is nothing
50
+ to wait for (e.g. commands that only emit events).
51
+ """
52
+
53
+ sid = session_id or executor.context.current_session_id()
54
+ if sid is None:
55
+ raise RuntimeError("No active session")
56
+
57
+ agent = executor.context.current_agent
58
+ if agent is None or agent.session.id != sid:
59
+ await executor.submit_and_wait(op.InitAgentOperation(session_id=sid))
60
+ agent = executor.context.current_agent
61
+
62
+ if agent is None:
63
+ raise RuntimeError("Failed to initialize agent")
64
+
65
+ submission_id = uuid4().hex
66
+
67
+ # Render the raw user input in the TUI even when it resolves to an event-only command.
68
+ await executor.context.emit_event(
69
+ events.UserMessageEvent(content=user_input.text, session_id=sid, images=user_input.images)
70
+ )
71
+
72
+ cmd_result = await dispatch_command(user_input, agent, submission_id=submission_id)
73
+ operations: list[op.Operation] = list(cmd_result.operations or [])
74
+
75
+ run_ops = [candidate for candidate in operations if isinstance(candidate, op.RunAgentOperation)]
76
+ if len(run_ops) > 1:
77
+ raise ValueError("Multiple RunAgentOperation results are not supported")
78
+
79
+ for run_op in run_ops:
80
+ run_op.persist_user_input = cmd_result.persist_user_input
81
+ run_op.emit_user_message_event = False
82
+
83
+ if cmd_result.events:
84
+ for evt in cmd_result.events:
85
+ if cmd_result.persist_events and isinstance(evt, events.DeveloperMessageEvent):
86
+ agent.session.append_history([evt.item])
87
+ await executor.context.emit_event(evt)
88
+
89
+ submitted_ids: list[str] = []
90
+ for operation_item in operations:
91
+ submitted_ids.append(await executor.submit(operation_item))
92
+
93
+ if not submitted_ids:
94
+ # Ensure event-only commands are fully rendered before showing the next prompt.
95
+ await event_queue.join()
96
+ return None
97
+
98
+ if run_ops:
99
+ return run_ops[0].id
100
+ return submitted_ids[-1]
101
+
102
+
103
+ async def run_interactive(init_config: AppInitConfig, session_id: str | None = None) -> None:
104
+ """Run the interactive REPL (TUI).
105
+
106
+ If session_id is None, a new session is created.
107
+ If session_id is provided, attempts to resume that session.
108
+ """
109
+
110
+ update_terminal_title()
111
+
112
+ cfg = load_config()
113
+ theme: str | None = cfg.theme
114
+ if theme is None:
115
+ detected = is_light_terminal_background()
116
+ if detected is True:
117
+ theme = "light"
118
+ elif detected is False:
119
+ theme = "dark"
120
+
121
+ display: ui.DisplayABC = TUIDisplay(theme=theme)
122
+ if init_config.debug:
123
+ display = ui.DebugEventDisplay(display)
124
+
125
+ components = await initialize_app_components(
126
+ init_config=init_config,
127
+ display=display,
128
+ on_model_change=update_terminal_title,
129
+ )
130
+
131
+ def _status_provider() -> REPLStatusSnapshot:
132
+ update_message = get_update_message()
133
+ return build_repl_status_snapshot(update_message)
134
+
135
+ def _stop_rich_bottom_ui() -> None:
136
+ active_display = components.display
137
+ if isinstance(active_display, TUIDisplay):
138
+ active_display.hide_progress_ui()
139
+ elif (
140
+ isinstance(active_display, ui.DebugEventDisplay)
141
+ and active_display.wrapped_display
142
+ and isinstance(active_display.wrapped_display, TUIDisplay)
143
+ ):
144
+ active_display.wrapped_display.hide_progress_ui()
145
+
146
+ is_light_background: bool | None = None
147
+ if theme == "light":
148
+ is_light_background = True
149
+ elif theme == "dark":
150
+ is_light_background = False
151
+
152
+ def _get_active_session_id() -> str | None:
153
+ """Get the current active session ID dynamically.
154
+
155
+ This is necessary because /clear creates a new session with a different id.
156
+ """
157
+
158
+ return components.executor.context.current_session_id()
159
+
160
+ async def _change_model_from_prompt(model_name: str) -> None:
161
+ sid = _get_active_session_id()
162
+ if not sid:
163
+ return
164
+ await components.executor.submit_and_wait(
165
+ op.ChangeModelOperation(
166
+ session_id=sid,
167
+ model_name=model_name,
168
+ save_as_default=False,
169
+ defer_thinking_selection=True,
170
+ emit_welcome_event=True,
171
+ emit_switch_message=False,
172
+ )
173
+ )
174
+
175
+ def _get_current_llm_config() -> llm_param.LLMConfigParameter | None:
176
+ agent = components.executor.context.current_agent
177
+ if agent is None:
178
+ return None
179
+ return agent.profile.llm_client.get_llm_config()
180
+
181
+ async def _change_thinking_from_prompt(thinking: llm_param.Thinking) -> None:
182
+ sid = _get_active_session_id()
183
+ if not sid:
184
+ return
185
+ await components.executor.submit_and_wait(
186
+ op.ChangeThinkingOperation(
187
+ session_id=sid,
188
+ thinking=thinking,
189
+ emit_welcome_event=True,
190
+ emit_switch_message=False,
191
+ )
192
+ )
193
+
194
+ input_provider: ui.InputProviderABC = PromptToolkitInput(
195
+ status_provider=_status_provider,
196
+ pre_prompt=_stop_rich_bottom_ui,
197
+ is_light_background=is_light_background,
198
+ get_current_model_config_name=lambda: (
199
+ components.executor.context.current_agent.session.model_config_name
200
+ if components.executor.context.current_agent is not None
201
+ else None
202
+ ),
203
+ on_change_model=_change_model_from_prompt,
204
+ get_current_llm_config=_get_current_llm_config,
205
+ on_change_thinking=_change_thinking_from_prompt,
206
+ command_info_provider=get_command_info_list,
207
+ )
208
+
209
+ loop = asyncio.get_running_loop()
210
+
211
+ def _get_tui_display() -> TUIDisplay | None:
212
+ active_display = components.display
213
+ if isinstance(active_display, TUIDisplay):
214
+ return active_display
215
+ if (
216
+ isinstance(active_display, ui.DebugEventDisplay)
217
+ and active_display.wrapped_display
218
+ and isinstance(active_display.wrapped_display, TUIDisplay)
219
+ ):
220
+ return active_display.wrapped_display
221
+ return None
222
+
223
+ @contextlib.contextmanager
224
+ def _double_ctrl_c_to_exit_while_running(*, window_seconds: float = 2.0):
225
+ """Require double Ctrl+C to exit while waiting for task completion."""
226
+
227
+ def _show_toast_once() -> None:
228
+ def _emit() -> None:
229
+ tui_display = _get_tui_display()
230
+ if tui_display is not None:
231
+ with contextlib.suppress(Exception):
232
+ tui_display.show_sigint_exit_toast(window_seconds=window_seconds)
233
+ return
234
+ print(SIGINT_DOUBLE_PRESS_EXIT_TEXT, file=sys.stderr)
235
+
236
+ with contextlib.suppress(Exception):
237
+ loop.call_soon_threadsafe(_emit)
238
+ return
239
+ _emit()
240
+
241
+ def _hide_progress() -> None:
242
+ def _emit() -> None:
243
+ tui_display = _get_tui_display()
244
+ if tui_display is None:
245
+ return
246
+ with contextlib.suppress(Exception):
247
+ tui_display.hide_progress_ui()
248
+
249
+ with contextlib.suppress(Exception):
250
+ loop.call_soon_threadsafe(_emit)
251
+
252
+ restore_sigint = install_sigint_double_press_exit(
253
+ _show_toast_once,
254
+ _hide_progress,
255
+ window_seconds=window_seconds,
256
+ )
257
+ try:
258
+ yield
259
+ finally:
260
+ with contextlib.suppress(Exception):
261
+ restore_sigint()
262
+
263
+ exit_hint_printed = False
264
+
265
+ try:
266
+ await initialize_session(components.executor, components.event_queue, session_id=session_id)
267
+ backfill_session_model_config(
268
+ components.executor.context.current_agent,
269
+ init_config.model,
270
+ components.config.main_model,
271
+ is_new_session=session_id is None,
272
+ )
273
+
274
+ await input_provider.start()
275
+ async for user_input in input_provider.iter_inputs():
276
+ if user_input.text.strip().lower() in {"exit", ":q", "quit"}:
277
+ break
278
+ if user_input.text.strip() == "":
279
+ continue
280
+
281
+ active_session_id = _get_active_session_id()
282
+ is_interactive = has_interactive_command(user_input.text)
283
+
284
+ wait_id = await submit_user_input_payload(
285
+ executor=components.executor,
286
+ event_queue=components.event_queue,
287
+ user_input=user_input,
288
+ session_id=active_session_id,
289
+ )
290
+
291
+ if wait_id is None:
292
+ continue
293
+
294
+ if is_interactive:
295
+ with _double_ctrl_c_to_exit_while_running():
296
+ await components.executor.wait_for(wait_id)
297
+ continue
298
+
299
+ async def _on_esc_interrupt() -> None:
300
+ await components.executor.submit(op.InterruptOperation(target_session_id=_get_active_session_id()))
301
+
302
+ stop_event, esc_task = start_esc_interrupt_monitor(_on_esc_interrupt)
303
+ try:
304
+ with _double_ctrl_c_to_exit_while_running():
305
+ await components.executor.wait_for(wait_id)
306
+ finally:
307
+ stop_event.set()
308
+ with contextlib.suppress(Exception):
309
+ await esc_task
310
+
311
+ except KeyboardInterrupt:
312
+ await handle_keyboard_interrupt(components.executor)
313
+ exit_hint_printed = True
314
+ finally:
315
+ await cleanup_app_components(components)
316
+
317
+ if not exit_hint_printed:
318
+ active_session_id = components.executor.context.current_session_id()
319
+ if active_session_id and Session.exists(active_session_id):
320
+ log(f"Session ID: {active_session_id}")
321
+ log(f"Resume with: klaude --resume-by-id {active_session_id}")
@@ -0,0 +1,56 @@
1
+ # Terminal utilities
2
+ import os
3
+ from functools import lru_cache
4
+
5
+
6
+ @lru_cache(maxsize=1)
7
+ def supports_osc8_hyperlinks() -> bool:
8
+ """Check if the current terminal supports OSC 8 hyperlinks.
9
+
10
+ Based on known terminal support. Returns False for unknown terminals.
11
+ """
12
+ term_program = os.environ.get("TERM_PROGRAM", "").lower()
13
+ term = os.environ.get("TERM", "").lower()
14
+
15
+ # Known terminals that do NOT support OSC 8
16
+ unsupported = ("warp", "apple_terminal")
17
+ if any(t in term_program for t in unsupported):
18
+ return False
19
+
20
+ # Known terminals that support OSC 8
21
+ supported = (
22
+ "iterm.app",
23
+ "ghostty",
24
+ "wezterm",
25
+ "kitty",
26
+ "alacritty",
27
+ "hyper",
28
+ "contour",
29
+ "vscode",
30
+ )
31
+ if any(t in term_program for t in supported):
32
+ return True
33
+
34
+ # Kitty sets TERM to xterm-kitty
35
+ if "kitty" in term:
36
+ return True
37
+
38
+ # Ghostty sets TERM to xterm-ghostty
39
+ if "ghostty" in term:
40
+ return True
41
+
42
+ # Windows Terminal
43
+ if os.environ.get("WT_SESSION"):
44
+ return True
45
+
46
+ # VTE-based terminals (GNOME Terminal, etc.) version 0.50+
47
+ vte_version = os.environ.get("VTE_VERSION", "")
48
+ if vte_version:
49
+ try:
50
+ if int(vte_version) >= 5000:
51
+ return True
52
+ except ValueError:
53
+ pass
54
+
55
+ # Default to False for unknown terminals
56
+ return False
@@ -9,7 +9,7 @@ import time
9
9
  import tty
10
10
  from typing import BinaryIO, Final
11
11
 
12
- from klaude_code.trace import DebugType, log_debug
12
+ from klaude_code.log import DebugType, log_debug
13
13
 
14
14
  ST: Final[bytes] = b"\x1b\\" # ESC \
15
15
  BEL: Final[int] = 7
@@ -12,7 +12,7 @@ from collections.abc import Callable, Coroutine
12
12
  from types import FrameType
13
13
  from typing import Any
14
14
 
15
- from klaude_code.trace import log
15
+ from klaude_code.log import log
16
16
 
17
17
 
18
18
  def start_esc_interrupt_monitor(
@@ -8,7 +8,7 @@ from enum import Enum
8
8
  from typing import TextIO, cast
9
9
 
10
10
  from klaude_code.const import NOTIFY_COMPACT_LIMIT
11
- from klaude_code.trace import DebugType, log_debug
11
+ from klaude_code.log import DebugType, log_debug
12
12
 
13
13
  # Environment variable for tmux test signal channel
14
14
  TMUX_SIGNAL_ENV = "KLAUDE_TEST_SIGNAL"
@@ -238,9 +238,31 @@ def _build_choices_tokens[T](
238
238
  return tokens
239
239
 
240
240
 
241
- def _build_rounded_frame(body: Container) -> HSplit:
241
+ def _build_rounded_frame(body: Container, *, padding_x: int = 0, padding_y: int = 0) -> HSplit:
242
242
  """Build a rounded border frame around the given container."""
243
243
  border = partial(Window, style="class:frame.border", height=1)
244
+ pad = partial(Window, style="class:frame", char=" ", always_hide_cursor=Always())
245
+
246
+ inner: Container = body
247
+ if padding_y > 0:
248
+ inner = HSplit(
249
+ [
250
+ pad(height=padding_y, dont_extend_height=Always()),
251
+ body,
252
+ pad(height=padding_y, dont_extend_height=Always()),
253
+ ],
254
+ padding=0,
255
+ style="class:frame",
256
+ )
257
+
258
+ middle_children: list[Container] = [border(width=1, char="│")]
259
+ if padding_x > 0:
260
+ middle_children.append(pad(width=padding_x))
261
+ middle_children.append(inner)
262
+ if padding_x > 0:
263
+ middle_children.append(pad(width=padding_x))
264
+ middle_children.append(border(width=1, char="│"))
265
+
244
266
  top = VSplit(
245
267
  [
246
268
  border(width=1, char="╭"),
@@ -250,14 +272,7 @@ def _build_rounded_frame(body: Container) -> HSplit:
250
272
  height=1,
251
273
  padding=0,
252
274
  )
253
- middle = VSplit(
254
- [
255
- border(width=1, char="│"),
256
- body,
257
- border(width=1, char="│"),
258
- ],
259
- padding=0,
260
- )
275
+ middle = VSplit(middle_children, padding=0)
261
276
  bottom = VSplit(
262
277
  [
263
278
  border(width=1, char="╰"),
@@ -273,6 +288,8 @@ def _build_rounded_frame(body: Container) -> HSplit:
273
288
  def _build_search_container(
274
289
  search_buffer: Buffer,
275
290
  search_placeholder: str,
291
+ *,
292
+ frame: bool = True,
276
293
  ) -> tuple[Window, Container]:
277
294
  """Build the search input container with placeholder."""
278
295
  placeholder_text = f"{search_placeholder} · ↑↓ to select · esc to quit"
@@ -303,8 +320,10 @@ def _build_search_container(
303
320
  content=input_window,
304
321
  floats=[Float(content=placeholder_window, top=0, left=0)],
305
322
  )
306
- framed = _build_rounded_frame(VSplit([search_prefix_window, search_input_container], padding=0))
307
- return input_window, framed
323
+ search_row: Container = VSplit([search_prefix_window, search_input_container], padding=0)
324
+ if frame:
325
+ return input_window, _build_rounded_frame(search_row)
326
+ return input_window, search_row
308
327
 
309
328
 
310
329
  # ---------------------------------------------------------------------------
@@ -436,7 +455,7 @@ def select_one[T](
436
455
 
437
456
  base_style = Style(
438
457
  [
439
- ("frame.border", "fg:ansibrightblack"),
458
+ ("frame.border", "fg:ansibrightblack dim"),
440
459
  ("frame.label", "fg:ansibrightblack italic"),
441
460
  ("search_prefix", "fg:ansibrightblack"),
442
461
  ("search_placeholder", "fg:ansibrightblack italic"),
@@ -619,17 +638,17 @@ class SelectOverlay[T]:
619
638
  search_container: Container | None = None
620
639
  if self._use_search_filter and self._search_buffer is not None:
621
640
  self._search_input_window, search_container = _build_search_container(
622
- self._search_buffer, self._search_placeholder
641
+ self._search_buffer,
642
+ self._search_placeholder,
643
+ frame=False,
623
644
  )
624
645
 
625
646
  root_children: list[Container] = [header_window, spacer_window, list_window]
626
647
  if search_container is not None:
627
648
  root_children.append(search_container)
628
649
 
629
- return ConditionalContainer(
630
- content=HSplit(root_children, padding=0),
631
- filter=Condition(lambda: self._is_open),
632
- )
650
+ framed_content = _build_rounded_frame(HSplit(root_children, padding=0), padding_x=1)
651
+ return ConditionalContainer(content=framed_content, filter=Condition(lambda: self._is_open))
633
652
 
634
653
  @property
635
654
  def is_open(self) -> bool:
@@ -1,56 +1,16 @@
1
- """
2
- UI Module - Display and Input Abstractions for klaude-code
3
-
4
- This module provides the UI layer for klaude-code, including display modes
5
- and input providers. The UI is designed around three main concepts:
1
+ """UI interfaces and lightweight displays.
6
2
 
7
- Display Modes:
8
- - REPLDisplay: Interactive terminal mode with Rich rendering, spinners, and live updates
9
- - ExecDisplay: Non-interactive exec mode that only outputs task results
10
- - DebugEventDisplay: Decorator that logs all events for debugging purposes
3
+ This package intentionally contains only frontend-agnostic interfaces and
4
+ minimal display implementations.
11
5
 
12
- Input Providers:
13
- - PromptToolkitInput: Interactive input with prompt-toolkit (completions, keybindings)
14
-
15
- Factory Functions:
16
- - create_default_display(): Creates the appropriate display for interactive mode
17
- - create_exec_display(): Creates the appropriate display for exec mode
6
+ Terminal (Rich/prompt-toolkit) UI lives in `klaude_code.tui`.
18
7
  """
19
8
 
20
9
  # --- Abstract Interfaces ---
21
10
  from .core.display import DisplayABC
22
11
  from .core.input import InputProviderABC
23
- from .modes.debug.display import DebugEventDisplay
24
- from .modes.exec.display import ExecDisplay, StreamJsonDisplay
25
-
26
- # --- Display Mode Implementations ---
27
- from .modes.repl.display import REPLDisplay
28
-
29
- # --- Input Implementations ---
30
- from .modes.repl.input_prompt_toolkit import PromptToolkitInput
31
- from .terminal.notifier import TerminalNotifier
32
-
33
-
34
- def create_default_display(
35
- debug: bool = False,
36
- theme: str | None = None,
37
- notifier: TerminalNotifier | None = None,
38
- ) -> DisplayABC:
39
- """
40
- Create the default display for interactive REPL mode.
41
-
42
- Args:
43
- debug: If True, wrap the display with DebugEventDisplay to log all events.
44
- theme: Optional theme name ("light" or "dark") for syntax highlighting.
45
- notifier: Optional terminal notifier for desktop notifications.
46
-
47
- Returns:
48
- A DisplayABC implementation suitable for interactive use.
49
- """
50
- repl_display = REPLDisplay(theme=theme, notifier=notifier)
51
- if debug:
52
- return DebugEventDisplay(repl_display)
53
- return repl_display
12
+ from .debug_mode import DebugEventDisplay
13
+ from .exec_mode import ExecDisplay, StreamJsonDisplay
54
14
 
55
15
 
56
16
  def create_exec_display(debug: bool = False, stream_json: bool = False) -> DisplayABC:
@@ -77,10 +37,6 @@ __all__ = [
77
37
  "DisplayABC",
78
38
  "ExecDisplay",
79
39
  "InputProviderABC",
80
- "PromptToolkitInput",
81
- "REPLDisplay",
82
40
  "StreamJsonDisplay",
83
- "TerminalNotifier",
84
- "create_default_display",
85
41
  "create_exec_display",
86
42
  ]
@@ -1,8 +1,8 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from asyncio import Queue
3
3
 
4
+ from klaude_code.log import log
4
5
  from klaude_code.protocol import events
5
- from klaude_code.trace import log
6
6
 
7
7
 
8
8
  class DisplayABC(ABC):
@@ -11,7 +11,7 @@ class DisplayABC(ABC):
11
11
 
12
12
  A Display is responsible for rendering events from the executor to the user.
13
13
  Implementations can range from simple text output (ExecDisplay) to rich
14
- interactive terminals (REPLDisplay) or debugging wrappers (DebugEventDisplay).
14
+ interactive terminals (TUIDisplay) or debugging wrappers (DebugEventDisplay).
15
15
 
16
16
  Lifecycle:
17
17
  1. start() is called once before any events are consumed.
@@ -19,7 +19,7 @@ class DisplayABC(ABC):
19
19
  3. stop() is called once when the display is shutting down (after EndEvent).
20
20
 
21
21
  Typical Usage:
22
- display = create_default_display()
22
+ # See klaude_code.tui.display.TUIDisplay for the interactive terminal frontend.
23
23
  await display.consume_event_loop(event_queue)
24
24
 
25
25
  # Or manually:
@@ -20,7 +20,8 @@ class InputProviderABC(ABC):
20
20
  3. stop() is called once when input collection is complete.
21
21
 
22
22
  Typical Usage:
23
- input_provider = PromptToolkitInput(status_provider=my_status_fn)
23
+ # For the interactive terminal input implementation, see
24
+ # klaude_code.tui.input.prompt_toolkit.PromptToolkitInput.
24
25
  await input_provider.start()
25
26
  try:
26
27
  async for user_input in input_provider.iter_inputs():
@@ -2,8 +2,8 @@ import os
2
2
  from typing import override
3
3
 
4
4
  from klaude_code.const import DEFAULT_DEBUG_LOG_FILE
5
+ from klaude_code.log import DebugType, log_debug
5
6
  from klaude_code.protocol import events
6
- from klaude_code.trace import DebugType, log_debug
7
7
  from klaude_code.ui.core.display import DisplayABC
8
8
 
9
9
 
@@ -3,7 +3,6 @@ from typing import override
3
3
 
4
4
  from klaude_code.protocol import events
5
5
  from klaude_code.ui.core.display import DisplayABC
6
- from klaude_code.ui.terminal.progress_bar import OSC94States, emit_osc94
7
6
 
8
7
 
9
8
  class ExecDisplay(DisplayABC):
@@ -14,12 +13,10 @@ class ExecDisplay(DisplayABC):
14
13
  """Only handle TaskFinishEvent."""
15
14
  match event:
16
15
  case events.TaskStartEvent():
17
- emit_osc94(OSC94States.INDETERMINATE)
16
+ pass
18
17
  case events.ErrorEvent() as e:
19
- emit_osc94(OSC94States.HIDDEN)
20
18
  print(f"Error: {e.error_message}")
21
19
  case events.TaskFinishEvent() as e:
22
- emit_osc94(OSC94States.HIDDEN)
23
20
  # Print the task result when task finishes
24
21
  if e.task_result.strip():
25
22
  print(e.task_result)