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.
Files changed (49) hide show
  1. klaude_code/app/runtime.py +5 -1
  2. klaude_code/cli/cost_cmd.py +4 -4
  3. klaude_code/cli/list_model.py +1 -2
  4. klaude_code/cli/main.py +10 -0
  5. klaude_code/config/assets/builtin_config.yaml +15 -14
  6. klaude_code/const.py +4 -3
  7. klaude_code/core/agent_profile.py +23 -0
  8. klaude_code/core/bash_mode.py +276 -0
  9. klaude_code/core/executor.py +40 -7
  10. klaude_code/core/manager/llm_clients.py +1 -0
  11. klaude_code/core/manager/llm_clients_builder.py +2 -2
  12. klaude_code/core/memory.py +140 -0
  13. klaude_code/core/prompts/prompt-sub-agent-web.md +2 -2
  14. klaude_code/core/reminders.py +17 -89
  15. klaude_code/core/tool/offload.py +4 -4
  16. klaude_code/core/tool/web/web_fetch_tool.md +2 -1
  17. klaude_code/core/tool/web/web_fetch_tool.py +1 -1
  18. klaude_code/core/turn.py +9 -4
  19. klaude_code/protocol/events.py +17 -0
  20. klaude_code/protocol/op.py +12 -0
  21. klaude_code/protocol/op_handler.py +5 -0
  22. klaude_code/session/templates/mermaid_viewer.html +85 -0
  23. klaude_code/tui/command/resume_cmd.py +1 -1
  24. klaude_code/tui/commands.py +15 -0
  25. klaude_code/tui/components/command_output.py +4 -5
  26. klaude_code/tui/components/developer.py +1 -3
  27. klaude_code/tui/components/metadata.py +28 -25
  28. klaude_code/tui/components/rich/code_panel.py +31 -16
  29. klaude_code/tui/components/rich/markdown.py +56 -124
  30. klaude_code/tui/components/rich/theme.py +22 -12
  31. klaude_code/tui/components/thinking.py +0 -35
  32. klaude_code/tui/components/tools.py +4 -2
  33. klaude_code/tui/components/user_input.py +49 -59
  34. klaude_code/tui/components/welcome.py +47 -2
  35. klaude_code/tui/display.py +14 -6
  36. klaude_code/tui/input/completers.py +8 -0
  37. klaude_code/tui/input/key_bindings.py +37 -1
  38. klaude_code/tui/input/prompt_toolkit.py +57 -31
  39. klaude_code/tui/machine.py +108 -28
  40. klaude_code/tui/renderer.py +117 -19
  41. klaude_code/tui/runner.py +22 -0
  42. klaude_code/tui/terminal/notifier.py +11 -12
  43. klaude_code/tui/terminal/selector.py +1 -1
  44. klaude_code/ui/terminal/title.py +4 -2
  45. {klaude_code-2.9.1.dist-info → klaude_code-2.10.1.dist-info}/METADATA +1 -1
  46. {klaude_code-2.9.1.dist-info → klaude_code-2.10.1.dist-info}/RECORD +48 -47
  47. klaude_code/tui/components/assistant.py +0 -2
  48. {klaude_code-2.9.1.dist-info → klaude_code-2.10.1.dist-info}/WHEEL +0 -0
  49. {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
- return _render_tool_call_tree(mark=MARK_BASH, tool_name=tool_name, details=highlighted)
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 get_available_skills
8
- from klaude_code.tui.components.common import create_grid
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 @-file patterns only when they appear at the beginning of the line
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
- AT_FILE_RENDER_PATTERN = re.compile(r'(?<!\S)@("([^"]+)"|\S+)')
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
- has_at = "@" in text
30
- has_skill = "$" in text or "\u00a5" in text # $ or ¥
31
-
32
- if not has_at and not has_skill:
33
- return Text(text, style=other_style)
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
- result = Text("")
55
- last_end = 0
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
- if last_end < len(text):
65
- result.append_text(Text(text[last_end:], other_style))
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
- line = line.expandtabs(TAB_EXPAND_WIDTH)
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]) if len(splits) > 1 else Text(""),
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
- grid = create_grid()
102
- grid.padding = (0, 0)
103
- mark = Text(USER_MESSAGE_MARK, style=ThemeKey.USER_INPUT_PROMPT)
104
- grid.add_row(mark, Group(*renderables))
105
- return grid
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(" Interrupted by user\n", style=ThemeKey.INTERRUPT)
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 = "└─ " if is_last else "├─ "
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 = "└─ " if is_last else "├─ "
129
+ prefix = "╰─ " if is_last else "├─ "
85
130
  label = f"[{group_name}]"
86
131
  panel_content.append_text(
87
132
  Text.assemble(
@@ -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
- await self._renderer.execute(self._machine.begin_replay())
35
- for item in event.events:
36
- commands = self._machine.transition_replay(item)
37
- if commands:
38
- await self._renderer.execute(commands)
39
- await self._renderer.execute(self._machine.end_replay())
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
- if not _should_submit_instead_of_accepting_completion(buf) and _accept_current_completion(buf):
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 = "bg:#2a2a2a fg:#5a5a5a"
69
- PLACEHOLDER_SYMBOL_STYLE_LIGHT_BG = "bg:#e6e6e6 fg:#7a7a7a"
70
- PLACEHOLDER_SYMBOL_STYLE_UNKNOWN_BG = "bg:#2a2a2a fg:#8a8a8a"
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
- toolbar_text = left_text + padding
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
- (text_style, "files"),
638
- (text_style, " • "),
639
- (symbol_style, " $ "),
640
- (text_style, " "),
641
- (text_style, "skills"),
642
- (text_style, " • "),
643
- (symbol_style, " / "),
644
- (text_style, " "),
645
- (text_style, "commands"),
646
- (text_style, " • "),
647
- (symbol_style, " ctrl-l "),
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: