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
File without changes
@@ -0,0 +1 @@
1
+ # Package init for CLI; intentionally empty.
@@ -0,0 +1,298 @@
1
+ import asyncio
2
+ import os
3
+ import subprocess
4
+ import sys
5
+ import uuid
6
+ from importlib.metadata import version as pkg_version
7
+
8
+ import typer
9
+
10
+ from klaude_code.cli.runtime import DEBUG_FILTER_HELP, AppInitConfig, resolve_debug_settings, run_exec, run_interactive
11
+ from klaude_code.cli.session_cmd import register_session_commands
12
+ from klaude_code.config import config_path, display_models_and_providers, load_config, select_model_from_config
13
+ from klaude_code.session import Session, resume_select_session
14
+ from klaude_code.trace import log
15
+ from klaude_code.ui.terminal.color import is_light_terminal_background
16
+
17
+
18
+ def set_terminal_title(title: str) -> None:
19
+ """Set terminal window title using ANSI escape sequence."""
20
+ sys.stdout.write(f"\033]0;{title}\007")
21
+ sys.stdout.flush()
22
+
23
+
24
+ def _version_callback(value: bool) -> None:
25
+ """Show version and exit."""
26
+ if value:
27
+ try:
28
+ ver = pkg_version("klaude-code")
29
+ except Exception:
30
+ ver = "unknown"
31
+ print(f"klaude-code {ver}")
32
+ raise typer.Exit(0)
33
+
34
+
35
+ app = typer.Typer(
36
+ add_completion=False,
37
+ pretty_exceptions_enable=False,
38
+ no_args_is_help=False,
39
+ )
40
+
41
+ session_app = typer.Typer(help="Manage sessions for the current project")
42
+ register_session_commands(session_app)
43
+ app.add_typer(session_app, name="session")
44
+
45
+
46
+ @app.command("list")
47
+ def list_models() -> None:
48
+ """List all models and providers configuration"""
49
+ config = load_config()
50
+ if config is None:
51
+ raise typer.Exit(1)
52
+
53
+ # Auto-detect theme when not explicitly set in config, to match other CLI entrypoints.
54
+ if config.theme is None:
55
+ detected = is_light_terminal_background()
56
+ if detected is True:
57
+ config.theme = "light"
58
+ elif detected is False:
59
+ config.theme = "dark"
60
+
61
+ display_models_and_providers(config)
62
+
63
+
64
+ @app.command("config")
65
+ @app.command("conf", hidden=True)
66
+ def edit_config() -> None:
67
+ """Open the configuration file in $EDITOR or default system editor"""
68
+ editor = os.environ.get("EDITOR")
69
+
70
+ # If no EDITOR is set, prioritize TextEdit on macOS
71
+ if not editor:
72
+ # Try common editors in order of preference on other platforms
73
+ for cmd in [
74
+ "code",
75
+ "nvim",
76
+ "vim",
77
+ "nano",
78
+ ]:
79
+ try:
80
+ subprocess.run(["which", cmd], check=True, capture_output=True)
81
+ editor = cmd
82
+ break
83
+ except (subprocess.CalledProcessError, FileNotFoundError):
84
+ continue
85
+
86
+ # If no editor found, try platform-specific defaults
87
+ if not editor:
88
+ if sys.platform == "darwin": # macOS
89
+ editor = "open"
90
+ elif sys.platform == "win32": # Windows
91
+ editor = "notepad"
92
+ else: # Linux and other Unix systems
93
+ editor = "xdg-open"
94
+
95
+ # Ensure config file exists
96
+ config = load_config()
97
+ if config is None:
98
+ raise typer.Exit(1)
99
+
100
+ try:
101
+ if editor == "open -a TextEdit":
102
+ subprocess.run(["open", "-a", "TextEdit", str(config_path)], check=True)
103
+ elif editor in ["open", "xdg-open"]:
104
+ # For open/xdg-open, we need to pass the file directly
105
+ subprocess.run([editor, str(config_path)], check=True)
106
+ else:
107
+ subprocess.run([editor, str(config_path)], check=True)
108
+ except subprocess.CalledProcessError as e:
109
+ log((f"Error: Failed to open editor: {e}", "red"))
110
+ raise typer.Exit(1)
111
+ except FileNotFoundError:
112
+ log((f"Error: Editor '{editor}' not found", "red"))
113
+ log("Please install a text editor or set your $EDITOR environment variable")
114
+ raise typer.Exit(1)
115
+
116
+
117
+ @app.command("exec")
118
+ def exec_command(
119
+ input_content: str = typer.Argument("", help="Input message to execute"),
120
+ model: str | None = typer.Option(
121
+ None,
122
+ "--model",
123
+ "-m",
124
+ help="Override model config name (uses main model by default)",
125
+ rich_help_panel="LLM",
126
+ ),
127
+ select_model: bool = typer.Option(
128
+ False,
129
+ "--select-model",
130
+ "-s",
131
+ help="Interactively choose a model at startup",
132
+ rich_help_panel="LLM",
133
+ ),
134
+ debug: bool = typer.Option(
135
+ False,
136
+ "--debug",
137
+ "-d",
138
+ help="Enable debug mode",
139
+ rich_help_panel="Debug",
140
+ ),
141
+ debug_filter: str | None = typer.Option(
142
+ None,
143
+ "--debug-filter",
144
+ help=DEBUG_FILTER_HELP,
145
+ rich_help_panel="Debug",
146
+ ),
147
+ vanilla: bool = typer.Option(
148
+ False,
149
+ "--vanilla",
150
+ help="Vanilla mode exposes the model's raw API behavior: it provides only minimal tools (Bash, Read & Edit) and omits system prompts and reminders.",
151
+ ),
152
+ stream_json: bool = typer.Option(
153
+ False,
154
+ "--stream-json",
155
+ help="Stream all events as JSON lines to stdout.",
156
+ ),
157
+ ) -> None:
158
+ """Execute non-interactively with provided input."""
159
+
160
+ # Set terminal title with current folder name
161
+ folder_name = os.path.basename(os.getcwd())
162
+ set_terminal_title(f"{folder_name}: klaude")
163
+
164
+ parts: list[str] = []
165
+
166
+ # Handle stdin input
167
+ if not sys.stdin.isatty():
168
+ try:
169
+ stdin = sys.stdin.read().rstrip("\n")
170
+ if stdin:
171
+ parts.append(stdin)
172
+ except Exception as e:
173
+ log((f"Error reading from stdin: {e}", "red"))
174
+
175
+ if input_content:
176
+ parts.append(input_content)
177
+
178
+ input_content = "\n".join(parts)
179
+ if len(input_content) == 0:
180
+ log(("Error: No input content provided", "red"))
181
+ raise typer.Exit(1)
182
+
183
+ chosen_model = model
184
+ if select_model:
185
+ # Prefer the explicitly provided model as default; otherwise main model
186
+ config = load_config()
187
+ if config is None:
188
+ raise typer.Exit(1)
189
+ default_name = model or config.main_model
190
+ chosen_model = select_model_from_config(preferred=default_name)
191
+ if chosen_model is None:
192
+ return
193
+
194
+ debug_enabled, debug_filters = resolve_debug_settings(debug, debug_filter)
195
+
196
+ init_config = AppInitConfig(
197
+ model=chosen_model,
198
+ debug=debug_enabled,
199
+ vanilla=vanilla,
200
+ is_exec_mode=True,
201
+ debug_filters=debug_filters,
202
+ stream_json=stream_json,
203
+ )
204
+
205
+ asyncio.run(
206
+ run_exec(
207
+ init_config=init_config,
208
+ input_content=input_content,
209
+ )
210
+ )
211
+
212
+
213
+ @app.callback(invoke_without_command=True)
214
+ def main_callback(
215
+ ctx: typer.Context,
216
+ version: bool = typer.Option(
217
+ False,
218
+ "--version",
219
+ "-V",
220
+ help="Show version and exit",
221
+ callback=_version_callback,
222
+ is_eager=True,
223
+ ),
224
+ model: str | None = typer.Option(
225
+ None,
226
+ "--model",
227
+ "-m",
228
+ help="Override model config name (uses main model by default)",
229
+ rich_help_panel="LLM",
230
+ ),
231
+ continue_: bool = typer.Option(False, "--continue", "-c", help="Continue from latest session"),
232
+ resume: bool = typer.Option(False, "--resume", "-r", help="Select a session to resume for this project"),
233
+ select_model: bool = typer.Option(
234
+ False,
235
+ "--select-model",
236
+ "-s",
237
+ help="Interactively choose a model at startup",
238
+ rich_help_panel="LLM",
239
+ ),
240
+ debug: bool = typer.Option(
241
+ False,
242
+ "--debug",
243
+ "-d",
244
+ help="Enable debug mode",
245
+ rich_help_panel="Debug",
246
+ ),
247
+ debug_filter: str | None = typer.Option(
248
+ None,
249
+ "--debug-filter",
250
+ help=DEBUG_FILTER_HELP,
251
+ rich_help_panel="Debug",
252
+ ),
253
+ vanilla: bool = typer.Option(
254
+ False,
255
+ "--vanilla",
256
+ help="Vanilla mode exposes the model's raw API behavior: it provides only minimal tools (Bash, Read & Edit) and omits system prompts and reminders.",
257
+ ),
258
+ ) -> None:
259
+ # Only run interactive mode when no subcommand is invoked
260
+ if ctx.invoked_subcommand is None:
261
+ # Set terminal title with current folder name
262
+ folder_name = os.path.basename(os.getcwd())
263
+ set_terminal_title(f"{folder_name}: klaude")
264
+ # Interactive mode
265
+ chosen_model = model
266
+ if select_model:
267
+ chosen_model = select_model_from_config(preferred=model)
268
+ if chosen_model is None:
269
+ return
270
+
271
+ # Resolve session id before entering asyncio loop
272
+ session_id: str | None = None
273
+ if resume:
274
+ session_id = resume_select_session()
275
+ if session_id is None:
276
+ return
277
+ # If user didn't pick, allow fallback to --continue
278
+ if session_id is None and continue_:
279
+ session_id = Session.most_recent_session_id()
280
+ # If still no session_id, generate a new one for a new session
281
+ if session_id is None:
282
+ session_id = uuid.uuid4().hex
283
+
284
+ debug_enabled, debug_filters = resolve_debug_settings(debug, debug_filter)
285
+
286
+ init_config = AppInitConfig(
287
+ model=chosen_model,
288
+ debug=debug_enabled,
289
+ vanilla=vanilla,
290
+ debug_filters=debug_filters,
291
+ )
292
+
293
+ asyncio.run(
294
+ run_interactive(
295
+ init_config=init_config,
296
+ session_id=session_id,
297
+ )
298
+ )
@@ -0,0 +1,331 @@
1
+ import asyncio
2
+ import sys
3
+ import uuid
4
+ from dataclasses import dataclass
5
+ from typing import Any, Protocol
6
+
7
+ import typer
8
+ from rich.text import Text
9
+
10
+ from klaude_code import ui
11
+ from klaude_code.command import has_interactive_command
12
+ from klaude_code.config import Config, load_config
13
+ from klaude_code.core.agent import Agent, DefaultModelProfileProvider, VanillaModelProfileProvider
14
+ from klaude_code.core.executor import Executor
15
+ from klaude_code.core.manager import build_llm_clients
16
+ from klaude_code.core.tool import SkillLoader, SkillTool
17
+ from klaude_code.protocol import events, op
18
+ from klaude_code.protocol.model import UserInputPayload
19
+ from klaude_code.protocol.sub_agent import iter_sub_agent_profiles
20
+ from klaude_code.trace import DebugType, log, set_debug_logging
21
+ from klaude_code.ui.modes.repl import build_repl_status_snapshot
22
+ from klaude_code.ui.modes.repl.input_prompt_toolkit import REPLStatusSnapshot
23
+ from klaude_code.ui.terminal.color import is_light_terminal_background
24
+ from klaude_code.ui.terminal.control import install_sigint_double_press_exit, start_esc_interrupt_monitor
25
+ from klaude_code.ui.terminal.progress_bar import OSC94States, emit_osc94
26
+ from klaude_code.version import get_update_message
27
+
28
+
29
+ class PrintCapable(Protocol):
30
+ """Protocol for objects that can print styled content."""
31
+
32
+ def print(self, *objects: Any, style: Any | None = None, end: str = "\n") -> None: ...
33
+
34
+
35
+ DEBUG_FILTER_HELP = "Comma-separated debug types: " + ", ".join(dt.value for dt in DebugType)
36
+
37
+
38
+ def _parse_debug_filters(raw: str | None) -> set[DebugType] | None:
39
+ if raw is None:
40
+ return None
41
+ filters: set[DebugType] = set()
42
+ for chunk in raw.split(","):
43
+ normalized = chunk.strip().lower().replace("-", "_")
44
+ if not normalized:
45
+ continue
46
+ try:
47
+ filters.add(DebugType(normalized))
48
+ except ValueError: # pragma: no cover - user input validation
49
+ valid_options = ", ".join(dt.value for dt in DebugType)
50
+ log(
51
+ (
52
+ f"Invalid debug filter '{normalized}'. Valid options: {valid_options}",
53
+ "red",
54
+ )
55
+ )
56
+ raise typer.Exit(2) from None
57
+ return filters or None
58
+
59
+
60
+ def resolve_debug_settings(flag: bool, raw_filters: str | None) -> tuple[bool, set[DebugType] | None]:
61
+ filters = _parse_debug_filters(raw_filters)
62
+ effective_flag = flag or (filters is not None)
63
+ return effective_flag, filters
64
+
65
+
66
+ @dataclass
67
+ class AppInitConfig:
68
+ """Configuration for initializing the application components."""
69
+
70
+ model: str | None
71
+ debug: bool
72
+ vanilla: bool
73
+ is_exec_mode: bool = False
74
+ debug_filters: set[DebugType] | None = None
75
+ stream_json: bool = False
76
+
77
+
78
+ @dataclass
79
+ class AppComponents:
80
+ """Initialized application components."""
81
+
82
+ config: Config
83
+ executor: Executor
84
+ executor_task: asyncio.Task[None]
85
+ event_queue: asyncio.Queue[events.Event]
86
+ display: ui.DisplayABC
87
+ display_task: asyncio.Task[None]
88
+ theme: str | None
89
+
90
+
91
+ async def initialize_app_components(init_config: AppInitConfig) -> AppComponents:
92
+ """Initialize all application components (LLM clients, executor, UI)."""
93
+ set_debug_logging(init_config.debug, filters=init_config.debug_filters)
94
+
95
+ config = load_config()
96
+ if config is None:
97
+ raise typer.Exit(1)
98
+
99
+ # Initialize skills
100
+ skill_loader = SkillLoader()
101
+ skill_loader.discover_skills()
102
+ SkillTool.set_skill_loader(skill_loader)
103
+
104
+ # Initialize LLM clients
105
+ try:
106
+ enabled_sub_agents = [p.name for p in iter_sub_agent_profiles()]
107
+ llm_clients = build_llm_clients(
108
+ config,
109
+ model_override=init_config.model,
110
+ enabled_sub_agents=enabled_sub_agents,
111
+ )
112
+ except ValueError as exc:
113
+ if init_config.model:
114
+ log(
115
+ (
116
+ f"Error: model '{init_config.model}' is not defined in the config",
117
+ "red",
118
+ )
119
+ )
120
+ log(("Hint: run `klaude list` to view available models", "yellow"))
121
+ else:
122
+ log((f"Error: failed to load the default model configuration: {exc}", "red"))
123
+ raise typer.Exit(2) from None
124
+
125
+ model_profile_provider = VanillaModelProfileProvider() if init_config.vanilla else DefaultModelProfileProvider()
126
+
127
+ # Create event queue for communication between executor and UI
128
+ event_queue: asyncio.Queue[events.Event] = asyncio.Queue()
129
+
130
+ # Create executor with the LLM client
131
+ executor = Executor(
132
+ event_queue,
133
+ llm_clients,
134
+ model_profile_provider=model_profile_provider,
135
+ )
136
+
137
+ # Start executor in background
138
+ executor_task = asyncio.create_task(executor.start())
139
+
140
+ theme: str | None = config.theme
141
+ if theme is None:
142
+ # Auto-detect theme from terminal background when config does not specify a theme.
143
+ detected = is_light_terminal_background()
144
+ if detected is True:
145
+ theme = "light"
146
+ elif detected is False:
147
+ theme = "dark"
148
+
149
+ # Set up UI components using factory functions
150
+ display: ui.DisplayABC
151
+ if init_config.is_exec_mode:
152
+ display = ui.create_exec_display(debug=init_config.debug, stream_json=init_config.stream_json)
153
+ else:
154
+ display = ui.create_default_display(debug=init_config.debug, theme=theme)
155
+
156
+ # Start UI display task
157
+ display_task = asyncio.create_task(display.consume_event_loop(event_queue))
158
+
159
+ return AppComponents(
160
+ config=config,
161
+ executor=executor,
162
+ executor_task=executor_task,
163
+ event_queue=event_queue,
164
+ display=display,
165
+ display_task=display_task,
166
+ theme=theme,
167
+ )
168
+
169
+
170
+ async def cleanup_app_components(components: AppComponents) -> None:
171
+ """Clean up all application components."""
172
+ try:
173
+ # Clean shutdown
174
+ await components.executor.stop()
175
+ components.executor_task.cancel()
176
+
177
+ # Signal UI to stop
178
+ await components.event_queue.put(events.EndEvent())
179
+ await components.display_task
180
+ finally:
181
+ # Always attempt to clear Ghostty progress bar and restore cursor visibility
182
+ try:
183
+ emit_osc94(OSC94States.HIDDEN)
184
+ except Exception:
185
+ # Best-effort only; never fail cleanup due to OSC errors
186
+ pass
187
+
188
+ try:
189
+ # Ensure the terminal cursor is visible even if Rich's Status spinner
190
+ # did not get a chance to stop cleanly (e.g. on KeyboardInterrupt).
191
+ stream = getattr(sys, "__stdout__", None) or sys.stdout
192
+ stream.write("\033[?25h")
193
+ stream.flush()
194
+ except Exception:
195
+ # If this fails the shell can still recover via `reset`/`stty sane`.
196
+ pass
197
+
198
+
199
+ async def _handle_keyboard_interrupt(executor: Executor) -> None:
200
+ """Handle Ctrl+C by logging and sending a global interrupt."""
201
+
202
+ log("Bye!")
203
+ try:
204
+ await executor.submit(op.InterruptOperation(target_session_id=None))
205
+ except Exception:
206
+ # Executor might already be stopping
207
+ pass
208
+
209
+
210
+ async def run_exec(init_config: AppInitConfig, input_content: str) -> None:
211
+ """Run a single command non-interactively using the provided configuration."""
212
+
213
+ components = await initialize_app_components(init_config)
214
+
215
+ try:
216
+ # Generate a new session ID for exec mode
217
+ session_id = uuid.uuid4().hex
218
+
219
+ # Init Agent
220
+ await components.executor.submit_and_wait(op.InitAgentOperation(session_id=session_id))
221
+ await components.event_queue.join()
222
+
223
+ # Submit the input content directly
224
+ await components.executor.submit_and_wait(
225
+ op.UserInputOperation(input=UserInputPayload(text=input_content), session_id=session_id)
226
+ )
227
+
228
+ except KeyboardInterrupt:
229
+ await _handle_keyboard_interrupt(components.executor)
230
+ finally:
231
+ await cleanup_app_components(components)
232
+
233
+
234
+ async def run_interactive(init_config: AppInitConfig, session_id: str | None = None) -> None:
235
+ """Run the interactive REPL using the provided configuration."""
236
+
237
+ components = await initialize_app_components(init_config)
238
+
239
+ # No theme persistence from CLI anymore; config.theme controls theme when set.
240
+
241
+ # Create status provider for bottom toolbar
242
+ def _status_provider() -> REPLStatusSnapshot:
243
+ agent: Agent | None = None
244
+ if session_id and session_id in components.executor.context.active_agents:
245
+ agent = components.executor.context.active_agents[session_id]
246
+
247
+ # Check for updates (returns None if uv not available)
248
+ update_message = get_update_message()
249
+
250
+ return build_repl_status_snapshot(agent=agent, update_message=update_message)
251
+
252
+ # Set up input provider for interactive mode
253
+ input_provider: ui.InputProviderABC = ui.PromptToolkitInput(status_provider=_status_provider)
254
+
255
+ # --- Custom Ctrl+C handler: double-press within 2s to exit, single press shows toast ---
256
+ def _show_toast_once() -> None:
257
+ MSG = "Press ctrl+c again to exit"
258
+ try:
259
+ # Keep message short; avoid interfering with spinner layout
260
+ printer: PrintCapable | None = None
261
+
262
+ # Check if it's a REPLDisplay with renderer
263
+ if isinstance(components.display, ui.REPLDisplay):
264
+ printer = components.display.renderer
265
+ # Check if it's a DebugEventDisplay wrapping a REPLDisplay
266
+ elif isinstance(components.display, ui.DebugEventDisplay) and components.display.wrapped_display:
267
+ if isinstance(components.display.wrapped_display, ui.REPLDisplay):
268
+ printer = components.display.wrapped_display.renderer
269
+
270
+ if printer is not None:
271
+ printer.print(Text(f" {MSG} ", style="bold yellow reverse"))
272
+ else:
273
+ print(MSG, file=sys.stderr)
274
+ except Exception:
275
+ # Fallback if themed print is unavailable
276
+ print(MSG, file=sys.stderr)
277
+
278
+ def _hide_progress() -> None:
279
+ try:
280
+ emit_osc94(OSC94States.HIDDEN)
281
+ except Exception:
282
+ pass
283
+
284
+ restore_sigint = install_sigint_double_press_exit(_show_toast_once, _hide_progress)
285
+
286
+ try:
287
+ # Init Agent
288
+ await components.executor.submit_and_wait(op.InitAgentOperation(session_id=session_id))
289
+ await components.event_queue.join()
290
+ # Input
291
+ await input_provider.start()
292
+ async for user_input in input_provider.iter_inputs():
293
+ # Handle special commands
294
+ if user_input.text.strip().lower() in {"exit", ":q", "quit"}:
295
+ break
296
+ elif user_input.text.strip() == "":
297
+ continue
298
+ # Submit user input operation - directly use the payload from iter_inputs
299
+ submission_id = await components.executor.submit(
300
+ op.UserInputOperation(input=user_input, session_id=session_id)
301
+ )
302
+ # If it's an interactive command (e.g., /model), avoid starting the ESC monitor
303
+ # to prevent TTY conflicts with interactive prompts (questionary/prompt_toolkit).
304
+ if has_interactive_command(user_input.text):
305
+ await components.executor.wait_for(submission_id)
306
+ else:
307
+ # Esc monitor for long-running, interruptible operations
308
+ async def _on_esc_interrupt() -> None:
309
+ await components.executor.submit(op.InterruptOperation(target_session_id=session_id))
310
+
311
+ stop_event, esc_task = start_esc_interrupt_monitor(_on_esc_interrupt)
312
+ # Wait for this specific task to complete before accepting next input
313
+ try:
314
+ await components.executor.wait_for(submission_id)
315
+ finally:
316
+ # Stop ESC monitor and wait for it to finish cleaning up TTY
317
+ stop_event.set()
318
+ try:
319
+ await esc_task
320
+ except Exception:
321
+ pass
322
+
323
+ except KeyboardInterrupt:
324
+ await _handle_keyboard_interrupt(components.executor)
325
+ finally:
326
+ try:
327
+ # Restore original SIGINT handler
328
+ restore_sigint()
329
+ except Exception:
330
+ pass
331
+ await cleanup_app_components(components)