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
|
@@ -1,163 +1,615 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
import shutil
|
|
4
|
-
from collections.abc import AsyncIterator, Callable
|
|
5
|
+
from collections.abc import AsyncIterator, Awaitable, Callable
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import NamedTuple, override
|
|
7
8
|
|
|
9
|
+
import prompt_toolkit.layout.menus as pt_menus
|
|
8
10
|
from prompt_toolkit import PromptSession
|
|
11
|
+
from prompt_toolkit.application.current import get_app
|
|
9
12
|
from prompt_toolkit.buffer import Buffer
|
|
10
|
-
from prompt_toolkit.completion import ThreadedCompleter
|
|
13
|
+
from prompt_toolkit.completion import Completion, ThreadedCompleter
|
|
14
|
+
from prompt_toolkit.cursor_shapes import CursorShape
|
|
15
|
+
from prompt_toolkit.data_structures import Point
|
|
11
16
|
from prompt_toolkit.filters import Condition
|
|
12
|
-
from prompt_toolkit.formatted_text import FormattedText
|
|
17
|
+
from prompt_toolkit.formatted_text import FormattedText, StyleAndTextTuples, to_formatted_text
|
|
13
18
|
from prompt_toolkit.history import FileHistory
|
|
19
|
+
from prompt_toolkit.key_binding import merge_key_bindings
|
|
20
|
+
from prompt_toolkit.layout import Float
|
|
21
|
+
from prompt_toolkit.layout.containers import Container, FloatContainer, Window
|
|
22
|
+
from prompt_toolkit.layout.controls import BufferControl, UIContent
|
|
23
|
+
from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu
|
|
14
24
|
from prompt_toolkit.patch_stdout import patch_stdout
|
|
15
25
|
from prompt_toolkit.styles import Style
|
|
16
|
-
|
|
26
|
+
from prompt_toolkit.utils import get_cwidth
|
|
27
|
+
|
|
28
|
+
from klaude_code.config import load_config
|
|
29
|
+
from klaude_code.config.config import ModelEntry
|
|
30
|
+
from klaude_code.config.thinking import (
|
|
31
|
+
format_current_thinking,
|
|
32
|
+
get_thinking_picker_data,
|
|
33
|
+
parse_thinking_value,
|
|
34
|
+
)
|
|
35
|
+
from klaude_code.protocol import llm_param
|
|
36
|
+
from klaude_code.protocol.commands import CommandInfo
|
|
17
37
|
from klaude_code.protocol.model import UserInputPayload
|
|
18
38
|
from klaude_code.ui.core.input import InputProviderABC
|
|
19
39
|
from klaude_code.ui.modes.repl.clipboard import capture_clipboard_tag, copy_to_clipboard, extract_images_from_text
|
|
20
40
|
from klaude_code.ui.modes.repl.completers import AT_TOKEN_PATTERN, create_repl_completer
|
|
21
41
|
from klaude_code.ui.modes.repl.key_bindings import create_key_bindings
|
|
22
|
-
from klaude_code.ui.
|
|
42
|
+
from klaude_code.ui.renderers.user_input import USER_MESSAGE_MARK
|
|
43
|
+
from klaude_code.ui.terminal.color import is_light_terminal_background
|
|
44
|
+
from klaude_code.ui.terminal.selector import SelectItem, SelectOverlay, build_model_select_items
|
|
23
45
|
|
|
24
46
|
|
|
25
47
|
class REPLStatusSnapshot(NamedTuple):
|
|
26
48
|
"""Snapshot of REPL status for bottom toolbar display."""
|
|
27
49
|
|
|
28
|
-
model_name: str
|
|
29
|
-
context_usage_percent: float | None
|
|
30
|
-
llm_calls: int
|
|
31
|
-
tool_calls: int
|
|
32
50
|
update_message: str | None = None
|
|
33
51
|
|
|
34
52
|
|
|
35
|
-
|
|
53
|
+
COMPLETION_SELECTED_DARK_BG = "ansigreen"
|
|
54
|
+
COMPLETION_SELECTED_LIGHT_BG = "ansigreen"
|
|
55
|
+
COMPLETION_SELECTED_UNKNOWN_BG = "ansigreen"
|
|
36
56
|
COMPLETION_MENU = "ansibrightblack"
|
|
37
|
-
INPUT_PROMPT_STYLE = "ansimagenta"
|
|
57
|
+
INPUT_PROMPT_STYLE = "ansimagenta bold"
|
|
58
|
+
PLACEHOLDER_TEXT_STYLE_DARK_BG = "fg:#5a5a5a italic"
|
|
59
|
+
PLACEHOLDER_TEXT_STYLE_LIGHT_BG = "fg:#7a7a7a italic"
|
|
60
|
+
PLACEHOLDER_TEXT_STYLE_UNKNOWN_BG = "fg:#8a8a8a italic"
|
|
61
|
+
PLACEHOLDER_SYMBOL_STYLE_DARK_BG = "bg:#2a2a2a fg:#5a5a5a"
|
|
62
|
+
PLACEHOLDER_SYMBOL_STYLE_LIGHT_BG = "bg:#e6e6e6 fg:#7a7a7a"
|
|
63
|
+
PLACEHOLDER_SYMBOL_STYLE_UNKNOWN_BG = "bg:#2a2a2a fg:#8a8a8a"
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
# Layout helpers
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _left_align_completion_menus(container: Container) -> None:
|
|
72
|
+
"""Force completion menus to render at column 0.
|
|
73
|
+
|
|
74
|
+
prompt_toolkit's default completion menu floats are positioned relative to the
|
|
75
|
+
cursor (`xcursor=True`). That makes the popup indent as the caret moves.
|
|
76
|
+
We walk the layout tree and rewrite the Float positioning for completion menus
|
|
77
|
+
to keep them fixed at the left edge.
|
|
78
|
+
"""
|
|
79
|
+
if isinstance(container, FloatContainer):
|
|
80
|
+
for flt in container.floats:
|
|
81
|
+
if isinstance(flt.content, (CompletionsMenu, MultiColumnCompletionsMenu)):
|
|
82
|
+
flt.xcursor = False
|
|
83
|
+
flt.left = 0
|
|
84
|
+
|
|
85
|
+
for child in container.get_children():
|
|
86
|
+
_left_align_completion_menus(child)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _find_first_float_container(container: Container) -> FloatContainer | None:
|
|
90
|
+
if isinstance(container, FloatContainer):
|
|
91
|
+
return container
|
|
92
|
+
for child in container.get_children():
|
|
93
|
+
found = _find_first_float_container(child)
|
|
94
|
+
if found is not None:
|
|
95
|
+
return found
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _find_window_for_buffer(container: Container, target_buffer: Buffer) -> Window | None:
|
|
100
|
+
if isinstance(container, Window):
|
|
101
|
+
content = container.content
|
|
102
|
+
if isinstance(content, BufferControl) and content.buffer is target_buffer:
|
|
103
|
+
return container
|
|
104
|
+
|
|
105
|
+
for child in container.get_children():
|
|
106
|
+
found = _find_window_for_buffer(child, target_buffer)
|
|
107
|
+
if found is not None:
|
|
108
|
+
return found
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _patch_completion_menu_controls(container: Container) -> None:
|
|
113
|
+
"""Replace prompt_toolkit completion menu controls with customized versions."""
|
|
114
|
+
if isinstance(container, Window):
|
|
115
|
+
content = container.content
|
|
116
|
+
if isinstance(content, pt_menus.CompletionsMenuControl) and not isinstance(
|
|
117
|
+
content, _KlaudeCompletionsMenuControl
|
|
118
|
+
):
|
|
119
|
+
container.content = _KlaudeCompletionsMenuControl()
|
|
120
|
+
|
|
121
|
+
for child in container.get_children():
|
|
122
|
+
_patch_completion_menu_controls(child)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# Custom completion menu control
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class _KlaudeCompletionsMenuControl(pt_menus.CompletionsMenuControl):
|
|
131
|
+
"""CompletionsMenuControl with stable 2-char left prefix.
|
|
132
|
+
|
|
133
|
+
Requirements:
|
|
134
|
+
- Add a 2-character prefix for every row.
|
|
135
|
+
- Render "-> " for the selected row, and " " for non-selected rows.
|
|
136
|
+
|
|
137
|
+
Keep completion text unstyled so that the menu's current-row style can
|
|
138
|
+
override it entirely.
|
|
139
|
+
"""
|
|
140
|
+
|
|
141
|
+
_PREFIX_WIDTH = 2
|
|
142
|
+
|
|
143
|
+
def _get_menu_width(self, max_width: int, complete_state: pt_menus.CompletionState) -> int: # pyright: ignore[reportPrivateImportUsage]
|
|
144
|
+
"""Return the width of the main column.
|
|
145
|
+
|
|
146
|
+
This is prompt_toolkit's default implementation, except we reserve one
|
|
147
|
+
extra character for the 2-char prefix ("-> "/" ").
|
|
148
|
+
"""
|
|
149
|
+
return min(
|
|
150
|
+
max_width,
|
|
151
|
+
max(
|
|
152
|
+
self.MIN_WIDTH,
|
|
153
|
+
max(get_cwidth(c.display_text) for c in complete_state.completions) + 3,
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
def create_content(self, width: int, height: int) -> UIContent:
|
|
158
|
+
complete_state = get_app().current_buffer.complete_state
|
|
159
|
+
if complete_state:
|
|
160
|
+
completions = complete_state.completions
|
|
161
|
+
index = complete_state.complete_index
|
|
162
|
+
|
|
163
|
+
menu_width = self._get_menu_width(width, complete_state)
|
|
164
|
+
menu_meta_width = self._get_menu_meta_width(width - menu_width, complete_state)
|
|
165
|
+
show_meta = self._show_meta(complete_state)
|
|
166
|
+
|
|
167
|
+
def get_line(i: int) -> StyleAndTextTuples:
|
|
168
|
+
completion = completions[i]
|
|
169
|
+
is_current_completion = i == index
|
|
170
|
+
|
|
171
|
+
result = self._get_menu_item_fragments_with_cursor(
|
|
172
|
+
completion,
|
|
173
|
+
is_current_completion,
|
|
174
|
+
menu_width,
|
|
175
|
+
space_after=True,
|
|
176
|
+
)
|
|
177
|
+
if show_meta:
|
|
178
|
+
result += self._get_menu_item_meta_fragments(
|
|
179
|
+
completion,
|
|
180
|
+
is_current_completion,
|
|
181
|
+
menu_meta_width,
|
|
182
|
+
)
|
|
183
|
+
return result
|
|
184
|
+
|
|
185
|
+
return UIContent(
|
|
186
|
+
get_line=get_line,
|
|
187
|
+
cursor_position=Point(x=0, y=index or 0),
|
|
188
|
+
line_count=len(completions),
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
return UIContent()
|
|
192
|
+
|
|
193
|
+
def _get_menu_item_fragments_with_cursor(
|
|
194
|
+
self,
|
|
195
|
+
completion: Completion,
|
|
196
|
+
is_current_completion: bool,
|
|
197
|
+
width: int,
|
|
198
|
+
*,
|
|
199
|
+
space_after: bool = False,
|
|
200
|
+
) -> StyleAndTextTuples:
|
|
201
|
+
if is_current_completion:
|
|
202
|
+
style_str = f"class:completion-menu.completion.current {completion.style} {completion.selected_style}"
|
|
203
|
+
prefix = "→ "
|
|
204
|
+
else:
|
|
205
|
+
style_str = "class:completion-menu.completion " + completion.style
|
|
206
|
+
prefix = " "
|
|
207
|
+
|
|
208
|
+
max_text_width = width - self._PREFIX_WIDTH - (1 if space_after else 0)
|
|
209
|
+
text, text_width = pt_menus._trim_formatted_text(completion.display, max_text_width) # pyright: ignore[reportPrivateUsage]
|
|
210
|
+
padding = " " * (width - self._PREFIX_WIDTH - text_width)
|
|
211
|
+
|
|
212
|
+
return to_formatted_text(
|
|
213
|
+
[("", prefix), *text, ("", padding)],
|
|
214
|
+
style=style_str,
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ---------------------------------------------------------------------------
|
|
219
|
+
# PromptToolkitInput
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
38
221
|
|
|
39
222
|
|
|
40
223
|
class PromptToolkitInput(InputProviderABC):
|
|
41
224
|
def __init__(
|
|
42
225
|
self,
|
|
43
|
-
prompt: str =
|
|
226
|
+
prompt: str = USER_MESSAGE_MARK,
|
|
44
227
|
status_provider: Callable[[], REPLStatusSnapshot] | None = None,
|
|
45
|
-
|
|
228
|
+
pre_prompt: Callable[[], None] | None = None,
|
|
229
|
+
post_prompt: Callable[[], None] | None = None,
|
|
230
|
+
is_light_background: bool | None = None,
|
|
231
|
+
on_change_model: Callable[[str], Awaitable[None]] | None = None,
|
|
232
|
+
get_current_model_config_name: Callable[[], str | None] | None = None,
|
|
233
|
+
on_change_thinking: Callable[[llm_param.Thinking], Awaitable[None]] | None = None,
|
|
234
|
+
get_current_llm_config: Callable[[], llm_param.LLMConfigParameter | None] | None = None,
|
|
235
|
+
command_info_provider: Callable[[], list[CommandInfo]] | None = None,
|
|
236
|
+
):
|
|
46
237
|
self._status_provider = status_provider
|
|
238
|
+
self._pre_prompt = pre_prompt
|
|
239
|
+
self._post_prompt = post_prompt
|
|
240
|
+
self._on_change_model = on_change_model
|
|
241
|
+
self._get_current_model_config_name = get_current_model_config_name
|
|
242
|
+
self._on_change_thinking = on_change_thinking
|
|
243
|
+
self._get_current_llm_config = get_current_llm_config
|
|
244
|
+
self._command_info_provider = command_info_provider
|
|
245
|
+
|
|
246
|
+
# Use provided value if available to avoid redundant TTY queries that may interfere
|
|
247
|
+
# with prompt_toolkit's terminal state after interactive UIs have been used.
|
|
248
|
+
self._is_light_terminal_background = (
|
|
249
|
+
is_light_background if is_light_background is not None else is_light_terminal_background(timeout=0.2)
|
|
250
|
+
)
|
|
47
251
|
|
|
48
|
-
|
|
49
|
-
self.
|
|
252
|
+
self._session = self._build_prompt_session(prompt)
|
|
253
|
+
self._setup_model_picker()
|
|
254
|
+
self._setup_thinking_picker()
|
|
255
|
+
self._apply_layout_customizations()
|
|
50
256
|
|
|
257
|
+
def _build_prompt_session(self, prompt: str) -> PromptSession[str]:
|
|
258
|
+
"""Build the prompt_toolkit PromptSession with key bindings and styles."""
|
|
51
259
|
project = str(Path.cwd()).strip("/").replace("/", "-")
|
|
52
|
-
history_path = Path.home() / ".klaude" / "projects" /
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
260
|
+
history_path = Path.home() / ".klaude" / "projects" / project / "input" / "input_history.txt"
|
|
261
|
+
history_path.parent.mkdir(parents=True, exist_ok=True)
|
|
262
|
+
history_path.touch(exist_ok=True)
|
|
263
|
+
|
|
264
|
+
# Model and thinking pickers will be set up later; create placeholder condition
|
|
265
|
+
self._model_picker: SelectOverlay[str] | None = None
|
|
266
|
+
self._thinking_picker: SelectOverlay[str] | None = None
|
|
267
|
+
input_enabled = Condition(
|
|
268
|
+
lambda: (self._model_picker is None or not self._model_picker.is_open)
|
|
269
|
+
and (self._thinking_picker is None or not self._thinking_picker.is_open)
|
|
270
|
+
)
|
|
60
271
|
|
|
61
|
-
# Create key bindings with injected dependencies
|
|
62
272
|
kb = create_key_bindings(
|
|
63
273
|
capture_clipboard_tag=capture_clipboard_tag,
|
|
64
274
|
copy_to_clipboard=copy_to_clipboard,
|
|
65
275
|
at_token_pattern=AT_TOKEN_PATTERN,
|
|
276
|
+
input_enabled=input_enabled,
|
|
277
|
+
open_model_picker=self._open_model_picker,
|
|
278
|
+
open_thinking_picker=self._open_thinking_picker,
|
|
66
279
|
)
|
|
67
280
|
|
|
68
|
-
|
|
281
|
+
# Select completion selected color based on terminal background
|
|
282
|
+
if self._is_light_terminal_background is True:
|
|
283
|
+
completion_selected = COMPLETION_SELECTED_LIGHT_BG
|
|
284
|
+
elif self._is_light_terminal_background is False:
|
|
285
|
+
completion_selected = COMPLETION_SELECTED_DARK_BG
|
|
286
|
+
else:
|
|
287
|
+
completion_selected = COMPLETION_SELECTED_UNKNOWN_BG
|
|
288
|
+
|
|
289
|
+
return PromptSession(
|
|
69
290
|
[(INPUT_PROMPT_STYLE, prompt)],
|
|
70
|
-
history=FileHistory(history_path),
|
|
291
|
+
history=FileHistory(str(history_path)),
|
|
71
292
|
multiline=True,
|
|
293
|
+
cursor=CursorShape.BLINKING_BEAM,
|
|
72
294
|
prompt_continuation=[(INPUT_PROMPT_STYLE, " ")],
|
|
73
295
|
key_bindings=kb,
|
|
74
|
-
completer=ThreadedCompleter(create_repl_completer()),
|
|
296
|
+
completer=ThreadedCompleter(create_repl_completer(command_info_provider=self._command_info_provider)),
|
|
75
297
|
complete_while_typing=True,
|
|
76
298
|
erase_when_done=True,
|
|
77
|
-
|
|
78
|
-
mouse_support=mouse_support_filter,
|
|
299
|
+
mouse_support=False,
|
|
79
300
|
style=Style.from_dict(
|
|
80
301
|
{
|
|
81
302
|
"completion-menu": "bg:default",
|
|
82
303
|
"completion-menu.border": "bg:default",
|
|
83
304
|
"scrollbar.background": "bg:default",
|
|
84
305
|
"scrollbar.button": "bg:default",
|
|
85
|
-
"completion-menu.completion":
|
|
306
|
+
"completion-menu.completion": "bg:default fg:default",
|
|
86
307
|
"completion-menu.meta.completion": f"bg:default fg:{COMPLETION_MENU}",
|
|
87
|
-
"completion-menu.completion.current": f"noreverse bg:default fg:{
|
|
88
|
-
"completion-menu.meta.completion.current": f"bg:default fg:{
|
|
308
|
+
"completion-menu.completion.current": f"noreverse bg:default fg:{completion_selected}",
|
|
309
|
+
"completion-menu.meta.completion.current": f"bg:default fg:{completion_selected}",
|
|
310
|
+
# Embedded selector overlay styles
|
|
311
|
+
"pointer": "ansigreen",
|
|
312
|
+
"highlighted": "ansigreen",
|
|
313
|
+
"text": "ansibrightblack",
|
|
314
|
+
"question": "bold",
|
|
315
|
+
"msg": "",
|
|
316
|
+
"meta": "fg:ansibrightblack",
|
|
317
|
+
"frame.border": "fg:ansibrightblack",
|
|
318
|
+
"search_prefix": "fg:ansibrightblack",
|
|
319
|
+
"search_placeholder": "fg:ansibrightblack italic",
|
|
320
|
+
"search_input": "",
|
|
321
|
+
# Empty bottom-toolbar style
|
|
322
|
+
"bottom-toolbar": "bg:default fg:default noreverse",
|
|
323
|
+
"bottom-toolbar.text": "bg:default fg:default noreverse",
|
|
89
324
|
}
|
|
90
325
|
),
|
|
91
326
|
)
|
|
92
327
|
|
|
328
|
+
def _setup_model_picker(self) -> None:
|
|
329
|
+
"""Initialize the model picker overlay and attach it to the layout."""
|
|
330
|
+
model_picker = SelectOverlay[str](
|
|
331
|
+
pointer="→",
|
|
332
|
+
use_search_filter=True,
|
|
333
|
+
search_placeholder="type to search",
|
|
334
|
+
list_height=10,
|
|
335
|
+
on_select=self._handle_model_selected,
|
|
336
|
+
)
|
|
337
|
+
self._model_picker = model_picker
|
|
338
|
+
|
|
339
|
+
# Merge overlay key bindings with existing session key bindings
|
|
340
|
+
existing_kb = self._session.key_bindings
|
|
341
|
+
if existing_kb is not None:
|
|
342
|
+
merged_kb = merge_key_bindings([existing_kb, model_picker.key_bindings])
|
|
343
|
+
self._session.key_bindings = merged_kb
|
|
344
|
+
|
|
345
|
+
# Attach overlay as a float above the prompt
|
|
346
|
+
with contextlib.suppress(Exception):
|
|
347
|
+
root = self._session.app.layout.container
|
|
348
|
+
overlay_float = Float(content=model_picker.container, bottom=1, left=0)
|
|
349
|
+
|
|
350
|
+
# Always attach this overlay at the top level so it is not clipped by
|
|
351
|
+
# small nested FloatContainers (e.g. the completion-menu container).
|
|
352
|
+
if isinstance(root, FloatContainer):
|
|
353
|
+
root.floats.append(overlay_float)
|
|
354
|
+
else:
|
|
355
|
+
self._session.app.layout.container = FloatContainer(content=root, floats=[overlay_float])
|
|
356
|
+
|
|
357
|
+
def _setup_thinking_picker(self) -> None:
|
|
358
|
+
"""Initialize the thinking picker overlay and attach it to the layout."""
|
|
359
|
+
thinking_picker = SelectOverlay[str](
|
|
360
|
+
pointer="→",
|
|
361
|
+
use_search_filter=False,
|
|
362
|
+
list_height=6,
|
|
363
|
+
on_select=self._handle_thinking_selected,
|
|
364
|
+
)
|
|
365
|
+
self._thinking_picker = thinking_picker
|
|
366
|
+
|
|
367
|
+
# Merge overlay key bindings with existing session key bindings
|
|
368
|
+
existing_kb = self._session.key_bindings
|
|
369
|
+
if existing_kb is not None:
|
|
370
|
+
merged_kb = merge_key_bindings([existing_kb, thinking_picker.key_bindings])
|
|
371
|
+
self._session.key_bindings = merged_kb
|
|
372
|
+
|
|
373
|
+
# Attach overlay as a float above the prompt
|
|
374
|
+
with contextlib.suppress(Exception):
|
|
375
|
+
root = self._session.app.layout.container
|
|
376
|
+
overlay_float = Float(content=thinking_picker.container, bottom=1, left=0)
|
|
377
|
+
|
|
378
|
+
if isinstance(root, FloatContainer):
|
|
379
|
+
root.floats.append(overlay_float)
|
|
380
|
+
else:
|
|
381
|
+
self._session.app.layout.container = FloatContainer(content=root, floats=[overlay_float])
|
|
382
|
+
|
|
383
|
+
def _apply_layout_customizations(self) -> None:
|
|
384
|
+
"""Apply layout customizations after session is created."""
|
|
385
|
+
# Make the Escape key feel responsive
|
|
386
|
+
with contextlib.suppress(Exception):
|
|
387
|
+
self._session.app.ttimeoutlen = 0.05
|
|
388
|
+
|
|
389
|
+
# Keep completion popups left-aligned
|
|
390
|
+
with contextlib.suppress(Exception):
|
|
391
|
+
_left_align_completion_menus(self._session.app.layout.container)
|
|
392
|
+
|
|
393
|
+
# Customize completion rendering
|
|
394
|
+
with contextlib.suppress(Exception):
|
|
395
|
+
_patch_completion_menu_controls(self._session.app.layout.container)
|
|
396
|
+
|
|
397
|
+
# Reserve more vertical space while the model picker overlay is open.
|
|
398
|
+
# prompt_toolkit's default multiline prompt caps out at ~9 lines.
|
|
399
|
+
self._patch_prompt_height_for_model_picker()
|
|
400
|
+
|
|
401
|
+
# Ensure completion menu has default selection
|
|
402
|
+
self._session.default_buffer.on_completions_changed += self._select_first_completion_on_open # pyright: ignore[reportUnknownMemberType]
|
|
403
|
+
|
|
404
|
+
def _patch_prompt_height_for_model_picker(self) -> None:
|
|
405
|
+
if self._model_picker is None and self._thinking_picker is None:
|
|
406
|
+
return
|
|
407
|
+
|
|
408
|
+
with contextlib.suppress(Exception):
|
|
409
|
+
root = self._session.app.layout.container
|
|
410
|
+
input_window = _find_window_for_buffer(root, self._session.default_buffer)
|
|
411
|
+
if input_window is None:
|
|
412
|
+
return
|
|
413
|
+
|
|
414
|
+
original_height = input_window.height
|
|
415
|
+
|
|
416
|
+
def _height(): # type: ignore[no-untyped-def]
|
|
417
|
+
picker_open = (self._model_picker is not None and self._model_picker.is_open) or (
|
|
418
|
+
self._thinking_picker is not None and self._thinking_picker.is_open
|
|
419
|
+
)
|
|
420
|
+
if picker_open:
|
|
421
|
+
# Target 20 rows, but cap to the current terminal size.
|
|
422
|
+
# Leave a small buffer to avoid triggering "Window too small".
|
|
423
|
+
try:
|
|
424
|
+
rows = get_app().output.get_size().rows
|
|
425
|
+
except Exception:
|
|
426
|
+
rows = 0
|
|
427
|
+
return max(3, min(20, rows - 2))
|
|
428
|
+
|
|
429
|
+
if callable(original_height):
|
|
430
|
+
return original_height()
|
|
431
|
+
return original_height
|
|
432
|
+
|
|
433
|
+
input_window.height = _height
|
|
434
|
+
|
|
435
|
+
def _select_first_completion_on_open(self, buf) -> None: # type: ignore[no-untyped-def]
|
|
436
|
+
"""Default to selecting the first completion without inserting it."""
|
|
93
437
|
try:
|
|
94
|
-
|
|
438
|
+
state = buf.complete_state # type: ignore[reportUnknownMemberType]
|
|
439
|
+
if state is None:
|
|
440
|
+
return
|
|
441
|
+
if not state.completions: # type: ignore[reportUnknownMemberType]
|
|
442
|
+
return
|
|
443
|
+
if state.complete_index is None: # type: ignore[reportUnknownMemberType]
|
|
444
|
+
state.complete_index = 0 # type: ignore[reportUnknownMemberType]
|
|
445
|
+
with contextlib.suppress(Exception):
|
|
446
|
+
self._session.app.invalidate()
|
|
95
447
|
except Exception:
|
|
96
|
-
|
|
97
|
-
|
|
448
|
+
return
|
|
449
|
+
|
|
450
|
+
# -------------------------------------------------------------------------
|
|
451
|
+
# Model picker
|
|
452
|
+
# -------------------------------------------------------------------------
|
|
453
|
+
|
|
454
|
+
def _build_model_picker_items(self) -> tuple[list[SelectItem[str]], str | None]:
|
|
455
|
+
config = load_config()
|
|
456
|
+
models: list[ModelEntry] = sorted(
|
|
457
|
+
config.iter_model_entries(only_available=True),
|
|
458
|
+
key=lambda m: m.model_name.lower(),
|
|
459
|
+
)
|
|
460
|
+
if not models:
|
|
461
|
+
return [], None
|
|
462
|
+
|
|
463
|
+
items = build_model_select_items(models)
|
|
464
|
+
|
|
465
|
+
initial = None
|
|
466
|
+
if self._get_current_model_config_name is not None:
|
|
467
|
+
with contextlib.suppress(Exception):
|
|
468
|
+
initial = self._get_current_model_config_name()
|
|
469
|
+
if initial is None:
|
|
470
|
+
initial = config.main_model
|
|
471
|
+
return items, initial
|
|
98
472
|
|
|
99
|
-
def
|
|
100
|
-
|
|
473
|
+
def _open_model_picker(self) -> None:
|
|
474
|
+
if self._model_picker is None:
|
|
475
|
+
return
|
|
476
|
+
items, initial = self._build_model_picker_items()
|
|
477
|
+
if not items:
|
|
478
|
+
return
|
|
479
|
+
self._model_picker.set_content(message="Select a model:", items=items, initial_value=initial)
|
|
480
|
+
self._model_picker.open()
|
|
481
|
+
|
|
482
|
+
async def _handle_model_selected(self, model_name: str) -> None:
|
|
483
|
+
current = None
|
|
484
|
+
if self._get_current_model_config_name is not None:
|
|
485
|
+
with contextlib.suppress(Exception):
|
|
486
|
+
current = self._get_current_model_config_name()
|
|
487
|
+
if current is not None and model_name == current:
|
|
488
|
+
return
|
|
489
|
+
if self._on_change_model is None:
|
|
490
|
+
return
|
|
491
|
+
await self._on_change_model(model_name)
|
|
492
|
+
|
|
493
|
+
# -------------------------------------------------------------------------
|
|
494
|
+
# Thinking picker
|
|
495
|
+
# -------------------------------------------------------------------------
|
|
496
|
+
|
|
497
|
+
def _build_thinking_picker_items(
|
|
498
|
+
self, config: llm_param.LLMConfigParameter
|
|
499
|
+
) -> tuple[list[SelectItem[str]], str | None]:
|
|
500
|
+
data = get_thinking_picker_data(config)
|
|
501
|
+
if data is None:
|
|
502
|
+
return [], None
|
|
503
|
+
|
|
504
|
+
items: list[SelectItem[str]] = [
|
|
505
|
+
SelectItem(title=[("class:text", opt.label + "\n")], value=opt.value, search_text=opt.label)
|
|
506
|
+
for opt in data.options
|
|
507
|
+
]
|
|
508
|
+
return items, data.current_value
|
|
509
|
+
|
|
510
|
+
def _open_thinking_picker(self) -> None:
|
|
511
|
+
if self._thinking_picker is None:
|
|
512
|
+
return
|
|
513
|
+
if self._get_current_llm_config is None:
|
|
514
|
+
return
|
|
515
|
+
config = self._get_current_llm_config()
|
|
516
|
+
if config is None:
|
|
517
|
+
return
|
|
518
|
+
items, initial = self._build_thinking_picker_items(config)
|
|
519
|
+
if not items:
|
|
520
|
+
return
|
|
521
|
+
current = format_current_thinking(config)
|
|
522
|
+
self._thinking_picker.set_content(
|
|
523
|
+
message=f"Select thinking level (current: {current}):", items=items, initial_value=initial
|
|
524
|
+
)
|
|
525
|
+
self._thinking_picker.open()
|
|
101
526
|
|
|
102
|
-
|
|
527
|
+
async def _handle_thinking_selected(self, value: str) -> None:
|
|
528
|
+
if self._on_change_thinking is None:
|
|
529
|
+
return
|
|
530
|
+
|
|
531
|
+
new_thinking = parse_thinking_value(value)
|
|
532
|
+
if new_thinking is None:
|
|
533
|
+
return
|
|
534
|
+
await self._on_change_thinking(new_thinking)
|
|
535
|
+
|
|
536
|
+
# -------------------------------------------------------------------------
|
|
537
|
+
# Bottom toolbar
|
|
538
|
+
# -------------------------------------------------------------------------
|
|
539
|
+
|
|
540
|
+
def _get_bottom_toolbar(self) -> FormattedText | None:
|
|
541
|
+
"""Return bottom toolbar content.
|
|
542
|
+
|
|
543
|
+
This is used inside the prompt_toolkit Application, so avoid printing or
|
|
544
|
+
doing any blocking IO here.
|
|
103
545
|
"""
|
|
104
|
-
# Check for update message first
|
|
105
546
|
update_message: str | None = None
|
|
106
|
-
if self._status_provider:
|
|
547
|
+
if self._status_provider is not None:
|
|
107
548
|
try:
|
|
108
549
|
status = self._status_provider()
|
|
109
550
|
update_message = status.update_message
|
|
110
|
-
except
|
|
111
|
-
|
|
551
|
+
except (AttributeError, RuntimeError):
|
|
552
|
+
update_message = None
|
|
112
553
|
|
|
113
|
-
# If
|
|
114
|
-
|
|
115
|
-
|
|
554
|
+
# If nothing to show, return a blank line to actively clear any previously
|
|
555
|
+
# rendered content. (When `bottom_toolbar` is a callable, prompt_toolkit
|
|
556
|
+
# will still reserve the toolbar line.)
|
|
557
|
+
if not update_message:
|
|
116
558
|
try:
|
|
117
559
|
terminal_width = shutil.get_terminal_size().columns
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
toolbar_text = left_text + padding
|
|
122
|
-
return FormattedText([("#ansiyellow", toolbar_text)])
|
|
123
|
-
|
|
124
|
-
# Normal mode: Left side: path and git branch
|
|
125
|
-
left_parts: list[str] = []
|
|
126
|
-
left_parts.append(show_path_with_tilde())
|
|
127
|
-
|
|
128
|
-
git_branch = get_current_git_branch()
|
|
129
|
-
if git_branch:
|
|
130
|
-
left_parts.append(git_branch)
|
|
131
|
-
|
|
132
|
-
# Right side: status info
|
|
133
|
-
right_parts: list[str] = []
|
|
134
|
-
if self._status_provider:
|
|
135
|
-
try:
|
|
136
|
-
status = self._status_provider()
|
|
137
|
-
model_name = status.model_name or "N/A"
|
|
138
|
-
right_parts.append(model_name)
|
|
139
|
-
|
|
140
|
-
# Add context if available
|
|
141
|
-
if status.context_usage_percent is not None:
|
|
142
|
-
right_parts.append(f"context {status.context_usage_percent:.1f}%")
|
|
143
|
-
except Exception:
|
|
144
|
-
pass
|
|
560
|
+
except (OSError, ValueError):
|
|
561
|
+
terminal_width = 0
|
|
562
|
+
return FormattedText([("", " " * max(0, terminal_width))])
|
|
145
563
|
|
|
146
|
-
|
|
147
|
-
left_text = " " + " · ".join(left_parts)
|
|
148
|
-
right_text = (" · ".join(right_parts) + " ") if right_parts else " "
|
|
149
|
-
|
|
150
|
-
# Calculate padding
|
|
564
|
+
left_text = " " + update_message
|
|
151
565
|
try:
|
|
152
566
|
terminal_width = shutil.get_terminal_size().columns
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
except Exception:
|
|
567
|
+
padding = " " * max(0, terminal_width - len(left_text))
|
|
568
|
+
except (OSError, ValueError):
|
|
156
569
|
padding = ""
|
|
157
570
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
571
|
+
toolbar_text = left_text + padding
|
|
572
|
+
return FormattedText([("#ansiyellow", toolbar_text)])
|
|
573
|
+
|
|
574
|
+
# -------------------------------------------------------------------------
|
|
575
|
+
# Placeholder
|
|
576
|
+
# -------------------------------------------------------------------------
|
|
577
|
+
|
|
578
|
+
def _render_input_placeholder(self) -> FormattedText:
|
|
579
|
+
if self._is_light_terminal_background is True:
|
|
580
|
+
text_style = PLACEHOLDER_TEXT_STYLE_LIGHT_BG
|
|
581
|
+
symbol_style = PLACEHOLDER_SYMBOL_STYLE_LIGHT_BG
|
|
582
|
+
elif self._is_light_terminal_background is False:
|
|
583
|
+
text_style = PLACEHOLDER_TEXT_STYLE_DARK_BG
|
|
584
|
+
symbol_style = PLACEHOLDER_SYMBOL_STYLE_DARK_BG
|
|
585
|
+
else:
|
|
586
|
+
text_style = PLACEHOLDER_TEXT_STYLE_UNKNOWN_BG
|
|
587
|
+
symbol_style = PLACEHOLDER_SYMBOL_STYLE_UNKNOWN_BG
|
|
588
|
+
|
|
589
|
+
return FormattedText(
|
|
590
|
+
[
|
|
591
|
+
(text_style, " " * 10),
|
|
592
|
+
(symbol_style, " @ "),
|
|
593
|
+
(text_style, " "),
|
|
594
|
+
(text_style, "files"),
|
|
595
|
+
(text_style, " "),
|
|
596
|
+
(symbol_style, " $ "),
|
|
597
|
+
(text_style, " "),
|
|
598
|
+
(text_style, "skills"),
|
|
599
|
+
(text_style, " "),
|
|
600
|
+
(symbol_style, " / "),
|
|
601
|
+
(text_style, " "),
|
|
602
|
+
(text_style, "commands"),
|
|
603
|
+
(text_style, " "),
|
|
604
|
+
(symbol_style, " ctrl-l "),
|
|
605
|
+
(text_style, " "),
|
|
606
|
+
(text_style, "models"),
|
|
607
|
+
]
|
|
608
|
+
)
|
|
609
|
+
|
|
610
|
+
# -------------------------------------------------------------------------
|
|
611
|
+
# InputProviderABC implementation
|
|
612
|
+
# -------------------------------------------------------------------------
|
|
161
613
|
|
|
162
614
|
async def start(self) -> None:
|
|
163
615
|
pass
|
|
@@ -168,31 +620,26 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
168
620
|
@override
|
|
169
621
|
async def iter_inputs(self) -> AsyncIterator[UserInputPayload]:
|
|
170
622
|
while True:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
623
|
+
if self._pre_prompt is not None:
|
|
624
|
+
with contextlib.suppress(Exception):
|
|
625
|
+
self._pre_prompt()
|
|
626
|
+
|
|
627
|
+
# Keep ANSI escape sequences intact while prompt_toolkit is active.
|
|
628
|
+
# This allows Rich-rendered panels (e.g. WelcomeEvent) to display with
|
|
629
|
+
# proper styling instead of showing raw escape codes.
|
|
630
|
+
with patch_stdout(raw=True):
|
|
631
|
+
line: str = await self._session.prompt_async(
|
|
632
|
+
placeholder=self._render_input_placeholder(),
|
|
633
|
+
bottom_toolbar=self._get_bottom_toolbar,
|
|
634
|
+
)
|
|
635
|
+
if self._post_prompt is not None:
|
|
636
|
+
with contextlib.suppress(Exception):
|
|
637
|
+
self._post_prompt()
|
|
175
638
|
|
|
176
639
|
# Extract images referenced in the input text
|
|
177
640
|
images = extract_images_from_text(line)
|
|
178
641
|
|
|
179
642
|
yield UserInputPayload(text=line, images=images if images else None)
|
|
180
643
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
Mouse stays disabled when input is empty. It is enabled only when
|
|
185
|
-
the user has entered more than one line of text.
|
|
186
|
-
"""
|
|
187
|
-
try:
|
|
188
|
-
text = buf.text
|
|
189
|
-
except Exception:
|
|
190
|
-
return
|
|
191
|
-
self._mouse_enabled = self._should_enable_mouse(text)
|
|
192
|
-
|
|
193
|
-
def _should_enable_mouse(self, text: str) -> bool:
|
|
194
|
-
"""Return True when mouse support should be enabled for current input."""
|
|
195
|
-
if not text.strip():
|
|
196
|
-
return False
|
|
197
|
-
# Enable mouse only when input spans multiple lines.
|
|
198
|
-
return "\n" in text
|
|
644
|
+
# Note: Mouse support is intentionally disabled at the PromptSession
|
|
645
|
+
# level so that terminals retain their native scrollback behavior.
|