klaude-code 2.9.1__py3-none-any.whl → 2.10.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- klaude_code/app/runtime.py +5 -1
- klaude_code/cli/cost_cmd.py +4 -4
- klaude_code/cli/list_model.py +1 -2
- klaude_code/cli/main.py +10 -0
- klaude_code/config/assets/builtin_config.yaml +15 -14
- klaude_code/const.py +4 -3
- klaude_code/core/agent_profile.py +23 -0
- klaude_code/core/bash_mode.py +276 -0
- klaude_code/core/executor.py +40 -7
- klaude_code/core/manager/llm_clients.py +1 -0
- klaude_code/core/manager/llm_clients_builder.py +2 -2
- klaude_code/core/memory.py +140 -0
- klaude_code/core/prompts/prompt-sub-agent-web.md +2 -2
- klaude_code/core/reminders.py +17 -89
- klaude_code/core/tool/offload.py +4 -4
- klaude_code/core/tool/web/web_fetch_tool.md +2 -1
- klaude_code/core/tool/web/web_fetch_tool.py +1 -1
- klaude_code/core/turn.py +9 -4
- klaude_code/protocol/events.py +17 -0
- klaude_code/protocol/op.py +12 -0
- klaude_code/protocol/op_handler.py +5 -0
- klaude_code/session/templates/mermaid_viewer.html +85 -0
- klaude_code/tui/command/resume_cmd.py +1 -1
- klaude_code/tui/commands.py +15 -0
- klaude_code/tui/components/command_output.py +4 -5
- klaude_code/tui/components/developer.py +1 -3
- klaude_code/tui/components/metadata.py +28 -25
- klaude_code/tui/components/rich/code_panel.py +31 -16
- klaude_code/tui/components/rich/markdown.py +56 -124
- klaude_code/tui/components/rich/theme.py +22 -12
- klaude_code/tui/components/thinking.py +0 -35
- klaude_code/tui/components/tools.py +4 -2
- klaude_code/tui/components/user_input.py +49 -59
- klaude_code/tui/components/welcome.py +47 -2
- klaude_code/tui/display.py +14 -6
- klaude_code/tui/input/completers.py +8 -0
- klaude_code/tui/input/key_bindings.py +37 -1
- klaude_code/tui/input/prompt_toolkit.py +57 -31
- klaude_code/tui/machine.py +108 -28
- klaude_code/tui/renderer.py +117 -19
- klaude_code/tui/runner.py +22 -0
- klaude_code/tui/terminal/notifier.py +11 -12
- klaude_code/tui/terminal/selector.py +1 -1
- klaude_code/ui/terminal/title.py +4 -2
- {klaude_code-2.9.1.dist-info → klaude_code-2.10.1.dist-info}/METADATA +1 -1
- {klaude_code-2.9.1.dist-info → klaude_code-2.10.1.dist-info}/RECORD +48 -47
- klaude_code/tui/components/assistant.py +0 -2
- {klaude_code-2.9.1.dist-info → klaude_code-2.10.1.dist-info}/WHEEL +0 -0
- {klaude_code-2.9.1.dist-info → klaude_code-2.10.1.dist-info}/entry_points.txt +0 -0
|
@@ -4,6 +4,7 @@ from typing import Any, cast
|
|
|
4
4
|
|
|
5
5
|
from rich import box
|
|
6
6
|
from rich.console import Group, RenderableType
|
|
7
|
+
from rich.padding import Padding
|
|
7
8
|
from rich.panel import Panel
|
|
8
9
|
from rich.style import Style
|
|
9
10
|
from rich.text import Text
|
|
@@ -166,7 +167,6 @@ def render_bash_tool_call(arguments: str) -> RenderableType:
|
|
|
166
167
|
if isinstance(command, str) and command.strip():
|
|
167
168
|
cmd_str = command.strip()
|
|
168
169
|
highlighted = highlight_bash_command(cmd_str)
|
|
169
|
-
highlighted.stylize(ThemeKey.CODE_BACKGROUND)
|
|
170
170
|
|
|
171
171
|
display_line_count = len(highlighted.plain.splitlines())
|
|
172
172
|
|
|
@@ -189,7 +189,8 @@ def render_bash_tool_call(arguments: str) -> RenderableType:
|
|
|
189
189
|
highlighted.append(f" {timeout_ms // 1000}s", style=ThemeKey.TOOL_TIMEOUT)
|
|
190
190
|
else:
|
|
191
191
|
highlighted.append(f" {timeout_ms}ms", style=ThemeKey.TOOL_TIMEOUT)
|
|
192
|
-
|
|
192
|
+
padded = Padding(highlighted, pad=0, style=ThemeKey.CODE_BACKGROUND, expand=False)
|
|
193
|
+
return _render_tool_call_tree(mark=MARK_BASH, tool_name=tool_name, details=padded)
|
|
193
194
|
else:
|
|
194
195
|
summary = Text("", ThemeKey.TOOL_PARAM)
|
|
195
196
|
if isinstance(timeout_ms, int):
|
|
@@ -565,6 +566,7 @@ _TOOL_ACTIVE_FORM: dict[str, str] = {
|
|
|
565
566
|
tools.WEB_SEARCH: "Searching Web",
|
|
566
567
|
tools.REPORT_BACK: "Reporting",
|
|
567
568
|
tools.IMAGE_GEN: "Generating Image",
|
|
569
|
+
tools.TASK: "Spawning Task",
|
|
568
570
|
}
|
|
569
571
|
|
|
570
572
|
|
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
import re
|
|
2
2
|
|
|
3
3
|
from rich.console import Group, RenderableType
|
|
4
|
+
from rich.padding import Padding
|
|
4
5
|
from rich.text import Text
|
|
5
6
|
|
|
6
7
|
from klaude_code.const import TAB_EXPAND_WIDTH
|
|
7
|
-
from klaude_code.skill import
|
|
8
|
-
from klaude_code.tui.components.
|
|
8
|
+
from klaude_code.skill import list_skill_names
|
|
9
|
+
from klaude_code.tui.components.bash_syntax import highlight_bash_command
|
|
9
10
|
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
10
11
|
|
|
11
|
-
# Match
|
|
12
|
+
# Match inline patterns only when they appear at the beginning of the line
|
|
12
13
|
# or immediately after whitespace, to avoid treating mid-word email-like
|
|
13
14
|
# patterns such as foo@bar.com as file references.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
# Match $skill or ¥skill pattern inline (at start of line or after whitespace)
|
|
17
|
-
SKILL_RENDER_PATTERN = re.compile(r"(?<!\S)[$¥](\S+)")
|
|
18
|
-
|
|
15
|
+
# Group 1 is present only for $/¥ skills and captures the skill token (without the $/¥).
|
|
16
|
+
INLINE_RENDER_PATTERN = re.compile(r'(?<!\S)(?:@(?:"[^"]+"|\S+)|[$¥](\S+))')
|
|
19
17
|
USER_MESSAGE_MARK = "❯ "
|
|
20
18
|
|
|
21
19
|
|
|
@@ -24,86 +22,78 @@ def render_at_and_skill_patterns(
|
|
|
24
22
|
at_style: str = ThemeKey.USER_INPUT_AT_PATTERN,
|
|
25
23
|
skill_style: str = ThemeKey.USER_INPUT_SKILL,
|
|
26
24
|
other_style: str = ThemeKey.USER_INPUT,
|
|
25
|
+
available_skill_names: set[str] | None = None,
|
|
27
26
|
) -> Text:
|
|
28
27
|
"""Render text with highlighted @file and $skill patterns."""
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
# Collect all matches with their styles
|
|
36
|
-
matches: list[tuple[int, int, str]] = [] # (start, end, style)
|
|
37
|
-
|
|
38
|
-
if has_at:
|
|
39
|
-
for match in AT_FILE_RENDER_PATTERN.finditer(text):
|
|
40
|
-
matches.append((match.start(), match.end(), at_style))
|
|
41
|
-
|
|
42
|
-
if has_skill:
|
|
43
|
-
for match in SKILL_RENDER_PATTERN.finditer(text):
|
|
44
|
-
skill_name = match.group(1)
|
|
45
|
-
if _is_valid_skill_name(skill_name):
|
|
46
|
-
matches.append((match.start(), match.end(), skill_style))
|
|
47
|
-
|
|
48
|
-
if not matches:
|
|
49
|
-
return Text(text, style=other_style)
|
|
50
|
-
|
|
51
|
-
# Sort by start position
|
|
52
|
-
matches.sort(key=lambda x: x[0])
|
|
28
|
+
result = Text(text, style=other_style, overflow="fold")
|
|
29
|
+
for match in INLINE_RENDER_PATTERN.finditer(text):
|
|
30
|
+
skill_name = match.group(1)
|
|
31
|
+
if skill_name is None:
|
|
32
|
+
result.stylize(at_style, match.start(), match.end())
|
|
33
|
+
continue
|
|
53
34
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
for start, end, style in matches:
|
|
57
|
-
if start < last_end:
|
|
58
|
-
continue # Skip overlapping matches
|
|
59
|
-
if start > last_end:
|
|
60
|
-
result.append_text(Text(text[last_end:start], other_style))
|
|
61
|
-
result.append_text(Text(text[start:end], style))
|
|
62
|
-
last_end = end
|
|
35
|
+
if available_skill_names is None:
|
|
36
|
+
available_skill_names = set(list_skill_names())
|
|
63
37
|
|
|
64
|
-
|
|
65
|
-
|
|
38
|
+
short = skill_name.split(":")[-1] if ":" in skill_name else skill_name
|
|
39
|
+
if skill_name in available_skill_names or short in available_skill_names:
|
|
40
|
+
result.stylize(skill_style, match.start(), match.end())
|
|
66
41
|
|
|
67
42
|
return result
|
|
68
43
|
|
|
69
44
|
|
|
70
|
-
def _is_valid_skill_name(name: str) -> bool:
|
|
71
|
-
"""Check if a skill name is valid (exists in loaded skills)."""
|
|
72
|
-
short = name.split(":")[-1] if ":" in name else name
|
|
73
|
-
available_skills = get_available_skills()
|
|
74
|
-
return any(skill_name in (name, short) for skill_name, _, _ in available_skills)
|
|
75
|
-
|
|
76
|
-
|
|
77
45
|
def render_user_input(content: str) -> RenderableType:
|
|
78
46
|
"""Render a user message as a group of quoted lines with styles.
|
|
79
47
|
|
|
80
48
|
- Highlights slash command token on the first line
|
|
81
49
|
- Highlights @file and $skill patterns in all lines
|
|
50
|
+
- Wrapped in a Panel for block-style background
|
|
82
51
|
"""
|
|
83
52
|
lines = content.strip().split("\n")
|
|
53
|
+
is_bash_mode = bool(lines) and lines[0].startswith("!")
|
|
54
|
+
|
|
55
|
+
available_skill_names: set[str] | None = None
|
|
56
|
+
|
|
84
57
|
renderables: list[RenderableType] = []
|
|
85
58
|
for i, line in enumerate(lines):
|
|
86
|
-
|
|
59
|
+
if not line.strip():
|
|
60
|
+
continue
|
|
61
|
+
if "\t" in line:
|
|
62
|
+
line = line.expandtabs(TAB_EXPAND_WIDTH)
|
|
63
|
+
|
|
64
|
+
if is_bash_mode and i == 0:
|
|
65
|
+
renderables.append(highlight_bash_command(line[1:]))
|
|
66
|
+
continue
|
|
67
|
+
if is_bash_mode and i > 0:
|
|
68
|
+
renderables.append(highlight_bash_command(line))
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
if available_skill_names is None and ("$" in line or "\u00a5" in line):
|
|
72
|
+
available_skill_names = set(list_skill_names())
|
|
87
73
|
# Handle slash command on first line
|
|
88
74
|
if i == 0 and line.startswith("/"):
|
|
89
75
|
splits = line.split(" ", maxsplit=1)
|
|
90
76
|
line_text = Text.assemble(
|
|
91
77
|
(splits[0], ThemeKey.USER_INPUT_SLASH_COMMAND),
|
|
92
78
|
" ",
|
|
93
|
-
render_at_and_skill_patterns(splits[1]
|
|
79
|
+
render_at_and_skill_patterns(splits[1], available_skill_names=available_skill_names)
|
|
80
|
+
if len(splits) > 1
|
|
81
|
+
else Text(""),
|
|
82
|
+
overflow="fold",
|
|
94
83
|
)
|
|
95
84
|
renderables.append(line_text)
|
|
96
85
|
continue
|
|
97
86
|
|
|
98
87
|
# Render @file and $skill patterns
|
|
99
|
-
renderables.append(render_at_and_skill_patterns(line))
|
|
88
|
+
renderables.append(render_at_and_skill_patterns(line, available_skill_names=available_skill_names))
|
|
100
89
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
90
|
+
return Padding(
|
|
91
|
+
Group(*renderables),
|
|
92
|
+
pad=(0, 1),
|
|
93
|
+
style=ThemeKey.USER_INPUT,
|
|
94
|
+
expand=False,
|
|
95
|
+
)
|
|
106
96
|
|
|
107
97
|
|
|
108
98
|
def render_interrupt() -> RenderableType:
|
|
109
|
-
return Text("
|
|
99
|
+
return Text("Interrupted by user", style=ThemeKey.INTERRUPT)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from importlib.metadata import PackageNotFoundError, version
|
|
2
|
+
from pathlib import Path
|
|
2
3
|
|
|
3
4
|
from rich.console import Group, RenderableType
|
|
4
5
|
from rich.text import Text
|
|
@@ -10,6 +11,19 @@ from klaude_code.tui.components.rich.theme import ThemeKey
|
|
|
10
11
|
from klaude_code.ui.common import format_model_params
|
|
11
12
|
|
|
12
13
|
|
|
14
|
+
def _format_memory_path(path: str, *, work_dir: Path) -> str:
|
|
15
|
+
"""Format memory path for display - show relative path or ~ for home."""
|
|
16
|
+
p = Path(path)
|
|
17
|
+
try:
|
|
18
|
+
return str(p.relative_to(work_dir))
|
|
19
|
+
except ValueError:
|
|
20
|
+
pass
|
|
21
|
+
try:
|
|
22
|
+
return f"~/{p.relative_to(Path.home())}"
|
|
23
|
+
except ValueError:
|
|
24
|
+
return path
|
|
25
|
+
|
|
26
|
+
|
|
13
27
|
def _get_version() -> str:
|
|
14
28
|
"""Get the current version of klaude-code."""
|
|
15
29
|
try:
|
|
@@ -50,7 +64,7 @@ def render_welcome(e: events.WelcomeEvent) -> RenderableType:
|
|
|
50
64
|
# Render config items with tree-style prefixes
|
|
51
65
|
for i, param_str in enumerate(param_strings):
|
|
52
66
|
is_last = i == len(param_strings) - 1
|
|
53
|
-
prefix = "
|
|
67
|
+
prefix = "╰─ " if is_last else "├─ "
|
|
54
68
|
panel_content.append_text(
|
|
55
69
|
Text.assemble(
|
|
56
70
|
("\n", ThemeKey.WELCOME_INFO),
|
|
@@ -59,6 +73,37 @@ def render_welcome(e: events.WelcomeEvent) -> RenderableType:
|
|
|
59
73
|
)
|
|
60
74
|
)
|
|
61
75
|
|
|
76
|
+
# Loaded memories summary
|
|
77
|
+
work_dir = Path(e.work_dir)
|
|
78
|
+
loaded_memories = e.loaded_memories or {}
|
|
79
|
+
user_memories = loaded_memories.get("user") or []
|
|
80
|
+
project_memories = loaded_memories.get("project") or []
|
|
81
|
+
|
|
82
|
+
memory_groups: list[tuple[str, list[str]]] = []
|
|
83
|
+
if user_memories:
|
|
84
|
+
memory_groups.append(("user", user_memories))
|
|
85
|
+
if project_memories:
|
|
86
|
+
memory_groups.append(("project", project_memories))
|
|
87
|
+
|
|
88
|
+
if memory_groups:
|
|
89
|
+
panel_content.append_text(Text("\n\n", style=ThemeKey.WELCOME_INFO))
|
|
90
|
+
panel_content.append_text(Text("context", style=ThemeKey.WELCOME_HIGHLIGHT))
|
|
91
|
+
|
|
92
|
+
label_width = len("[project]")
|
|
93
|
+
|
|
94
|
+
for i, (group_name, paths) in enumerate(memory_groups):
|
|
95
|
+
is_last = i == len(memory_groups) - 1
|
|
96
|
+
prefix = "╰─ " if is_last else "├─ "
|
|
97
|
+
label = f"[{group_name}]"
|
|
98
|
+
formatted_paths = ", ".join(_format_memory_path(p, work_dir=work_dir) for p in paths)
|
|
99
|
+
panel_content.append_text(
|
|
100
|
+
Text.assemble(
|
|
101
|
+
("\n", ThemeKey.WELCOME_INFO),
|
|
102
|
+
(prefix, ThemeKey.LINES),
|
|
103
|
+
(f"{label.ljust(label_width)} {formatted_paths}", ThemeKey.WELCOME_INFO),
|
|
104
|
+
)
|
|
105
|
+
)
|
|
106
|
+
|
|
62
107
|
# Loaded skills summary is provided by core via WelcomeEvent to keep TUI decoupled.
|
|
63
108
|
loaded_skills = e.loaded_skills or {}
|
|
64
109
|
user_skills = loaded_skills.get("user") or []
|
|
@@ -81,7 +126,7 @@ def render_welcome(e: events.WelcomeEvent) -> RenderableType:
|
|
|
81
126
|
|
|
82
127
|
for i, (group_name, skills) in enumerate(skill_groups):
|
|
83
128
|
is_last = i == len(skill_groups) - 1
|
|
84
|
-
prefix = "
|
|
129
|
+
prefix = "╰─ " if is_last else "├─ "
|
|
85
130
|
label = f"[{group_name}]"
|
|
86
131
|
panel_content.append_text(
|
|
87
132
|
Text.assemble(
|
klaude_code/tui/display.py
CHANGED
|
@@ -31,12 +31,20 @@ class TUIDisplay(DisplayABC):
|
|
|
31
31
|
@override
|
|
32
32
|
async def consume_event(self, event: events.Event) -> None:
|
|
33
33
|
if isinstance(event, events.ReplayHistoryEvent):
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
34
|
+
# Replay does not need streaming UI; disable bottom Live rendering to avoid
|
|
35
|
+
# repaint overhead and flicker while reconstructing history.
|
|
36
|
+
self._renderer.stop_bottom_live()
|
|
37
|
+
self._renderer.set_stream_renderable(None)
|
|
38
|
+
self._renderer.set_replay_mode(True)
|
|
39
|
+
try:
|
|
40
|
+
await self._renderer.execute(self._machine.begin_replay())
|
|
41
|
+
for item in event.events:
|
|
42
|
+
commands = self._machine.transition_replay(item)
|
|
43
|
+
if commands:
|
|
44
|
+
await self._renderer.execute(commands)
|
|
45
|
+
await self._renderer.execute(self._machine.end_replay())
|
|
46
|
+
finally:
|
|
47
|
+
self._renderer.set_replay_mode(False)
|
|
40
48
|
return
|
|
41
49
|
|
|
42
50
|
commands = self._machine.transition(event)
|
|
@@ -219,6 +219,14 @@ class _ComboCompleter(Completer):
|
|
|
219
219
|
document: Document,
|
|
220
220
|
complete_event, # type: ignore[override]
|
|
221
221
|
) -> Iterable[Completion]:
|
|
222
|
+
# Bash mode: disable all completions.
|
|
223
|
+
# A command is considered bash mode only when the first character is `!` (or full-width `!`).
|
|
224
|
+
try:
|
|
225
|
+
if document.text.startswith(("!", "!")):
|
|
226
|
+
return
|
|
227
|
+
except Exception:
|
|
228
|
+
pass
|
|
229
|
+
|
|
222
230
|
# Try slash command completion first (only on first line)
|
|
223
231
|
if document.cursor_position_row == 0 and self._slash_completer.is_slash_command_context(document):
|
|
224
232
|
yield from self._slash_completer.get_completions(document, complete_event)
|
|
@@ -76,6 +76,9 @@ def create_key_bindings(
|
|
|
76
76
|
term_program = os.environ.get("TERM_PROGRAM", "").lower()
|
|
77
77
|
swallow_next_control_j = False
|
|
78
78
|
|
|
79
|
+
def _is_bash_mode_text(text: str) -> bool:
|
|
80
|
+
return text.startswith(("!", "!"))
|
|
81
|
+
|
|
79
82
|
def _data_requests_newline(data: str) -> bool:
|
|
80
83
|
"""Return True when incoming key data should insert a newline.
|
|
81
84
|
|
|
@@ -374,6 +377,33 @@ def create_key_bindings(
|
|
|
374
377
|
buf = event.current_buffer
|
|
375
378
|
doc = buf.document # type: ignore
|
|
376
379
|
|
|
380
|
+
# Normalize a leading full-width exclamation mark to ASCII so that:
|
|
381
|
+
# - UI echo shows `!cmd` consistently
|
|
382
|
+
# - history stores `!cmd` (not `!cmd`)
|
|
383
|
+
# - bash-mode detection is stable
|
|
384
|
+
try:
|
|
385
|
+
current_text = buf.text # type: ignore[reportUnknownMemberType]
|
|
386
|
+
cursor_pos = int(buf.cursor_position) # type: ignore[reportUnknownMemberType]
|
|
387
|
+
except Exception:
|
|
388
|
+
current_text = ""
|
|
389
|
+
cursor_pos = 0
|
|
390
|
+
|
|
391
|
+
if current_text.startswith("!"):
|
|
392
|
+
normalized = "!" + current_text[1:]
|
|
393
|
+
if normalized != current_text:
|
|
394
|
+
with contextlib.suppress(Exception):
|
|
395
|
+
buf.text = normalized # type: ignore[reportUnknownMemberType]
|
|
396
|
+
buf.cursor_position = min(cursor_pos, len(normalized)) # type: ignore[reportUnknownMemberType]
|
|
397
|
+
current_text = normalized
|
|
398
|
+
|
|
399
|
+
# Bash mode: if there is no command after `!` (ignoring only space/tab),
|
|
400
|
+
# ignore Enter but keep the input text as-is.
|
|
401
|
+
if _is_bash_mode_text(current_text):
|
|
402
|
+
after_bang = current_text[1:]
|
|
403
|
+
command = after_bang.lstrip(" \t")
|
|
404
|
+
if command == "":
|
|
405
|
+
return
|
|
406
|
+
|
|
377
407
|
data = getattr(event, "data", "")
|
|
378
408
|
if isinstance(data, str) and _data_requests_newline(data):
|
|
379
409
|
_insert_newline(event)
|
|
@@ -393,7 +423,13 @@ def create_key_bindings(
|
|
|
393
423
|
# When completions are visible, Enter accepts the current selection.
|
|
394
424
|
# This aligns with common TUI completion UX: navigation doesn't modify
|
|
395
425
|
# the buffer, and Enter/Tab inserts the selected option.
|
|
396
|
-
|
|
426
|
+
#
|
|
427
|
+
# Bash mode disables completions entirely, so always prefer submitting.
|
|
428
|
+
if (
|
|
429
|
+
not _is_bash_mode_text(current_text)
|
|
430
|
+
and not _should_submit_instead_of_accepting_completion(buf)
|
|
431
|
+
and _accept_current_completion(buf)
|
|
432
|
+
):
|
|
397
433
|
return
|
|
398
434
|
|
|
399
435
|
# Before submitting, expand any folded paste markers so that:
|
|
@@ -62,12 +62,13 @@ COMPLETION_SELECTED_LIGHT_BG = "ansigreen"
|
|
|
62
62
|
COMPLETION_SELECTED_UNKNOWN_BG = "ansigreen"
|
|
63
63
|
COMPLETION_MENU = "ansibrightblack"
|
|
64
64
|
INPUT_PROMPT_STYLE = "ansimagenta bold"
|
|
65
|
+
INPUT_PROMPT_BASH_STYLE = "ansigreen bold"
|
|
65
66
|
PLACEHOLDER_TEXT_STYLE_DARK_BG = "fg:#5a5a5a"
|
|
66
67
|
PLACEHOLDER_TEXT_STYLE_LIGHT_BG = "fg:#7a7a7a"
|
|
67
68
|
PLACEHOLDER_TEXT_STYLE_UNKNOWN_BG = "fg:#8a8a8a"
|
|
68
|
-
PLACEHOLDER_SYMBOL_STYLE_DARK_BG = "
|
|
69
|
-
PLACEHOLDER_SYMBOL_STYLE_LIGHT_BG = "
|
|
70
|
-
PLACEHOLDER_SYMBOL_STYLE_UNKNOWN_BG = "
|
|
69
|
+
PLACEHOLDER_SYMBOL_STYLE_DARK_BG = "fg:ansiblue"
|
|
70
|
+
PLACEHOLDER_SYMBOL_STYLE_LIGHT_BG = "fg:ansiblue"
|
|
71
|
+
PLACEHOLDER_SYMBOL_STYLE_UNKNOWN_BG = "fg:ansiblue"
|
|
71
72
|
|
|
72
73
|
|
|
73
74
|
# ---------------------------------------------------------------------------
|
|
@@ -244,6 +245,7 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
244
245
|
get_current_llm_config: Callable[[], llm_param.LLMConfigParameter | None] | None = None,
|
|
245
246
|
command_info_provider: Callable[[], list[CommandInfo]] | None = None,
|
|
246
247
|
):
|
|
248
|
+
self._prompt_text = prompt
|
|
247
249
|
self._status_provider = status_provider
|
|
248
250
|
self._pre_prompt = pre_prompt
|
|
249
251
|
self._post_prompt = post_prompt
|
|
@@ -296,11 +298,11 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
296
298
|
completion_selected = COMPLETION_SELECTED_UNKNOWN_BG
|
|
297
299
|
|
|
298
300
|
return PromptSession(
|
|
301
|
+
# Use a stable prompt string; we override the style dynamically in prompt_async.
|
|
299
302
|
[(INPUT_PROMPT_STYLE, prompt)],
|
|
300
303
|
history=FileHistory(str(history_path)),
|
|
301
304
|
multiline=True,
|
|
302
305
|
cursor=CursorShape.BLINKING_BEAM,
|
|
303
|
-
prompt_continuation=[(INPUT_PROMPT_STYLE, " ")],
|
|
304
306
|
key_bindings=kb,
|
|
305
307
|
completer=ThreadedCompleter(create_repl_completer(command_info_provider=self._command_info_provider)),
|
|
306
308
|
complete_while_typing=True,
|
|
@@ -340,6 +342,24 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
340
342
|
),
|
|
341
343
|
)
|
|
342
344
|
|
|
345
|
+
def _is_bash_mode_active(self) -> bool:
|
|
346
|
+
try:
|
|
347
|
+
text = self._session.default_buffer.text
|
|
348
|
+
return text.startswith(("!", "!"))
|
|
349
|
+
except Exception:
|
|
350
|
+
return False
|
|
351
|
+
|
|
352
|
+
def _get_prompt_message(self) -> FormattedText:
|
|
353
|
+
style = INPUT_PROMPT_BASH_STYLE if self._is_bash_mode_active() else INPUT_PROMPT_STYLE
|
|
354
|
+
return FormattedText([(style, self._prompt_text)])
|
|
355
|
+
|
|
356
|
+
def _bash_mode_toolbar_fragments(self) -> StyleAndTextTuples:
|
|
357
|
+
if not self._is_bash_mode_active():
|
|
358
|
+
return []
|
|
359
|
+
return [
|
|
360
|
+
("fg:ansigreen", " bash mode"),
|
|
361
|
+
]
|
|
362
|
+
|
|
343
363
|
def _setup_model_picker(self) -> None:
|
|
344
364
|
"""Initialize the model picker overlay and attach it to the layout."""
|
|
345
365
|
model_picker = SelectOverlay[str](
|
|
@@ -600,18 +620,32 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
600
620
|
display_text = f"Debug log: {debug_log_path}"
|
|
601
621
|
text_style = "fg:ansibrightblack"
|
|
602
622
|
|
|
623
|
+
bash_frags = self._bash_mode_toolbar_fragments()
|
|
624
|
+
bash_plain = "".join(frag[1] for frag in bash_frags)
|
|
625
|
+
|
|
603
626
|
if display_text:
|
|
604
627
|
left_text = " " + display_text
|
|
605
628
|
try:
|
|
606
629
|
terminal_width = shutil.get_terminal_size().columns
|
|
607
|
-
padding = " " * max(0, terminal_width - len(left_text))
|
|
608
630
|
except (OSError, ValueError):
|
|
631
|
+
terminal_width = 0
|
|
632
|
+
|
|
633
|
+
if terminal_width > 0 and bash_plain:
|
|
634
|
+
# Keep the right-side bash mode hint visible by truncating the left side if needed.
|
|
635
|
+
reserved = len(bash_plain)
|
|
636
|
+
max_left = max(0, terminal_width - reserved)
|
|
637
|
+
if len(left_text) > max_left:
|
|
638
|
+
left_text = left_text[: max_left - 1] + "…" if max_left >= 2 else ""
|
|
639
|
+
padding = " " * max(0, terminal_width - len(left_text) - reserved)
|
|
640
|
+
else:
|
|
609
641
|
padding = ""
|
|
610
642
|
|
|
611
|
-
|
|
612
|
-
return FormattedText([(text_style, toolbar_text)])
|
|
643
|
+
return FormattedText([(text_style, left_text + padding), *bash_frags])
|
|
613
644
|
|
|
614
|
-
# Show shortcut hints when nothing else to display
|
|
645
|
+
# Show shortcut hints when nothing else to display.
|
|
646
|
+
# In bash mode, prefer showing only the bash hint (no placeholder shortcuts).
|
|
647
|
+
if bash_frags:
|
|
648
|
+
return FormattedText([("fg:default", " "), *bash_frags])
|
|
615
649
|
return self._render_shortcut_hints()
|
|
616
650
|
|
|
617
651
|
# -------------------------------------------------------------------------
|
|
@@ -632,29 +666,20 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
632
666
|
return FormattedText(
|
|
633
667
|
[
|
|
634
668
|
(text_style, " "),
|
|
635
|
-
(symbol_style, "
|
|
636
|
-
(text_style, " "),
|
|
637
|
-
(
|
|
638
|
-
(text_style, " • "),
|
|
639
|
-
(symbol_style, "
|
|
640
|
-
(text_style, " "),
|
|
641
|
-
(
|
|
642
|
-
(text_style, " • "),
|
|
643
|
-
(symbol_style, "
|
|
644
|
-
(text_style, " "),
|
|
645
|
-
(
|
|
646
|
-
(text_style, " • "),
|
|
647
|
-
(symbol_style, "
|
|
648
|
-
(text_style, " "),
|
|
649
|
-
(text_style, "models"),
|
|
650
|
-
(text_style, " • "),
|
|
651
|
-
(symbol_style, " ctrl-t "),
|
|
652
|
-
(text_style, " "),
|
|
653
|
-
(text_style, "think"),
|
|
654
|
-
(text_style, " • "),
|
|
655
|
-
(symbol_style, " ctrl-v "),
|
|
656
|
-
(text_style, " "),
|
|
657
|
-
(text_style, "paste image"),
|
|
669
|
+
(symbol_style, "@"),
|
|
670
|
+
(text_style, " files • "),
|
|
671
|
+
(symbol_style, "$"),
|
|
672
|
+
(text_style, " skills • "),
|
|
673
|
+
(symbol_style, "/"),
|
|
674
|
+
(text_style, " commands • "),
|
|
675
|
+
(symbol_style, "!"),
|
|
676
|
+
(text_style, " shell • "),
|
|
677
|
+
(symbol_style, "ctrl-l"),
|
|
678
|
+
(text_style, " models • "),
|
|
679
|
+
(symbol_style, "ctrl-t"),
|
|
680
|
+
(text_style, " think • "),
|
|
681
|
+
(symbol_style, "ctrl-v"),
|
|
682
|
+
(text_style, " paste image"),
|
|
658
683
|
]
|
|
659
684
|
)
|
|
660
685
|
|
|
@@ -680,6 +705,7 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
680
705
|
# proper styling instead of showing raw escape codes.
|
|
681
706
|
with patch_stdout(raw=True):
|
|
682
707
|
line: str = await self._session.prompt_async(
|
|
708
|
+
message=self._get_prompt_message,
|
|
683
709
|
bottom_toolbar=self._get_bottom_toolbar,
|
|
684
710
|
)
|
|
685
711
|
if self._post_prompt is not None:
|