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.
- klaude_code/auth/__init__.py +24 -0
- klaude_code/auth/codex/__init__.py +20 -0
- klaude_code/auth/codex/exceptions.py +17 -0
- klaude_code/auth/codex/jwt_utils.py +45 -0
- klaude_code/auth/codex/oauth.py +229 -0
- klaude_code/auth/codex/token_manager.py +84 -0
- klaude_code/cli/auth_cmd.py +73 -0
- klaude_code/cli/config_cmd.py +91 -0
- klaude_code/cli/cost_cmd.py +338 -0
- klaude_code/cli/debug.py +78 -0
- klaude_code/cli/list_model.py +307 -0
- klaude_code/cli/main.py +233 -134
- klaude_code/cli/runtime.py +309 -117
- klaude_code/{version.py → cli/self_update.py} +114 -5
- klaude_code/cli/session_cmd.py +37 -21
- klaude_code/command/__init__.py +88 -27
- klaude_code/command/clear_cmd.py +8 -7
- klaude_code/command/command_abc.py +31 -31
- klaude_code/command/debug_cmd.py +79 -0
- klaude_code/command/export_cmd.py +19 -53
- klaude_code/command/export_online_cmd.py +154 -0
- klaude_code/command/fork_session_cmd.py +267 -0
- klaude_code/command/help_cmd.py +7 -8
- klaude_code/command/model_cmd.py +60 -10
- klaude_code/command/model_select.py +84 -0
- klaude_code/command/prompt-jj-describe.md +32 -0
- klaude_code/command/prompt_command.py +19 -11
- klaude_code/command/refresh_cmd.py +8 -10
- klaude_code/command/registry.py +139 -40
- klaude_code/command/release_notes_cmd.py +84 -0
- klaude_code/command/resume_cmd.py +111 -0
- klaude_code/command/status_cmd.py +104 -60
- klaude_code/command/terminal_setup_cmd.py +7 -9
- klaude_code/command/thinking_cmd.py +98 -0
- klaude_code/config/__init__.py +14 -6
- klaude_code/config/assets/__init__.py +1 -0
- klaude_code/config/assets/builtin_config.yaml +303 -0
- klaude_code/config/builtin_config.py +38 -0
- klaude_code/config/config.py +378 -109
- klaude_code/config/select_model.py +117 -53
- klaude_code/config/thinking.py +269 -0
- klaude_code/{const/__init__.py → const.py} +50 -19
- klaude_code/core/agent.py +20 -28
- klaude_code/core/executor.py +327 -112
- klaude_code/core/manager/__init__.py +2 -4
- klaude_code/core/manager/llm_clients.py +1 -15
- klaude_code/core/manager/llm_clients_builder.py +10 -11
- klaude_code/core/manager/sub_agent_manager.py +37 -6
- klaude_code/core/prompt.py +63 -44
- klaude_code/core/prompts/prompt-claude-code.md +2 -13
- klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
- klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
- klaude_code/core/prompts/prompt-codex.md +9 -42
- klaude_code/core/prompts/prompt-minimal.md +12 -0
- klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
- klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
- klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
- klaude_code/core/reminders.py +283 -95
- klaude_code/core/task.py +113 -75
- klaude_code/core/tool/__init__.py +24 -31
- klaude_code/core/tool/file/_utils.py +36 -0
- klaude_code/core/tool/file/apply_patch.py +17 -25
- klaude_code/core/tool/file/apply_patch_tool.py +57 -77
- klaude_code/core/tool/file/diff_builder.py +151 -0
- klaude_code/core/tool/file/edit_tool.py +50 -63
- klaude_code/core/tool/file/move_tool.md +41 -0
- klaude_code/core/tool/file/move_tool.py +435 -0
- klaude_code/core/tool/file/read_tool.md +1 -1
- klaude_code/core/tool/file/read_tool.py +86 -86
- klaude_code/core/tool/file/write_tool.py +59 -69
- klaude_code/core/tool/report_back_tool.py +84 -0
- klaude_code/core/tool/shell/bash_tool.py +265 -22
- klaude_code/core/tool/shell/command_safety.py +3 -6
- klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
- klaude_code/core/tool/sub_agent_tool.py +13 -2
- klaude_code/core/tool/todo/todo_write_tool.md +0 -157
- klaude_code/core/tool/todo/todo_write_tool.py +1 -1
- klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
- klaude_code/core/tool/todo/update_plan_tool.py +1 -1
- klaude_code/core/tool/tool_abc.py +18 -0
- klaude_code/core/tool/tool_context.py +27 -12
- klaude_code/core/tool/tool_registry.py +7 -7
- klaude_code/core/tool/tool_runner.py +44 -36
- klaude_code/core/tool/truncation.py +29 -14
- klaude_code/core/tool/web/mermaid_tool.md +43 -0
- klaude_code/core/tool/web/mermaid_tool.py +2 -5
- klaude_code/core/tool/web/web_fetch_tool.md +1 -1
- klaude_code/core/tool/web/web_fetch_tool.py +112 -22
- klaude_code/core/tool/web/web_search_tool.md +23 -0
- klaude_code/core/tool/web/web_search_tool.py +130 -0
- klaude_code/core/turn.py +168 -66
- klaude_code/llm/__init__.py +2 -10
- klaude_code/llm/anthropic/client.py +190 -178
- klaude_code/llm/anthropic/input.py +39 -15
- klaude_code/llm/bedrock/__init__.py +3 -0
- klaude_code/llm/bedrock/client.py +60 -0
- klaude_code/llm/client.py +7 -21
- klaude_code/llm/codex/__init__.py +5 -0
- klaude_code/llm/codex/client.py +149 -0
- klaude_code/llm/google/__init__.py +3 -0
- klaude_code/llm/google/client.py +309 -0
- klaude_code/llm/google/input.py +215 -0
- klaude_code/llm/input_common.py +3 -9
- klaude_code/llm/openai_compatible/client.py +72 -164
- klaude_code/llm/openai_compatible/input.py +6 -4
- klaude_code/llm/openai_compatible/stream.py +273 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
- klaude_code/llm/openrouter/client.py +89 -160
- klaude_code/llm/openrouter/input.py +18 -30
- klaude_code/llm/openrouter/reasoning.py +118 -0
- klaude_code/llm/registry.py +39 -7
- klaude_code/llm/responses/client.py +184 -171
- klaude_code/llm/responses/input.py +20 -1
- klaude_code/llm/usage.py +17 -12
- klaude_code/protocol/commands.py +17 -1
- klaude_code/protocol/events.py +31 -4
- klaude_code/protocol/llm_param.py +13 -10
- klaude_code/protocol/model.py +232 -29
- klaude_code/protocol/op.py +90 -1
- klaude_code/protocol/op_handler.py +35 -1
- klaude_code/protocol/sub_agent/__init__.py +117 -0
- klaude_code/protocol/sub_agent/explore.py +63 -0
- klaude_code/protocol/sub_agent/oracle.py +91 -0
- klaude_code/protocol/sub_agent/task.py +61 -0
- klaude_code/protocol/sub_agent/web.py +79 -0
- klaude_code/protocol/tools.py +4 -2
- klaude_code/session/__init__.py +2 -2
- klaude_code/session/codec.py +71 -0
- klaude_code/session/export.py +293 -86
- klaude_code/session/selector.py +89 -67
- klaude_code/session/session.py +320 -309
- klaude_code/session/store.py +220 -0
- klaude_code/session/templates/export_session.html +595 -83
- klaude_code/session/templates/mermaid_viewer.html +926 -0
- klaude_code/skill/__init__.py +27 -0
- klaude_code/skill/assets/deslop/SKILL.md +17 -0
- klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
- klaude_code/skill/assets/handoff/SKILL.md +39 -0
- klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
- klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
- klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
- klaude_code/skill/manager.py +70 -0
- klaude_code/skill/system_skills.py +192 -0
- klaude_code/trace/__init__.py +20 -2
- klaude_code/trace/log.py +150 -5
- klaude_code/ui/__init__.py +4 -9
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/core/stage_manager.py +7 -7
- klaude_code/ui/modes/debug/display.py +2 -1
- klaude_code/ui/modes/repl/__init__.py +3 -48
- klaude_code/ui/modes/repl/clipboard.py +5 -5
- klaude_code/ui/modes/repl/completers.py +487 -123
- klaude_code/ui/modes/repl/display.py +5 -4
- klaude_code/ui/modes/repl/event_handler.py +370 -117
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
- klaude_code/ui/modes/repl/key_bindings.py +146 -23
- klaude_code/ui/modes/repl/renderer.py +189 -99
- klaude_code/ui/renderers/assistant.py +9 -2
- klaude_code/ui/renderers/bash_syntax.py +178 -0
- klaude_code/ui/renderers/common.py +78 -0
- klaude_code/ui/renderers/developer.py +104 -48
- klaude_code/ui/renderers/diffs.py +87 -6
- klaude_code/ui/renderers/errors.py +11 -6
- klaude_code/ui/renderers/mermaid_viewer.py +57 -0
- klaude_code/ui/renderers/metadata.py +112 -76
- klaude_code/ui/renderers/sub_agent.py +92 -7
- klaude_code/ui/renderers/thinking.py +40 -18
- klaude_code/ui/renderers/tools.py +405 -227
- klaude_code/ui/renderers/user_input.py +73 -13
- klaude_code/ui/rich/__init__.py +10 -1
- klaude_code/ui/rich/cjk_wrap.py +228 -0
- klaude_code/ui/rich/code_panel.py +131 -0
- klaude_code/ui/rich/live.py +17 -0
- klaude_code/ui/rich/markdown.py +305 -170
- klaude_code/ui/rich/searchable_text.py +10 -13
- klaude_code/ui/rich/status.py +190 -49
- klaude_code/ui/rich/theme.py +135 -39
- klaude_code/ui/terminal/__init__.py +55 -0
- klaude_code/ui/terminal/color.py +1 -1
- klaude_code/ui/terminal/control.py +13 -22
- klaude_code/ui/terminal/notifier.py +44 -4
- klaude_code/ui/terminal/selector.py +658 -0
- klaude_code/ui/utils/common.py +0 -18
- klaude_code-1.8.0.dist-info/METADATA +377 -0
- klaude_code-1.8.0.dist-info/RECORD +219 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
- klaude_code/command/diff_cmd.py +0 -138
- klaude_code/command/prompt-dev-docs-update.md +0 -56
- klaude_code/command/prompt-dev-docs.md +0 -46
- klaude_code/config/list_model.py +0 -162
- klaude_code/core/manager/agent_manager.py +0 -127
- klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
- klaude_code/core/tool/file/multi_edit_tool.md +0 -42
- klaude_code/core/tool/file/multi_edit_tool.py +0 -199
- klaude_code/core/tool/memory/memory_tool.md +0 -16
- klaude_code/core/tool/memory/memory_tool.py +0 -462
- klaude_code/llm/openrouter/reasoning_handler.py +0 -209
- klaude_code/protocol/sub_agent.py +0 -348
- klaude_code/ui/utils/debouncer.py +0 -42
- klaude_code-1.2.6.dist-info/METADATA +0 -178
- klaude_code-1.2.6.dist-info/RECORD +0 -167
- /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
- /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
- /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
klaude_code/cli/runtime.py
CHANGED
|
@@ -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.
|
|
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.
|
|
17
|
-
from klaude_code.protocol import
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
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
|
|
267
|
-
|
|
268
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
#
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
327
|
-
|
|
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}")
|