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.
Files changed (167) hide show
  1. klaude_code/__init__.py +0 -0
  2. klaude_code/cli/__init__.py +1 -0
  3. klaude_code/cli/main.py +298 -0
  4. klaude_code/cli/runtime.py +331 -0
  5. klaude_code/cli/session_cmd.py +80 -0
  6. klaude_code/command/__init__.py +43 -0
  7. klaude_code/command/clear_cmd.py +20 -0
  8. klaude_code/command/command_abc.py +92 -0
  9. klaude_code/command/diff_cmd.py +138 -0
  10. klaude_code/command/export_cmd.py +86 -0
  11. klaude_code/command/help_cmd.py +51 -0
  12. klaude_code/command/model_cmd.py +43 -0
  13. klaude_code/command/prompt-dev-docs-update.md +56 -0
  14. klaude_code/command/prompt-dev-docs.md +46 -0
  15. klaude_code/command/prompt-init.md +45 -0
  16. klaude_code/command/prompt_command.py +69 -0
  17. klaude_code/command/refresh_cmd.py +43 -0
  18. klaude_code/command/registry.py +110 -0
  19. klaude_code/command/status_cmd.py +111 -0
  20. klaude_code/command/terminal_setup_cmd.py +252 -0
  21. klaude_code/config/__init__.py +11 -0
  22. klaude_code/config/config.py +177 -0
  23. klaude_code/config/list_model.py +162 -0
  24. klaude_code/config/select_model.py +67 -0
  25. klaude_code/const/__init__.py +133 -0
  26. klaude_code/core/__init__.py +0 -0
  27. klaude_code/core/agent.py +165 -0
  28. klaude_code/core/executor.py +485 -0
  29. klaude_code/core/manager/__init__.py +19 -0
  30. klaude_code/core/manager/agent_manager.py +127 -0
  31. klaude_code/core/manager/llm_clients.py +42 -0
  32. klaude_code/core/manager/llm_clients_builder.py +49 -0
  33. klaude_code/core/manager/sub_agent_manager.py +86 -0
  34. klaude_code/core/prompt.py +89 -0
  35. klaude_code/core/prompts/prompt-claude-code.md +98 -0
  36. klaude_code/core/prompts/prompt-codex.md +331 -0
  37. klaude_code/core/prompts/prompt-gemini.md +43 -0
  38. klaude_code/core/prompts/prompt-subagent-explore.md +27 -0
  39. klaude_code/core/prompts/prompt-subagent-oracle.md +23 -0
  40. klaude_code/core/prompts/prompt-subagent-webfetch.md +46 -0
  41. klaude_code/core/prompts/prompt-subagent.md +8 -0
  42. klaude_code/core/reminders.py +445 -0
  43. klaude_code/core/task.py +237 -0
  44. klaude_code/core/tool/__init__.py +75 -0
  45. klaude_code/core/tool/file/__init__.py +0 -0
  46. klaude_code/core/tool/file/apply_patch.py +492 -0
  47. klaude_code/core/tool/file/apply_patch_tool.md +1 -0
  48. klaude_code/core/tool/file/apply_patch_tool.py +204 -0
  49. klaude_code/core/tool/file/edit_tool.md +9 -0
  50. klaude_code/core/tool/file/edit_tool.py +274 -0
  51. klaude_code/core/tool/file/multi_edit_tool.md +42 -0
  52. klaude_code/core/tool/file/multi_edit_tool.py +199 -0
  53. klaude_code/core/tool/file/read_tool.md +14 -0
  54. klaude_code/core/tool/file/read_tool.py +326 -0
  55. klaude_code/core/tool/file/write_tool.md +8 -0
  56. klaude_code/core/tool/file/write_tool.py +146 -0
  57. klaude_code/core/tool/memory/__init__.py +0 -0
  58. klaude_code/core/tool/memory/memory_tool.md +16 -0
  59. klaude_code/core/tool/memory/memory_tool.py +462 -0
  60. klaude_code/core/tool/memory/skill_loader.py +245 -0
  61. klaude_code/core/tool/memory/skill_tool.md +24 -0
  62. klaude_code/core/tool/memory/skill_tool.py +97 -0
  63. klaude_code/core/tool/shell/__init__.py +0 -0
  64. klaude_code/core/tool/shell/bash_tool.md +43 -0
  65. klaude_code/core/tool/shell/bash_tool.py +123 -0
  66. klaude_code/core/tool/shell/command_safety.py +363 -0
  67. klaude_code/core/tool/sub_agent_tool.py +83 -0
  68. klaude_code/core/tool/todo/__init__.py +0 -0
  69. klaude_code/core/tool/todo/todo_write_tool.md +182 -0
  70. klaude_code/core/tool/todo/todo_write_tool.py +121 -0
  71. klaude_code/core/tool/todo/update_plan_tool.md +3 -0
  72. klaude_code/core/tool/todo/update_plan_tool.py +104 -0
  73. klaude_code/core/tool/tool_abc.py +25 -0
  74. klaude_code/core/tool/tool_context.py +106 -0
  75. klaude_code/core/tool/tool_registry.py +78 -0
  76. klaude_code/core/tool/tool_runner.py +252 -0
  77. klaude_code/core/tool/truncation.py +170 -0
  78. klaude_code/core/tool/web/__init__.py +0 -0
  79. klaude_code/core/tool/web/mermaid_tool.md +21 -0
  80. klaude_code/core/tool/web/mermaid_tool.py +76 -0
  81. klaude_code/core/tool/web/web_fetch_tool.md +8 -0
  82. klaude_code/core/tool/web/web_fetch_tool.py +159 -0
  83. klaude_code/core/turn.py +220 -0
  84. klaude_code/llm/__init__.py +21 -0
  85. klaude_code/llm/anthropic/__init__.py +3 -0
  86. klaude_code/llm/anthropic/client.py +221 -0
  87. klaude_code/llm/anthropic/input.py +200 -0
  88. klaude_code/llm/client.py +49 -0
  89. klaude_code/llm/input_common.py +239 -0
  90. klaude_code/llm/openai_compatible/__init__.py +3 -0
  91. klaude_code/llm/openai_compatible/client.py +211 -0
  92. klaude_code/llm/openai_compatible/input.py +109 -0
  93. klaude_code/llm/openai_compatible/tool_call_accumulator.py +80 -0
  94. klaude_code/llm/openrouter/__init__.py +3 -0
  95. klaude_code/llm/openrouter/client.py +200 -0
  96. klaude_code/llm/openrouter/input.py +160 -0
  97. klaude_code/llm/openrouter/reasoning_handler.py +209 -0
  98. klaude_code/llm/registry.py +22 -0
  99. klaude_code/llm/responses/__init__.py +3 -0
  100. klaude_code/llm/responses/client.py +216 -0
  101. klaude_code/llm/responses/input.py +167 -0
  102. klaude_code/llm/usage.py +109 -0
  103. klaude_code/protocol/__init__.py +4 -0
  104. klaude_code/protocol/commands.py +21 -0
  105. klaude_code/protocol/events.py +163 -0
  106. klaude_code/protocol/llm_param.py +147 -0
  107. klaude_code/protocol/model.py +287 -0
  108. klaude_code/protocol/op.py +89 -0
  109. klaude_code/protocol/op_handler.py +28 -0
  110. klaude_code/protocol/sub_agent.py +348 -0
  111. klaude_code/protocol/tools.py +15 -0
  112. klaude_code/session/__init__.py +4 -0
  113. klaude_code/session/export.py +624 -0
  114. klaude_code/session/selector.py +76 -0
  115. klaude_code/session/session.py +474 -0
  116. klaude_code/session/templates/export_session.html +1434 -0
  117. klaude_code/trace/__init__.py +3 -0
  118. klaude_code/trace/log.py +168 -0
  119. klaude_code/ui/__init__.py +91 -0
  120. klaude_code/ui/core/__init__.py +1 -0
  121. klaude_code/ui/core/display.py +103 -0
  122. klaude_code/ui/core/input.py +71 -0
  123. klaude_code/ui/core/stage_manager.py +55 -0
  124. klaude_code/ui/modes/__init__.py +1 -0
  125. klaude_code/ui/modes/debug/__init__.py +1 -0
  126. klaude_code/ui/modes/debug/display.py +36 -0
  127. klaude_code/ui/modes/exec/__init__.py +1 -0
  128. klaude_code/ui/modes/exec/display.py +63 -0
  129. klaude_code/ui/modes/repl/__init__.py +51 -0
  130. klaude_code/ui/modes/repl/clipboard.py +152 -0
  131. klaude_code/ui/modes/repl/completers.py +429 -0
  132. klaude_code/ui/modes/repl/display.py +60 -0
  133. klaude_code/ui/modes/repl/event_handler.py +375 -0
  134. klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
  135. klaude_code/ui/modes/repl/key_bindings.py +170 -0
  136. klaude_code/ui/modes/repl/renderer.py +281 -0
  137. klaude_code/ui/renderers/__init__.py +0 -0
  138. klaude_code/ui/renderers/assistant.py +21 -0
  139. klaude_code/ui/renderers/common.py +8 -0
  140. klaude_code/ui/renderers/developer.py +158 -0
  141. klaude_code/ui/renderers/diffs.py +215 -0
  142. klaude_code/ui/renderers/errors.py +16 -0
  143. klaude_code/ui/renderers/metadata.py +190 -0
  144. klaude_code/ui/renderers/sub_agent.py +71 -0
  145. klaude_code/ui/renderers/thinking.py +39 -0
  146. klaude_code/ui/renderers/tools.py +551 -0
  147. klaude_code/ui/renderers/user_input.py +65 -0
  148. klaude_code/ui/rich/__init__.py +1 -0
  149. klaude_code/ui/rich/live.py +65 -0
  150. klaude_code/ui/rich/markdown.py +308 -0
  151. klaude_code/ui/rich/quote.py +34 -0
  152. klaude_code/ui/rich/searchable_text.py +71 -0
  153. klaude_code/ui/rich/status.py +240 -0
  154. klaude_code/ui/rich/theme.py +274 -0
  155. klaude_code/ui/terminal/__init__.py +1 -0
  156. klaude_code/ui/terminal/color.py +244 -0
  157. klaude_code/ui/terminal/control.py +147 -0
  158. klaude_code/ui/terminal/notifier.py +107 -0
  159. klaude_code/ui/terminal/progress_bar.py +87 -0
  160. klaude_code/ui/utils/__init__.py +1 -0
  161. klaude_code/ui/utils/common.py +108 -0
  162. klaude_code/ui/utils/debouncer.py +42 -0
  163. klaude_code/version.py +163 -0
  164. klaude_code-1.2.6.dist-info/METADATA +178 -0
  165. klaude_code-1.2.6.dist-info/RECORD +167 -0
  166. klaude_code-1.2.6.dist-info/WHEEL +4 -0
  167. klaude_code-1.2.6.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,3 @@
1
+ from .log import DebugType, is_debug_enabled, log, log_debug, logger, set_debug_logging
2
+
3
+ __all__ = ["log", "log_debug", "logger", "set_debug_logging", "DebugType", "is_debug_enabled"]
@@ -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
+ )