klaude-code 1.2.26__py3-none-any.whl → 1.2.28__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/cli/config_cmd.py +1 -5
- klaude_code/cli/debug.py +9 -1
- klaude_code/cli/list_model.py +170 -129
- klaude_code/cli/main.py +76 -19
- klaude_code/cli/runtime.py +15 -11
- klaude_code/cli/self_update.py +2 -1
- klaude_code/cli/session_cmd.py +1 -1
- klaude_code/command/__init__.py +3 -0
- klaude_code/command/export_online_cmd.py +15 -12
- klaude_code/command/fork_session_cmd.py +42 -0
- klaude_code/config/__init__.py +3 -1
- klaude_code/config/assets/__init__.py +1 -0
- klaude_code/config/assets/builtin_config.yaml +233 -0
- klaude_code/config/builtin_config.py +37 -0
- klaude_code/config/config.py +332 -112
- klaude_code/config/select_model.py +46 -8
- klaude_code/core/executor.py +6 -3
- klaude_code/core/manager/llm_clients_builder.py +4 -1
- klaude_code/core/reminders.py +52 -16
- klaude_code/core/tool/file/edit_tool.py +4 -4
- klaude_code/core/tool/file/write_tool.py +4 -4
- klaude_code/core/tool/shell/bash_tool.py +2 -2
- klaude_code/core/tool/web/mermaid_tool.md +17 -0
- klaude_code/core/tool/web/mermaid_tool.py +2 -2
- klaude_code/llm/openai_compatible/stream.py +2 -1
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
- klaude_code/protocol/commands.py +1 -0
- klaude_code/protocol/model.py +1 -0
- klaude_code/session/export.py +52 -7
- klaude_code/session/selector.py +2 -2
- klaude_code/session/session.py +26 -4
- klaude_code/trace/log.py +7 -1
- klaude_code/ui/modes/repl/__init__.py +3 -44
- klaude_code/ui/modes/repl/completers.py +39 -7
- klaude_code/ui/modes/repl/event_handler.py +8 -6
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +33 -66
- klaude_code/ui/modes/repl/key_bindings.py +4 -4
- klaude_code/ui/modes/repl/renderer.py +1 -6
- klaude_code/ui/renderers/common.py +11 -4
- klaude_code/ui/renderers/developer.py +17 -0
- klaude_code/ui/renderers/diffs.py +1 -1
- klaude_code/ui/renderers/errors.py +10 -5
- klaude_code/ui/renderers/metadata.py +2 -2
- klaude_code/ui/renderers/tools.py +8 -4
- klaude_code/ui/rich/markdown.py +5 -5
- klaude_code/ui/rich/theme.py +7 -3
- klaude_code/ui/terminal/color.py +1 -1
- klaude_code/ui/terminal/control.py +4 -4
- {klaude_code-1.2.26.dist-info → klaude_code-1.2.28.dist-info}/METADATA +121 -127
- {klaude_code-1.2.26.dist-info → klaude_code-1.2.28.dist-info}/RECORD +52 -48
- {klaude_code-1.2.26.dist-info → klaude_code-1.2.28.dist-info}/entry_points.txt +1 -0
- {klaude_code-1.2.26.dist-info → klaude_code-1.2.28.dist-info}/WHEEL +0 -0
|
@@ -204,7 +204,7 @@ class _SkillCompleter(Completer):
|
|
|
204
204
|
from klaude_code.skill import get_available_skills
|
|
205
205
|
|
|
206
206
|
return get_available_skills()
|
|
207
|
-
except
|
|
207
|
+
except (ImportError, RuntimeError):
|
|
208
208
|
return []
|
|
209
209
|
|
|
210
210
|
def is_skill_context(self, document: Document) -> bool:
|
|
@@ -398,8 +398,14 @@ class _AtFilesCompleter(Completer):
|
|
|
398
398
|
|
|
399
399
|
if not results:
|
|
400
400
|
if self._has_cmd("fd"):
|
|
401
|
+
# First, get immediate children matching the keyword (depth=0).
|
|
402
|
+
# fd's traversal order is not depth-first, so --max-results may
|
|
403
|
+
# truncate shallow matches. We ensure depth=0 items are always included.
|
|
404
|
+
immediate = self._get_immediate_matches(cwd, key_norm)
|
|
401
405
|
# Use fd to search anywhere in full path (files and directories), case-insensitive
|
|
402
|
-
|
|
406
|
+
fd_results, truncated = self._run_fd_search(cwd, key_norm, max_results=max_scan_results)
|
|
407
|
+
# Merge: immediate matches first, then fd results (deduped in _filter_and_format)
|
|
408
|
+
results = immediate + fd_results
|
|
403
409
|
elif self._has_cmd("rg"):
|
|
404
410
|
# Use rg to search only in current directory
|
|
405
411
|
rg_cache_ttl = max(self._cache_ttl, 30.0)
|
|
@@ -451,10 +457,11 @@ class _AtFilesCompleter(Completer):
|
|
|
451
457
|
keyword_norm: str,
|
|
452
458
|
) -> list[str]:
|
|
453
459
|
# Filter to keyword (case-insensitive) and rank by:
|
|
454
|
-
# 1.
|
|
460
|
+
# 1. Directory depth (shallower first)
|
|
461
|
+
# 2. Basename hit first, then path hit position, then length
|
|
455
462
|
# Since both fd and rg now search from current directory, all paths are relative to cwd
|
|
456
463
|
kn = keyword_norm
|
|
457
|
-
out: list[tuple[str, tuple[int, int, int, int]]] = []
|
|
464
|
+
out: list[tuple[str, tuple[int, int, int, int, int]]] = []
|
|
458
465
|
for p in paths_from_root:
|
|
459
466
|
pl = p.lower()
|
|
460
467
|
if kn not in pl:
|
|
@@ -469,7 +476,9 @@ class _AtFilesCompleter(Completer):
|
|
|
469
476
|
base = os.path.basename(rel_to_cwd.rstrip("/")).lower()
|
|
470
477
|
base_pos = base.find(kn)
|
|
471
478
|
path_pos = pl.find(kn)
|
|
479
|
+
depth = rel_to_cwd.rstrip("/").count("/")
|
|
472
480
|
score = (
|
|
481
|
+
depth,
|
|
473
482
|
0 if base_pos != -1 else 1,
|
|
474
483
|
base_pos if base_pos != -1 else 10_000,
|
|
475
484
|
path_pos,
|
|
@@ -497,7 +506,7 @@ class _AtFilesCompleter(Completer):
|
|
|
497
506
|
try:
|
|
498
507
|
if (cwd / s).is_dir():
|
|
499
508
|
uniq[idx] = f"{s}/"
|
|
500
|
-
except
|
|
509
|
+
except OSError:
|
|
501
510
|
continue
|
|
502
511
|
return uniq
|
|
503
512
|
|
|
@@ -530,7 +539,7 @@ class _AtFilesCompleter(Completer):
|
|
|
530
539
|
if tag != "search":
|
|
531
540
|
return root, None
|
|
532
541
|
return root, kw
|
|
533
|
-
except
|
|
542
|
+
except ValueError:
|
|
534
543
|
return None, None
|
|
535
544
|
|
|
536
545
|
# ---- Utilities ----
|
|
@@ -680,10 +689,32 @@ class _AtFilesCompleter(Completer):
|
|
|
680
689
|
if p.is_dir() and not rel.endswith("/"):
|
|
681
690
|
rel += "/"
|
|
682
691
|
items.append(rel)
|
|
683
|
-
except
|
|
692
|
+
except OSError:
|
|
684
693
|
return []
|
|
685
694
|
return items[: min(self._max_results, 100)]
|
|
686
695
|
|
|
696
|
+
def _get_immediate_matches(self, cwd: Path, keyword_norm: str) -> list[str]:
|
|
697
|
+
"""Get immediate children of cwd that match the keyword (case-insensitive).
|
|
698
|
+
|
|
699
|
+
This ensures depth=0 matches are always included, even when fd's
|
|
700
|
+
--max-results truncates before reaching them.
|
|
701
|
+
"""
|
|
702
|
+
excluded = {".git", ".venv", "node_modules"}
|
|
703
|
+
items: list[str] = []
|
|
704
|
+
try:
|
|
705
|
+
for p in cwd.iterdir():
|
|
706
|
+
name = p.name
|
|
707
|
+
if name in excluded:
|
|
708
|
+
continue
|
|
709
|
+
if keyword_norm in name.lower():
|
|
710
|
+
rel = name
|
|
711
|
+
if p.is_dir():
|
|
712
|
+
rel += "/"
|
|
713
|
+
items.append(rel)
|
|
714
|
+
except OSError:
|
|
715
|
+
return []
|
|
716
|
+
return items
|
|
717
|
+
|
|
687
718
|
def _run_cmd(self, cmd: list[str], cwd: Path | None = None, *, timeout_sec: float) -> _CmdResult:
|
|
688
719
|
cmd_str = " ".join(cmd)
|
|
689
720
|
start = time.monotonic()
|
|
@@ -691,6 +722,7 @@ class _AtFilesCompleter(Completer):
|
|
|
691
722
|
p = subprocess.run(
|
|
692
723
|
cmd,
|
|
693
724
|
cwd=str(cwd) if cwd else None,
|
|
725
|
+
stdin=subprocess.DEVNULL,
|
|
694
726
|
stdout=subprocess.PIPE,
|
|
695
727
|
stderr=subprocess.DEVNULL,
|
|
696
728
|
text=True,
|
|
@@ -167,10 +167,9 @@ class ActivityState:
|
|
|
167
167
|
return activity_text
|
|
168
168
|
if self._composing:
|
|
169
169
|
# Main status text with creative verb
|
|
170
|
-
text = Text.
|
|
171
|
-
|
|
172
|
-
(f"({self._buffer_length:,})", ThemeKey.STATUS_TEXT)
|
|
173
|
-
)
|
|
170
|
+
text = Text("Composing", style=ThemeKey.STATUS_TEXT_BOLD)
|
|
171
|
+
if self._buffer_length > 0:
|
|
172
|
+
text.append(f" ({self._buffer_length:,})", style=ThemeKey.STATUS_TEXT)
|
|
174
173
|
return text
|
|
175
174
|
return None
|
|
176
175
|
|
|
@@ -246,13 +245,16 @@ class SpinnerStatusState:
|
|
|
246
245
|
"""Get current spinner status as rich Text (without context)."""
|
|
247
246
|
activity_text = self._activity.get_activity_text()
|
|
248
247
|
|
|
249
|
-
base_status = self.
|
|
248
|
+
base_status = self._reasoning_status or self._todo_status
|
|
250
249
|
|
|
251
250
|
if base_status:
|
|
252
|
-
result = Text(base_status, style=ThemeKey.STATUS_TEXT_BOLD)
|
|
253
251
|
if activity_text:
|
|
252
|
+
result = Text()
|
|
253
|
+
result.append(base_status, style=ThemeKey.STATUS_TEXT_BOLD_ITALIC)
|
|
254
254
|
result.append(" | ")
|
|
255
255
|
result.append_text(activity_text)
|
|
256
|
+
else:
|
|
257
|
+
result = Text(base_status, style=ThemeKey.STATUS_TEXT_BOLD_ITALIC)
|
|
256
258
|
elif activity_text:
|
|
257
259
|
activity_text.append(" …")
|
|
258
260
|
result = activity_text
|
|
@@ -21,16 +21,11 @@ from klaude_code.ui.modes.repl.completers import AT_TOKEN_PATTERN, create_repl_c
|
|
|
21
21
|
from klaude_code.ui.modes.repl.key_bindings import create_key_bindings
|
|
22
22
|
from klaude_code.ui.renderers.user_input import USER_MESSAGE_MARK
|
|
23
23
|
from klaude_code.ui.terminal.color import is_light_terminal_background
|
|
24
|
-
from klaude_code.ui.utils.common import get_current_git_branch, show_path_with_tilde
|
|
25
24
|
|
|
26
25
|
|
|
27
26
|
class REPLStatusSnapshot(NamedTuple):
|
|
28
27
|
"""Snapshot of REPL status for bottom toolbar display."""
|
|
29
28
|
|
|
30
|
-
model_name: str
|
|
31
|
-
context_usage_percent: float | None
|
|
32
|
-
llm_calls: int
|
|
33
|
-
tool_calls: int
|
|
34
29
|
update_message: str | None = None
|
|
35
30
|
|
|
36
31
|
|
|
@@ -54,11 +49,16 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
54
49
|
status_provider: Callable[[], REPLStatusSnapshot] | None = None,
|
|
55
50
|
pre_prompt: Callable[[], None] | None = None,
|
|
56
51
|
post_prompt: Callable[[], None] | None = None,
|
|
52
|
+
is_light_background: bool | None = None,
|
|
57
53
|
): # ▌
|
|
58
54
|
self._status_provider = status_provider
|
|
59
55
|
self._pre_prompt = pre_prompt
|
|
60
56
|
self._post_prompt = post_prompt
|
|
61
|
-
|
|
57
|
+
# Use provided value if available to avoid redundant TTY queries that may interfere
|
|
58
|
+
# with prompt_toolkit's terminal state after questionary has been used.
|
|
59
|
+
self._is_light_terminal_background = (
|
|
60
|
+
is_light_background if is_light_background is not None else is_light_terminal_background(timeout=0.2)
|
|
61
|
+
)
|
|
62
62
|
|
|
63
63
|
project = str(Path.cwd()).strip("/").replace("/", "-")
|
|
64
64
|
history_path = Path.home() / ".klaude" / "projects" / project / "input" / "input_history.txt"
|
|
@@ -91,7 +91,6 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
91
91
|
completer=ThreadedCompleter(create_repl_completer()),
|
|
92
92
|
complete_while_typing=True,
|
|
93
93
|
erase_when_done=True,
|
|
94
|
-
bottom_toolbar=self._render_bottom_toolbar,
|
|
95
94
|
mouse_support=False,
|
|
96
95
|
style=Style.from_dict(
|
|
97
96
|
{
|
|
@@ -107,68 +106,29 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
107
106
|
),
|
|
108
107
|
)
|
|
109
108
|
|
|
110
|
-
def
|
|
111
|
-
"""
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if update_message:
|
|
126
|
-
left_text = " " + update_message
|
|
127
|
-
try:
|
|
128
|
-
terminal_width = shutil.get_terminal_size().columns
|
|
129
|
-
padding = " " * max(0, terminal_width - len(left_text))
|
|
130
|
-
except Exception:
|
|
131
|
-
padding = ""
|
|
132
|
-
toolbar_text = left_text + padding
|
|
133
|
-
return FormattedText([("#ansiyellow", toolbar_text)])
|
|
134
|
-
|
|
135
|
-
# Normal mode: Left side: path and git branch
|
|
136
|
-
left_parts: list[str] = []
|
|
137
|
-
left_parts.append(show_path_with_tilde())
|
|
138
|
-
|
|
139
|
-
git_branch = get_current_git_branch()
|
|
140
|
-
if git_branch:
|
|
141
|
-
left_parts.append(git_branch)
|
|
142
|
-
|
|
143
|
-
# Right side: status info
|
|
144
|
-
right_parts: list[str] = []
|
|
145
|
-
if self._status_provider:
|
|
146
|
-
try:
|
|
147
|
-
status = self._status_provider()
|
|
148
|
-
model_name = status.model_name or "N/A"
|
|
149
|
-
right_parts.append(model_name)
|
|
150
|
-
|
|
151
|
-
# Add context if available
|
|
152
|
-
if status.context_usage_percent is not None:
|
|
153
|
-
right_parts.append(f"context {status.context_usage_percent:.1f}%")
|
|
154
|
-
except Exception:
|
|
155
|
-
pass
|
|
156
|
-
|
|
157
|
-
# Build left and right text with borders
|
|
158
|
-
left_text = " " + " · ".join(left_parts)
|
|
159
|
-
right_text = (" · ".join(right_parts) + " ") if right_parts else " "
|
|
160
|
-
|
|
161
|
-
# Calculate padding
|
|
109
|
+
def _get_bottom_toolbar(self) -> FormattedText | None:
|
|
110
|
+
"""Return bottom toolbar content only when there's an update message available."""
|
|
111
|
+
if not self._status_provider:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
try:
|
|
115
|
+
status = self._status_provider()
|
|
116
|
+
update_message = status.update_message
|
|
117
|
+
except (AttributeError, RuntimeError):
|
|
118
|
+
return None
|
|
119
|
+
|
|
120
|
+
if not update_message:
|
|
121
|
+
return None
|
|
122
|
+
|
|
123
|
+
left_text = " " + update_message
|
|
162
124
|
try:
|
|
163
125
|
terminal_width = shutil.get_terminal_size().columns
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
except Exception:
|
|
126
|
+
padding = " " * max(0, terminal_width - len(left_text))
|
|
127
|
+
except (OSError, ValueError):
|
|
167
128
|
padding = ""
|
|
168
129
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
return FormattedText([("#2c7eac", toolbar_text)])
|
|
130
|
+
toolbar_text = left_text + padding
|
|
131
|
+
return FormattedText([("#ansiyellow", toolbar_text)])
|
|
172
132
|
|
|
173
133
|
def _render_input_placeholder(self) -> FormattedText:
|
|
174
134
|
if self._is_light_terminal_background is True:
|
|
@@ -210,8 +170,15 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
210
170
|
if self._pre_prompt is not None:
|
|
211
171
|
with contextlib.suppress(Exception):
|
|
212
172
|
self._pre_prompt()
|
|
173
|
+
|
|
174
|
+
# Only show bottom toolbar if there's an update message
|
|
175
|
+
bottom_toolbar = self._get_bottom_toolbar()
|
|
176
|
+
|
|
213
177
|
with patch_stdout():
|
|
214
|
-
line: str = await self._session.prompt_async(
|
|
178
|
+
line: str = await self._session.prompt_async(
|
|
179
|
+
placeholder=self._render_input_placeholder(),
|
|
180
|
+
bottom_toolbar=bottom_toolbar,
|
|
181
|
+
)
|
|
215
182
|
if self._post_prompt is not None:
|
|
216
183
|
with contextlib.suppress(Exception):
|
|
217
184
|
self._post_prompt()
|
|
@@ -52,7 +52,7 @@ def create_key_bindings(
|
|
|
52
52
|
buf.delete_before_cursor() # remove the sentinel backslash # type: ignore[reportUnknownMemberType]
|
|
53
53
|
buf.insert_text("\n") # type: ignore[reportUnknownMemberType]
|
|
54
54
|
return
|
|
55
|
-
except
|
|
55
|
+
except (AttributeError, TypeError):
|
|
56
56
|
# Fall through to default behavior if anything goes wrong
|
|
57
57
|
pass
|
|
58
58
|
|
|
@@ -111,7 +111,7 @@ def create_key_bindings(
|
|
|
111
111
|
|
|
112
112
|
if should_refresh:
|
|
113
113
|
buf.start_completion(select_first=False) # type: ignore[reportUnknownMemberType]
|
|
114
|
-
except
|
|
114
|
+
except (AttributeError, TypeError):
|
|
115
115
|
pass
|
|
116
116
|
|
|
117
117
|
@kb.add("left")
|
|
@@ -136,7 +136,7 @@ def create_key_bindings(
|
|
|
136
136
|
# Default behavior: move one character left when possible.
|
|
137
137
|
if doc.cursor_position > 0: # type: ignore[reportUnknownMemberType]
|
|
138
138
|
buf.cursor_left() # type: ignore[reportUnknownMemberType]
|
|
139
|
-
except
|
|
139
|
+
except (AttributeError, IndexError, TypeError):
|
|
140
140
|
pass
|
|
141
141
|
|
|
142
142
|
@kb.add("right")
|
|
@@ -163,7 +163,7 @@ def create_key_bindings(
|
|
|
163
163
|
# Default behavior: move one character right when possible.
|
|
164
164
|
if doc.cursor_position < len(doc.text): # type: ignore[reportUnknownMemberType]
|
|
165
165
|
buf.cursor_right() # type: ignore[reportUnknownMemberType]
|
|
166
|
-
except
|
|
166
|
+
except (AttributeError, IndexError, TypeError):
|
|
167
167
|
pass
|
|
168
168
|
|
|
169
169
|
return kb
|
|
@@ -266,12 +266,7 @@ class REPLRenderer:
|
|
|
266
266
|
self.print(r_user_input.render_interrupt())
|
|
267
267
|
|
|
268
268
|
def display_error(self, event: events.ErrorEvent) -> None:
|
|
269
|
-
self.print(
|
|
270
|
-
r_errors.render_error(
|
|
271
|
-
truncate_display(event.error_message),
|
|
272
|
-
indent=0,
|
|
273
|
-
)
|
|
274
|
-
)
|
|
269
|
+
self.print(r_errors.render_error(truncate_display(event.error_message)))
|
|
275
270
|
|
|
276
271
|
# -------------------------------------------------------------------------
|
|
277
272
|
# Spinner control methods
|
|
@@ -37,10 +37,17 @@ def truncate_display(
|
|
|
37
37
|
|
|
38
38
|
if len(lines) > max_lines:
|
|
39
39
|
truncated_lines = len(lines) - max_lines
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
|
|
41
|
+
# If the hidden section is too small, show everything instead of inserting
|
|
42
|
+
# the "(more N lines)" indicator.
|
|
43
|
+
if truncated_lines < 5:
|
|
44
|
+
truncated_lines = 0
|
|
45
|
+
head_lines = lines
|
|
46
|
+
else:
|
|
47
|
+
head_count = max_lines // 2
|
|
48
|
+
tail_count = max_lines - head_count
|
|
49
|
+
head_lines = lines[:head_count]
|
|
50
|
+
tail_lines = lines[-tail_count:]
|
|
44
51
|
else:
|
|
45
52
|
head_lines = lines
|
|
46
53
|
|
|
@@ -120,6 +120,8 @@ def render_command_output(e: events.DeveloperMessageEvent) -> RenderableType:
|
|
|
120
120
|
return _render_status_output(e.item.command_output)
|
|
121
121
|
case commands.CommandName.RELEASE_NOTES:
|
|
122
122
|
return Padding.indent(NoInsetMarkdown(e.item.content or ""), level=2)
|
|
123
|
+
case commands.CommandName.FORK_SESSION:
|
|
124
|
+
return _render_fork_session_output(e.item.command_output)
|
|
123
125
|
case _:
|
|
124
126
|
content = e.item.content or "(no content)"
|
|
125
127
|
style = ThemeKey.TOOL_RESULT if not e.item.command_output.is_error else ThemeKey.ERROR
|
|
@@ -145,6 +147,21 @@ def _format_cost(cost: float | None, currency: str = "USD") -> str:
|
|
|
145
147
|
return f"{symbol}{cost:.2f}"
|
|
146
148
|
|
|
147
149
|
|
|
150
|
+
def _render_fork_session_output(command_output: model.CommandOutput) -> RenderableType:
|
|
151
|
+
"""Render fork session output with usage instructions."""
|
|
152
|
+
if not isinstance(command_output.ui_extra, model.SessionIdUIExtra):
|
|
153
|
+
return Text("(no session id)", style=ThemeKey.METADATA)
|
|
154
|
+
|
|
155
|
+
session_id = command_output.ui_extra.session_id
|
|
156
|
+
grid = Table.grid(padding=(0, 1))
|
|
157
|
+
grid.add_column(style=ThemeKey.METADATA, overflow="fold")
|
|
158
|
+
|
|
159
|
+
grid.add_row(Text("Session forked. To continue in a new conversation:", style=ThemeKey.METADATA))
|
|
160
|
+
grid.add_row(Text(f" klaude --resume-by-id {session_id}", style=ThemeKey.METADATA_BOLD))
|
|
161
|
+
|
|
162
|
+
return Padding.indent(grid, level=2)
|
|
163
|
+
|
|
164
|
+
|
|
148
165
|
def _render_status_output(command_output: model.CommandOutput) -> RenderableType:
|
|
149
166
|
"""Render session status with total cost and per-model breakdown."""
|
|
150
167
|
if not isinstance(command_output.ui_extra, model.SessionStatusUIExtra):
|
|
@@ -148,7 +148,7 @@ def render_diff(diff_text: str, show_file_name: bool = False) -> RenderableType:
|
|
|
148
148
|
plus = parts[2] # like '+12,4'
|
|
149
149
|
new_start = int(plus[1:].split(",")[0])
|
|
150
150
|
new_ln = new_start
|
|
151
|
-
except
|
|
151
|
+
except (IndexError, ValueError):
|
|
152
152
|
new_ln = None
|
|
153
153
|
if has_rendered_diff_content:
|
|
154
154
|
grid.add_row(Text(f"{'⋮':>{const.DIFF_PREFIX_WIDTH}}", style=ThemeKey.TOOL_RESULT), "")
|
|
@@ -5,12 +5,17 @@ from klaude_code.ui.renderers.common import create_grid
|
|
|
5
5
|
from klaude_code.ui.rich.theme import ThemeKey
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
def render_error(error_msg: Text
|
|
9
|
-
"""
|
|
8
|
+
def render_error(error_msg: Text) -> RenderableType:
|
|
9
|
+
"""Render error with X mark for error events."""
|
|
10
|
+
grid = create_grid()
|
|
11
|
+
error_msg.style = ThemeKey.ERROR
|
|
12
|
+
grid.add_row(Text("✘", style=ThemeKey.ERROR_BOLD), error_msg)
|
|
13
|
+
return grid
|
|
14
|
+
|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
"""
|
|
16
|
+
def render_tool_error(error_msg: Text) -> RenderableType:
|
|
17
|
+
"""Render error with indent for tool results."""
|
|
13
18
|
grid = create_grid()
|
|
14
19
|
error_msg.style = ThemeKey.ERROR
|
|
15
|
-
grid.add_row(Text("
|
|
20
|
+
grid.add_row(Text(" "), error_msg)
|
|
16
21
|
return grid
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from importlib.metadata import version
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
2
|
|
|
3
3
|
from rich import box
|
|
4
4
|
from rich.console import Group, RenderableType
|
|
@@ -17,7 +17,7 @@ def _get_version() -> str:
|
|
|
17
17
|
"""Get the current version of klaude-code."""
|
|
18
18
|
try:
|
|
19
19
|
return version("klaude-code")
|
|
20
|
-
except
|
|
20
|
+
except PackageNotFoundError:
|
|
21
21
|
return "unknown"
|
|
22
22
|
|
|
23
23
|
|
|
@@ -18,7 +18,7 @@ from klaude_code.ui.rich.theme import ThemeKey
|
|
|
18
18
|
|
|
19
19
|
# Tool markers (Unicode symbols for UI display)
|
|
20
20
|
MARK_GENERIC = "⚒"
|
|
21
|
-
MARK_BASH = "
|
|
21
|
+
MARK_BASH = "$"
|
|
22
22
|
MARK_PLAN = "Ξ"
|
|
23
23
|
MARK_READ = "←"
|
|
24
24
|
MARK_EDIT = "±"
|
|
@@ -205,9 +205,13 @@ def render_write_tool_call(arguments: str) -> RenderableType:
|
|
|
205
205
|
grid = create_grid()
|
|
206
206
|
try:
|
|
207
207
|
json_dict = json.loads(arguments)
|
|
208
|
-
file_path = json_dict.get("file_path")
|
|
208
|
+
file_path = json_dict.get("file_path", "")
|
|
209
209
|
tool_name_column = Text.assemble((MARK_WRITE, ThemeKey.TOOL_MARK), " ", ("Write", ThemeKey.TOOL_NAME))
|
|
210
|
-
|
|
210
|
+
# Markdown files show path in result panel, skip here to avoid duplication
|
|
211
|
+
if file_path.endswith(".md"):
|
|
212
|
+
arguments_column = Text("")
|
|
213
|
+
else:
|
|
214
|
+
arguments_column = render_path(file_path, ThemeKey.TOOL_PARAM_FILE_PATH)
|
|
211
215
|
except json.JSONDecodeError:
|
|
212
216
|
tool_name_column = Text.assemble((MARK_WRITE, ThemeKey.TOOL_MARK), " ", ("Write", ThemeKey.TOOL_NAME))
|
|
213
217
|
arguments_column = Text(
|
|
@@ -569,7 +573,7 @@ def render_tool_result(e: events.ToolResultEvent, *, code_theme: str = "monokai"
|
|
|
569
573
|
# Handle error case
|
|
570
574
|
if e.status == "error" and e.ui_extra is None:
|
|
571
575
|
error_msg = truncate_display(e.result)
|
|
572
|
-
return r_errors.
|
|
576
|
+
return r_errors.render_tool_error(error_msg)
|
|
573
577
|
|
|
574
578
|
# Render multiple ui blocks if present
|
|
575
579
|
if isinstance(e.ui_extra, model.MultiUIExtra) and e.ui_extra.items:
|
klaude_code/ui/rich/markdown.py
CHANGED
|
@@ -54,11 +54,11 @@ class Divider(MarkdownElement):
|
|
|
54
54
|
yield Rule(style=style, characters="-")
|
|
55
55
|
|
|
56
56
|
|
|
57
|
-
class
|
|
57
|
+
class MarkdownTable(TableElement):
|
|
58
58
|
"""A table element with MINIMAL_HEAVY_HEAD box style."""
|
|
59
59
|
|
|
60
60
|
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
61
|
-
table = Table(box=box.MARKDOWN)
|
|
61
|
+
table = Table(box=box.MARKDOWN, border_style=console.get_style("markdown.table.border"))
|
|
62
62
|
|
|
63
63
|
if self.header is not None and self.header.row is not None:
|
|
64
64
|
for column in self.header.row.cells:
|
|
@@ -97,7 +97,7 @@ class NoInsetMarkdown(Markdown):
|
|
|
97
97
|
"code_block": NoInsetCodeBlock,
|
|
98
98
|
"heading_open": LeftHeading,
|
|
99
99
|
"hr": Divider,
|
|
100
|
-
"table_open":
|
|
100
|
+
"table_open": MarkdownTable,
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
|
|
@@ -110,7 +110,7 @@ class ThinkingMarkdown(Markdown):
|
|
|
110
110
|
"code_block": ThinkingCodeBlock,
|
|
111
111
|
"heading_open": LeftHeading,
|
|
112
112
|
"hr": Divider,
|
|
113
|
-
"table_open":
|
|
113
|
+
"table_open": MarkdownTable,
|
|
114
114
|
}
|
|
115
115
|
|
|
116
116
|
|
|
@@ -192,7 +192,7 @@ class MarkdownStream:
|
|
|
192
192
|
|
|
193
193
|
try:
|
|
194
194
|
tokens = self._parser.parse(text)
|
|
195
|
-
except Exception:
|
|
195
|
+
except Exception: # markdown-it-py may raise various internal errors during parsing
|
|
196
196
|
return 0
|
|
197
197
|
|
|
198
198
|
top_level: list[Token] = [token for token in tokens if token.level == 0 and token.map is not None]
|
klaude_code/ui/rich/theme.py
CHANGED
|
@@ -130,6 +130,7 @@ class ThemeKey(str, Enum):
|
|
|
130
130
|
STATUS_SPINNER = "spinner.status"
|
|
131
131
|
STATUS_TEXT = "spinner.status.text"
|
|
132
132
|
STATUS_TEXT_BOLD = "spinner.status.text.bold"
|
|
133
|
+
STATUS_TEXT_BOLD_ITALIC = "spinner.status.text.bold_italic"
|
|
133
134
|
# STATUS
|
|
134
135
|
STATUS_HINT = "status.hint"
|
|
135
136
|
# USER_INPUT
|
|
@@ -222,7 +223,7 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
222
223
|
ThemeKey.ERROR_BOLD.value: "bold " + palette.red,
|
|
223
224
|
ThemeKey.INTERRUPT.value: "reverse bold " + palette.red,
|
|
224
225
|
# USER_INPUT
|
|
225
|
-
ThemeKey.USER_INPUT.value:
|
|
226
|
+
ThemeKey.USER_INPUT.value: palette.magenta,
|
|
226
227
|
ThemeKey.USER_INPUT_PROMPT.value: "bold " + palette.magenta,
|
|
227
228
|
ThemeKey.USER_INPUT_AT_PATTERN.value: palette.purple,
|
|
228
229
|
ThemeKey.USER_INPUT_SLASH_COMMAND.value: "bold reverse " + palette.blue,
|
|
@@ -234,7 +235,8 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
234
235
|
# STATUS
|
|
235
236
|
ThemeKey.STATUS_SPINNER.value: palette.blue,
|
|
236
237
|
ThemeKey.STATUS_TEXT.value: palette.blue,
|
|
237
|
-
ThemeKey.STATUS_TEXT_BOLD.value: "bold
|
|
238
|
+
ThemeKey.STATUS_TEXT_BOLD.value: "bold " + palette.blue,
|
|
239
|
+
ThemeKey.STATUS_TEXT_BOLD_ITALIC.value: "bold italic " + palette.blue,
|
|
238
240
|
ThemeKey.STATUS_HINT.value: palette.grey2,
|
|
239
241
|
# REMINDER
|
|
240
242
|
ThemeKey.REMINDER.value: palette.grey1,
|
|
@@ -276,7 +278,7 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
276
278
|
ThemeKey.RESUME_FLAG.value: "bold reverse " + palette.green,
|
|
277
279
|
ThemeKey.RESUME_INFO.value: palette.green,
|
|
278
280
|
# CONFIGURATION DISPLAY
|
|
279
|
-
ThemeKey.CONFIG_TABLE_HEADER.value: palette.
|
|
281
|
+
ThemeKey.CONFIG_TABLE_HEADER.value: palette.grey1,
|
|
280
282
|
ThemeKey.CONFIG_STATUS_OK.value: palette.green,
|
|
281
283
|
ThemeKey.CONFIG_STATUS_PRIMARY.value: palette.yellow,
|
|
282
284
|
ThemeKey.CONFIG_STATUS_ERROR.value: palette.red,
|
|
@@ -299,6 +301,7 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
299
301
|
"markdown.item.number": palette.grey2,
|
|
300
302
|
"markdown.link": "underline " + palette.blue,
|
|
301
303
|
"markdown.link_url": "underline " + palette.blue,
|
|
304
|
+
"markdown.table.border": palette.grey2,
|
|
302
305
|
}
|
|
303
306
|
),
|
|
304
307
|
thinking_markdown_theme=Theme(
|
|
@@ -319,6 +322,7 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
319
322
|
"markdown.link": "underline " + palette.blue,
|
|
320
323
|
"markdown.link_url": "underline " + palette.blue,
|
|
321
324
|
"markdown.strong": "bold italic " + palette.grey1,
|
|
325
|
+
"markdown.table.border": palette.grey2,
|
|
322
326
|
}
|
|
323
327
|
),
|
|
324
328
|
code_theme=palette.code_theme,
|
klaude_code/ui/terminal/color.py
CHANGED
|
@@ -182,7 +182,7 @@ def _parse_osc_color_response(data: bytes) -> tuple[int, int, int] | None:
|
|
|
182
182
|
|
|
183
183
|
try:
|
|
184
184
|
text = data.decode("ascii", errors="ignore")
|
|
185
|
-
except
|
|
185
|
+
except LookupError: # encoding lookup failure (should not happen with "ascii")
|
|
186
186
|
return None
|
|
187
187
|
|
|
188
188
|
match = _OSC_BG_REGEX.search(text)
|
|
@@ -48,7 +48,7 @@ def start_esc_interrupt_monitor(
|
|
|
48
48
|
try:
|
|
49
49
|
fd = sys.stdin.fileno()
|
|
50
50
|
old = termios.tcgetattr(fd)
|
|
51
|
-
except
|
|
51
|
+
except OSError as exc: # pragma: no cover - environment dependent
|
|
52
52
|
log((f"esc monitor init error: {exc}", "r red"))
|
|
53
53
|
return
|
|
54
54
|
|
|
@@ -60,7 +60,7 @@ def start_esc_interrupt_monitor(
|
|
|
60
60
|
continue
|
|
61
61
|
try:
|
|
62
62
|
ch = os.read(fd, 1).decode(errors="ignore")
|
|
63
|
-
except
|
|
63
|
+
except OSError:
|
|
64
64
|
continue
|
|
65
65
|
if ch != "\x1b":
|
|
66
66
|
continue
|
|
@@ -71,7 +71,7 @@ def start_esc_interrupt_monitor(
|
|
|
71
71
|
while r2:
|
|
72
72
|
try:
|
|
73
73
|
seq += os.read(fd, 1).decode(errors="ignore")
|
|
74
|
-
except
|
|
74
|
+
except OSError:
|
|
75
75
|
break
|
|
76
76
|
r2, _, _ = select.select([sys.stdin], [], [], 0.0)
|
|
77
77
|
|
|
@@ -127,7 +127,7 @@ def install_sigint_double_press_exit(
|
|
|
127
127
|
|
|
128
128
|
try:
|
|
129
129
|
signal.signal(signal.SIGINT, _handler)
|
|
130
|
-
except
|
|
130
|
+
except (OSError, ValueError): # pragma: no cover - platform dependent
|
|
131
131
|
# If installing the handler fails, restore() will be a no-op.
|
|
132
132
|
return lambda: None
|
|
133
133
|
|