klaude-code 2.0.1__py3-none-any.whl → 2.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) 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 +10 -1
  14. klaude_code/core/agent.py +9 -62
  15. klaude_code/core/agent_profile.py +284 -0
  16. klaude_code/core/executor.py +343 -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 +107 -155
  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 +42 -44
  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/skill/skill_tool.py +3 -1
  30. klaude_code/core/tool/sub_agent_tool.py +8 -7
  31. klaude_code/core/tool/todo/todo_write_tool.py +3 -9
  32. klaude_code/core/tool/todo/update_plan_tool.py +3 -5
  33. klaude_code/core/tool/tool_abc.py +2 -1
  34. klaude_code/core/tool/tool_registry.py +2 -33
  35. klaude_code/core/tool/tool_runner.py +13 -10
  36. klaude_code/core/tool/web/mermaid_tool.py +3 -1
  37. klaude_code/core/tool/web/web_fetch_tool.py +5 -3
  38. klaude_code/core/tool/web/web_search_tool.py +5 -3
  39. klaude_code/core/turn.py +86 -26
  40. klaude_code/llm/anthropic/client.py +1 -1
  41. klaude_code/llm/bedrock/client.py +1 -1
  42. klaude_code/llm/claude/client.py +1 -1
  43. klaude_code/llm/codex/client.py +1 -1
  44. klaude_code/llm/google/client.py +1 -1
  45. klaude_code/llm/openai_compatible/client.py +1 -1
  46. klaude_code/llm/openai_compatible/tool_call_accumulator.py +1 -1
  47. klaude_code/llm/openrouter/client.py +1 -1
  48. klaude_code/llm/openrouter/reasoning.py +1 -1
  49. klaude_code/llm/responses/client.py +1 -1
  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 +23 -0
  58. klaude_code/protocol/message.py +3 -11
  59. klaude_code/protocol/model.py +78 -9
  60. klaude_code/protocol/op.py +5 -0
  61. klaude_code/protocol/sub_agent/explore.py +0 -15
  62. klaude_code/protocol/sub_agent/task.py +1 -1
  63. klaude_code/protocol/sub_agent/web.py +1 -17
  64. klaude_code/protocol/tools.py +0 -1
  65. klaude_code/session/session.py +6 -5
  66. klaude_code/skill/assets/create-plan/SKILL.md +76 -0
  67. klaude_code/skill/loader.py +1 -1
  68. klaude_code/skill/system_skills.py +1 -1
  69. klaude_code/tui/__init__.py +8 -0
  70. klaude_code/{command → tui/command}/clear_cmd.py +2 -1
  71. klaude_code/{command → tui/command}/debug_cmd.py +4 -3
  72. klaude_code/{command → tui/command}/export_cmd.py +2 -1
  73. klaude_code/{command → tui/command}/export_online_cmd.py +6 -5
  74. klaude_code/{command → tui/command}/fork_session_cmd.py +10 -9
  75. klaude_code/{command → tui/command}/help_cmd.py +3 -2
  76. klaude_code/{command → tui/command}/model_cmd.py +5 -4
  77. klaude_code/{command → tui/command}/model_select.py +2 -2
  78. klaude_code/{command → tui/command}/prompt_command.py +4 -3
  79. klaude_code/{command → tui/command}/refresh_cmd.py +3 -1
  80. klaude_code/{command → tui/command}/registry.py +16 -6
  81. klaude_code/{command → tui/command}/release_notes_cmd.py +3 -2
  82. klaude_code/{command → tui/command}/resume_cmd.py +6 -5
  83. klaude_code/{command → tui/command}/status_cmd.py +4 -3
  84. klaude_code/{command → tui/command}/terminal_setup_cmd.py +4 -3
  85. klaude_code/{command → tui/command}/thinking_cmd.py +4 -3
  86. klaude_code/tui/commands.py +164 -0
  87. klaude_code/{ui/renderers → tui/components}/assistant.py +3 -3
  88. klaude_code/{ui/renderers → tui/components}/bash_syntax.py +2 -2
  89. klaude_code/{ui/renderers → tui/components}/common.py +1 -1
  90. klaude_code/tui/components/developer.py +231 -0
  91. klaude_code/{ui/renderers → tui/components}/diffs.py +2 -2
  92. klaude_code/{ui/renderers → tui/components}/errors.py +2 -2
  93. klaude_code/{ui/renderers → tui/components}/metadata.py +34 -21
  94. klaude_code/{ui → tui/components}/rich/markdown.py +78 -34
  95. klaude_code/{ui → tui/components}/rich/status.py +2 -2
  96. klaude_code/{ui → tui/components}/rich/theme.py +12 -5
  97. klaude_code/{ui/renderers → tui/components}/sub_agent.py +23 -43
  98. klaude_code/{ui/renderers → tui/components}/thinking.py +3 -3
  99. klaude_code/{ui/renderers → tui/components}/tools.py +11 -48
  100. klaude_code/{ui/renderers → tui/components}/user_input.py +3 -20
  101. klaude_code/tui/display.py +85 -0
  102. klaude_code/{ui/modes/repl → tui/input}/__init__.py +1 -1
  103. klaude_code/{ui/modes/repl → tui/input}/completers.py +1 -1
  104. klaude_code/{ui/modes/repl/input_prompt_toolkit.py → tui/input/prompt_toolkit.py} +11 -7
  105. klaude_code/tui/machine.py +606 -0
  106. klaude_code/tui/renderer.py +707 -0
  107. klaude_code/tui/runner.py +321 -0
  108. klaude_code/tui/terminal/__init__.py +56 -0
  109. klaude_code/{ui → tui}/terminal/color.py +1 -1
  110. klaude_code/{ui → tui}/terminal/control.py +1 -1
  111. klaude_code/{ui → tui}/terminal/notifier.py +1 -1
  112. klaude_code/{ui → tui}/terminal/selector.py +36 -17
  113. klaude_code/ui/__init__.py +6 -50
  114. klaude_code/ui/core/display.py +3 -3
  115. klaude_code/ui/core/input.py +2 -1
  116. klaude_code/ui/{modes/debug/display.py → debug_mode.py} +1 -1
  117. klaude_code/ui/{modes/exec/display.py → exec_mode.py} +1 -4
  118. klaude_code/ui/terminal/__init__.py +6 -54
  119. klaude_code/ui/terminal/title.py +31 -0
  120. klaude_code/update.py +163 -0
  121. {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/METADATA +1 -1
  122. klaude_code-2.1.0.dist-info/RECORD +235 -0
  123. klaude_code/cli/runtime.py +0 -525
  124. klaude_code/core/prompt.py +0 -108
  125. klaude_code/core/tool/file/move_tool.md +0 -41
  126. klaude_code/core/tool/file/move_tool.py +0 -435
  127. klaude_code/core/tool/tool_context.py +0 -148
  128. klaude_code/protocol/events.py +0 -194
  129. klaude_code/skill/assets/dev-docs/SKILL.md +0 -108
  130. klaude_code/trace/__init__.py +0 -21
  131. klaude_code/ui/core/stage_manager.py +0 -48
  132. klaude_code/ui/modes/__init__.py +0 -1
  133. klaude_code/ui/modes/debug/__init__.py +0 -1
  134. klaude_code/ui/modes/exec/__init__.py +0 -1
  135. klaude_code/ui/modes/repl/display.py +0 -61
  136. klaude_code/ui/modes/repl/event_handler.py +0 -634
  137. klaude_code/ui/modes/repl/renderer.py +0 -463
  138. klaude_code/ui/renderers/developer.py +0 -215
  139. klaude_code/ui/utils/__init__.py +0 -1
  140. klaude_code-2.0.1.dist-info/RECORD +0 -229
  141. /klaude_code/{trace/log.py → log.py} +0 -0
  142. /klaude_code/{command → tui/command}/__init__.py +0 -0
  143. /klaude_code/{command → tui/command}/command_abc.py +0 -0
  144. /klaude_code/{command → tui/command}/prompt-commit.md +0 -0
  145. /klaude_code/{command → tui/command}/prompt-init.md +0 -0
  146. /klaude_code/{ui/renderers → tui/components}/__init__.py +0 -0
  147. /klaude_code/{ui/renderers → tui/components}/mermaid_viewer.py +0 -0
  148. /klaude_code/{ui → tui/components}/rich/__init__.py +0 -0
  149. /klaude_code/{ui → tui/components}/rich/cjk_wrap.py +0 -0
  150. /klaude_code/{ui → tui/components}/rich/code_panel.py +0 -0
  151. /klaude_code/{ui → tui/components}/rich/live.py +0 -0
  152. /klaude_code/{ui → tui/components}/rich/quote.py +0 -0
  153. /klaude_code/{ui → tui/components}/rich/searchable_text.py +0 -0
  154. /klaude_code/{ui/modes/repl → tui/input}/clipboard.py +0 -0
  155. /klaude_code/{ui/modes/repl → tui/input}/key_bindings.py +0 -0
  156. /klaude_code/{ui → tui}/terminal/image.py +0 -0
  157. /klaude_code/{ui → tui}/terminal/progress_bar.py +0 -0
  158. /klaude_code/ui/{utils/common.py → common.py} +0 -0
  159. {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/WHEEL +0 -0
  160. {klaude_code-2.0.1.dist-info → klaude_code-2.1.0.dist-info}/entry_points.txt +0 -0
@@ -1,525 +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
- from klaude_code.ui.terminal.progress_bar import OSC94States, emit_osc94
29
-
30
-
31
- class PrintCapable(Protocol):
32
- """Protocol for objects that can print styled content."""
33
-
34
- def print(self, *objects: Any, style: Any | None = None, end: str = "\n") -> None: ...
35
-
36
-
37
- @dataclass
38
- class AppInitConfig:
39
- """Configuration for initializing the application components."""
40
-
41
- model: str | None
42
- debug: bool
43
- vanilla: bool
44
- is_exec_mode: bool = False
45
- debug_filters: set[DebugType] | None = None
46
- stream_json: bool = False
47
-
48
-
49
- @dataclass
50
- class AppComponents:
51
- """Initialized application components."""
52
-
53
- config: Config
54
- executor: Executor
55
- executor_task: asyncio.Task[None]
56
- event_queue: asyncio.Queue[events.Event]
57
- display: ui.DisplayABC
58
- display_task: asyncio.Task[None]
59
- theme: str | None
60
-
61
-
62
- async def submit_user_input_payload(
63
- *,
64
- executor: Executor,
65
- event_queue: asyncio.Queue[events.Event],
66
- user_input: UserInputPayload,
67
- session_id: str | None,
68
- ) -> str | None:
69
- """Parse/dispatch a user input payload and submit resulting operations.
70
-
71
- The UI/CLI layer owns slash command parsing and any interactive prompts.
72
- Core only executes concrete operations.
73
-
74
- Returns a submission id that should be awaited, or None if there is nothing
75
- to wait for (e.g. commands that only emit events).
76
- """
77
-
78
- sid = session_id or executor.context.current_session_id()
79
- if sid is None:
80
- raise RuntimeError("No active session")
81
-
82
- agent = executor.context.current_agent
83
- if agent is None or agent.session.id != sid:
84
- await executor.submit_and_wait(op.InitAgentOperation(session_id=sid))
85
- agent = executor.context.current_agent
86
-
87
- if agent is None:
88
- raise RuntimeError("Failed to initialize agent")
89
-
90
- submission_id = uuid4().hex
91
-
92
- await executor.context.emit_event(
93
- events.UserMessageEvent(content=user_input.text, session_id=sid, images=user_input.images)
94
- )
95
-
96
- result = await dispatch_command(user_input, agent, submission_id=submission_id)
97
- operations: list[op.Operation] = list(result.operations or [])
98
-
99
- run_ops = [candidate for candidate in operations if isinstance(candidate, op.RunAgentOperation)]
100
- if len(run_ops) > 1:
101
- raise ValueError("Multiple RunAgentOperation results are not supported")
102
-
103
- persisted_user_input = run_ops[0].input if run_ops else user_input
104
-
105
- if result.persist_user_input:
106
- agent.session.append_history(
107
- [
108
- protocol_message.UserMessage(
109
- parts=protocol_message.parts_from_text_and_images(
110
- persisted_user_input.text,
111
- persisted_user_input.images,
112
- )
113
- )
114
- ]
115
- )
116
-
117
- if result.events:
118
- for evt in result.events:
119
- if result.persist_events and isinstance(evt, events.DeveloperMessageEvent):
120
- agent.session.append_history([evt.item])
121
- await executor.context.emit_event(evt)
122
-
123
- submitted_ids: list[str] = []
124
- for operation_item in operations:
125
- submitted_ids.append(await executor.submit(operation_item))
126
-
127
- if not submitted_ids:
128
- # Ensure event-only commands are fully rendered before showing the next prompt.
129
- await event_queue.join()
130
- return None
131
-
132
- if run_ops:
133
- return run_ops[0].id
134
- return submitted_ids[-1]
135
-
136
-
137
- async def initialize_app_components(init_config: AppInitConfig) -> AppComponents:
138
- """Initialize all application components (LLM clients, executor, UI)."""
139
- set_debug_logging(init_config.debug, filters=init_config.debug_filters)
140
-
141
- config = load_config()
142
-
143
- # Initialize LLM clients
144
- try:
145
- llm_clients = build_llm_clients(
146
- config,
147
- model_override=init_config.model,
148
- )
149
- except ValueError as exc:
150
- if init_config.model:
151
- log(
152
- (
153
- f"Error: model '{init_config.model}' is not defined in the config",
154
- "red",
155
- )
156
- )
157
- log(("Hint: run `klaude list` to view available models", "yellow"))
158
- else:
159
- log((f"Error: failed to load the default model configuration: {exc}", "red"))
160
- raise typer.Exit(2) from None
161
-
162
- model_profile_provider = VanillaModelProfileProvider() if init_config.vanilla else DefaultModelProfileProvider()
163
-
164
- # Create event queue for communication between executor and UI
165
- event_queue: asyncio.Queue[events.Event] = asyncio.Queue()
166
-
167
- # Create executor with the LLM client
168
- executor = Executor(
169
- event_queue,
170
- llm_clients,
171
- model_profile_provider=model_profile_provider,
172
- on_model_change=update_terminal_title,
173
- )
174
-
175
- # Update terminal title with initial model name
176
- update_terminal_title(llm_clients.main.model_name)
177
-
178
- # Start executor in background
179
- executor_task = asyncio.create_task(executor.start())
180
-
181
- theme: str | None = config.theme
182
- if theme is None and not init_config.is_exec_mode:
183
- # Auto-detect theme from terminal background when config does not specify a theme.
184
- # Skip detection in exec mode to avoid TTY race conditions with parent process's
185
- # ESC monitor when running as a subprocess.
186
- detected = is_light_terminal_background()
187
- if detected is True:
188
- theme = "light"
189
- elif detected is False:
190
- theme = "dark"
191
-
192
- # Set up UI components using factory functions
193
- display: ui.DisplayABC
194
- if init_config.is_exec_mode:
195
- display = ui.create_exec_display(debug=init_config.debug, stream_json=init_config.stream_json)
196
- else:
197
- display = ui.create_default_display(debug=init_config.debug, theme=theme)
198
-
199
- # Start UI display task
200
- display_task = asyncio.create_task(display.consume_event_loop(event_queue))
201
-
202
- return AppComponents(
203
- config=config,
204
- executor=executor,
205
- executor_task=executor_task,
206
- event_queue=event_queue,
207
- display=display,
208
- display_task=display_task,
209
- theme=theme,
210
- )
211
-
212
-
213
- async def initialize_session(
214
- executor: Executor,
215
- event_queue: asyncio.Queue[events.Event],
216
- session_id: str | None = None,
217
- ) -> str | None:
218
- """Initialize a session and return the active session ID.
219
-
220
- Args:
221
- executor: The executor to submit operations to.
222
- event_queue: The event queue for synchronization.
223
- session_id: Optional session ID to resume. If None, creates a new session.
224
-
225
- Returns:
226
- The active session ID, or None if no session is active.
227
- """
228
- await executor.submit_and_wait(op.InitAgentOperation(session_id=session_id))
229
- await event_queue.join()
230
-
231
- active_session_id = executor.context.current_session_id()
232
- return active_session_id or session_id
233
-
234
-
235
- def _backfill_session_model_config(
236
- agent: Agent | None,
237
- model_override: str | None,
238
- default_model: str | None,
239
- is_new_session: bool,
240
- ) -> None:
241
- """Backfill model_config_name and model_thinking on newly created sessions."""
242
- if agent is None or agent.session.model_config_name is not None:
243
- return
244
-
245
- if model_override is not None:
246
- agent.session.model_config_name = model_override
247
- elif is_new_session and default_model is not None:
248
- agent.session.model_config_name = default_model
249
- else:
250
- return
251
-
252
- if agent.session.model_thinking is None and agent.profile:
253
- agent.session.model_thinking = agent.profile.llm_client.get_llm_config().thinking
254
- # Don't save here - session will be saved when first message is sent via append_history()
255
-
256
-
257
- async def cleanup_app_components(components: AppComponents) -> None:
258
- """Clean up all application components."""
259
- try:
260
- # Clean shutdown
261
- await components.executor.stop()
262
- components.executor_task.cancel()
263
- with contextlib.suppress(asyncio.CancelledError):
264
- await components.executor_task
265
- with contextlib.suppress(Exception):
266
- await close_default_store()
267
-
268
- # Signal UI to stop
269
- await components.event_queue.put(events.EndEvent())
270
- await components.display_task
271
- finally:
272
- # Always attempt to clear Ghostty progress bar and restore cursor visibility
273
- # Best-effort only; never fail cleanup due to OSC errors
274
- with contextlib.suppress(Exception):
275
- emit_osc94(OSC94States.HIDDEN)
276
-
277
- # Ensure the terminal cursor is visible even if Rich's Status spinner
278
- # did not get a chance to stop cleanly (e.g. on KeyboardInterrupt).
279
- # If this fails the shell can still recover via `reset`/`stty sane`.
280
- with contextlib.suppress(Exception):
281
- stream = getattr(sys, "__stdout__", None) or sys.stdout
282
- stream.write("\033[?25h")
283
- stream.flush()
284
-
285
-
286
- async def _handle_keyboard_interrupt(executor: Executor) -> None:
287
- """Handle Ctrl+C by logging and sending a global interrupt."""
288
-
289
- log("Bye!")
290
- session_id = executor.context.current_session_id()
291
- if session_id and Session.exists(session_id):
292
- log(("Resume with:", "dim"), (f"klaude --resume-by-id {session_id}", "green"))
293
- # Executor might already be stopping
294
- with contextlib.suppress(Exception):
295
- await executor.submit(op.InterruptOperation(target_session_id=None))
296
-
297
-
298
- async def run_exec(init_config: AppInitConfig, input_content: str) -> None:
299
- """Run a single command non-interactively using the provided configuration."""
300
-
301
- components = await initialize_app_components(init_config)
302
-
303
- try:
304
- session_id = await initialize_session(components.executor, components.event_queue)
305
- _backfill_session_model_config(
306
- components.executor.context.current_agent,
307
- init_config.model,
308
- components.config.main_model,
309
- is_new_session=True,
310
- )
311
-
312
- wait_id = await submit_user_input_payload(
313
- executor=components.executor,
314
- event_queue=components.event_queue,
315
- user_input=UserInputPayload(text=input_content),
316
- session_id=session_id,
317
- )
318
- if wait_id is not None:
319
- await components.executor.wait_for(wait_id)
320
- await components.event_queue.join()
321
-
322
- except KeyboardInterrupt:
323
- await _handle_keyboard_interrupt(components.executor)
324
- finally:
325
- await cleanup_app_components(components)
326
-
327
-
328
- async def run_interactive(init_config: AppInitConfig, session_id: str | None = None) -> None:
329
- """Run the interactive REPL using the provided configuration.
330
-
331
- If session_id is None, a new session is created with an auto-generated ID.
332
- If session_id is provided, attempts to resume that session.
333
- """
334
- components = await initialize_app_components(init_config)
335
-
336
- # No theme persistence from CLI anymore; config.theme controls theme when set.
337
-
338
- # Create status provider for bottom toolbar
339
- def _status_provider() -> REPLStatusSnapshot:
340
- update_message = get_update_message()
341
- return build_repl_status_snapshot(update_message)
342
-
343
- # Set up input provider for interactive mode
344
- def _stop_rich_bottom_ui() -> None:
345
- display = components.display
346
- if isinstance(display, ui.REPLDisplay):
347
- display.renderer.spinner_stop()
348
- display.renderer.stop_bottom_live()
349
- elif (
350
- isinstance(display, ui.DebugEventDisplay)
351
- and display.wrapped_display
352
- and isinstance(display.wrapped_display, ui.REPLDisplay)
353
- ):
354
- display.wrapped_display.renderer.spinner_stop()
355
- display.wrapped_display.renderer.stop_bottom_live()
356
-
357
- # Pass the pre-detected theme to avoid redundant TTY queries.
358
- # Querying the terminal background again after an interactive selection
359
- # can interfere with prompt_toolkit's terminal state and break history navigation.
360
- is_light_background: bool | None = None
361
- if components.theme == "light":
362
- is_light_background = True
363
- elif components.theme == "dark":
364
- is_light_background = False
365
-
366
- def _get_active_session_id() -> str | None:
367
- """Get the current active session ID dynamically.
368
-
369
- This is necessary because /clear command creates a new session with a different ID.
370
- """
371
-
372
- return components.executor.context.current_session_id()
373
-
374
- async def _change_model_from_prompt(model_name: str) -> None:
375
- sid = _get_active_session_id()
376
- if not sid:
377
- return
378
- await components.executor.submit_and_wait(
379
- op.ChangeModelOperation(
380
- session_id=sid,
381
- model_name=model_name,
382
- save_as_default=False,
383
- defer_thinking_selection=True,
384
- emit_welcome_event=True,
385
- emit_switch_message=False,
386
- )
387
- )
388
-
389
- def _get_current_llm_config() -> llm_param.LLMConfigParameter | None:
390
- agent = components.executor.context.current_agent
391
- if agent is None:
392
- return None
393
- return agent.profile.llm_client.get_llm_config()
394
-
395
- async def _change_thinking_from_prompt(thinking: llm_param.Thinking) -> None:
396
- sid = _get_active_session_id()
397
- if not sid:
398
- return
399
- await components.executor.submit_and_wait(
400
- op.ChangeThinkingOperation(
401
- session_id=sid,
402
- thinking=thinking,
403
- emit_welcome_event=True,
404
- emit_switch_message=False,
405
- )
406
- )
407
-
408
- # Inject command name checker into user_input renderer (for slash command highlighting)
409
- from klaude_code.ui.renderers.user_input import set_command_name_checker
410
-
411
- set_command_name_checker(is_slash_command_name)
412
-
413
- input_provider: ui.InputProviderABC = ui.PromptToolkitInput(
414
- status_provider=_status_provider,
415
- pre_prompt=_stop_rich_bottom_ui,
416
- is_light_background=is_light_background,
417
- get_current_model_config_name=lambda: (
418
- components.executor.context.current_agent.session.model_config_name
419
- if components.executor.context.current_agent is not None
420
- else None
421
- ),
422
- on_change_model=_change_model_from_prompt,
423
- get_current_llm_config=_get_current_llm_config,
424
- on_change_thinking=_change_thinking_from_prompt,
425
- command_info_provider=get_command_info_list,
426
- )
427
-
428
- # --- Custom Ctrl+C handler: double-press within 2s to exit, single press shows toast ---
429
- def _show_toast_once() -> None:
430
- MSG = "Press ctrl+c again to exit"
431
- try:
432
- # Keep message short; avoid interfering with spinner layout
433
- printer: PrintCapable | None = None
434
-
435
- # Check if it's a REPLDisplay with renderer
436
- if isinstance(components.display, ui.REPLDisplay):
437
- printer = components.display.renderer
438
- # Check if it's a DebugEventDisplay wrapping a REPLDisplay
439
- elif (
440
- isinstance(components.display, ui.DebugEventDisplay)
441
- and components.display.wrapped_display
442
- and isinstance(components.display.wrapped_display, ui.REPLDisplay)
443
- ):
444
- printer = components.display.wrapped_display.renderer
445
-
446
- if printer is not None:
447
- printer.print(Text(f" {MSG} ", style="bold yellow reverse"))
448
- else:
449
- print(MSG, file=sys.stderr)
450
- except (AttributeError, TypeError, RuntimeError):
451
- # Fallback if themed print is unavailable (e.g., display not ready or Rich internal error)
452
- print(MSG, file=sys.stderr)
453
-
454
- def _hide_progress() -> None:
455
- with contextlib.suppress(Exception):
456
- emit_osc94(OSC94States.HIDDEN)
457
-
458
- restore_sigint = install_sigint_double_press_exit(_show_toast_once, _hide_progress)
459
-
460
- exit_hint_printed = False
461
-
462
- try:
463
- await initialize_session(components.executor, components.event_queue, session_id=session_id)
464
- _backfill_session_model_config(
465
- components.executor.context.current_agent,
466
- init_config.model,
467
- components.config.main_model,
468
- is_new_session=session_id is None,
469
- )
470
-
471
- # Input
472
- await input_provider.start()
473
- async for user_input in input_provider.iter_inputs():
474
- # Handle special commands
475
- if user_input.text.strip().lower() in {"exit", ":q", "quit"}:
476
- break
477
- elif user_input.text.strip() == "":
478
- continue
479
- # Use dynamic session_id lookup to handle /clear creating new sessions.
480
- # UI/CLI parses commands and submits concrete operations; core executes operations.
481
- active_session_id = _get_active_session_id()
482
- is_interactive = has_interactive_command(user_input.text)
483
-
484
- wait_id = await submit_user_input_payload(
485
- executor=components.executor,
486
- event_queue=components.event_queue,
487
- user_input=user_input,
488
- session_id=active_session_id,
489
- )
490
-
491
- if wait_id is None:
492
- continue
493
-
494
- if is_interactive:
495
- await components.executor.wait_for(wait_id)
496
- continue
497
-
498
- # Esc monitor for long-running, interruptible operations
499
- async def _on_esc_interrupt() -> None:
500
- await components.executor.submit(op.InterruptOperation(target_session_id=_get_active_session_id()))
501
-
502
- stop_event, esc_task = start_esc_interrupt_monitor(_on_esc_interrupt)
503
- # Wait for this specific task to complete before accepting next input
504
- try:
505
- await components.executor.wait_for(wait_id)
506
- finally:
507
- # Stop ESC monitor and wait for it to finish cleaning up TTY
508
- stop_event.set()
509
- with contextlib.suppress(Exception):
510
- await esc_task
511
-
512
- except KeyboardInterrupt:
513
- await _handle_keyboard_interrupt(components.executor)
514
- exit_hint_printed = True
515
- finally:
516
- # Restore original SIGINT handler
517
- with contextlib.suppress(Exception):
518
- restore_sigint()
519
- await cleanup_app_components(components)
520
-
521
- if not exit_hint_printed:
522
- active_session_id = components.executor.context.current_session_id()
523
- if active_session_id and Session.exists(active_session_id):
524
- log(f"Session ID: {active_session_id}")
525
- 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,41 +0,0 @@
1
- Moves a range of lines from one file to another.
2
-
3
- Usage:
4
- - Cuts lines from `start_line` to `end_line` (inclusive, 1-indexed) from the source file
5
- - Pastes them into the target file at `insert_line` (inserted before that line)
6
- - Both files must have been read first using the Read tool
7
- - To create a new target file, set `insert_line` to 1 and ensure target file does not exist
8
- - For same-file moves, line numbers refer to the original file state before any changes
9
- - Use this tool when refactoring code into separate modules to avoid passing large code blocks twice
10
- - To move files or directories, use the Bash tool with `mv` command instead
11
-
12
- Return format:
13
- The tool returns context snippets showing the state after the operation:
14
-
15
- 1. Source file context (after cut): Shows 3 lines before and after the cut location
16
- 2. Target file context (after insert): Shows 3 lines before the inserted content, the inserted content itself, and 3 lines after
17
-
18
- Example output:
19
- ```
20
- Cut 8 lines from /path/source.py (lines 9-16) and pasted into /path/target.py (updated) at line 10.
21
-
22
- Source file context (after cut):
23
- 6 return value
24
- 7
25
- 8
26
- -------- cut here --------
27
- 9 class NextClass:
28
- 10 pass
29
- 11
30
-
31
- Target file context (after insert):
32
- 7 return {}
33
- 8
34
- 9
35
- -------- inserted --------
36
- 10 class MovedClass:
37
- ...
38
- 17 return result
39
- -------- end --------
40
- 18 # Next section
41
- ```