klaude-code 2.0.2__py3-none-any.whl → 2.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. klaude_code/app/__init__.py +12 -0
  2. klaude_code/app/runtime.py +215 -0
  3. klaude_code/cli/auth_cmd.py +2 -2
  4. klaude_code/cli/config_cmd.py +2 -2
  5. klaude_code/cli/cost_cmd.py +1 -1
  6. klaude_code/cli/debug.py +12 -36
  7. klaude_code/cli/list_model.py +3 -3
  8. klaude_code/cli/main.py +17 -60
  9. klaude_code/cli/self_update.py +2 -187
  10. klaude_code/cli/session_cmd.py +2 -2
  11. klaude_code/config/config.py +1 -1
  12. klaude_code/config/select_model.py +1 -1
  13. klaude_code/const.py +9 -1
  14. klaude_code/core/agent.py +9 -62
  15. klaude_code/core/agent_profile.py +291 -0
  16. klaude_code/core/executor.py +335 -230
  17. klaude_code/core/manager/llm_clients_builder.py +1 -1
  18. klaude_code/core/manager/sub_agent_manager.py +16 -29
  19. klaude_code/core/reminders.py +84 -103
  20. klaude_code/core/task.py +12 -20
  21. klaude_code/core/tool/__init__.py +5 -19
  22. klaude_code/core/tool/context.py +84 -0
  23. klaude_code/core/tool/file/apply_patch_tool.py +18 -21
  24. klaude_code/core/tool/file/edit_tool.py +39 -42
  25. klaude_code/core/tool/file/read_tool.py +14 -9
  26. klaude_code/core/tool/file/write_tool.py +12 -13
  27. klaude_code/core/tool/report_back_tool.py +4 -1
  28. klaude_code/core/tool/shell/bash_tool.py +6 -11
  29. klaude_code/core/tool/sub_agent_tool.py +8 -7
  30. klaude_code/core/tool/todo/todo_write_tool.py +3 -9
  31. klaude_code/core/tool/todo/update_plan_tool.py +3 -5
  32. klaude_code/core/tool/tool_abc.py +2 -1
  33. klaude_code/core/tool/tool_registry.py +2 -33
  34. klaude_code/core/tool/tool_runner.py +13 -10
  35. klaude_code/core/tool/web/mermaid_tool.py +3 -1
  36. klaude_code/core/tool/web/web_fetch_tool.py +5 -3
  37. klaude_code/core/tool/web/web_search_tool.py +5 -3
  38. klaude_code/core/turn.py +87 -30
  39. klaude_code/llm/anthropic/client.py +1 -1
  40. klaude_code/llm/bedrock/client.py +1 -1
  41. klaude_code/llm/claude/client.py +1 -1
  42. klaude_code/llm/codex/client.py +1 -1
  43. klaude_code/llm/google/client.py +1 -1
  44. klaude_code/llm/openai_compatible/client.py +1 -1
  45. klaude_code/llm/openai_compatible/tool_call_accumulator.py +1 -1
  46. klaude_code/llm/openrouter/client.py +1 -1
  47. klaude_code/llm/openrouter/reasoning.py +1 -1
  48. klaude_code/llm/responses/client.py +1 -1
  49. klaude_code/protocol/commands.py +1 -0
  50. klaude_code/protocol/events/__init__.py +57 -0
  51. klaude_code/protocol/events/base.py +18 -0
  52. klaude_code/protocol/events/chat.py +20 -0
  53. klaude_code/protocol/events/lifecycle.py +22 -0
  54. klaude_code/protocol/events/metadata.py +15 -0
  55. klaude_code/protocol/events/streaming.py +43 -0
  56. klaude_code/protocol/events/system.py +53 -0
  57. klaude_code/protocol/events/tools.py +27 -0
  58. klaude_code/protocol/op.py +5 -0
  59. klaude_code/protocol/tools.py +0 -1
  60. klaude_code/session/session.py +6 -7
  61. klaude_code/skill/assets/create-plan/SKILL.md +76 -0
  62. klaude_code/skill/loader.py +32 -88
  63. klaude_code/skill/manager.py +38 -0
  64. klaude_code/skill/system_skills.py +1 -1
  65. klaude_code/tui/__init__.py +8 -0
  66. klaude_code/{command → tui/command}/__init__.py +3 -0
  67. klaude_code/{command → tui/command}/clear_cmd.py +2 -1
  68. klaude_code/tui/command/copy_cmd.py +53 -0
  69. klaude_code/{command → tui/command}/debug_cmd.py +3 -2
  70. klaude_code/{command → tui/command}/export_cmd.py +2 -1
  71. klaude_code/{command → tui/command}/export_online_cmd.py +2 -1
  72. klaude_code/{command → tui/command}/fork_session_cmd.py +4 -3
  73. klaude_code/{command → tui/command}/help_cmd.py +2 -1
  74. klaude_code/{command → tui/command}/model_cmd.py +4 -3
  75. klaude_code/{command → tui/command}/model_select.py +2 -2
  76. klaude_code/{command → tui/command}/prompt_command.py +4 -3
  77. klaude_code/{command → tui/command}/refresh_cmd.py +3 -1
  78. klaude_code/{command → tui/command}/registry.py +6 -5
  79. klaude_code/{command → tui/command}/release_notes_cmd.py +2 -1
  80. klaude_code/{command → tui/command}/resume_cmd.py +4 -3
  81. klaude_code/{command → tui/command}/status_cmd.py +2 -1
  82. klaude_code/{command → tui/command}/terminal_setup_cmd.py +2 -1
  83. klaude_code/{command → tui/command}/thinking_cmd.py +3 -2
  84. klaude_code/tui/commands.py +164 -0
  85. klaude_code/{ui/renderers → tui/components}/assistant.py +3 -3
  86. klaude_code/{ui/renderers → tui/components}/bash_syntax.py +2 -2
  87. klaude_code/{ui/renderers → tui/components}/common.py +1 -1
  88. klaude_code/{ui/renderers → tui/components}/developer.py +4 -4
  89. klaude_code/{ui/renderers → tui/components}/diffs.py +2 -2
  90. klaude_code/{ui/renderers → tui/components}/errors.py +2 -2
  91. klaude_code/{ui/renderers → tui/components}/metadata.py +7 -7
  92. klaude_code/{ui → tui/components}/rich/markdown.py +9 -23
  93. klaude_code/{ui → tui/components}/rich/status.py +2 -2
  94. klaude_code/{ui → tui/components}/rich/theme.py +3 -1
  95. klaude_code/{ui/renderers → tui/components}/sub_agent.py +23 -43
  96. klaude_code/{ui/renderers → tui/components}/thinking.py +3 -3
  97. klaude_code/{ui/renderers → tui/components}/tools.py +13 -17
  98. klaude_code/{ui/renderers → tui/components}/user_input.py +3 -20
  99. klaude_code/tui/display.py +85 -0
  100. klaude_code/{ui/modes/repl → tui/input}/__init__.py +1 -1
  101. klaude_code/{ui/modes/repl → tui/input}/completers.py +1 -1
  102. klaude_code/{ui/modes/repl/input_prompt_toolkit.py → tui/input/prompt_toolkit.py} +6 -6
  103. klaude_code/tui/machine.py +608 -0
  104. klaude_code/tui/renderer.py +707 -0
  105. klaude_code/tui/runner.py +321 -0
  106. klaude_code/tui/terminal/__init__.py +56 -0
  107. klaude_code/{ui → tui}/terminal/color.py +1 -1
  108. klaude_code/{ui → tui}/terminal/control.py +1 -1
  109. klaude_code/{ui → tui}/terminal/notifier.py +1 -1
  110. klaude_code/ui/__init__.py +6 -50
  111. klaude_code/ui/core/display.py +3 -3
  112. klaude_code/ui/core/input.py +2 -1
  113. klaude_code/ui/{modes/debug/display.py → debug_mode.py} +1 -1
  114. klaude_code/ui/{modes/exec/display.py → exec_mode.py} +0 -2
  115. klaude_code/ui/terminal/__init__.py +6 -54
  116. klaude_code/ui/terminal/title.py +31 -0
  117. klaude_code/update.py +163 -0
  118. {klaude_code-2.0.2.dist-info → klaude_code-2.1.1.dist-info}/METADATA +1 -1
  119. klaude_code-2.1.1.dist-info/RECORD +233 -0
  120. klaude_code/cli/runtime.py +0 -518
  121. klaude_code/core/prompt.py +0 -108
  122. klaude_code/core/tool/skill/skill_tool.md +0 -24
  123. klaude_code/core/tool/skill/skill_tool.py +0 -87
  124. klaude_code/core/tool/tool_context.py +0 -148
  125. klaude_code/protocol/events.py +0 -195
  126. klaude_code/skill/assets/dev-docs/SKILL.md +0 -108
  127. klaude_code/trace/__init__.py +0 -21
  128. klaude_code/ui/core/stage_manager.py +0 -48
  129. klaude_code/ui/modes/__init__.py +0 -1
  130. klaude_code/ui/modes/debug/__init__.py +0 -1
  131. klaude_code/ui/modes/exec/__init__.py +0 -1
  132. klaude_code/ui/modes/repl/display.py +0 -61
  133. klaude_code/ui/modes/repl/event_handler.py +0 -629
  134. klaude_code/ui/modes/repl/renderer.py +0 -464
  135. klaude_code/ui/renderers/__init__.py +0 -0
  136. klaude_code/ui/utils/__init__.py +0 -1
  137. klaude_code-2.0.2.dist-info/RECORD +0 -227
  138. /klaude_code/{trace/log.py → log.py} +0 -0
  139. /klaude_code/{command → tui/command}/command_abc.py +0 -0
  140. /klaude_code/{command → tui/command}/prompt-commit.md +0 -0
  141. /klaude_code/{command → tui/command}/prompt-init.md +0 -0
  142. /klaude_code/{core/tool/skill → tui/components}/__init__.py +0 -0
  143. /klaude_code/{ui/renderers → tui/components}/mermaid_viewer.py +0 -0
  144. /klaude_code/{ui → tui/components}/rich/__init__.py +0 -0
  145. /klaude_code/{ui → tui/components}/rich/cjk_wrap.py +0 -0
  146. /klaude_code/{ui → tui/components}/rich/code_panel.py +0 -0
  147. /klaude_code/{ui → tui/components}/rich/live.py +0 -0
  148. /klaude_code/{ui → tui/components}/rich/quote.py +0 -0
  149. /klaude_code/{ui → tui/components}/rich/searchable_text.py +0 -0
  150. /klaude_code/{ui/modes/repl → tui/input}/clipboard.py +0 -0
  151. /klaude_code/{ui/modes/repl → tui/input}/key_bindings.py +0 -0
  152. /klaude_code/{ui → tui}/terminal/image.py +0 -0
  153. /klaude_code/{ui → tui}/terminal/progress_bar.py +0 -0
  154. /klaude_code/{ui → tui}/terminal/selector.py +0 -0
  155. /klaude_code/ui/{utils/common.py → common.py} +0 -0
  156. {klaude_code-2.0.2.dist-info → klaude_code-2.1.1.dist-info}/WHEEL +0 -0
  157. {klaude_code-2.0.2.dist-info → klaude_code-2.1.1.dist-info}/entry_points.txt +0 -0
@@ -1,518 +0,0 @@
1
- import asyncio
2
- import contextlib
3
- import sys
4
- from dataclasses import dataclass
5
- from typing import Any, Protocol
6
- from uuid import uuid4
7
-
8
- import typer
9
- from rich.text import Text
10
-
11
- from klaude_code import ui
12
- from klaude_code.cli.main import update_terminal_title
13
- from klaude_code.cli.self_update import get_update_message
14
- from klaude_code.command import dispatch_command, get_command_info_list, has_interactive_command, is_slash_command_name
15
- from klaude_code.config import Config, load_config
16
- from klaude_code.core.agent import Agent, DefaultModelProfileProvider, VanillaModelProfileProvider
17
- from klaude_code.core.executor import Executor
18
- from klaude_code.core.manager import build_llm_clients
19
- from klaude_code.protocol import events, llm_param, op
20
- from klaude_code.protocol import message as protocol_message
21
- from klaude_code.protocol.message import UserInputPayload
22
- from klaude_code.session.session import Session, close_default_store
23
- from klaude_code.trace import DebugType, log, set_debug_logging
24
- from klaude_code.ui.modes.repl import build_repl_status_snapshot
25
- from klaude_code.ui.modes.repl.input_prompt_toolkit import REPLStatusSnapshot
26
- from klaude_code.ui.terminal.color import is_light_terminal_background
27
- from klaude_code.ui.terminal.control import install_sigint_double_press_exit, start_esc_interrupt_monitor
28
-
29
-
30
- class PrintCapable(Protocol):
31
- """Protocol for objects that can print styled content."""
32
-
33
- def print(self, *objects: Any, style: Any | None = None, end: str = "\n") -> None: ...
34
-
35
-
36
- @dataclass
37
- class AppInitConfig:
38
- """Configuration for initializing the application components."""
39
-
40
- model: str | None
41
- debug: bool
42
- vanilla: bool
43
- is_exec_mode: bool = False
44
- debug_filters: set[DebugType] | None = None
45
- stream_json: bool = False
46
-
47
-
48
- @dataclass
49
- class AppComponents:
50
- """Initialized application components."""
51
-
52
- config: Config
53
- executor: Executor
54
- executor_task: asyncio.Task[None]
55
- event_queue: asyncio.Queue[events.Event]
56
- display: ui.DisplayABC
57
- display_task: asyncio.Task[None]
58
- theme: str | None
59
-
60
-
61
- async def submit_user_input_payload(
62
- *,
63
- executor: Executor,
64
- event_queue: asyncio.Queue[events.Event],
65
- user_input: UserInputPayload,
66
- session_id: str | None,
67
- ) -> str | None:
68
- """Parse/dispatch a user input payload and submit resulting operations.
69
-
70
- The UI/CLI layer owns slash command parsing and any interactive prompts.
71
- Core only executes concrete operations.
72
-
73
- Returns a submission id that should be awaited, or None if there is nothing
74
- to wait for (e.g. commands that only emit events).
75
- """
76
-
77
- sid = session_id or executor.context.current_session_id()
78
- if sid is None:
79
- raise RuntimeError("No active session")
80
-
81
- agent = executor.context.current_agent
82
- if agent is None or agent.session.id != sid:
83
- await executor.submit_and_wait(op.InitAgentOperation(session_id=sid))
84
- agent = executor.context.current_agent
85
-
86
- if agent is None:
87
- raise RuntimeError("Failed to initialize agent")
88
-
89
- submission_id = uuid4().hex
90
-
91
- await executor.context.emit_event(
92
- events.UserMessageEvent(content=user_input.text, session_id=sid, images=user_input.images)
93
- )
94
-
95
- result = await dispatch_command(user_input, agent, submission_id=submission_id)
96
- operations: list[op.Operation] = list(result.operations or [])
97
-
98
- run_ops = [candidate for candidate in operations if isinstance(candidate, op.RunAgentOperation)]
99
- if len(run_ops) > 1:
100
- raise ValueError("Multiple RunAgentOperation results are not supported")
101
-
102
- persisted_user_input = run_ops[0].input if run_ops else user_input
103
-
104
- if result.persist_user_input:
105
- agent.session.append_history(
106
- [
107
- protocol_message.UserMessage(
108
- parts=protocol_message.parts_from_text_and_images(
109
- persisted_user_input.text,
110
- persisted_user_input.images,
111
- )
112
- )
113
- ]
114
- )
115
-
116
- if result.events:
117
- for evt in result.events:
118
- if result.persist_events and isinstance(evt, events.DeveloperMessageEvent):
119
- agent.session.append_history([evt.item])
120
- await executor.context.emit_event(evt)
121
-
122
- submitted_ids: list[str] = []
123
- for operation_item in operations:
124
- submitted_ids.append(await executor.submit(operation_item))
125
-
126
- if not submitted_ids:
127
- # Ensure event-only commands are fully rendered before showing the next prompt.
128
- await event_queue.join()
129
- return None
130
-
131
- if run_ops:
132
- return run_ops[0].id
133
- return submitted_ids[-1]
134
-
135
-
136
- async def initialize_app_components(init_config: AppInitConfig) -> AppComponents:
137
- """Initialize all application components (LLM clients, executor, UI)."""
138
- set_debug_logging(init_config.debug, filters=init_config.debug_filters)
139
-
140
- config = load_config()
141
-
142
- # Initialize LLM clients
143
- try:
144
- llm_clients = build_llm_clients(
145
- config,
146
- model_override=init_config.model,
147
- )
148
- except ValueError as exc:
149
- if init_config.model:
150
- log(
151
- (
152
- f"Error: model '{init_config.model}' is not defined in the config",
153
- "red",
154
- )
155
- )
156
- log(("Hint: run `klaude list` to view available models", "yellow"))
157
- else:
158
- log((f"Error: failed to load the default model configuration: {exc}", "red"))
159
- raise typer.Exit(2) from None
160
-
161
- model_profile_provider = VanillaModelProfileProvider() if init_config.vanilla else DefaultModelProfileProvider()
162
-
163
- # Create event queue for communication between executor and UI
164
- event_queue: asyncio.Queue[events.Event] = asyncio.Queue()
165
-
166
- # Create executor with the LLM client
167
- executor = Executor(
168
- event_queue,
169
- llm_clients,
170
- model_profile_provider=model_profile_provider,
171
- on_model_change=update_terminal_title,
172
- )
173
-
174
- # Update terminal title with initial model name
175
- update_terminal_title(llm_clients.main.model_name)
176
-
177
- # Start executor in background
178
- executor_task = asyncio.create_task(executor.start())
179
-
180
- theme: str | None = config.theme
181
- if theme is None and not init_config.is_exec_mode:
182
- # Auto-detect theme from terminal background when config does not specify a theme.
183
- # Skip detection in exec mode to avoid TTY race conditions with parent process's
184
- # ESC monitor when running as a subprocess.
185
- detected = is_light_terminal_background()
186
- if detected is True:
187
- theme = "light"
188
- elif detected is False:
189
- theme = "dark"
190
-
191
- # Set up UI components using factory functions
192
- display: ui.DisplayABC
193
- if init_config.is_exec_mode:
194
- display = ui.create_exec_display(debug=init_config.debug, stream_json=init_config.stream_json)
195
- else:
196
- display = ui.create_default_display(debug=init_config.debug, theme=theme)
197
-
198
- # Start UI display task
199
- display_task = asyncio.create_task(display.consume_event_loop(event_queue))
200
-
201
- return AppComponents(
202
- config=config,
203
- executor=executor,
204
- executor_task=executor_task,
205
- event_queue=event_queue,
206
- display=display,
207
- display_task=display_task,
208
- theme=theme,
209
- )
210
-
211
-
212
- async def initialize_session(
213
- executor: Executor,
214
- event_queue: asyncio.Queue[events.Event],
215
- session_id: str | None = None,
216
- ) -> str | None:
217
- """Initialize a session and return the active session ID.
218
-
219
- Args:
220
- executor: The executor to submit operations to.
221
- event_queue: The event queue for synchronization.
222
- session_id: Optional session ID to resume. If None, creates a new session.
223
-
224
- Returns:
225
- The active session ID, or None if no session is active.
226
- """
227
- await executor.submit_and_wait(op.InitAgentOperation(session_id=session_id))
228
- await event_queue.join()
229
-
230
- active_session_id = executor.context.current_session_id()
231
- return active_session_id or session_id
232
-
233
-
234
- def _backfill_session_model_config(
235
- agent: Agent | None,
236
- model_override: str | None,
237
- default_model: str | None,
238
- is_new_session: bool,
239
- ) -> None:
240
- """Backfill model_config_name and model_thinking on newly created sessions."""
241
- if agent is None or agent.session.model_config_name is not None:
242
- return
243
-
244
- if model_override is not None:
245
- agent.session.model_config_name = model_override
246
- elif is_new_session and default_model is not None:
247
- agent.session.model_config_name = default_model
248
- else:
249
- return
250
-
251
- if agent.session.model_thinking is None and agent.profile:
252
- agent.session.model_thinking = agent.profile.llm_client.get_llm_config().thinking
253
- # Don't save here - session will be saved when first message is sent via append_history()
254
-
255
-
256
- async def cleanup_app_components(components: AppComponents) -> None:
257
- """Clean up all application components."""
258
- try:
259
- # Clean shutdown
260
- await components.executor.stop()
261
- components.executor_task.cancel()
262
- with contextlib.suppress(asyncio.CancelledError):
263
- await components.executor_task
264
- with contextlib.suppress(Exception):
265
- await close_default_store()
266
-
267
- # Signal UI to stop
268
- await components.event_queue.put(events.EndEvent())
269
- await components.display_task
270
- finally:
271
- # Ensure the terminal cursor is visible even if Rich's Status spinner
272
- # did not get a chance to stop cleanly (e.g. on KeyboardInterrupt).
273
- # If this fails the shell can still recover via `reset`/`stty sane`.
274
- with contextlib.suppress(Exception):
275
- stream = getattr(sys, "__stdout__", None) or sys.stdout
276
- stream.write("\033[?25h")
277
- stream.flush()
278
-
279
-
280
- async def _handle_keyboard_interrupt(executor: Executor) -> None:
281
- """Handle Ctrl+C by logging and sending a global interrupt."""
282
-
283
- log("Bye!")
284
- session_id = executor.context.current_session_id()
285
- if session_id and Session.exists(session_id):
286
- log(("Resume with:", "dim"), (f"klaude --resume-by-id {session_id}", "green"))
287
- # Executor might already be stopping
288
- with contextlib.suppress(Exception):
289
- await executor.submit(op.InterruptOperation(target_session_id=None))
290
-
291
-
292
- async def run_exec(init_config: AppInitConfig, input_content: str) -> None:
293
- """Run a single command non-interactively using the provided configuration."""
294
-
295
- components = await initialize_app_components(init_config)
296
-
297
- try:
298
- session_id = await initialize_session(components.executor, components.event_queue)
299
- _backfill_session_model_config(
300
- components.executor.context.current_agent,
301
- init_config.model,
302
- components.config.main_model,
303
- is_new_session=True,
304
- )
305
-
306
- wait_id = await submit_user_input_payload(
307
- executor=components.executor,
308
- event_queue=components.event_queue,
309
- user_input=UserInputPayload(text=input_content),
310
- session_id=session_id,
311
- )
312
- if wait_id is not None:
313
- await components.executor.wait_for(wait_id)
314
- await components.event_queue.join()
315
-
316
- except KeyboardInterrupt:
317
- await _handle_keyboard_interrupt(components.executor)
318
- finally:
319
- await cleanup_app_components(components)
320
-
321
-
322
- async def run_interactive(init_config: AppInitConfig, session_id: str | None = None) -> None:
323
- """Run the interactive REPL using the provided configuration.
324
-
325
- If session_id is None, a new session is created with an auto-generated ID.
326
- If session_id is provided, attempts to resume that session.
327
- """
328
- components = await initialize_app_components(init_config)
329
-
330
- # No theme persistence from CLI anymore; config.theme controls theme when set.
331
-
332
- # Create status provider for bottom toolbar
333
- def _status_provider() -> REPLStatusSnapshot:
334
- update_message = get_update_message()
335
- return build_repl_status_snapshot(update_message)
336
-
337
- # Set up input provider for interactive mode
338
- def _stop_rich_bottom_ui() -> None:
339
- display = components.display
340
- if isinstance(display, ui.REPLDisplay):
341
- display.renderer.spinner_stop()
342
- display.renderer.stop_bottom_live()
343
- elif (
344
- isinstance(display, ui.DebugEventDisplay)
345
- and display.wrapped_display
346
- and isinstance(display.wrapped_display, ui.REPLDisplay)
347
- ):
348
- display.wrapped_display.renderer.spinner_stop()
349
- display.wrapped_display.renderer.stop_bottom_live()
350
-
351
- # Pass the pre-detected theme to avoid redundant TTY queries.
352
- # Querying the terminal background again after an interactive selection
353
- # can interfere with prompt_toolkit's terminal state and break history navigation.
354
- is_light_background: bool | None = None
355
- if components.theme == "light":
356
- is_light_background = True
357
- elif components.theme == "dark":
358
- is_light_background = False
359
-
360
- def _get_active_session_id() -> str | None:
361
- """Get the current active session ID dynamically.
362
-
363
- This is necessary because /clear command creates a new session with a different ID.
364
- """
365
-
366
- return components.executor.context.current_session_id()
367
-
368
- async def _change_model_from_prompt(model_name: str) -> None:
369
- sid = _get_active_session_id()
370
- if not sid:
371
- return
372
- await components.executor.submit_and_wait(
373
- op.ChangeModelOperation(
374
- session_id=sid,
375
- model_name=model_name,
376
- save_as_default=False,
377
- defer_thinking_selection=True,
378
- emit_welcome_event=True,
379
- emit_switch_message=False,
380
- )
381
- )
382
-
383
- def _get_current_llm_config() -> llm_param.LLMConfigParameter | None:
384
- agent = components.executor.context.current_agent
385
- if agent is None:
386
- return None
387
- return agent.profile.llm_client.get_llm_config()
388
-
389
- async def _change_thinking_from_prompt(thinking: llm_param.Thinking) -> None:
390
- sid = _get_active_session_id()
391
- if not sid:
392
- return
393
- await components.executor.submit_and_wait(
394
- op.ChangeThinkingOperation(
395
- session_id=sid,
396
- thinking=thinking,
397
- emit_welcome_event=True,
398
- emit_switch_message=False,
399
- )
400
- )
401
-
402
- # Inject command name checker into user_input renderer (for slash command highlighting)
403
- from klaude_code.ui.renderers.user_input import set_command_name_checker
404
-
405
- set_command_name_checker(is_slash_command_name)
406
-
407
- input_provider: ui.InputProviderABC = ui.PromptToolkitInput(
408
- status_provider=_status_provider,
409
- pre_prompt=_stop_rich_bottom_ui,
410
- is_light_background=is_light_background,
411
- get_current_model_config_name=lambda: (
412
- components.executor.context.current_agent.session.model_config_name
413
- if components.executor.context.current_agent is not None
414
- else None
415
- ),
416
- on_change_model=_change_model_from_prompt,
417
- get_current_llm_config=_get_current_llm_config,
418
- on_change_thinking=_change_thinking_from_prompt,
419
- command_info_provider=get_command_info_list,
420
- )
421
-
422
- # --- Custom Ctrl+C handler: double-press within 2s to exit, single press shows toast ---
423
- def _show_toast_once() -> None:
424
- MSG = "Press ctrl+c again to exit"
425
- try:
426
- # Keep message short; avoid interfering with spinner layout
427
- printer: PrintCapable | None = None
428
-
429
- # Check if it's a REPLDisplay with renderer
430
- if isinstance(components.display, ui.REPLDisplay):
431
- printer = components.display.renderer
432
- # Check if it's a DebugEventDisplay wrapping a REPLDisplay
433
- elif (
434
- isinstance(components.display, ui.DebugEventDisplay)
435
- and components.display.wrapped_display
436
- and isinstance(components.display.wrapped_display, ui.REPLDisplay)
437
- ):
438
- printer = components.display.wrapped_display.renderer
439
-
440
- if printer is not None:
441
- printer.print(Text(f" {MSG} ", style="bold yellow reverse"))
442
- else:
443
- print(MSG, file=sys.stderr)
444
- except (AttributeError, TypeError, RuntimeError):
445
- # Fallback if themed print is unavailable (e.g., display not ready or Rich internal error)
446
- print(MSG, file=sys.stderr)
447
-
448
- def _hide_progress() -> None:
449
- return
450
-
451
- restore_sigint = install_sigint_double_press_exit(_show_toast_once, _hide_progress)
452
-
453
- exit_hint_printed = False
454
-
455
- try:
456
- await initialize_session(components.executor, components.event_queue, session_id=session_id)
457
- _backfill_session_model_config(
458
- components.executor.context.current_agent,
459
- init_config.model,
460
- components.config.main_model,
461
- is_new_session=session_id is None,
462
- )
463
-
464
- # Input
465
- await input_provider.start()
466
- async for user_input in input_provider.iter_inputs():
467
- # Handle special commands
468
- if user_input.text.strip().lower() in {"exit", ":q", "quit"}:
469
- break
470
- elif user_input.text.strip() == "":
471
- continue
472
- # Use dynamic session_id lookup to handle /clear creating new sessions.
473
- # UI/CLI parses commands and submits concrete operations; core executes operations.
474
- active_session_id = _get_active_session_id()
475
- is_interactive = has_interactive_command(user_input.text)
476
-
477
- wait_id = await submit_user_input_payload(
478
- executor=components.executor,
479
- event_queue=components.event_queue,
480
- user_input=user_input,
481
- session_id=active_session_id,
482
- )
483
-
484
- if wait_id is None:
485
- continue
486
-
487
- if is_interactive:
488
- await components.executor.wait_for(wait_id)
489
- continue
490
-
491
- # Esc monitor for long-running, interruptible operations
492
- async def _on_esc_interrupt() -> None:
493
- await components.executor.submit(op.InterruptOperation(target_session_id=_get_active_session_id()))
494
-
495
- stop_event, esc_task = start_esc_interrupt_monitor(_on_esc_interrupt)
496
- # Wait for this specific task to complete before accepting next input
497
- try:
498
- await components.executor.wait_for(wait_id)
499
- finally:
500
- # Stop ESC monitor and wait for it to finish cleaning up TTY
501
- stop_event.set()
502
- with contextlib.suppress(Exception):
503
- await esc_task
504
-
505
- except KeyboardInterrupt:
506
- await _handle_keyboard_interrupt(components.executor)
507
- exit_hint_printed = True
508
- finally:
509
- # Restore original SIGINT handler
510
- with contextlib.suppress(Exception):
511
- restore_sigint()
512
- await cleanup_app_components(components)
513
-
514
- if not exit_hint_printed:
515
- active_session_id = components.executor.context.current_session_id()
516
- if active_session_id and Session.exists(active_session_id):
517
- log(f"Session ID: {active_session_id}")
518
- log(f"Resume with: klaude --resume-by-id {active_session_id}")
@@ -1,108 +0,0 @@
1
- import datetime
2
- import shutil
3
- from functools import cache
4
- from importlib.resources import files
5
- from pathlib import Path
6
-
7
- from klaude_code.protocol import llm_param
8
- from klaude_code.protocol.sub_agent import get_sub_agent_profile
9
-
10
- COMMAND_DESCRIPTIONS: dict[str, str] = {
11
- "rg": "ripgrep - fast text search",
12
- "fd": "simple and fast alternative to find",
13
- "tree": "directory listing as a tree",
14
- "sg": "ast-grep - AST-aware code search",
15
- "jj": "jujutsu - Git-compatible version control system",
16
- }
17
-
18
- # Mapping from logical prompt keys to resource file paths under the core/prompt directory.
19
- PROMPT_FILES: dict[str, str] = {
20
- "main_codex": "prompts/prompt-codex.md",
21
- "main_gpt_5_1_codex_max": "prompts/prompt-codex-gpt-5-1-codex-max.md",
22
- "main_gpt_5_2_codex": "prompts/prompt-codex-gpt-5-2-codex.md",
23
- "main": "prompts/prompt-claude-code.md",
24
- "main_gemini": "prompts/prompt-gemini.md", # https://ai.google.dev/gemini-api/docs/prompting-strategies?hl=zh-cn#agentic-si-template
25
- }
26
-
27
-
28
- @cache
29
- def _load_prompt_by_path(prompt_path: str) -> str:
30
- """Load and cache prompt content from a file path relative to core package."""
31
- return files(__package__).joinpath(prompt_path).read_text(encoding="utf-8").strip()
32
-
33
-
34
- def _load_base_prompt(file_key: str) -> str:
35
- """Load and cache the base prompt content from file."""
36
- try:
37
- prompt_path = PROMPT_FILES[file_key]
38
- except KeyError as exc:
39
- raise ValueError(f"Unknown prompt key: {file_key}") from exc
40
-
41
- return _load_prompt_by_path(prompt_path)
42
-
43
-
44
- def _get_file_key(model_name: str, protocol: llm_param.LLMClientProtocol) -> str:
45
- """Determine which prompt file to use based on model."""
46
- match model_name:
47
- case name if "gpt-5.2-codex" in name:
48
- return "main_gpt_5_2_codex"
49
- case name if "gpt-5.1-codex-max" in name:
50
- return "main_gpt_5_1_codex_max"
51
- case name if "gpt-5" in name:
52
- return "main_codex"
53
- case name if "gemini" in name:
54
- return "main_gemini"
55
- case _:
56
- return "main"
57
-
58
-
59
- def _build_env_info(model_name: str) -> str:
60
- """Build environment info section with dynamic runtime values."""
61
- cwd = Path.cwd()
62
- today = datetime.datetime.now().strftime("%Y-%m-%d")
63
- is_git_repo = (cwd / ".git").exists()
64
- is_empty_dir = not any(cwd.iterdir())
65
-
66
- available_tools: list[str] = []
67
- for command, desc in COMMAND_DESCRIPTIONS.items():
68
- if shutil.which(command) is not None:
69
- available_tools.append(f"{command}: {desc}")
70
-
71
- cwd_display = f"{cwd} (empty)" if is_empty_dir else str(cwd)
72
- env_lines: list[str] = [
73
- "",
74
- "",
75
- "Here is useful information about the environment you are running in:",
76
- "<env>",
77
- f"Working directory: {cwd_display}",
78
- f"Today's Date: {today}",
79
- f"Is directory a git repo: {is_git_repo}",
80
- f"You are powered by the model: {model_name}",
81
- ]
82
-
83
- if available_tools:
84
- env_lines.append("Prefer to use the following CLI utilities:")
85
- for tool in available_tools:
86
- env_lines.append(f"- {tool}")
87
-
88
- env_lines.append("</env>")
89
-
90
- return "\n".join(env_lines)
91
-
92
-
93
- def load_system_prompt(
94
- model_name: str, protocol: llm_param.LLMClientProtocol, sub_agent_type: str | None = None
95
- ) -> str:
96
- """Get system prompt content for the given model and sub-agent type."""
97
- if sub_agent_type is not None:
98
- profile = get_sub_agent_profile(sub_agent_type)
99
- base_prompt = _load_prompt_by_path(profile.prompt_file)
100
- else:
101
- file_key = _get_file_key(model_name, protocol)
102
- base_prompt = _load_base_prompt(file_key)
103
-
104
- if protocol == llm_param.LLMClientProtocol.CODEX_OAUTH:
105
- # Do not append environment info for Codex protocol
106
- return base_prompt
107
-
108
- return base_prompt + _build_env_info(model_name)
@@ -1,24 +0,0 @@
1
- Execute a skill within the main conversation
2
-
3
- <skills_instructions>
4
- When users ask you to perform tasks, check if any of the available skills below can help complete the task more effectively. Skills provide specialized capabilities and domain knowledge.
5
-
6
- How to use skills:
7
- - Invoke skills using this tool with the skill name only (no arguments)
8
- - When you invoke a skill, you will see <command-message>The "{name}" skill is loading</command-message>
9
- - The skill's prompt will expand and provide detailed instructions on how to complete the task
10
-
11
- Examples:
12
- - command: "pdf" - invoke the pdf skill
13
- - command: "xlsx" - invoke the xlsx skill
14
- - command: "document-skills:pdf" - invoke using fully qualified name
15
-
16
- Important:
17
- - Only use skills listed in <available_skills> below
18
- - Do not invoke a skill that is already running
19
- - Do not use this tool for built-in CLI commands (like /help, /clear, etc.)
20
- </skills_instructions>
21
-
22
- <available_skills>
23
- $skills_xml
24
- </available_skills>