klaude-code 1.2.6__py3-none-any.whl → 1.8.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 (205) hide show
  1. klaude_code/auth/__init__.py +24 -0
  2. klaude_code/auth/codex/__init__.py +20 -0
  3. klaude_code/auth/codex/exceptions.py +17 -0
  4. klaude_code/auth/codex/jwt_utils.py +45 -0
  5. klaude_code/auth/codex/oauth.py +229 -0
  6. klaude_code/auth/codex/token_manager.py +84 -0
  7. klaude_code/cli/auth_cmd.py +73 -0
  8. klaude_code/cli/config_cmd.py +91 -0
  9. klaude_code/cli/cost_cmd.py +338 -0
  10. klaude_code/cli/debug.py +78 -0
  11. klaude_code/cli/list_model.py +307 -0
  12. klaude_code/cli/main.py +233 -134
  13. klaude_code/cli/runtime.py +309 -117
  14. klaude_code/{version.py → cli/self_update.py} +114 -5
  15. klaude_code/cli/session_cmd.py +37 -21
  16. klaude_code/command/__init__.py +88 -27
  17. klaude_code/command/clear_cmd.py +8 -7
  18. klaude_code/command/command_abc.py +31 -31
  19. klaude_code/command/debug_cmd.py +79 -0
  20. klaude_code/command/export_cmd.py +19 -53
  21. klaude_code/command/export_online_cmd.py +154 -0
  22. klaude_code/command/fork_session_cmd.py +267 -0
  23. klaude_code/command/help_cmd.py +7 -8
  24. klaude_code/command/model_cmd.py +60 -10
  25. klaude_code/command/model_select.py +84 -0
  26. klaude_code/command/prompt-jj-describe.md +32 -0
  27. klaude_code/command/prompt_command.py +19 -11
  28. klaude_code/command/refresh_cmd.py +8 -10
  29. klaude_code/command/registry.py +139 -40
  30. klaude_code/command/release_notes_cmd.py +84 -0
  31. klaude_code/command/resume_cmd.py +111 -0
  32. klaude_code/command/status_cmd.py +104 -60
  33. klaude_code/command/terminal_setup_cmd.py +7 -9
  34. klaude_code/command/thinking_cmd.py +98 -0
  35. klaude_code/config/__init__.py +14 -6
  36. klaude_code/config/assets/__init__.py +1 -0
  37. klaude_code/config/assets/builtin_config.yaml +303 -0
  38. klaude_code/config/builtin_config.py +38 -0
  39. klaude_code/config/config.py +378 -109
  40. klaude_code/config/select_model.py +117 -53
  41. klaude_code/config/thinking.py +269 -0
  42. klaude_code/{const/__init__.py → const.py} +50 -19
  43. klaude_code/core/agent.py +20 -28
  44. klaude_code/core/executor.py +327 -112
  45. klaude_code/core/manager/__init__.py +2 -4
  46. klaude_code/core/manager/llm_clients.py +1 -15
  47. klaude_code/core/manager/llm_clients_builder.py +10 -11
  48. klaude_code/core/manager/sub_agent_manager.py +37 -6
  49. klaude_code/core/prompt.py +63 -44
  50. klaude_code/core/prompts/prompt-claude-code.md +2 -13
  51. klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
  52. klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
  53. klaude_code/core/prompts/prompt-codex.md +9 -42
  54. klaude_code/core/prompts/prompt-minimal.md +12 -0
  55. klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
  56. klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
  57. klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
  58. klaude_code/core/reminders.py +283 -95
  59. klaude_code/core/task.py +113 -75
  60. klaude_code/core/tool/__init__.py +24 -31
  61. klaude_code/core/tool/file/_utils.py +36 -0
  62. klaude_code/core/tool/file/apply_patch.py +17 -25
  63. klaude_code/core/tool/file/apply_patch_tool.py +57 -77
  64. klaude_code/core/tool/file/diff_builder.py +151 -0
  65. klaude_code/core/tool/file/edit_tool.py +50 -63
  66. klaude_code/core/tool/file/move_tool.md +41 -0
  67. klaude_code/core/tool/file/move_tool.py +435 -0
  68. klaude_code/core/tool/file/read_tool.md +1 -1
  69. klaude_code/core/tool/file/read_tool.py +86 -86
  70. klaude_code/core/tool/file/write_tool.py +59 -69
  71. klaude_code/core/tool/report_back_tool.py +84 -0
  72. klaude_code/core/tool/shell/bash_tool.py +265 -22
  73. klaude_code/core/tool/shell/command_safety.py +3 -6
  74. klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
  75. klaude_code/core/tool/sub_agent_tool.py +13 -2
  76. klaude_code/core/tool/todo/todo_write_tool.md +0 -157
  77. klaude_code/core/tool/todo/todo_write_tool.py +1 -1
  78. klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
  79. klaude_code/core/tool/todo/update_plan_tool.py +1 -1
  80. klaude_code/core/tool/tool_abc.py +18 -0
  81. klaude_code/core/tool/tool_context.py +27 -12
  82. klaude_code/core/tool/tool_registry.py +7 -7
  83. klaude_code/core/tool/tool_runner.py +44 -36
  84. klaude_code/core/tool/truncation.py +29 -14
  85. klaude_code/core/tool/web/mermaid_tool.md +43 -0
  86. klaude_code/core/tool/web/mermaid_tool.py +2 -5
  87. klaude_code/core/tool/web/web_fetch_tool.md +1 -1
  88. klaude_code/core/tool/web/web_fetch_tool.py +112 -22
  89. klaude_code/core/tool/web/web_search_tool.md +23 -0
  90. klaude_code/core/tool/web/web_search_tool.py +130 -0
  91. klaude_code/core/turn.py +168 -66
  92. klaude_code/llm/__init__.py +2 -10
  93. klaude_code/llm/anthropic/client.py +190 -178
  94. klaude_code/llm/anthropic/input.py +39 -15
  95. klaude_code/llm/bedrock/__init__.py +3 -0
  96. klaude_code/llm/bedrock/client.py +60 -0
  97. klaude_code/llm/client.py +7 -21
  98. klaude_code/llm/codex/__init__.py +5 -0
  99. klaude_code/llm/codex/client.py +149 -0
  100. klaude_code/llm/google/__init__.py +3 -0
  101. klaude_code/llm/google/client.py +309 -0
  102. klaude_code/llm/google/input.py +215 -0
  103. klaude_code/llm/input_common.py +3 -9
  104. klaude_code/llm/openai_compatible/client.py +72 -164
  105. klaude_code/llm/openai_compatible/input.py +6 -4
  106. klaude_code/llm/openai_compatible/stream.py +273 -0
  107. klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
  108. klaude_code/llm/openrouter/client.py +89 -160
  109. klaude_code/llm/openrouter/input.py +18 -30
  110. klaude_code/llm/openrouter/reasoning.py +118 -0
  111. klaude_code/llm/registry.py +39 -7
  112. klaude_code/llm/responses/client.py +184 -171
  113. klaude_code/llm/responses/input.py +20 -1
  114. klaude_code/llm/usage.py +17 -12
  115. klaude_code/protocol/commands.py +17 -1
  116. klaude_code/protocol/events.py +31 -4
  117. klaude_code/protocol/llm_param.py +13 -10
  118. klaude_code/protocol/model.py +232 -29
  119. klaude_code/protocol/op.py +90 -1
  120. klaude_code/protocol/op_handler.py +35 -1
  121. klaude_code/protocol/sub_agent/__init__.py +117 -0
  122. klaude_code/protocol/sub_agent/explore.py +63 -0
  123. klaude_code/protocol/sub_agent/oracle.py +91 -0
  124. klaude_code/protocol/sub_agent/task.py +61 -0
  125. klaude_code/protocol/sub_agent/web.py +79 -0
  126. klaude_code/protocol/tools.py +4 -2
  127. klaude_code/session/__init__.py +2 -2
  128. klaude_code/session/codec.py +71 -0
  129. klaude_code/session/export.py +293 -86
  130. klaude_code/session/selector.py +89 -67
  131. klaude_code/session/session.py +320 -309
  132. klaude_code/session/store.py +220 -0
  133. klaude_code/session/templates/export_session.html +595 -83
  134. klaude_code/session/templates/mermaid_viewer.html +926 -0
  135. klaude_code/skill/__init__.py +27 -0
  136. klaude_code/skill/assets/deslop/SKILL.md +17 -0
  137. klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
  138. klaude_code/skill/assets/handoff/SKILL.md +39 -0
  139. klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
  140. klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
  141. klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
  142. klaude_code/skill/manager.py +70 -0
  143. klaude_code/skill/system_skills.py +192 -0
  144. klaude_code/trace/__init__.py +20 -2
  145. klaude_code/trace/log.py +150 -5
  146. klaude_code/ui/__init__.py +4 -9
  147. klaude_code/ui/core/input.py +1 -1
  148. klaude_code/ui/core/stage_manager.py +7 -7
  149. klaude_code/ui/modes/debug/display.py +2 -1
  150. klaude_code/ui/modes/repl/__init__.py +3 -48
  151. klaude_code/ui/modes/repl/clipboard.py +5 -5
  152. klaude_code/ui/modes/repl/completers.py +487 -123
  153. klaude_code/ui/modes/repl/display.py +5 -4
  154. klaude_code/ui/modes/repl/event_handler.py +370 -117
  155. klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
  156. klaude_code/ui/modes/repl/key_bindings.py +146 -23
  157. klaude_code/ui/modes/repl/renderer.py +189 -99
  158. klaude_code/ui/renderers/assistant.py +9 -2
  159. klaude_code/ui/renderers/bash_syntax.py +178 -0
  160. klaude_code/ui/renderers/common.py +78 -0
  161. klaude_code/ui/renderers/developer.py +104 -48
  162. klaude_code/ui/renderers/diffs.py +87 -6
  163. klaude_code/ui/renderers/errors.py +11 -6
  164. klaude_code/ui/renderers/mermaid_viewer.py +57 -0
  165. klaude_code/ui/renderers/metadata.py +112 -76
  166. klaude_code/ui/renderers/sub_agent.py +92 -7
  167. klaude_code/ui/renderers/thinking.py +40 -18
  168. klaude_code/ui/renderers/tools.py +405 -227
  169. klaude_code/ui/renderers/user_input.py +73 -13
  170. klaude_code/ui/rich/__init__.py +10 -1
  171. klaude_code/ui/rich/cjk_wrap.py +228 -0
  172. klaude_code/ui/rich/code_panel.py +131 -0
  173. klaude_code/ui/rich/live.py +17 -0
  174. klaude_code/ui/rich/markdown.py +305 -170
  175. klaude_code/ui/rich/searchable_text.py +10 -13
  176. klaude_code/ui/rich/status.py +190 -49
  177. klaude_code/ui/rich/theme.py +135 -39
  178. klaude_code/ui/terminal/__init__.py +55 -0
  179. klaude_code/ui/terminal/color.py +1 -1
  180. klaude_code/ui/terminal/control.py +13 -22
  181. klaude_code/ui/terminal/notifier.py +44 -4
  182. klaude_code/ui/terminal/selector.py +658 -0
  183. klaude_code/ui/utils/common.py +0 -18
  184. klaude_code-1.8.0.dist-info/METADATA +377 -0
  185. klaude_code-1.8.0.dist-info/RECORD +219 -0
  186. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
  187. klaude_code/command/diff_cmd.py +0 -138
  188. klaude_code/command/prompt-dev-docs-update.md +0 -56
  189. klaude_code/command/prompt-dev-docs.md +0 -46
  190. klaude_code/config/list_model.py +0 -162
  191. klaude_code/core/manager/agent_manager.py +0 -127
  192. klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
  193. klaude_code/core/tool/file/multi_edit_tool.md +0 -42
  194. klaude_code/core/tool/file/multi_edit_tool.py +0 -199
  195. klaude_code/core/tool/memory/memory_tool.md +0 -16
  196. klaude_code/core/tool/memory/memory_tool.py +0 -462
  197. klaude_code/llm/openrouter/reasoning_handler.py +0 -209
  198. klaude_code/protocol/sub_agent.py +0 -348
  199. klaude_code/ui/utils/debouncer.py +0 -42
  200. klaude_code-1.2.6.dist-info/METADATA +0 -178
  201. klaude_code-1.2.6.dist-info/RECORD +0 -167
  202. /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
  203. /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
  204. /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
  205. {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
@@ -1,29 +1,31 @@
1
1
  import asyncio
2
+ import contextlib
2
3
  import sys
3
- import uuid
4
4
  from dataclasses import dataclass
5
5
  from typing import Any, Protocol
6
+ from uuid import uuid4
6
7
 
7
8
  import typer
8
9
  from rich.text import Text
9
10
 
10
11
  from klaude_code import ui
11
- from klaude_code.command import has_interactive_command
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
12
15
  from klaude_code.config import Config, load_config
13
16
  from klaude_code.core.agent import Agent, DefaultModelProfileProvider, VanillaModelProfileProvider
14
17
  from klaude_code.core.executor import Executor
15
18
  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
19
+ from klaude_code.protocol import events, llm_param, op
20
+ from klaude_code.protocol import model as protocol_model
18
21
  from klaude_code.protocol.model import UserInputPayload
19
- from klaude_code.protocol.sub_agent import iter_sub_agent_profiles
22
+ from klaude_code.session.session import Session, close_default_store
20
23
  from klaude_code.trace import DebugType, log, set_debug_logging
21
24
  from klaude_code.ui.modes.repl import build_repl_status_snapshot
22
25
  from klaude_code.ui.modes.repl.input_prompt_toolkit import REPLStatusSnapshot
23
26
  from klaude_code.ui.terminal.color import is_light_terminal_background
24
27
  from klaude_code.ui.terminal.control import install_sigint_double_press_exit, start_esc_interrupt_monitor
25
28
  from klaude_code.ui.terminal.progress_bar import OSC94States, emit_osc94
26
- from klaude_code.version import get_update_message
27
29
 
28
30
 
29
31
  class PrintCapable(Protocol):
@@ -32,37 +34,6 @@ class PrintCapable(Protocol):
32
34
  def print(self, *objects: Any, style: Any | None = None, end: str = "\n") -> None: ...
33
35
 
34
36
 
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
37
  @dataclass
67
38
  class AppInitConfig:
68
39
  """Configuration for initializing the application components."""
@@ -88,26 +59,90 @@ class AppComponents:
88
59
  theme: str | None
89
60
 
90
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_model.UserMessageItem(
109
+ content=persisted_user_input.text,
110
+ images=persisted_user_input.images,
111
+ )
112
+ ]
113
+ )
114
+
115
+ if result.events:
116
+ for evt in result.events:
117
+ if result.persist_events and isinstance(evt, events.DeveloperMessageEvent):
118
+ agent.session.append_history([evt.item])
119
+ await executor.context.emit_event(evt)
120
+
121
+ submitted_ids: list[str] = []
122
+ for operation_item in operations:
123
+ submitted_ids.append(await executor.submit(operation_item))
124
+
125
+ if not submitted_ids:
126
+ # Ensure event-only commands are fully rendered before showing the next prompt.
127
+ await event_queue.join()
128
+ return None
129
+
130
+ if run_ops:
131
+ return run_ops[0].id
132
+ return submitted_ids[-1]
133
+
134
+
91
135
  async def initialize_app_components(init_config: AppInitConfig) -> AppComponents:
92
136
  """Initialize all application components (LLM clients, executor, UI)."""
93
137
  set_debug_logging(init_config.debug, filters=init_config.debug_filters)
94
138
 
95
139
  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
140
 
104
141
  # Initialize LLM clients
105
142
  try:
106
- enabled_sub_agents = [p.name for p in iter_sub_agent_profiles()]
107
143
  llm_clients = build_llm_clients(
108
144
  config,
109
145
  model_override=init_config.model,
110
- enabled_sub_agents=enabled_sub_agents,
111
146
  )
112
147
  except ValueError as exc:
113
148
  if init_config.model:
@@ -132,14 +167,20 @@ async def initialize_app_components(init_config: AppInitConfig) -> AppComponents
132
167
  event_queue,
133
168
  llm_clients,
134
169
  model_profile_provider=model_profile_provider,
170
+ on_model_change=update_terminal_title,
135
171
  )
136
172
 
173
+ # Update terminal title with initial model name
174
+ update_terminal_title(llm_clients.main.model_name)
175
+
137
176
  # Start executor in background
138
177
  executor_task = asyncio.create_task(executor.start())
139
178
 
140
179
  theme: str | None = config.theme
141
- if theme is None:
180
+ if theme is None and not init_config.is_exec_mode:
142
181
  # Auto-detect theme from terminal background when config does not specify a theme.
182
+ # Skip detection in exec mode to avoid TTY race conditions with parent process's
183
+ # ESC monitor when running as a subprocess.
143
184
  detected = is_light_terminal_background()
144
185
  if detected is True:
145
186
  theme = "light"
@@ -167,44 +208,89 @@ async def initialize_app_components(init_config: AppInitConfig) -> AppComponents
167
208
  )
168
209
 
169
210
 
211
+ async def initialize_session(
212
+ executor: Executor,
213
+ event_queue: asyncio.Queue[events.Event],
214
+ session_id: str | None = None,
215
+ ) -> str | None:
216
+ """Initialize a session and return the active session ID.
217
+
218
+ Args:
219
+ executor: The executor to submit operations to.
220
+ event_queue: The event queue for synchronization.
221
+ session_id: Optional session ID to resume. If None, creates a new session.
222
+
223
+ Returns:
224
+ The active session ID, or None if no session is active.
225
+ """
226
+ await executor.submit_and_wait(op.InitAgentOperation(session_id=session_id))
227
+ await event_queue.join()
228
+
229
+ active_session_id = executor.context.current_session_id()
230
+ return active_session_id or session_id
231
+
232
+
233
+ def _backfill_session_model_config(
234
+ agent: Agent | None,
235
+ model_override: str | None,
236
+ default_model: str | None,
237
+ is_new_session: bool,
238
+ ) -> None:
239
+ """Backfill model_config_name and model_thinking on newly created sessions."""
240
+ if agent is None or agent.session.model_config_name is not None:
241
+ return
242
+
243
+ if model_override is not None:
244
+ agent.session.model_config_name = model_override
245
+ elif is_new_session and default_model is not None:
246
+ agent.session.model_config_name = default_model
247
+ else:
248
+ return
249
+
250
+ if agent.session.model_thinking is None and agent.profile:
251
+ agent.session.model_thinking = agent.profile.llm_client.get_llm_config().thinking
252
+ # Don't save here - session will be saved when first message is sent via append_history()
253
+
254
+
170
255
  async def cleanup_app_components(components: AppComponents) -> None:
171
256
  """Clean up all application components."""
172
257
  try:
173
258
  # Clean shutdown
174
259
  await components.executor.stop()
175
260
  components.executor_task.cancel()
261
+ with contextlib.suppress(asyncio.CancelledError):
262
+ await components.executor_task
263
+ with contextlib.suppress(Exception):
264
+ await close_default_store()
176
265
 
177
266
  # Signal UI to stop
178
267
  await components.event_queue.put(events.EndEvent())
179
268
  await components.display_task
180
269
  finally:
181
270
  # Always attempt to clear Ghostty progress bar and restore cursor visibility
182
- try:
271
+ # Best-effort only; never fail cleanup due to OSC errors
272
+ with contextlib.suppress(Exception):
183
273
  emit_osc94(OSC94States.HIDDEN)
184
- except Exception:
185
- # Best-effort only; never fail cleanup due to OSC errors
186
- pass
187
274
 
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).
275
+ # Ensure the terminal cursor is visible even if Rich's Status spinner
276
+ # did not get a chance to stop cleanly (e.g. on KeyboardInterrupt).
277
+ # If this fails the shell can still recover via `reset`/`stty sane`.
278
+ with contextlib.suppress(Exception):
191
279
  stream = getattr(sys, "__stdout__", None) or sys.stdout
192
280
  stream.write("\033[?25h")
193
281
  stream.flush()
194
- except Exception:
195
- # If this fails the shell can still recover via `reset`/`stty sane`.
196
- pass
197
282
 
198
283
 
199
284
  async def _handle_keyboard_interrupt(executor: Executor) -> None:
200
285
  """Handle Ctrl+C by logging and sending a global interrupt."""
201
286
 
202
287
  log("Bye!")
203
- try:
288
+ session_id = executor.context.current_session_id()
289
+ if session_id and Session.exists(session_id):
290
+ log(("Resume with:", "dim"), (f"klaude --resume-by-id {session_id}", "green"))
291
+ # Executor might already be stopping
292
+ with contextlib.suppress(Exception):
204
293
  await executor.submit(op.InterruptOperation(target_session_id=None))
205
- except Exception:
206
- # Executor might already be stopping
207
- pass
208
294
 
209
295
 
210
296
  async def run_exec(init_config: AppInitConfig, input_content: str) -> None:
@@ -213,17 +299,23 @@ async def run_exec(init_config: AppInitConfig, input_content: str) -> None:
213
299
  components = await initialize_app_components(init_config)
214
300
 
215
301
  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()
302
+ session_id = await initialize_session(components.executor, components.event_queue)
303
+ _backfill_session_model_config(
304
+ components.executor.context.current_agent,
305
+ init_config.model,
306
+ components.config.main_model,
307
+ is_new_session=True,
308
+ )
222
309
 
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)
310
+ wait_id = await submit_user_input_payload(
311
+ executor=components.executor,
312
+ event_queue=components.event_queue,
313
+ user_input=UserInputPayload(text=input_content),
314
+ session_id=session_id,
226
315
  )
316
+ if wait_id is not None:
317
+ await components.executor.wait_for(wait_id)
318
+ await components.event_queue.join()
227
319
 
228
320
  except KeyboardInterrupt:
229
321
  await _handle_keyboard_interrupt(components.executor)
@@ -232,25 +324,104 @@ async def run_exec(init_config: AppInitConfig, input_content: str) -> None:
232
324
 
233
325
 
234
326
  async def run_interactive(init_config: AppInitConfig, session_id: str | None = None) -> None:
235
- """Run the interactive REPL using the provided configuration."""
327
+ """Run the interactive REPL using the provided configuration.
236
328
 
329
+ If session_id is None, a new session is created with an auto-generated ID.
330
+ If session_id is provided, attempts to resume that session.
331
+ """
237
332
  components = await initialize_app_components(init_config)
238
333
 
239
334
  # No theme persistence from CLI anymore; config.theme controls theme when set.
240
335
 
241
336
  # Create status provider for bottom toolbar
242
337
  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
338
  update_message = get_update_message()
249
-
250
- return build_repl_status_snapshot(agent=agent, update_message=update_message)
339
+ return build_repl_status_snapshot(update_message)
251
340
 
252
341
  # Set up input provider for interactive mode
253
- input_provider: ui.InputProviderABC = ui.PromptToolkitInput(status_provider=_status_provider)
342
+ def _stop_rich_bottom_ui() -> None:
343
+ display = components.display
344
+ if isinstance(display, ui.REPLDisplay):
345
+ display.renderer.spinner_stop()
346
+ display.renderer.stop_bottom_live()
347
+ elif (
348
+ isinstance(display, ui.DebugEventDisplay)
349
+ and display.wrapped_display
350
+ and isinstance(display.wrapped_display, ui.REPLDisplay)
351
+ ):
352
+ display.wrapped_display.renderer.spinner_stop()
353
+ display.wrapped_display.renderer.stop_bottom_live()
354
+
355
+ # Pass the pre-detected theme to avoid redundant TTY queries.
356
+ # Querying the terminal background again after an interactive selection
357
+ # can interfere with prompt_toolkit's terminal state and break history navigation.
358
+ is_light_background: bool | None = None
359
+ if components.theme == "light":
360
+ is_light_background = True
361
+ elif components.theme == "dark":
362
+ is_light_background = False
363
+
364
+ def _get_active_session_id() -> str | None:
365
+ """Get the current active session ID dynamically.
366
+
367
+ This is necessary because /clear command creates a new session with a different ID.
368
+ """
369
+
370
+ return components.executor.context.current_session_id()
371
+
372
+ async def _change_model_from_prompt(model_name: str) -> None:
373
+ sid = _get_active_session_id()
374
+ if not sid:
375
+ return
376
+ await components.executor.submit_and_wait(
377
+ op.ChangeModelOperation(
378
+ session_id=sid,
379
+ model_name=model_name,
380
+ save_as_default=False,
381
+ defer_thinking_selection=True,
382
+ emit_welcome_event=True,
383
+ emit_switch_message=False,
384
+ )
385
+ )
386
+
387
+ def _get_current_llm_config() -> llm_param.LLMConfigParameter | None:
388
+ agent = components.executor.context.current_agent
389
+ if agent is None:
390
+ return None
391
+ return agent.profile.llm_client.get_llm_config()
392
+
393
+ async def _change_thinking_from_prompt(thinking: llm_param.Thinking) -> None:
394
+ sid = _get_active_session_id()
395
+ if not sid:
396
+ return
397
+ await components.executor.submit_and_wait(
398
+ op.ChangeThinkingOperation(
399
+ session_id=sid,
400
+ thinking=thinking,
401
+ emit_welcome_event=True,
402
+ emit_switch_message=False,
403
+ )
404
+ )
405
+
406
+ # Inject command name checker into user_input renderer (for slash command highlighting)
407
+ from klaude_code.ui.renderers.user_input import set_command_name_checker
408
+
409
+ set_command_name_checker(is_slash_command_name)
410
+
411
+ input_provider: ui.InputProviderABC = ui.PromptToolkitInput(
412
+ status_provider=_status_provider,
413
+ pre_prompt=_stop_rich_bottom_ui,
414
+ is_light_background=is_light_background,
415
+ get_current_model_config_name=lambda: (
416
+ components.executor.context.current_agent.session.model_config_name
417
+ if components.executor.context.current_agent is not None
418
+ else None
419
+ ),
420
+ on_change_model=_change_model_from_prompt,
421
+ get_current_llm_config=_get_current_llm_config,
422
+ on_change_thinking=_change_thinking_from_prompt,
423
+ command_info_provider=get_command_info_list,
424
+ )
254
425
 
255
426
  # --- Custom Ctrl+C handler: double-press within 2s to exit, single press shows toast ---
256
427
  def _show_toast_once() -> None:
@@ -263,30 +434,38 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
263
434
  if isinstance(components.display, ui.REPLDisplay):
264
435
  printer = components.display.renderer
265
436
  # 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
437
+ elif (
438
+ isinstance(components.display, ui.DebugEventDisplay)
439
+ and components.display.wrapped_display
440
+ and isinstance(components.display.wrapped_display, ui.REPLDisplay)
441
+ ):
442
+ printer = components.display.wrapped_display.renderer
269
443
 
270
444
  if printer is not None:
271
445
  printer.print(Text(f" {MSG} ", style="bold yellow reverse"))
272
446
  else:
273
447
  print(MSG, file=sys.stderr)
274
- except Exception:
275
- # Fallback if themed print is unavailable
448
+ except (AttributeError, TypeError, RuntimeError):
449
+ # Fallback if themed print is unavailable (e.g., display not ready or Rich internal error)
276
450
  print(MSG, file=sys.stderr)
277
451
 
278
452
  def _hide_progress() -> None:
279
- try:
453
+ with contextlib.suppress(Exception):
280
454
  emit_osc94(OSC94States.HIDDEN)
281
- except Exception:
282
- pass
283
455
 
284
456
  restore_sigint = install_sigint_double_press_exit(_show_toast_once, _hide_progress)
285
457
 
458
+ exit_hint_printed = False
459
+
286
460
  try:
287
- # Init Agent
288
- await components.executor.submit_and_wait(op.InitAgentOperation(session_id=session_id))
289
- await components.event_queue.join()
461
+ await initialize_session(components.executor, components.event_queue, session_id=session_id)
462
+ _backfill_session_model_config(
463
+ components.executor.context.current_agent,
464
+ init_config.model,
465
+ components.config.main_model,
466
+ is_new_session=session_id is None,
467
+ )
468
+
290
469
  # Input
291
470
  await input_provider.start()
292
471
  async for user_input in input_provider.iter_inputs():
@@ -295,37 +474,50 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
295
474
  break
296
475
  elif user_input.text.strip() == "":
297
476
  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)
477
+ # Use dynamic session_id lookup to handle /clear creating new sessions.
478
+ # UI/CLI parses commands and submits concrete operations; core executes operations.
479
+ active_session_id = _get_active_session_id()
480
+ is_interactive = has_interactive_command(user_input.text)
481
+
482
+ wait_id = await submit_user_input_payload(
483
+ executor=components.executor,
484
+ event_queue=components.event_queue,
485
+ user_input=user_input,
486
+ session_id=active_session_id,
301
487
  )
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
488
+
489
+ if wait_id is None:
490
+ continue
491
+
492
+ if is_interactive:
493
+ await components.executor.wait_for(wait_id)
494
+ continue
495
+
496
+ # Esc monitor for long-running, interruptible operations
497
+ async def _on_esc_interrupt() -> None:
498
+ await components.executor.submit(op.InterruptOperation(target_session_id=_get_active_session_id()))
499
+
500
+ stop_event, esc_task = start_esc_interrupt_monitor(_on_esc_interrupt)
501
+ # Wait for this specific task to complete before accepting next input
502
+ try:
503
+ await components.executor.wait_for(wait_id)
504
+ finally:
505
+ # Stop ESC monitor and wait for it to finish cleaning up TTY
506
+ stop_event.set()
507
+ with contextlib.suppress(Exception):
508
+ await esc_task
322
509
 
323
510
  except KeyboardInterrupt:
324
511
  await _handle_keyboard_interrupt(components.executor)
512
+ exit_hint_printed = True
325
513
  finally:
326
- try:
327
- # Restore original SIGINT handler
514
+ # Restore original SIGINT handler
515
+ with contextlib.suppress(Exception):
328
516
  restore_sigint()
329
- except Exception:
330
- pass
331
517
  await cleanup_app_components(components)
518
+
519
+ if not exit_hint_printed:
520
+ active_session_id = components.executor.context.current_session_id()
521
+ if active_session_id and Session.exists(active_session_id):
522
+ log(f"Session ID: {active_session_id}")
523
+ log(f"Resume with: klaude --resume-by-id {active_session_id}")