klaude-code 2.0.2__py3-none-any.whl → 2.1.1__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 (157) 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 +9 -1
  14. klaude_code/core/agent.py +9 -62
  15. klaude_code/core/agent_profile.py +291 -0
  16. klaude_code/core/executor.py +335 -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 +84 -103
  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 +39 -42
  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/sub_agent_tool.py +8 -7
  30. klaude_code/core/tool/todo/todo_write_tool.py +3 -9
  31. klaude_code/core/tool/todo/update_plan_tool.py +3 -5
  32. klaude_code/core/tool/tool_abc.py +2 -1
  33. klaude_code/core/tool/tool_registry.py +2 -33
  34. klaude_code/core/tool/tool_runner.py +13 -10
  35. klaude_code/core/tool/web/mermaid_tool.py +3 -1
  36. klaude_code/core/tool/web/web_fetch_tool.py +5 -3
  37. klaude_code/core/tool/web/web_search_tool.py +5 -3
  38. klaude_code/core/turn.py +87 -30
  39. klaude_code/llm/anthropic/client.py +1 -1
  40. klaude_code/llm/bedrock/client.py +1 -1
  41. klaude_code/llm/claude/client.py +1 -1
  42. klaude_code/llm/codex/client.py +1 -1
  43. klaude_code/llm/google/client.py +1 -1
  44. klaude_code/llm/openai_compatible/client.py +1 -1
  45. klaude_code/llm/openai_compatible/tool_call_accumulator.py +1 -1
  46. klaude_code/llm/openrouter/client.py +1 -1
  47. klaude_code/llm/openrouter/reasoning.py +1 -1
  48. klaude_code/llm/responses/client.py +1 -1
  49. klaude_code/protocol/commands.py +1 -0
  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 +27 -0
  58. klaude_code/protocol/op.py +5 -0
  59. klaude_code/protocol/tools.py +0 -1
  60. klaude_code/session/session.py +6 -7
  61. klaude_code/skill/assets/create-plan/SKILL.md +76 -0
  62. klaude_code/skill/loader.py +32 -88
  63. klaude_code/skill/manager.py +38 -0
  64. klaude_code/skill/system_skills.py +1 -1
  65. klaude_code/tui/__init__.py +8 -0
  66. klaude_code/{command → tui/command}/__init__.py +3 -0
  67. klaude_code/{command → tui/command}/clear_cmd.py +2 -1
  68. klaude_code/tui/command/copy_cmd.py +53 -0
  69. klaude_code/{command → tui/command}/debug_cmd.py +3 -2
  70. klaude_code/{command → tui/command}/export_cmd.py +2 -1
  71. klaude_code/{command → tui/command}/export_online_cmd.py +2 -1
  72. klaude_code/{command → tui/command}/fork_session_cmd.py +4 -3
  73. klaude_code/{command → tui/command}/help_cmd.py +2 -1
  74. klaude_code/{command → tui/command}/model_cmd.py +4 -3
  75. klaude_code/{command → tui/command}/model_select.py +2 -2
  76. klaude_code/{command → tui/command}/prompt_command.py +4 -3
  77. klaude_code/{command → tui/command}/refresh_cmd.py +3 -1
  78. klaude_code/{command → tui/command}/registry.py +6 -5
  79. klaude_code/{command → tui/command}/release_notes_cmd.py +2 -1
  80. klaude_code/{command → tui/command}/resume_cmd.py +4 -3
  81. klaude_code/{command → tui/command}/status_cmd.py +2 -1
  82. klaude_code/{command → tui/command}/terminal_setup_cmd.py +2 -1
  83. klaude_code/{command → tui/command}/thinking_cmd.py +3 -2
  84. klaude_code/tui/commands.py +164 -0
  85. klaude_code/{ui/renderers → tui/components}/assistant.py +3 -3
  86. klaude_code/{ui/renderers → tui/components}/bash_syntax.py +2 -2
  87. klaude_code/{ui/renderers → tui/components}/common.py +1 -1
  88. klaude_code/{ui/renderers → tui/components}/developer.py +4 -4
  89. klaude_code/{ui/renderers → tui/components}/diffs.py +2 -2
  90. klaude_code/{ui/renderers → tui/components}/errors.py +2 -2
  91. klaude_code/{ui/renderers → tui/components}/metadata.py +7 -7
  92. klaude_code/{ui → tui/components}/rich/markdown.py +9 -23
  93. klaude_code/{ui → tui/components}/rich/status.py +2 -2
  94. klaude_code/{ui → tui/components}/rich/theme.py +3 -1
  95. klaude_code/{ui/renderers → tui/components}/sub_agent.py +23 -43
  96. klaude_code/{ui/renderers → tui/components}/thinking.py +3 -3
  97. klaude_code/{ui/renderers → tui/components}/tools.py +13 -17
  98. klaude_code/{ui/renderers → tui/components}/user_input.py +3 -20
  99. klaude_code/tui/display.py +85 -0
  100. klaude_code/{ui/modes/repl → tui/input}/__init__.py +1 -1
  101. klaude_code/{ui/modes/repl → tui/input}/completers.py +1 -1
  102. klaude_code/{ui/modes/repl/input_prompt_toolkit.py → tui/input/prompt_toolkit.py} +6 -6
  103. klaude_code/tui/machine.py +608 -0
  104. klaude_code/tui/renderer.py +707 -0
  105. klaude_code/tui/runner.py +321 -0
  106. klaude_code/tui/terminal/__init__.py +56 -0
  107. klaude_code/{ui → tui}/terminal/color.py +1 -1
  108. klaude_code/{ui → tui}/terminal/control.py +1 -1
  109. klaude_code/{ui → tui}/terminal/notifier.py +1 -1
  110. klaude_code/ui/__init__.py +6 -50
  111. klaude_code/ui/core/display.py +3 -3
  112. klaude_code/ui/core/input.py +2 -1
  113. klaude_code/ui/{modes/debug/display.py → debug_mode.py} +1 -1
  114. klaude_code/ui/{modes/exec/display.py → exec_mode.py} +0 -2
  115. klaude_code/ui/terminal/__init__.py +6 -54
  116. klaude_code/ui/terminal/title.py +31 -0
  117. klaude_code/update.py +163 -0
  118. {klaude_code-2.0.2.dist-info → klaude_code-2.1.1.dist-info}/METADATA +1 -1
  119. klaude_code-2.1.1.dist-info/RECORD +233 -0
  120. klaude_code/cli/runtime.py +0 -518
  121. klaude_code/core/prompt.py +0 -108
  122. klaude_code/core/tool/skill/skill_tool.md +0 -24
  123. klaude_code/core/tool/skill/skill_tool.py +0 -87
  124. klaude_code/core/tool/tool_context.py +0 -148
  125. klaude_code/protocol/events.py +0 -195
  126. klaude_code/skill/assets/dev-docs/SKILL.md +0 -108
  127. klaude_code/trace/__init__.py +0 -21
  128. klaude_code/ui/core/stage_manager.py +0 -48
  129. klaude_code/ui/modes/__init__.py +0 -1
  130. klaude_code/ui/modes/debug/__init__.py +0 -1
  131. klaude_code/ui/modes/exec/__init__.py +0 -1
  132. klaude_code/ui/modes/repl/display.py +0 -61
  133. klaude_code/ui/modes/repl/event_handler.py +0 -629
  134. klaude_code/ui/modes/repl/renderer.py +0 -464
  135. klaude_code/ui/renderers/__init__.py +0 -0
  136. klaude_code/ui/utils/__init__.py +0 -1
  137. klaude_code-2.0.2.dist-info/RECORD +0 -227
  138. /klaude_code/{trace/log.py → log.py} +0 -0
  139. /klaude_code/{command → tui/command}/command_abc.py +0 -0
  140. /klaude_code/{command → tui/command}/prompt-commit.md +0 -0
  141. /klaude_code/{command → tui/command}/prompt-init.md +0 -0
  142. /klaude_code/{core/tool/skill → tui/components}/__init__.py +0 -0
  143. /klaude_code/{ui/renderers → tui/components}/mermaid_viewer.py +0 -0
  144. /klaude_code/{ui → tui/components}/rich/__init__.py +0 -0
  145. /klaude_code/{ui → tui/components}/rich/cjk_wrap.py +0 -0
  146. /klaude_code/{ui → tui/components}/rich/code_panel.py +0 -0
  147. /klaude_code/{ui → tui/components}/rich/live.py +0 -0
  148. /klaude_code/{ui → tui/components}/rich/quote.py +0 -0
  149. /klaude_code/{ui → tui/components}/rich/searchable_text.py +0 -0
  150. /klaude_code/{ui/modes/repl → tui/input}/clipboard.py +0 -0
  151. /klaude_code/{ui/modes/repl → tui/input}/key_bindings.py +0 -0
  152. /klaude_code/{ui → tui}/terminal/image.py +0 -0
  153. /klaude_code/{ui → tui}/terminal/progress_bar.py +0 -0
  154. /klaude_code/{ui → tui}/terminal/selector.py +0 -0
  155. /klaude_code/ui/{utils/common.py → common.py} +0 -0
  156. {klaude_code-2.0.2.dist-info → klaude_code-2.1.1.dist-info}/WHEEL +0 -0
  157. {klaude_code-2.0.2.dist-info → klaude_code-2.1.1.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"
@@ -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):
@@ -16,7 +15,6 @@ class ExecDisplay(DisplayABC):
16
15
  case events.TaskStartEvent():
17
16
  pass
18
17
  case events.ErrorEvent() as e:
19
- emit_osc94(OSC94States.ERROR)
20
18
  print(f"Error: {e.error_message}")
21
19
  case events.TaskFinishEvent() as e:
22
20
  # Print the task result when task finishes
@@ -1,56 +1,8 @@
1
- # Terminal utilities
2
- import os
3
- from functools import lru_cache
1
+ # Terminal UI helpers shared across frontends.
4
2
 
3
+ from .title import set_terminal_title, update_terminal_title
5
4
 
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
5
+ __all__ = [
6
+ "set_terminal_title",
7
+ "update_terminal_title",
8
+ ]
@@ -0,0 +1,31 @@
1
+ import contextlib
2
+ import os
3
+ import sys
4
+
5
+
6
+ def set_terminal_title(title: str) -> None:
7
+ """Set terminal window title using an ANSI escape sequence."""
8
+ # Never write terminal control sequences when stdout is not a TTY (pipes/CI/redirects).
9
+ # This avoids corrupting machine-readable output (e.g., JSON streaming) and log captures.
10
+ #
11
+ # Use the original stdout to bypass prompt_toolkit's `patch_stdout()`. Writing OSC
12
+ # sequences to the patched stdout can cause them to appear as visible text.
13
+ stream = getattr(sys, "__stdout__", None) or sys.stdout
14
+ try:
15
+ if not stream.isatty():
16
+ return
17
+ except Exception:
18
+ return
19
+
20
+ stream.write(f"\033]0;{title}\007")
21
+ with contextlib.suppress(Exception):
22
+ stream.flush()
23
+
24
+
25
+ def update_terminal_title(model_name: str | None = None) -> None:
26
+ """Update terminal title with folder name and optional model name."""
27
+ folder_name = os.path.basename(os.getcwd())
28
+ if model_name:
29
+ set_terminal_title(f"{folder_name}: klaude ✳ {model_name}")
30
+ else:
31
+ set_terminal_title(f"{folder_name}: klaude")