klaude-code 1.2.6__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- klaude_code/__init__.py +0 -0
- klaude_code/cli/__init__.py +1 -0
- klaude_code/cli/main.py +298 -0
- klaude_code/cli/runtime.py +331 -0
- klaude_code/cli/session_cmd.py +80 -0
- klaude_code/command/__init__.py +43 -0
- klaude_code/command/clear_cmd.py +20 -0
- klaude_code/command/command_abc.py +92 -0
- klaude_code/command/diff_cmd.py +138 -0
- klaude_code/command/export_cmd.py +86 -0
- klaude_code/command/help_cmd.py +51 -0
- klaude_code/command/model_cmd.py +43 -0
- klaude_code/command/prompt-dev-docs-update.md +56 -0
- klaude_code/command/prompt-dev-docs.md +46 -0
- klaude_code/command/prompt-init.md +45 -0
- klaude_code/command/prompt_command.py +69 -0
- klaude_code/command/refresh_cmd.py +43 -0
- klaude_code/command/registry.py +110 -0
- klaude_code/command/status_cmd.py +111 -0
- klaude_code/command/terminal_setup_cmd.py +252 -0
- klaude_code/config/__init__.py +11 -0
- klaude_code/config/config.py +177 -0
- klaude_code/config/list_model.py +162 -0
- klaude_code/config/select_model.py +67 -0
- klaude_code/const/__init__.py +133 -0
- klaude_code/core/__init__.py +0 -0
- klaude_code/core/agent.py +165 -0
- klaude_code/core/executor.py +485 -0
- klaude_code/core/manager/__init__.py +19 -0
- klaude_code/core/manager/agent_manager.py +127 -0
- klaude_code/core/manager/llm_clients.py +42 -0
- klaude_code/core/manager/llm_clients_builder.py +49 -0
- klaude_code/core/manager/sub_agent_manager.py +86 -0
- klaude_code/core/prompt.py +89 -0
- klaude_code/core/prompts/prompt-claude-code.md +98 -0
- klaude_code/core/prompts/prompt-codex.md +331 -0
- klaude_code/core/prompts/prompt-gemini.md +43 -0
- klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
- klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
- klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
- klaude_code/core/prompts/prompt-subagent.md +8 -0
- klaude_code/core/reminders.py +445 -0
- klaude_code/core/task.py +237 -0
- klaude_code/core/tool/__init__.py +75 -0
- klaude_code/core/tool/file/__init__.py +0 -0
- klaude_code/core/tool/file/apply_patch.py +492 -0
- klaude_code/core/tool/file/apply_patch_tool.md +1 -0
- klaude_code/core/tool/file/apply_patch_tool.py +204 -0
- klaude_code/core/tool/file/edit_tool.md +9 -0
- klaude_code/core/tool/file/edit_tool.py +274 -0
- klaude_code/core/tool/file/multi_edit_tool.md +42 -0
- klaude_code/core/tool/file/multi_edit_tool.py +199 -0
- klaude_code/core/tool/file/read_tool.md +14 -0
- klaude_code/core/tool/file/read_tool.py +326 -0
- klaude_code/core/tool/file/write_tool.md +8 -0
- klaude_code/core/tool/file/write_tool.py +146 -0
- klaude_code/core/tool/memory/__init__.py +0 -0
- klaude_code/core/tool/memory/memory_tool.md +16 -0
- klaude_code/core/tool/memory/memory_tool.py +462 -0
- klaude_code/core/tool/memory/skill_loader.py +245 -0
- klaude_code/core/tool/memory/skill_tool.md +24 -0
- klaude_code/core/tool/memory/skill_tool.py +97 -0
- klaude_code/core/tool/shell/__init__.py +0 -0
- klaude_code/core/tool/shell/bash_tool.md +43 -0
- klaude_code/core/tool/shell/bash_tool.py +123 -0
- klaude_code/core/tool/shell/command_safety.py +363 -0
- klaude_code/core/tool/sub_agent_tool.py +83 -0
- klaude_code/core/tool/todo/__init__.py +0 -0
- klaude_code/core/tool/todo/todo_write_tool.md +182 -0
- klaude_code/core/tool/todo/todo_write_tool.py +121 -0
- klaude_code/core/tool/todo/update_plan_tool.md +3 -0
- klaude_code/core/tool/todo/update_plan_tool.py +104 -0
- klaude_code/core/tool/tool_abc.py +25 -0
- klaude_code/core/tool/tool_context.py +106 -0
- klaude_code/core/tool/tool_registry.py +78 -0
- klaude_code/core/tool/tool_runner.py +252 -0
- klaude_code/core/tool/truncation.py +170 -0
- klaude_code/core/tool/web/__init__.py +0 -0
- klaude_code/core/tool/web/mermaid_tool.md +21 -0
- klaude_code/core/tool/web/mermaid_tool.py +76 -0
- klaude_code/core/tool/web/web_fetch_tool.md +8 -0
- klaude_code/core/tool/web/web_fetch_tool.py +159 -0
- klaude_code/core/turn.py +220 -0
- klaude_code/llm/__init__.py +21 -0
- klaude_code/llm/anthropic/__init__.py +3 -0
- klaude_code/llm/anthropic/client.py +221 -0
- klaude_code/llm/anthropic/input.py +200 -0
- klaude_code/llm/client.py +49 -0
- klaude_code/llm/input_common.py +239 -0
- klaude_code/llm/openai_compatible/__init__.py +3 -0
- klaude_code/llm/openai_compatible/client.py +211 -0
- klaude_code/llm/openai_compatible/input.py +109 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
- klaude_code/llm/openrouter/__init__.py +3 -0
- klaude_code/llm/openrouter/client.py +200 -0
- klaude_code/llm/openrouter/input.py +160 -0
- klaude_code/llm/openrouter/reasoning_handler.py +209 -0
- klaude_code/llm/registry.py +22 -0
- klaude_code/llm/responses/__init__.py +3 -0
- klaude_code/llm/responses/client.py +216 -0
- klaude_code/llm/responses/input.py +167 -0
- klaude_code/llm/usage.py +109 -0
- klaude_code/protocol/__init__.py +4 -0
- klaude_code/protocol/commands.py +21 -0
- klaude_code/protocol/events.py +163 -0
- klaude_code/protocol/llm_param.py +147 -0
- klaude_code/protocol/model.py +287 -0
- klaude_code/protocol/op.py +89 -0
- klaude_code/protocol/op_handler.py +28 -0
- klaude_code/protocol/sub_agent.py +348 -0
- klaude_code/protocol/tools.py +15 -0
- klaude_code/session/__init__.py +4 -0
- klaude_code/session/export.py +624 -0
- klaude_code/session/selector.py +76 -0
- klaude_code/session/session.py +474 -0
- klaude_code/session/templates/export_session.html +1434 -0
- klaude_code/trace/__init__.py +3 -0
- klaude_code/trace/log.py +168 -0
- klaude_code/ui/__init__.py +91 -0
- klaude_code/ui/core/__init__.py +1 -0
- klaude_code/ui/core/display.py +103 -0
- klaude_code/ui/core/input.py +71 -0
- klaude_code/ui/core/stage_manager.py +55 -0
- klaude_code/ui/modes/__init__.py +1 -0
- klaude_code/ui/modes/debug/__init__.py +1 -0
- klaude_code/ui/modes/debug/display.py +36 -0
- klaude_code/ui/modes/exec/__init__.py +1 -0
- klaude_code/ui/modes/exec/display.py +63 -0
- klaude_code/ui/modes/repl/__init__.py +51 -0
- klaude_code/ui/modes/repl/clipboard.py +152 -0
- klaude_code/ui/modes/repl/completers.py +429 -0
- klaude_code/ui/modes/repl/display.py +60 -0
- klaude_code/ui/modes/repl/event_handler.py +375 -0
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
- klaude_code/ui/modes/repl/key_bindings.py +170 -0
- klaude_code/ui/modes/repl/renderer.py +281 -0
- klaude_code/ui/renderers/__init__.py +0 -0
- klaude_code/ui/renderers/assistant.py +21 -0
- klaude_code/ui/renderers/common.py +8 -0
- klaude_code/ui/renderers/developer.py +158 -0
- klaude_code/ui/renderers/diffs.py +215 -0
- klaude_code/ui/renderers/errors.py +16 -0
- klaude_code/ui/renderers/metadata.py +190 -0
- klaude_code/ui/renderers/sub_agent.py +71 -0
- klaude_code/ui/renderers/thinking.py +39 -0
- klaude_code/ui/renderers/tools.py +551 -0
- klaude_code/ui/renderers/user_input.py +65 -0
- klaude_code/ui/rich/__init__.py +1 -0
- klaude_code/ui/rich/live.py +65 -0
- klaude_code/ui/rich/markdown.py +308 -0
- klaude_code/ui/rich/quote.py +34 -0
- klaude_code/ui/rich/searchable_text.py +71 -0
- klaude_code/ui/rich/status.py +240 -0
- klaude_code/ui/rich/theme.py +274 -0
- klaude_code/ui/terminal/__init__.py +1 -0
- klaude_code/ui/terminal/color.py +244 -0
- klaude_code/ui/terminal/control.py +147 -0
- klaude_code/ui/terminal/notifier.py +107 -0
- klaude_code/ui/terminal/progress_bar.py +87 -0
- klaude_code/ui/utils/__init__.py +1 -0
- klaude_code/ui/utils/common.py +108 -0
- klaude_code/ui/utils/debouncer.py +42 -0
- klaude_code/version.py +163 -0
- klaude_code-1.2.6.dist-info/METADATA +178 -0
- klaude_code-1.2.6.dist-info/RECORD +167 -0
- klaude_code-1.2.6.dist-info/WHEEL +4 -0
- klaude_code-1.2.6.dist-info/entry_points.txt +3 -0
klaude_code/trace/log.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from logging.handlers import RotatingFileHandler
|
|
4
|
+
from typing import Iterable
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.logging import RichHandler
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
|
|
10
|
+
from klaude_code import const
|
|
11
|
+
|
|
12
|
+
# Module-level logger
|
|
13
|
+
logger = logging.getLogger("klaude_code")
|
|
14
|
+
logger.setLevel(logging.DEBUG)
|
|
15
|
+
|
|
16
|
+
# Console for direct output (user-facing messages)
|
|
17
|
+
log_console = Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class DebugType(str, Enum):
|
|
21
|
+
"""Debug message categories for filtering."""
|
|
22
|
+
|
|
23
|
+
GENERAL = "general"
|
|
24
|
+
LLM_CONFIG = "llm_config"
|
|
25
|
+
LLM_PAYLOAD = "llm_payload"
|
|
26
|
+
LLM_STREAM = "llm_stream"
|
|
27
|
+
UI_EVENT = "ui_event"
|
|
28
|
+
RESPONSE = "response"
|
|
29
|
+
EXECUTION = "execution"
|
|
30
|
+
TERMINAL = "terminal"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class DebugTypeFilter(logging.Filter):
|
|
34
|
+
"""Filter log records based on DebugType."""
|
|
35
|
+
|
|
36
|
+
def __init__(self, allowed_types: set[DebugType] | None = None):
|
|
37
|
+
super().__init__()
|
|
38
|
+
self.allowed_types = allowed_types
|
|
39
|
+
|
|
40
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
41
|
+
if self.allowed_types is None:
|
|
42
|
+
return True
|
|
43
|
+
debug_type = getattr(record, "debug_type", DebugType.GENERAL)
|
|
44
|
+
return debug_type in self.allowed_types
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# Handler references for reconfiguration
|
|
48
|
+
_file_handler: RotatingFileHandler | None = None
|
|
49
|
+
_console_handler: RichHandler | None = None
|
|
50
|
+
_debug_filter: DebugTypeFilter | None = None
|
|
51
|
+
_debug_enabled = False
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def set_debug_logging(
|
|
55
|
+
enabled: bool,
|
|
56
|
+
*,
|
|
57
|
+
write_to_file: bool | None = None,
|
|
58
|
+
log_file: str | None = None,
|
|
59
|
+
filters: set[DebugType] | None = None,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Configure global debug logging behavior.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
enabled: Enable or disable debug logging
|
|
65
|
+
write_to_file: If True, write to file; if False, output to console
|
|
66
|
+
log_file: Path to the log file (default: debug.log)
|
|
67
|
+
filters: Set of DebugType to include; None means all types
|
|
68
|
+
"""
|
|
69
|
+
global _file_handler, _console_handler, _debug_filter, _debug_enabled
|
|
70
|
+
|
|
71
|
+
_debug_enabled = enabled
|
|
72
|
+
|
|
73
|
+
# Remove existing handlers
|
|
74
|
+
if _file_handler is not None:
|
|
75
|
+
logger.removeHandler(_file_handler)
|
|
76
|
+
_file_handler.close()
|
|
77
|
+
_file_handler = None
|
|
78
|
+
if _console_handler is not None:
|
|
79
|
+
logger.removeHandler(_console_handler)
|
|
80
|
+
_console_handler = None
|
|
81
|
+
|
|
82
|
+
if not enabled:
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
# Create filter
|
|
86
|
+
_debug_filter = DebugTypeFilter(filters)
|
|
87
|
+
|
|
88
|
+
# Determine output mode
|
|
89
|
+
use_file = write_to_file if write_to_file is not None else True
|
|
90
|
+
file_path = log_file if log_file is not None else const.DEFAULT_DEBUG_LOG_FILE
|
|
91
|
+
|
|
92
|
+
if use_file:
|
|
93
|
+
_file_handler = RotatingFileHandler(
|
|
94
|
+
file_path,
|
|
95
|
+
maxBytes=const.LOG_MAX_BYTES,
|
|
96
|
+
backupCount=const.LOG_BACKUP_COUNT,
|
|
97
|
+
encoding="utf-8",
|
|
98
|
+
)
|
|
99
|
+
_file_handler.setLevel(logging.DEBUG)
|
|
100
|
+
_file_handler.setFormatter(logging.Formatter("[%(asctime)s] %(debug_type_label)-12s %(message)s"))
|
|
101
|
+
_file_handler.addFilter(_debug_filter)
|
|
102
|
+
logger.addHandler(_file_handler)
|
|
103
|
+
else:
|
|
104
|
+
# Console handler with Rich formatting
|
|
105
|
+
_console_handler = RichHandler(
|
|
106
|
+
console=log_console,
|
|
107
|
+
show_time=False,
|
|
108
|
+
show_path=False,
|
|
109
|
+
rich_tracebacks=True,
|
|
110
|
+
)
|
|
111
|
+
_console_handler.setLevel(logging.DEBUG)
|
|
112
|
+
_console_handler.addFilter(_debug_filter)
|
|
113
|
+
logger.addHandler(_console_handler)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def log(*objects: str | tuple[str, str], style: str = "") -> None:
|
|
117
|
+
"""Output user-facing messages to console.
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
objects: Strings or (text, style) tuples to print
|
|
121
|
+
style: Default style for all objects
|
|
122
|
+
"""
|
|
123
|
+
log_console.print(
|
|
124
|
+
*((Text(obj[0], style=obj[1]) if isinstance(obj, tuple) else Text(obj)) for obj in objects),
|
|
125
|
+
style=style,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def log_debug(
|
|
130
|
+
*objects: str | tuple[str, str],
|
|
131
|
+
style: str | None = None,
|
|
132
|
+
debug_type: DebugType = DebugType.GENERAL,
|
|
133
|
+
) -> None:
|
|
134
|
+
"""Log debug messages with category support.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
objects: Strings or (text, style) tuples to log
|
|
138
|
+
style: Style hint (used for console output)
|
|
139
|
+
debug_type: Category of the debug message
|
|
140
|
+
"""
|
|
141
|
+
if not _debug_enabled:
|
|
142
|
+
return
|
|
143
|
+
|
|
144
|
+
message = _build_message(objects)
|
|
145
|
+
|
|
146
|
+
# Create log record with extra fields
|
|
147
|
+
extra = {
|
|
148
|
+
"debug_type": debug_type,
|
|
149
|
+
"debug_type_label": debug_type.value.upper(),
|
|
150
|
+
"style": style,
|
|
151
|
+
}
|
|
152
|
+
logger.debug(message, extra=extra)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _build_message(objects: Iterable[str | tuple[str, str]]) -> str:
|
|
156
|
+
"""Build plain text message from objects."""
|
|
157
|
+
parts: list[str] = []
|
|
158
|
+
for obj in objects:
|
|
159
|
+
if isinstance(obj, tuple):
|
|
160
|
+
parts.append(obj[0])
|
|
161
|
+
else:
|
|
162
|
+
parts.append(obj)
|
|
163
|
+
return " ".join(parts)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def is_debug_enabled() -> bool:
|
|
167
|
+
"""Check if debug logging is currently enabled."""
|
|
168
|
+
return _debug_enabled
|
|
@@ -0,0 +1,91 @@
|
|
|
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:
|
|
6
|
+
|
|
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
|
|
11
|
+
|
|
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
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# --- Abstract Interfaces ---
|
|
21
|
+
from .core.display import DisplayABC
|
|
22
|
+
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
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def create_exec_display(debug: bool = False, stream_json: bool = False) -> DisplayABC:
|
|
57
|
+
"""
|
|
58
|
+
Create a display for exec (non-interactive) mode.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
debug: If True, wrap the display with DebugEventDisplay to log all events.
|
|
62
|
+
stream_json: If True, stream all events as JSON lines instead of normal output.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
A DisplayABC implementation that only outputs task results.
|
|
66
|
+
"""
|
|
67
|
+
if stream_json:
|
|
68
|
+
return StreamJsonDisplay()
|
|
69
|
+
exec_display = ExecDisplay()
|
|
70
|
+
if debug:
|
|
71
|
+
return DebugEventDisplay(exec_display)
|
|
72
|
+
return exec_display
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
__all__ = [
|
|
76
|
+
# Abstract interfaces
|
|
77
|
+
"DisplayABC",
|
|
78
|
+
"InputProviderABC",
|
|
79
|
+
# Display mode implementations
|
|
80
|
+
"REPLDisplay",
|
|
81
|
+
"ExecDisplay",
|
|
82
|
+
"StreamJsonDisplay",
|
|
83
|
+
"DebugEventDisplay",
|
|
84
|
+
# Input implementations
|
|
85
|
+
"PromptToolkitInput",
|
|
86
|
+
# Factory functions
|
|
87
|
+
"create_default_display",
|
|
88
|
+
"create_exec_display",
|
|
89
|
+
# Supporting types
|
|
90
|
+
"TerminalNotifier",
|
|
91
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Core UI abstractions
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from asyncio import Queue
|
|
3
|
+
|
|
4
|
+
from klaude_code.protocol import events
|
|
5
|
+
from klaude_code.trace import log
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class DisplayABC(ABC):
|
|
9
|
+
"""
|
|
10
|
+
Abstract base class for UI display implementations.
|
|
11
|
+
|
|
12
|
+
A Display is responsible for rendering events from the executor to the user.
|
|
13
|
+
Implementations can range from simple text output (ExecDisplay) to rich
|
|
14
|
+
interactive terminals (REPLDisplay) or debugging wrappers (DebugEventDisplay).
|
|
15
|
+
|
|
16
|
+
Lifecycle:
|
|
17
|
+
1. start() is called once before any events are consumed.
|
|
18
|
+
2. consume_event() is called for each event from the executor.
|
|
19
|
+
3. stop() is called once when the display is shutting down (after EndEvent).
|
|
20
|
+
|
|
21
|
+
Typical Usage:
|
|
22
|
+
display = create_default_display()
|
|
23
|
+
await display.consume_event_loop(event_queue)
|
|
24
|
+
|
|
25
|
+
# Or manually:
|
|
26
|
+
await display.start()
|
|
27
|
+
try:
|
|
28
|
+
async for event in events:
|
|
29
|
+
if isinstance(event, EndEvent):
|
|
30
|
+
break
|
|
31
|
+
await display.consume_event(event)
|
|
32
|
+
finally:
|
|
33
|
+
await display.stop()
|
|
34
|
+
|
|
35
|
+
Thread Safety:
|
|
36
|
+
Display implementations should be used from a single async task.
|
|
37
|
+
The consume_event_loop method handles the standard event loop pattern.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
async def consume_event(self, event: events.Event) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Process a single event from the executor.
|
|
44
|
+
|
|
45
|
+
This method is called for each event except EndEvent, which triggers stop().
|
|
46
|
+
Implementations should handle all relevant event types and render them
|
|
47
|
+
appropriately for the user.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
event: The event to process. See klaude_code.protocol.events for types.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
async def start(self) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Initialize the display before processing events.
|
|
57
|
+
|
|
58
|
+
Called once before any consume_event calls. Use this for any setup
|
|
59
|
+
that needs to happen before rendering begins (e.g., initializing
|
|
60
|
+
terminal state, starting background tasks).
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
@abstractmethod
|
|
64
|
+
async def stop(self) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Clean up the display after all events have been processed.
|
|
67
|
+
|
|
68
|
+
Called once after EndEvent is received. Use this for cleanup such as
|
|
69
|
+
stopping spinners, restoring terminal state, or flushing output buffers.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
async def consume_event_loop(self, q: Queue[events.Event]) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Main event loop that processes events from a queue.
|
|
75
|
+
|
|
76
|
+
This is the standard entry point for running a display. It handles:
|
|
77
|
+
- Calling start() before processing
|
|
78
|
+
- Consuming events until EndEvent is received
|
|
79
|
+
- Calling stop() after EndEvent
|
|
80
|
+
- Error handling and logging for individual events
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
q: An asyncio Queue of events to process. The loop exits when
|
|
84
|
+
an EndEvent is received.
|
|
85
|
+
"""
|
|
86
|
+
await self.start()
|
|
87
|
+
while True:
|
|
88
|
+
event = await q.get()
|
|
89
|
+
try:
|
|
90
|
+
if isinstance(event, events.EndEvent):
|
|
91
|
+
await self.stop()
|
|
92
|
+
break
|
|
93
|
+
await self.consume_event(event)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
import traceback
|
|
96
|
+
|
|
97
|
+
log(
|
|
98
|
+
f"Error in consume_event_loop, {e.__class__.__name__}, {e}",
|
|
99
|
+
style="red",
|
|
100
|
+
)
|
|
101
|
+
log(traceback.format_exc(), style="red")
|
|
102
|
+
finally:
|
|
103
|
+
q.task_done()
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import AsyncIterator
|
|
5
|
+
|
|
6
|
+
from klaude_code.protocol.model import UserInputPayload
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InputProviderABC(ABC):
|
|
10
|
+
"""
|
|
11
|
+
Abstract base class for user input providers.
|
|
12
|
+
|
|
13
|
+
An InputProvider is responsible for collecting user input and yielding it
|
|
14
|
+
to the application. Implementations handle the specifics of input collection,
|
|
15
|
+
such as terminal readline, prompt-toolkit sessions, or other input sources.
|
|
16
|
+
|
|
17
|
+
Lifecycle:
|
|
18
|
+
1. start() is called once before any inputs are requested.
|
|
19
|
+
2. iter_inputs() yields user input strings until the user exits.
|
|
20
|
+
3. stop() is called once when input collection is complete.
|
|
21
|
+
|
|
22
|
+
Typical Usage:
|
|
23
|
+
input_provider = PromptToolkitInput(status_provider=my_status_fn)
|
|
24
|
+
await input_provider.start()
|
|
25
|
+
try:
|
|
26
|
+
async for user_input in input_provider.iter_inputs():
|
|
27
|
+
if user_input.text.strip().lower() in {"exit", "quit"}:
|
|
28
|
+
break
|
|
29
|
+
# Process user_input.text and user_input.images...
|
|
30
|
+
finally:
|
|
31
|
+
await input_provider.stop()
|
|
32
|
+
|
|
33
|
+
Thread Safety:
|
|
34
|
+
Input providers should be used from a single async task.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
async def start(self) -> None:
|
|
39
|
+
"""
|
|
40
|
+
Initialize the input provider before reading inputs.
|
|
41
|
+
|
|
42
|
+
Called once before iter_inputs(). Use this for any setup that needs
|
|
43
|
+
to happen before input collection begins (e.g., configuring terminal
|
|
44
|
+
settings, loading history).
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
async def stop(self) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Clean up the input provider after input collection is complete.
|
|
51
|
+
|
|
52
|
+
Called once after iter_inputs() finishes. Use this for cleanup such
|
|
53
|
+
as saving history, restoring terminal state, or releasing resources.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
async def iter_inputs(self) -> AsyncIterator[UserInputPayload]:
|
|
58
|
+
"""
|
|
59
|
+
Yield user input payloads asynchronously.
|
|
60
|
+
|
|
61
|
+
This is the main method for collecting user input. Each yield returns
|
|
62
|
+
one complete user input payload containing text and optional images
|
|
63
|
+
(e.g., after the user presses Enter). The iterator completes when the
|
|
64
|
+
user signals end of input (e.g., Ctrl+D) or when the application
|
|
65
|
+
requests shutdown.
|
|
66
|
+
|
|
67
|
+
Yields:
|
|
68
|
+
UserInputPayload with text and optional images.
|
|
69
|
+
"""
|
|
70
|
+
raise NotImplementedError
|
|
71
|
+
yield UserInputPayload(text="") # pyright: ignore[reportUnreachable]
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Awaitable, Callable
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Stage(Enum):
|
|
8
|
+
WAITING = "waiting"
|
|
9
|
+
THINKING = "thinking"
|
|
10
|
+
ASSISTANT = "assistant"
|
|
11
|
+
TOOL_CALL = "tool_call"
|
|
12
|
+
TOOL_RESULT = "tool_result"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class StageManager:
|
|
16
|
+
"""Manage display stage transitions and invoke lifecycle callbacks."""
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
*,
|
|
21
|
+
finish_assistant: Callable[[], Awaitable[None]],
|
|
22
|
+
on_enter_thinking: Callable[[], None],
|
|
23
|
+
):
|
|
24
|
+
self._stage = Stage.WAITING
|
|
25
|
+
self._finish_assistant = finish_assistant
|
|
26
|
+
self._on_enter_thinking = on_enter_thinking
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def current_stage(self) -> Stage:
|
|
30
|
+
return self._stage
|
|
31
|
+
|
|
32
|
+
async def transition_to(self, new_stage: Stage) -> None:
|
|
33
|
+
if self._stage == new_stage:
|
|
34
|
+
return
|
|
35
|
+
await self._leave_current_stage()
|
|
36
|
+
self._stage = new_stage
|
|
37
|
+
|
|
38
|
+
async def enter_thinking_stage(self) -> None:
|
|
39
|
+
if self._stage == Stage.THINKING:
|
|
40
|
+
return
|
|
41
|
+
await self.transition_to(Stage.THINKING)
|
|
42
|
+
self._on_enter_thinking()
|
|
43
|
+
|
|
44
|
+
async def finish_assistant(self) -> None:
|
|
45
|
+
if self._stage != Stage.ASSISTANT:
|
|
46
|
+
await self._finish_assistant()
|
|
47
|
+
return
|
|
48
|
+
await self._finish_assistant()
|
|
49
|
+
self._stage = Stage.WAITING
|
|
50
|
+
|
|
51
|
+
async def _leave_current_stage(self) -> None:
|
|
52
|
+
if self._stage == Stage.ASSISTANT:
|
|
53
|
+
await self.finish_assistant()
|
|
54
|
+
elif self._stage != Stage.WAITING:
|
|
55
|
+
self._stage = Stage.WAITING
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# UI mode implementations
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Debug mode
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from typing import override
|
|
2
|
+
|
|
3
|
+
from klaude_code import const
|
|
4
|
+
from klaude_code.protocol import events
|
|
5
|
+
from klaude_code.trace import DebugType, log_debug
|
|
6
|
+
from klaude_code.ui.core.display import DisplayABC
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DebugEventDisplay(DisplayABC):
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
wrapped_display: DisplayABC | None = None,
|
|
13
|
+
log_file: str = const.DEFAULT_DEBUG_LOG_FILE,
|
|
14
|
+
):
|
|
15
|
+
self.wrapped_display = wrapped_display
|
|
16
|
+
self.log_file = log_file
|
|
17
|
+
|
|
18
|
+
@override
|
|
19
|
+
async def consume_event(self, event: events.Event) -> None:
|
|
20
|
+
log_debug(
|
|
21
|
+
f"[{event.__class__.__name__}]",
|
|
22
|
+
event.model_dump_json(exclude_none=True),
|
|
23
|
+
style="magenta",
|
|
24
|
+
debug_type=DebugType.UI_EVENT,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
if self.wrapped_display:
|
|
28
|
+
await self.wrapped_display.consume_event(event)
|
|
29
|
+
|
|
30
|
+
async def start(self) -> None:
|
|
31
|
+
if self.wrapped_display:
|
|
32
|
+
await self.wrapped_display.start()
|
|
33
|
+
|
|
34
|
+
async def stop(self) -> None:
|
|
35
|
+
if self.wrapped_display:
|
|
36
|
+
await self.wrapped_display.stop()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Exec mode
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from typing import override
|
|
3
|
+
|
|
4
|
+
from klaude_code.protocol import events
|
|
5
|
+
from klaude_code.ui.core.display import DisplayABC
|
|
6
|
+
from klaude_code.ui.terminal.progress_bar import OSC94States, emit_osc94
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ExecDisplay(DisplayABC):
|
|
10
|
+
"""A display implementation for exec mode - only handles TaskFinishEvent."""
|
|
11
|
+
|
|
12
|
+
@override
|
|
13
|
+
async def consume_event(self, event: events.Event) -> None:
|
|
14
|
+
"""Only handle TaskFinishEvent."""
|
|
15
|
+
match event:
|
|
16
|
+
case events.TaskStartEvent():
|
|
17
|
+
emit_osc94(OSC94States.INDETERMINATE)
|
|
18
|
+
case events.ErrorEvent() as e:
|
|
19
|
+
emit_osc94(OSC94States.HIDDEN)
|
|
20
|
+
print(f"Error: {e.error_message}")
|
|
21
|
+
case events.TaskFinishEvent() as e:
|
|
22
|
+
emit_osc94(OSC94States.HIDDEN)
|
|
23
|
+
# Print the task result when task finishes
|
|
24
|
+
if e.task_result.strip():
|
|
25
|
+
print(e.task_result)
|
|
26
|
+
case _:
|
|
27
|
+
# Ignore all other events
|
|
28
|
+
pass
|
|
29
|
+
|
|
30
|
+
@override
|
|
31
|
+
async def start(self) -> None:
|
|
32
|
+
"""Do nothing on start."""
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
@override
|
|
36
|
+
async def stop(self) -> None:
|
|
37
|
+
"""Do nothing on stop."""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class StreamJsonDisplay(DisplayABC):
|
|
42
|
+
"""A display implementation that streams all events as JSON lines."""
|
|
43
|
+
|
|
44
|
+
@override
|
|
45
|
+
async def consume_event(self, event: events.Event) -> None:
|
|
46
|
+
"""Stream each event as a JSON line."""
|
|
47
|
+
if isinstance(event, events.EndEvent):
|
|
48
|
+
return
|
|
49
|
+
event_type = type(event).__name__
|
|
50
|
+
json_data = event.model_dump_json()
|
|
51
|
+
# Output format: {"type": "EventName", "data": {...}}
|
|
52
|
+
print(f'{{"type": "{event_type}", "data": {json_data}}}', flush=True)
|
|
53
|
+
sys.stdout.flush()
|
|
54
|
+
|
|
55
|
+
@override
|
|
56
|
+
async def start(self) -> None:
|
|
57
|
+
"""Do nothing on start."""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
@override
|
|
61
|
+
async def stop(self) -> None:
|
|
62
|
+
"""Do nothing on stop."""
|
|
63
|
+
pass
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from klaude_code.protocol import model
|
|
6
|
+
from klaude_code.ui.modes.repl.input_prompt_toolkit import REPLStatusSnapshot
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from klaude_code.core.agent import Agent
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_repl_status_snapshot(agent: "Agent | None", update_message: str | None) -> REPLStatusSnapshot:
|
|
13
|
+
"""Build a status snapshot for the REPL bottom toolbar.
|
|
14
|
+
|
|
15
|
+
Aggregates model name, context usage, and basic call counts from the
|
|
16
|
+
provided agent's session history.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
model_name = ""
|
|
20
|
+
context_usage_percent: float | None = None
|
|
21
|
+
llm_calls = 0
|
|
22
|
+
tool_calls = 0
|
|
23
|
+
|
|
24
|
+
if agent is not None:
|
|
25
|
+
profile = agent.profile
|
|
26
|
+
if profile is not None:
|
|
27
|
+
model_name = profile.llm_client.model_name or ""
|
|
28
|
+
else:
|
|
29
|
+
model_name = "N/A"
|
|
30
|
+
|
|
31
|
+
history = agent.session.conversation_history
|
|
32
|
+
for item in history:
|
|
33
|
+
if isinstance(item, model.AssistantMessageItem):
|
|
34
|
+
llm_calls += 1
|
|
35
|
+
elif isinstance(item, model.ToolCallItem):
|
|
36
|
+
tool_calls += 1
|
|
37
|
+
|
|
38
|
+
for item in reversed(history):
|
|
39
|
+
if isinstance(item, model.ResponseMetadataItem):
|
|
40
|
+
usage = item.usage
|
|
41
|
+
if usage is not None and hasattr(usage, "context_usage_percent"):
|
|
42
|
+
context_usage_percent = usage.context_usage_percent
|
|
43
|
+
break
|
|
44
|
+
|
|
45
|
+
return REPLStatusSnapshot(
|
|
46
|
+
model_name=model_name,
|
|
47
|
+
context_usage_percent=context_usage_percent,
|
|
48
|
+
llm_calls=llm_calls,
|
|
49
|
+
tool_calls=tool_calls,
|
|
50
|
+
update_message=update_message,
|
|
51
|
+
)
|