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.
- klaude_code/app/__init__.py +12 -0
- klaude_code/app/runtime.py +215 -0
- klaude_code/cli/auth_cmd.py +2 -2
- klaude_code/cli/config_cmd.py +2 -2
- klaude_code/cli/cost_cmd.py +1 -1
- klaude_code/cli/debug.py +12 -36
- klaude_code/cli/list_model.py +3 -3
- klaude_code/cli/main.py +17 -60
- klaude_code/cli/self_update.py +2 -187
- klaude_code/cli/session_cmd.py +2 -2
- klaude_code/config/config.py +1 -1
- klaude_code/config/select_model.py +1 -1
- klaude_code/const.py +10 -1
- klaude_code/core/agent.py +9 -62
- klaude_code/core/agent_profile.py +284 -0
- klaude_code/core/executor.py +343 -230
- klaude_code/core/manager/llm_clients_builder.py +1 -1
- klaude_code/core/manager/sub_agent_manager.py +16 -29
- klaude_code/core/reminders.py +107 -155
- klaude_code/core/task.py +12 -20
- klaude_code/core/tool/__init__.py +5 -19
- klaude_code/core/tool/context.py +84 -0
- klaude_code/core/tool/file/apply_patch_tool.py +18 -21
- klaude_code/core/tool/file/edit_tool.py +42 -44
- klaude_code/core/tool/file/read_tool.py +14 -9
- klaude_code/core/tool/file/write_tool.py +12 -13
- klaude_code/core/tool/report_back_tool.py +4 -1
- klaude_code/core/tool/shell/bash_tool.py +6 -11
- klaude_code/core/tool/skill/skill_tool.py +3 -1
- klaude_code/core/tool/sub_agent_tool.py +8 -7
- klaude_code/core/tool/todo/todo_write_tool.py +3 -9
- klaude_code/core/tool/todo/update_plan_tool.py +3 -5
- klaude_code/core/tool/tool_abc.py +2 -1
- klaude_code/core/tool/tool_registry.py +2 -33
- klaude_code/core/tool/tool_runner.py +13 -10
- klaude_code/core/tool/web/mermaid_tool.py +3 -1
- klaude_code/core/tool/web/web_fetch_tool.py +5 -3
- klaude_code/core/tool/web/web_search_tool.py +5 -3
- klaude_code/core/turn.py +86 -26
- klaude_code/llm/anthropic/client.py +1 -1
- klaude_code/llm/bedrock/client.py +1 -1
- klaude_code/llm/claude/client.py +1 -1
- klaude_code/llm/codex/client.py +1 -1
- klaude_code/llm/google/client.py +1 -1
- klaude_code/llm/openai_compatible/client.py +1 -1
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +1 -1
- klaude_code/llm/openrouter/client.py +1 -1
- klaude_code/llm/openrouter/reasoning.py +1 -1
- klaude_code/llm/responses/client.py +1 -1
- klaude_code/protocol/events/__init__.py +57 -0
- klaude_code/protocol/events/base.py +18 -0
- klaude_code/protocol/events/chat.py +20 -0
- klaude_code/protocol/events/lifecycle.py +22 -0
- klaude_code/protocol/events/metadata.py +15 -0
- klaude_code/protocol/events/streaming.py +43 -0
- klaude_code/protocol/events/system.py +53 -0
- klaude_code/protocol/events/tools.py +23 -0
- klaude_code/protocol/message.py +3 -11
- klaude_code/protocol/model.py +78 -9
- klaude_code/protocol/op.py +5 -0
- klaude_code/protocol/sub_agent/explore.py +0 -15
- klaude_code/protocol/sub_agent/task.py +1 -1
- klaude_code/protocol/sub_agent/web.py +1 -17
- klaude_code/protocol/tools.py +0 -1
- klaude_code/session/session.py +6 -5
- klaude_code/skill/assets/create-plan/SKILL.md +76 -0
- klaude_code/skill/loader.py +1 -1
- klaude_code/skill/system_skills.py +1 -1
- klaude_code/tui/__init__.py +8 -0
- klaude_code/{command → tui/command}/clear_cmd.py +2 -1
- klaude_code/{command → tui/command}/debug_cmd.py +4 -3
- klaude_code/{command → tui/command}/export_cmd.py +2 -1
- klaude_code/{command → tui/command}/export_online_cmd.py +6 -5
- klaude_code/{command → tui/command}/fork_session_cmd.py +10 -9
- klaude_code/{command → tui/command}/help_cmd.py +3 -2
- klaude_code/{command → tui/command}/model_cmd.py +5 -4
- klaude_code/{command → tui/command}/model_select.py +2 -2
- klaude_code/{command → tui/command}/prompt_command.py +4 -3
- klaude_code/{command → tui/command}/refresh_cmd.py +3 -1
- klaude_code/{command → tui/command}/registry.py +16 -6
- klaude_code/{command → tui/command}/release_notes_cmd.py +3 -2
- klaude_code/{command → tui/command}/resume_cmd.py +6 -5
- klaude_code/{command → tui/command}/status_cmd.py +4 -3
- klaude_code/{command → tui/command}/terminal_setup_cmd.py +4 -3
- klaude_code/{command → tui/command}/thinking_cmd.py +4 -3
- klaude_code/tui/commands.py +164 -0
- klaude_code/{ui/renderers → tui/components}/assistant.py +3 -3
- klaude_code/{ui/renderers → tui/components}/bash_syntax.py +2 -2
- klaude_code/{ui/renderers → tui/components}/common.py +1 -1
- klaude_code/tui/components/developer.py +231 -0
- klaude_code/{ui/renderers → tui/components}/diffs.py +2 -2
- klaude_code/{ui/renderers → tui/components}/errors.py +2 -2
- klaude_code/{ui/renderers → tui/components}/metadata.py +34 -21
- klaude_code/{ui → tui/components}/rich/markdown.py +78 -34
- klaude_code/{ui → tui/components}/rich/status.py +2 -2
- klaude_code/{ui → tui/components}/rich/theme.py +12 -5
- klaude_code/{ui/renderers → tui/components}/sub_agent.py +23 -43
- klaude_code/{ui/renderers → tui/components}/thinking.py +3 -3
- klaude_code/{ui/renderers → tui/components}/tools.py +11 -48
- klaude_code/{ui/renderers → tui/components}/user_input.py +3 -20
- klaude_code/tui/display.py +85 -0
- klaude_code/{ui/modes/repl → tui/input}/__init__.py +1 -1
- klaude_code/{ui/modes/repl → tui/input}/completers.py +1 -1
- klaude_code/{ui/modes/repl/input_prompt_toolkit.py → tui/input/prompt_toolkit.py} +11 -7
- klaude_code/tui/machine.py +606 -0
- klaude_code/tui/renderer.py +707 -0
- klaude_code/tui/runner.py +321 -0
- klaude_code/tui/terminal/__init__.py +56 -0
- klaude_code/{ui → tui}/terminal/color.py +1 -1
- klaude_code/{ui → tui}/terminal/control.py +1 -1
- klaude_code/{ui → tui}/terminal/notifier.py +1 -1
- klaude_code/{ui → tui}/terminal/selector.py +36 -17
- klaude_code/ui/__init__.py +6 -50
- klaude_code/ui/core/display.py +3 -3
- klaude_code/ui/core/input.py +2 -1
- klaude_code/ui/{modes/debug/display.py → debug_mode.py} +1 -1
- klaude_code/ui/{modes/exec/display.py → exec_mode.py} +1 -4
- klaude_code/ui/terminal/__init__.py +6 -54
- klaude_code/ui/terminal/title.py +31 -0
- klaude_code/update.py +163 -0
- {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/METADATA +1 -1
- klaude_code-2.1.0.dist-info/RECORD +235 -0
- klaude_code/cli/runtime.py +0 -525
- klaude_code/core/prompt.py +0 -108
- klaude_code/core/tool/file/move_tool.md +0 -41
- klaude_code/core/tool/file/move_tool.py +0 -435
- klaude_code/core/tool/tool_context.py +0 -148
- klaude_code/protocol/events.py +0 -194
- klaude_code/skill/assets/dev-docs/SKILL.md +0 -108
- klaude_code/trace/__init__.py +0 -21
- klaude_code/ui/core/stage_manager.py +0 -48
- klaude_code/ui/modes/__init__.py +0 -1
- klaude_code/ui/modes/debug/__init__.py +0 -1
- klaude_code/ui/modes/exec/__init__.py +0 -1
- klaude_code/ui/modes/repl/display.py +0 -61
- klaude_code/ui/modes/repl/event_handler.py +0 -634
- klaude_code/ui/modes/repl/renderer.py +0 -463
- klaude_code/ui/renderers/developer.py +0 -215
- klaude_code/ui/utils/__init__.py +0 -1
- klaude_code-2.0.1.dist-info/RECORD +0 -229
- /klaude_code/{trace/log.py → log.py} +0 -0
- /klaude_code/{command → tui/command}/__init__.py +0 -0
- /klaude_code/{command → tui/command}/command_abc.py +0 -0
- /klaude_code/{command → tui/command}/prompt-commit.md +0 -0
- /klaude_code/{command → tui/command}/prompt-init.md +0 -0
- /klaude_code/{ui/renderers → tui/components}/__init__.py +0 -0
- /klaude_code/{ui/renderers → tui/components}/mermaid_viewer.py +0 -0
- /klaude_code/{ui → tui/components}/rich/__init__.py +0 -0
- /klaude_code/{ui → tui/components}/rich/cjk_wrap.py +0 -0
- /klaude_code/{ui → tui/components}/rich/code_panel.py +0 -0
- /klaude_code/{ui → tui/components}/rich/live.py +0 -0
- /klaude_code/{ui → tui/components}/rich/quote.py +0 -0
- /klaude_code/{ui → tui/components}/rich/searchable_text.py +0 -0
- /klaude_code/{ui/modes/repl → tui/input}/clipboard.py +0 -0
- /klaude_code/{ui/modes/repl → tui/input}/key_bindings.py +0 -0
- /klaude_code/{ui → tui}/terminal/image.py +0 -0
- /klaude_code/{ui → tui}/terminal/progress_bar.py +0 -0
- /klaude_code/ui/{utils/common.py → common.py} +0 -0
- {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/WHEEL +0 -0
- {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
|
|
@@ -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.
|
|
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
|
-
|
|
307
|
-
|
|
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,
|
|
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
|
-
|
|
630
|
-
|
|
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:
|
klaude_code/ui/__init__.py
CHANGED
|
@@ -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
|
-
|
|
8
|
-
|
|
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
|
-
|
|
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 .
|
|
24
|
-
from .
|
|
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
|
]
|
klaude_code/ui/core/display.py
CHANGED
|
@@ -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 (
|
|
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
|
|
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:
|
klaude_code/ui/core/input.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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)
|