klaude-code 2.10.2__py3-none-any.whl → 2.10.4__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/AGENTS.md +4 -24
- klaude_code/auth/__init__.py +1 -17
- klaude_code/cli/auth_cmd.py +3 -53
- klaude_code/cli/list_model.py +0 -50
- klaude_code/config/assets/builtin_config.yaml +7 -35
- klaude_code/config/config.py +5 -42
- klaude_code/const.py +5 -2
- klaude_code/core/agent_profile.py +2 -10
- klaude_code/core/backtrack/__init__.py +3 -0
- klaude_code/core/backtrack/manager.py +48 -0
- klaude_code/core/memory.py +25 -9
- klaude_code/core/task.py +53 -7
- klaude_code/core/tool/__init__.py +2 -0
- klaude_code/core/tool/backtrack/__init__.py +3 -0
- klaude_code/core/tool/backtrack/backtrack_tool.md +17 -0
- klaude_code/core/tool/backtrack/backtrack_tool.py +65 -0
- klaude_code/core/tool/context.py +5 -0
- klaude_code/core/turn.py +3 -0
- klaude_code/llm/anthropic/input.py +28 -4
- klaude_code/llm/input_common.py +70 -1
- klaude_code/llm/openai_compatible/input.py +5 -2
- klaude_code/llm/openrouter/input.py +5 -2
- klaude_code/llm/registry.py +0 -1
- klaude_code/protocol/events.py +10 -0
- klaude_code/protocol/llm_param.py +0 -1
- klaude_code/protocol/message.py +10 -1
- klaude_code/protocol/tools.py +1 -0
- klaude_code/session/session.py +111 -2
- klaude_code/session/store.py +2 -0
- klaude_code/skill/assets/executing-plans/SKILL.md +84 -0
- klaude_code/skill/assets/writing-plans/SKILL.md +116 -0
- klaude_code/tui/commands.py +15 -0
- klaude_code/tui/components/developer.py +1 -1
- klaude_code/tui/components/errors.py +2 -4
- klaude_code/tui/components/metadata.py +5 -10
- klaude_code/tui/components/rich/markdown.py +5 -1
- klaude_code/tui/components/rich/status.py +7 -76
- klaude_code/tui/components/rich/theme.py +12 -2
- klaude_code/tui/components/tools.py +31 -18
- klaude_code/tui/components/user_input.py +1 -1
- klaude_code/tui/display.py +4 -0
- klaude_code/tui/input/completers.py +51 -17
- klaude_code/tui/input/images.py +127 -0
- klaude_code/tui/input/prompt_toolkit.py +16 -2
- klaude_code/tui/machine.py +26 -8
- klaude_code/tui/renderer.py +97 -0
- klaude_code/tui/runner.py +7 -2
- klaude_code/tui/terminal/image.py +28 -12
- klaude_code/ui/terminal/title.py +8 -3
- {klaude_code-2.10.2.dist-info → klaude_code-2.10.4.dist-info}/METADATA +1 -1
- {klaude_code-2.10.2.dist-info → klaude_code-2.10.4.dist-info}/RECORD +53 -56
- klaude_code/auth/antigravity/__init__.py +0 -20
- klaude_code/auth/antigravity/exceptions.py +0 -17
- klaude_code/auth/antigravity/oauth.py +0 -315
- klaude_code/auth/antigravity/pkce.py +0 -25
- klaude_code/auth/antigravity/token_manager.py +0 -27
- klaude_code/core/prompts/prompt-antigravity.md +0 -80
- klaude_code/llm/antigravity/__init__.py +0 -3
- klaude_code/llm/antigravity/client.py +0 -558
- klaude_code/llm/antigravity/input.py +0 -268
- klaude_code/skill/assets/create-plan/SKILL.md +0 -74
- {klaude_code-2.10.2.dist-info → klaude_code-2.10.4.dist-info}/WHEEL +0 -0
- {klaude_code-2.10.2.dist-info → klaude_code-2.10.4.dist-info}/entry_points.txt +0 -0
|
@@ -10,7 +10,6 @@ from rich.style import Style
|
|
|
10
10
|
from rich.text import Text
|
|
11
11
|
|
|
12
12
|
from klaude_code.const import (
|
|
13
|
-
BASH_OUTPUT_PANEL_THRESHOLD,
|
|
14
13
|
DIFF_PREFIX_WIDTH,
|
|
15
14
|
INVALID_TOOL_CALL_MAX_LENGTH,
|
|
16
15
|
QUERY_DISPLAY_TRUNCATE_LENGTH,
|
|
@@ -24,7 +23,6 @@ from klaude_code.tui.components import diffs as r_diffs
|
|
|
24
23
|
from klaude_code.tui.components import mermaid_viewer as r_mermaid_viewer
|
|
25
24
|
from klaude_code.tui.components.bash_syntax import highlight_bash_command
|
|
26
25
|
from klaude_code.tui.components.common import create_grid, truncate_middle
|
|
27
|
-
from klaude_code.tui.components.rich.code_panel import CodePanel
|
|
28
26
|
from klaude_code.tui.components.rich.markdown import NoInsetMarkdown
|
|
29
27
|
from klaude_code.tui.components.rich.quote import TreeQuote
|
|
30
28
|
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
@@ -40,6 +38,7 @@ MARK_MERMAID = "⧉"
|
|
|
40
38
|
MARK_WEB_FETCH = "→"
|
|
41
39
|
MARK_WEB_SEARCH = "✱"
|
|
42
40
|
MARK_DONE = "✔"
|
|
41
|
+
MARK_BACKTRACK = "↶"
|
|
43
42
|
|
|
44
43
|
# Todo status markers
|
|
45
44
|
MARK_TODO_PENDING = "▢"
|
|
@@ -168,22 +167,6 @@ def render_bash_tool_call(arguments: str) -> RenderableType:
|
|
|
168
167
|
cmd_str = command.strip()
|
|
169
168
|
highlighted = highlight_bash_command(cmd_str)
|
|
170
169
|
|
|
171
|
-
display_line_count = len(highlighted.plain.splitlines())
|
|
172
|
-
|
|
173
|
-
if display_line_count > BASH_OUTPUT_PANEL_THRESHOLD:
|
|
174
|
-
code_panel = CodePanel(highlighted, border_style=ThemeKey.LINES)
|
|
175
|
-
if isinstance(timeout_ms, int):
|
|
176
|
-
if timeout_ms >= 1000 and timeout_ms % 1000 == 0:
|
|
177
|
-
timeout_text = Text(f"{timeout_ms // 1000}s", style=ThemeKey.TOOL_TIMEOUT)
|
|
178
|
-
else:
|
|
179
|
-
timeout_text = Text(f"{timeout_ms}ms", style=ThemeKey.TOOL_TIMEOUT)
|
|
180
|
-
return _render_tool_call_tree(
|
|
181
|
-
mark=MARK_BASH,
|
|
182
|
-
tool_name=tool_name,
|
|
183
|
-
details=Group(code_panel, timeout_text),
|
|
184
|
-
)
|
|
185
|
-
else:
|
|
186
|
-
return _render_tool_call_tree(mark=MARK_BASH, tool_name=tool_name, details=code_panel)
|
|
187
170
|
if isinstance(timeout_ms, int):
|
|
188
171
|
if timeout_ms >= 1000 and timeout_ms % 1000 == 0:
|
|
189
172
|
highlighted.append(f" {timeout_ms // 1000}s", style=ThemeKey.TOOL_TIMEOUT)
|
|
@@ -552,6 +535,33 @@ def render_report_back_tool_call() -> RenderableType:
|
|
|
552
535
|
return _render_tool_call_tree(mark=MARK_DONE, tool_name="Report Back", details=None)
|
|
553
536
|
|
|
554
537
|
|
|
538
|
+
def render_backtrack_tool_call(arguments: str) -> RenderableType:
|
|
539
|
+
tool_name = "Backtrack"
|
|
540
|
+
|
|
541
|
+
try:
|
|
542
|
+
payload: dict[str, Any] = json.loads(arguments)
|
|
543
|
+
except json.JSONDecodeError:
|
|
544
|
+
details = Text(
|
|
545
|
+
arguments.strip()[:INVALID_TOOL_CALL_MAX_LENGTH],
|
|
546
|
+
style=ThemeKey.INVALID_TOOL_CALL_ARGS,
|
|
547
|
+
)
|
|
548
|
+
return _render_tool_call_tree(mark=MARK_BACKTRACK, tool_name=tool_name, details=details)
|
|
549
|
+
|
|
550
|
+
checkpoint_id = payload.get("checkpoint_id")
|
|
551
|
+
rationale = payload.get("rationale", "")
|
|
552
|
+
|
|
553
|
+
summary = Text("", ThemeKey.TOOL_PARAM)
|
|
554
|
+
if isinstance(checkpoint_id, int):
|
|
555
|
+
summary.append(f"Checkpoint {checkpoint_id}", ThemeKey.TOOL_PARAM_BOLD)
|
|
556
|
+
if rationale:
|
|
557
|
+
rationale_preview = rationale if len(rationale) <= 50 else rationale[:47] + "..."
|
|
558
|
+
if summary.plain:
|
|
559
|
+
summary.append(" - ")
|
|
560
|
+
summary.append(rationale_preview, ThemeKey.TOOL_PARAM)
|
|
561
|
+
|
|
562
|
+
return _render_tool_call_tree(mark=MARK_BACKTRACK, tool_name=tool_name, details=summary if summary.plain else None)
|
|
563
|
+
|
|
564
|
+
|
|
555
565
|
# Tool name to active form mapping (for spinner status)
|
|
556
566
|
_TOOL_ACTIVE_FORM: dict[str, str] = {
|
|
557
567
|
tools.BASH: "Bashing",
|
|
@@ -567,6 +577,7 @@ _TOOL_ACTIVE_FORM: dict[str, str] = {
|
|
|
567
577
|
tools.REPORT_BACK: "Reporting",
|
|
568
578
|
tools.IMAGE_GEN: "Generating Image",
|
|
569
579
|
tools.TASK: "Spawning Task",
|
|
580
|
+
tools.BACKTRACK: "Backtracking",
|
|
570
581
|
}
|
|
571
582
|
|
|
572
583
|
|
|
@@ -609,6 +620,8 @@ def render_tool_call(e: events.ToolCallEvent) -> RenderableType | None:
|
|
|
609
620
|
return render_mermaid_tool_call(e.arguments)
|
|
610
621
|
case tools.REPORT_BACK:
|
|
611
622
|
return render_report_back_tool_call()
|
|
623
|
+
case tools.BACKTRACK:
|
|
624
|
+
return render_backtrack_tool_call(e.arguments)
|
|
612
625
|
case tools.WEB_FETCH:
|
|
613
626
|
return render_web_fetch_tool_call(e.arguments)
|
|
614
627
|
case tools.WEB_SEARCH:
|
klaude_code/tui/display.py
CHANGED
|
@@ -101,3 +101,7 @@ class TUIDisplay(DisplayABC):
|
|
|
101
101
|
self._renderer.spinner_stop()
|
|
102
102
|
with contextlib.suppress(Exception):
|
|
103
103
|
self._renderer.stop_bottom_live()
|
|
104
|
+
|
|
105
|
+
def set_model_name(self, model_name: str | None) -> None:
|
|
106
|
+
"""Set model name for terminal title updates."""
|
|
107
|
+
self._machine.set_model_name(model_name)
|
|
@@ -673,34 +673,68 @@ class _AtFilesCompleter(Completer):
|
|
|
673
673
|
all_files_lower = self._git_file_list_lower or []
|
|
674
674
|
kn = keyword_norm
|
|
675
675
|
|
|
676
|
-
# Bound per-keystroke work
|
|
676
|
+
# Bound per-keystroke work.
|
|
677
|
+
#
|
|
678
|
+
# Important: When the keyword is common (e.g. "tools"), truncating the
|
|
679
|
+
# scan purely by number of matching *files* can accidentally hide valid
|
|
680
|
+
# directory completions that appear later in the git path order.
|
|
681
|
+
#
|
|
682
|
+
# Example: multiple */tools/ directories under different parents.
|
|
683
|
+
file_quota = max_results
|
|
684
|
+
dir_quota = max_results
|
|
685
|
+
scan_cap = max(2000, max_results * 200)
|
|
686
|
+
|
|
687
|
+
keyword_stripped = keyword_norm.strip("/")
|
|
688
|
+
keyword_basename = os.path.basename(keyword_stripped)
|
|
689
|
+
explicit_parent = "/" in keyword_stripped
|
|
690
|
+
|
|
691
|
+
def dir_matches_keyword(dir_path: str) -> bool:
|
|
692
|
+
if not keyword_basename:
|
|
693
|
+
return False
|
|
694
|
+
if explicit_parent:
|
|
695
|
+
# When user typed an explicit parent segment, match against the
|
|
696
|
+
# whole directory path (not just basename).
|
|
697
|
+
return kn in f"{dir_path}/".lower()
|
|
698
|
+
# Otherwise prioritize directories by basename match.
|
|
699
|
+
return keyword_basename in os.path.basename(dir_path).lower()
|
|
700
|
+
|
|
677
701
|
matching_files: list[str] = []
|
|
702
|
+
dir_list: list[str] = []
|
|
703
|
+
dir_seen: set[str] = set()
|
|
678
704
|
scan_truncated = False
|
|
705
|
+
scanned = 0
|
|
706
|
+
|
|
679
707
|
for p, pl in zip(all_files, all_files_lower, strict=False):
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
if
|
|
708
|
+
scanned += 1
|
|
709
|
+
if kn not in pl:
|
|
710
|
+
if scanned >= scan_cap and (matching_files or dir_list):
|
|
683
711
|
scan_truncated = True
|
|
684
712
|
break
|
|
713
|
+
continue
|
|
714
|
+
|
|
715
|
+
if len(matching_files) < file_quota:
|
|
716
|
+
matching_files.append(p)
|
|
685
717
|
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
for p in matching_files[: max_results * 3]:
|
|
718
|
+
# Collect matching parent directories, walking upwards until repo root.
|
|
719
|
+
# This allows completing into directories like "image/tools/" even
|
|
720
|
+
# when the matching file is nested deeper.
|
|
690
721
|
parent = os.path.dirname(p)
|
|
691
722
|
while parent and parent != ".":
|
|
692
|
-
|
|
723
|
+
if dir_matches_keyword(parent):
|
|
724
|
+
cand = f"{parent}/"
|
|
725
|
+
if cand not in dir_seen:
|
|
726
|
+
dir_seen.add(cand)
|
|
727
|
+
dir_list.append(cand)
|
|
728
|
+
if len(dir_list) >= dir_quota:
|
|
729
|
+
break
|
|
693
730
|
parent = os.path.dirname(parent)
|
|
694
731
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
dir_list = dir_list[:max_results]
|
|
699
|
-
dir_truncated = True
|
|
732
|
+
if len(matching_files) >= file_quota and len(dir_list) >= dir_quota:
|
|
733
|
+
scan_truncated = True
|
|
734
|
+
break
|
|
700
735
|
|
|
701
|
-
candidates =
|
|
702
|
-
|
|
703
|
-
return candidates, truncated
|
|
736
|
+
candidates = dir_list + matching_files
|
|
737
|
+
return candidates, scan_truncated
|
|
704
738
|
|
|
705
739
|
def _get_git_repo_root(self, cwd: Path) -> Path | None:
|
|
706
740
|
if not self._has_cmd("git"):
|
klaude_code/tui/input/images.py
CHANGED
|
@@ -28,6 +28,9 @@ from klaude_code.protocol.message import ImageFilePart
|
|
|
28
28
|
|
|
29
29
|
IMAGE_SUFFIXES = frozenset({".png", ".jpg", ".jpeg", ".gif", ".webp"})
|
|
30
30
|
|
|
31
|
+
# Claude API limit is 5MB, we use 4.5MB to have some margin
|
|
32
|
+
MAX_IMAGE_SIZE_BYTES = 4_500_000
|
|
33
|
+
|
|
31
34
|
IMAGE_MARKER_RE = re.compile(r'\[image (?P<path>"[^"]+"|[^\]]+)\]')
|
|
32
35
|
|
|
33
36
|
|
|
@@ -159,6 +162,127 @@ def _grab_clipboard_image(dest_path: Path) -> bool:
|
|
|
159
162
|
return _grab_clipboard_image_linux(dest_path)
|
|
160
163
|
|
|
161
164
|
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
# Image resizing for size limits
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _get_image_dimensions_macos(path: Path) -> tuple[int, int] | None:
|
|
171
|
+
"""Get image dimensions using sips on macOS."""
|
|
172
|
+
try:
|
|
173
|
+
result = subprocess.run(
|
|
174
|
+
["sips", "-g", "pixelWidth", "-g", "pixelHeight", str(path)],
|
|
175
|
+
capture_output=True,
|
|
176
|
+
text=True,
|
|
177
|
+
)
|
|
178
|
+
if result.returncode != 0:
|
|
179
|
+
return None
|
|
180
|
+
width = height = 0
|
|
181
|
+
for line in result.stdout.splitlines():
|
|
182
|
+
if "pixelWidth" in line:
|
|
183
|
+
width = int(line.split(":")[-1].strip())
|
|
184
|
+
elif "pixelHeight" in line:
|
|
185
|
+
height = int(line.split(":")[-1].strip())
|
|
186
|
+
if width > 0 and height > 0:
|
|
187
|
+
return (width, height)
|
|
188
|
+
except (OSError, ValueError):
|
|
189
|
+
pass
|
|
190
|
+
return None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _resize_image_macos(path: Path, scale: float) -> bool:
|
|
194
|
+
"""Resize image using sips on macOS. Modifies file in place."""
|
|
195
|
+
dims = _get_image_dimensions_macos(path)
|
|
196
|
+
if dims is None:
|
|
197
|
+
return False
|
|
198
|
+
new_width = max(1, int(dims[0] * scale))
|
|
199
|
+
try:
|
|
200
|
+
result = subprocess.run(
|
|
201
|
+
["sips", "--resampleWidth", str(new_width), str(path)],
|
|
202
|
+
capture_output=True,
|
|
203
|
+
)
|
|
204
|
+
return result.returncode == 0
|
|
205
|
+
except OSError:
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _resize_image_linux(path: Path, scale: float) -> bool:
|
|
210
|
+
"""Resize image using ImageMagick convert on Linux."""
|
|
211
|
+
if not shutil.which("convert"):
|
|
212
|
+
return False
|
|
213
|
+
percent = int(scale * 100)
|
|
214
|
+
try:
|
|
215
|
+
result = subprocess.run(
|
|
216
|
+
["convert", str(path), "-resize", f"{percent}%", str(path)],
|
|
217
|
+
capture_output=True,
|
|
218
|
+
)
|
|
219
|
+
return result.returncode == 0
|
|
220
|
+
except OSError:
|
|
221
|
+
return False
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _resize_image_windows(path: Path, scale: float) -> bool:
|
|
225
|
+
"""Resize image using PowerShell on Windows."""
|
|
226
|
+
script = f'''
|
|
227
|
+
Add-Type -AssemblyName System.Drawing
|
|
228
|
+
$img = [System.Drawing.Image]::FromFile("{path}")
|
|
229
|
+
$newWidth = [int]($img.Width * {scale})
|
|
230
|
+
$newHeight = [int]($img.Height * {scale})
|
|
231
|
+
$bmp = New-Object System.Drawing.Bitmap($newWidth, $newHeight)
|
|
232
|
+
$graphics = [System.Drawing.Graphics]::FromImage($bmp)
|
|
233
|
+
$graphics.DrawImage($img, 0, 0, $newWidth, $newHeight)
|
|
234
|
+
$img.Dispose()
|
|
235
|
+
$bmp.Save("{path}", [System.Drawing.Imaging.ImageFormat]::Png)
|
|
236
|
+
$bmp.Dispose()
|
|
237
|
+
$graphics.Dispose()
|
|
238
|
+
Write-Output "ok"
|
|
239
|
+
'''
|
|
240
|
+
try:
|
|
241
|
+
result = subprocess.run(
|
|
242
|
+
["powershell", "-Command", script],
|
|
243
|
+
capture_output=True,
|
|
244
|
+
text=True,
|
|
245
|
+
)
|
|
246
|
+
return result.returncode == 0 and "ok" in result.stdout
|
|
247
|
+
except OSError:
|
|
248
|
+
return False
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _resize_image(path: Path, scale: float) -> bool:
|
|
252
|
+
"""Resize image by scale factor. Modifies file in place."""
|
|
253
|
+
if sys.platform == "darwin":
|
|
254
|
+
return _resize_image_macos(path, scale)
|
|
255
|
+
elif sys.platform == "win32":
|
|
256
|
+
return _resize_image_windows(path, scale)
|
|
257
|
+
else:
|
|
258
|
+
return _resize_image_linux(path, scale)
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _ensure_image_size_limit(path: Path, max_bytes: int = MAX_IMAGE_SIZE_BYTES) -> None:
|
|
262
|
+
"""Resize image if it exceeds the size limit. Modifies file in place."""
|
|
263
|
+
try:
|
|
264
|
+
current_size = path.stat().st_size
|
|
265
|
+
if current_size <= max_bytes:
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
# Calculate scale factor based on size ratio
|
|
269
|
+
# We use sqrt because area scales with square of linear dimensions
|
|
270
|
+
scale = (max_bytes / current_size) ** 0.5
|
|
271
|
+
# Be a bit more aggressive to ensure we get under the limit
|
|
272
|
+
scale *= 0.9
|
|
273
|
+
|
|
274
|
+
for _ in range(5): # Max 5 resize attempts
|
|
275
|
+
if not _resize_image(path, scale):
|
|
276
|
+
return
|
|
277
|
+
new_size = path.stat().st_size
|
|
278
|
+
if new_size <= max_bytes:
|
|
279
|
+
return
|
|
280
|
+
# Still too large, reduce more
|
|
281
|
+
scale = 0.8
|
|
282
|
+
except OSError:
|
|
283
|
+
pass
|
|
284
|
+
|
|
285
|
+
|
|
162
286
|
def capture_clipboard_tag() -> str | None:
|
|
163
287
|
"""Capture an image from clipboard and return an [image ...] marker."""
|
|
164
288
|
|
|
@@ -174,6 +298,9 @@ def capture_clipboard_tag() -> str | None:
|
|
|
174
298
|
if not _grab_clipboard_image(path):
|
|
175
299
|
return None
|
|
176
300
|
|
|
301
|
+
# Resize if image exceeds size limit
|
|
302
|
+
_ensure_image_size_limit(path)
|
|
303
|
+
|
|
177
304
|
return format_image_marker(str(path))
|
|
178
305
|
|
|
179
306
|
|
|
@@ -61,7 +61,7 @@ COMPLETION_SELECTED_DARK_BG = "ansigreen"
|
|
|
61
61
|
COMPLETION_SELECTED_LIGHT_BG = "ansigreen"
|
|
62
62
|
COMPLETION_SELECTED_UNKNOWN_BG = "ansigreen"
|
|
63
63
|
COMPLETION_MENU = "ansibrightblack"
|
|
64
|
-
INPUT_PROMPT_STYLE = "
|
|
64
|
+
INPUT_PROMPT_STYLE = "ansicyan bold"
|
|
65
65
|
INPUT_PROMPT_BASH_STYLE = "ansigreen bold"
|
|
66
66
|
PLACEHOLDER_TEXT_STYLE_DARK_BG = "fg:#5a5a5a"
|
|
67
67
|
PLACEHOLDER_TEXT_STYLE_LIGHT_BG = "fg:#7a7a7a"
|
|
@@ -448,6 +448,9 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
448
448
|
# Keep a comfortable multiline editing area even when no completion
|
|
449
449
|
# space is reserved. (We set reserve_space_for_menu=0 to avoid the
|
|
450
450
|
# bottom toolbar jumping when completions open/close.)
|
|
451
|
+
#
|
|
452
|
+
# Also allow the input area to grow with content so that large multi-line
|
|
453
|
+
# inputs expand the prompt instead of scrolling within a fixed-height window.
|
|
451
454
|
base_rows = 10
|
|
452
455
|
|
|
453
456
|
def _height(): # type: ignore[no-untyped-def]
|
|
@@ -465,7 +468,18 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
465
468
|
elif isinstance(original_height_value, int):
|
|
466
469
|
original_min = int(original_height_value)
|
|
467
470
|
|
|
468
|
-
|
|
471
|
+
try:
|
|
472
|
+
buffer_line_count = int(self._session.default_buffer.document.line_count)
|
|
473
|
+
except Exception:
|
|
474
|
+
buffer_line_count = 1
|
|
475
|
+
|
|
476
|
+
# Grow with content (based on newline count), but keep a sensible minimum.
|
|
477
|
+
content_rows = max(1, buffer_line_count)
|
|
478
|
+
target_rows = max(base_rows, content_rows)
|
|
479
|
+
|
|
480
|
+
# When a picker overlay is open, keep enough height for it to be usable.
|
|
481
|
+
if picker_open:
|
|
482
|
+
target_rows = max(target_rows, 24)
|
|
469
483
|
|
|
470
484
|
# Cap to the current terminal size.
|
|
471
485
|
# Leave a small buffer to avoid triggering "Window too small".
|
klaude_code/tui/machine.py
CHANGED
|
@@ -24,6 +24,7 @@ from klaude_code.tui.commands import (
|
|
|
24
24
|
EndThinkingStream,
|
|
25
25
|
PrintBlankLine,
|
|
26
26
|
RenderAssistantImage,
|
|
27
|
+
RenderBacktrack,
|
|
27
28
|
RenderBashCommandEnd,
|
|
28
29
|
RenderBashCommandStart,
|
|
29
30
|
RenderCommand,
|
|
@@ -47,6 +48,7 @@ from klaude_code.tui.commands import (
|
|
|
47
48
|
StartThinkingStream,
|
|
48
49
|
TaskClockClear,
|
|
49
50
|
TaskClockStart,
|
|
51
|
+
UpdateTerminalTitlePrefix,
|
|
50
52
|
)
|
|
51
53
|
from klaude_code.tui.components.rich import status as r_status
|
|
52
54
|
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
@@ -65,6 +67,7 @@ FAST_TOOLS: frozenset[str] = frozenset(
|
|
|
65
67
|
tools.UPDATE_PLAN,
|
|
66
68
|
tools.APPLY_PATCH,
|
|
67
69
|
tools.REPORT_BACK,
|
|
70
|
+
tools.BACKTRACK,
|
|
68
71
|
}
|
|
69
72
|
)
|
|
70
73
|
|
|
@@ -135,7 +138,7 @@ class ActivityState:
|
|
|
135
138
|
for name, count in counts.items():
|
|
136
139
|
if not first:
|
|
137
140
|
activity_text.append(", ")
|
|
138
|
-
activity_text.append(Text(name, style=ThemeKey.
|
|
141
|
+
activity_text.append(Text(name, style=ThemeKey.STATUS_TEXT))
|
|
139
142
|
if count > 1:
|
|
140
143
|
activity_text.append(f" x {count}")
|
|
141
144
|
first = False
|
|
@@ -238,24 +241,21 @@ class SpinnerStatusState:
|
|
|
238
241
|
|
|
239
242
|
if extra_reasoning is not None:
|
|
240
243
|
if activity_text is None:
|
|
241
|
-
activity_text = Text(extra_reasoning, style=ThemeKey.
|
|
244
|
+
activity_text = Text(extra_reasoning, style=ThemeKey.STATUS_TEXT)
|
|
242
245
|
else:
|
|
243
|
-
prefixed = Text(extra_reasoning, style=ThemeKey.
|
|
246
|
+
prefixed = Text(extra_reasoning, style=ThemeKey.STATUS_TEXT)
|
|
244
247
|
prefixed.append(" , ")
|
|
245
248
|
prefixed.append_text(activity_text)
|
|
246
249
|
activity_text = prefixed
|
|
247
250
|
|
|
248
251
|
if base_status:
|
|
249
|
-
# Default "Thinking ..." uses normal style; custom headers use bold italic
|
|
250
|
-
is_default_reasoning = base_status in {STATUS_THINKING_TEXT, STATUS_RUNNING_TEXT}
|
|
251
|
-
status_style = ThemeKey.STATUS_TEXT if is_default_reasoning else ThemeKey.STATUS_TEXT_BOLD_ITALIC
|
|
252
252
|
if activity_text:
|
|
253
253
|
result = Text()
|
|
254
|
-
result.append(base_status, style=
|
|
254
|
+
result.append(base_status, style=ThemeKey.STATUS_TEXT)
|
|
255
255
|
result.append(" | ")
|
|
256
256
|
result.append_text(activity_text)
|
|
257
257
|
else:
|
|
258
|
-
result = Text(base_status, style=
|
|
258
|
+
result = Text(base_status, style=ThemeKey.STATUS_TEXT)
|
|
259
259
|
elif activity_text:
|
|
260
260
|
activity_text.append(" …")
|
|
261
261
|
result = activity_text
|
|
@@ -323,6 +323,10 @@ class DisplayStateMachine:
|
|
|
323
323
|
self._sessions: dict[str, _SessionState] = {}
|
|
324
324
|
self._primary_session_id: str | None = None
|
|
325
325
|
self._spinner = SpinnerStatusState()
|
|
326
|
+
self._model_name: str | None = None
|
|
327
|
+
|
|
328
|
+
def set_model_name(self, model_name: str | None) -> None:
|
|
329
|
+
self._model_name = model_name
|
|
326
330
|
|
|
327
331
|
def _reset_sessions(self) -> None:
|
|
328
332
|
self._sessions = {}
|
|
@@ -442,6 +446,7 @@ class DisplayStateMachine:
|
|
|
442
446
|
self._primary_session_id = e.session_id
|
|
443
447
|
if not is_replay:
|
|
444
448
|
cmds.append(TaskClockStart())
|
|
449
|
+
cmds.append(UpdateTerminalTitlePrefix(prefix="\u26ac", model_name=self._model_name))
|
|
445
450
|
|
|
446
451
|
if not is_replay:
|
|
447
452
|
cmds.append(SpinnerStart())
|
|
@@ -469,6 +474,18 @@ class DisplayStateMachine:
|
|
|
469
474
|
cmds.append(RenderCompactionSummary(summary=e.summary, kept_items_brief=kept_brief))
|
|
470
475
|
return cmds
|
|
471
476
|
|
|
477
|
+
case events.BacktrackEvent() as e:
|
|
478
|
+
cmds.append(
|
|
479
|
+
RenderBacktrack(
|
|
480
|
+
checkpoint_id=e.checkpoint_id,
|
|
481
|
+
note=e.note,
|
|
482
|
+
rationale=e.rationale,
|
|
483
|
+
original_user_message=e.original_user_message,
|
|
484
|
+
messages_discarded=e.messages_discarded,
|
|
485
|
+
)
|
|
486
|
+
)
|
|
487
|
+
return cmds
|
|
488
|
+
|
|
472
489
|
case events.DeveloperMessageEvent() as e:
|
|
473
490
|
cmds.append(RenderDeveloperMessage(e))
|
|
474
491
|
return cmds
|
|
@@ -747,6 +764,7 @@ class DisplayStateMachine:
|
|
|
747
764
|
self._spinner.reset()
|
|
748
765
|
cmds.append(SpinnerStop())
|
|
749
766
|
cmds.append(EmitTmuxSignal())
|
|
767
|
+
cmds.append(UpdateTerminalTitlePrefix(prefix="\u2714", model_name=self._model_name))
|
|
750
768
|
return cmds
|
|
751
769
|
|
|
752
770
|
case events.InterruptEvent() as e:
|
klaude_code/tui/renderer.py
CHANGED
|
@@ -35,6 +35,7 @@ from klaude_code.tui.commands import (
|
|
|
35
35
|
PrintBlankLine,
|
|
36
36
|
PrintRuleLine,
|
|
37
37
|
RenderAssistantImage,
|
|
38
|
+
RenderBacktrack,
|
|
38
39
|
RenderBashCommandEnd,
|
|
39
40
|
RenderBashCommandStart,
|
|
40
41
|
RenderCommand,
|
|
@@ -59,6 +60,7 @@ from klaude_code.tui.commands import (
|
|
|
59
60
|
StartThinkingStream,
|
|
60
61
|
TaskClockClear,
|
|
61
62
|
TaskClockStart,
|
|
63
|
+
UpdateTerminalTitlePrefix,
|
|
62
64
|
)
|
|
63
65
|
from klaude_code.tui.components import command_output as c_command_output
|
|
64
66
|
from klaude_code.tui.components import developer as c_developer
|
|
@@ -85,6 +87,7 @@ from klaude_code.tui.terminal.notifier import (
|
|
|
85
87
|
emit_tmux_signal,
|
|
86
88
|
)
|
|
87
89
|
from klaude_code.tui.terminal.progress_bar import OSC94States, emit_osc94
|
|
90
|
+
from klaude_code.ui.terminal.title import update_terminal_title
|
|
88
91
|
|
|
89
92
|
|
|
90
93
|
@dataclass
|
|
@@ -136,6 +139,7 @@ class _SessionStatus:
|
|
|
136
139
|
color: Style | None = None
|
|
137
140
|
color_index: int | None = None
|
|
138
141
|
sub_agent_state: model.SubAgentState | None = None
|
|
142
|
+
turn_first_dev_msg_rendered: bool = False
|
|
139
143
|
|
|
140
144
|
|
|
141
145
|
class TUICommandRenderer:
|
|
@@ -475,6 +479,10 @@ class TUICommandRenderer:
|
|
|
475
479
|
if not c_developer.need_render_developer_message(e):
|
|
476
480
|
return
|
|
477
481
|
with self.session_print_context(e.session_id):
|
|
482
|
+
session_status = self._sessions.get(e.session_id)
|
|
483
|
+
if session_status and not session_status.turn_first_dev_msg_rendered:
|
|
484
|
+
session_status.turn_first_dev_msg_rendered = True
|
|
485
|
+
self.print()
|
|
478
486
|
self.print(c_developer.render_developer_message(e))
|
|
479
487
|
|
|
480
488
|
# Display images from @ file references and user attachments
|
|
@@ -567,6 +575,8 @@ class TUICommandRenderer:
|
|
|
567
575
|
)
|
|
568
576
|
|
|
569
577
|
def display_turn_start(self, event: events.TurnStartEvent) -> None:
|
|
578
|
+
if event.session_id in self._sessions:
|
|
579
|
+
self._sessions[event.session_id].turn_first_dev_msg_rendered = False
|
|
570
580
|
if not self.is_sub_agent_session(event.session_id):
|
|
571
581
|
self.print()
|
|
572
582
|
|
|
@@ -610,6 +620,8 @@ class TUICommandRenderer:
|
|
|
610
620
|
sub_agent_color=self._current_sub_agent_color,
|
|
611
621
|
)
|
|
612
622
|
)
|
|
623
|
+
else:
|
|
624
|
+
self.print()
|
|
613
625
|
|
|
614
626
|
def display_interrupt(self) -> None:
|
|
615
627
|
self.print(c_user_input.render_interrupt())
|
|
@@ -678,6 +690,75 @@ class TUICommandRenderer:
|
|
|
678
690
|
|
|
679
691
|
self.print()
|
|
680
692
|
|
|
693
|
+
def display_backtrack(
|
|
694
|
+
self,
|
|
695
|
+
checkpoint_id: int,
|
|
696
|
+
note: str,
|
|
697
|
+
rationale: str,
|
|
698
|
+
original_user_message: str,
|
|
699
|
+
messages_discarded: int | None,
|
|
700
|
+
) -> None:
|
|
701
|
+
self.console.print(
|
|
702
|
+
Rule(
|
|
703
|
+
Text(f"Backtracked to Checkpoint {checkpoint_id}", style=ThemeKey.BACKTRACK),
|
|
704
|
+
characters="=",
|
|
705
|
+
style=ThemeKey.LINES,
|
|
706
|
+
)
|
|
707
|
+
)
|
|
708
|
+
self.print()
|
|
709
|
+
|
|
710
|
+
if messages_discarded:
|
|
711
|
+
self.console.print(Text(f" Discarded {messages_discarded} messages", style=ThemeKey.BACKTRACK_INFO))
|
|
712
|
+
|
|
713
|
+
if rationale:
|
|
714
|
+
self.console.print(Text(" Rationale:", style=ThemeKey.BACKTRACK_INFO))
|
|
715
|
+
rationale_preview = rationale[:300] + "..." if len(rationale) > 300 else rationale
|
|
716
|
+
self.console.print(
|
|
717
|
+
Padding(
|
|
718
|
+
Panel(
|
|
719
|
+
NoInsetMarkdown(
|
|
720
|
+
rationale_preview, code_theme=self.themes.code_theme, style=ThemeKey.BACKTRACK_NOTE
|
|
721
|
+
),
|
|
722
|
+
box=box.SIMPLE,
|
|
723
|
+
border_style=ThemeKey.LINES,
|
|
724
|
+
style=ThemeKey.BACKTRACK_NOTE,
|
|
725
|
+
),
|
|
726
|
+
(0, 0, 0, 4),
|
|
727
|
+
)
|
|
728
|
+
)
|
|
729
|
+
|
|
730
|
+
if original_user_message:
|
|
731
|
+
self.console.print(Text(" Returned to:", style=ThemeKey.BACKTRACK_INFO))
|
|
732
|
+
msg_preview = (
|
|
733
|
+
original_user_message[:200] + "..." if len(original_user_message) > 200 else original_user_message
|
|
734
|
+
)
|
|
735
|
+
self.console.print(
|
|
736
|
+
Padding(
|
|
737
|
+
Panel(
|
|
738
|
+
Text(msg_preview, style=ThemeKey.BACKTRACK_USER_MESSAGE),
|
|
739
|
+
box=box.SIMPLE,
|
|
740
|
+
border_style=ThemeKey.LINES,
|
|
741
|
+
),
|
|
742
|
+
(0, 0, 0, 4),
|
|
743
|
+
)
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
self.console.print(Text(" Summary:", style=ThemeKey.BACKTRACK_INFO))
|
|
747
|
+
note_preview = note[:300] + "..." if len(note) > 300 else note
|
|
748
|
+
self.console.print(
|
|
749
|
+
Padding(
|
|
750
|
+
Panel(
|
|
751
|
+
NoInsetMarkdown(note_preview, code_theme=self.themes.code_theme, style=ThemeKey.BACKTRACK_NOTE),
|
|
752
|
+
box=box.SIMPLE,
|
|
753
|
+
border_style=ThemeKey.LINES,
|
|
754
|
+
style=ThemeKey.BACKTRACK_NOTE,
|
|
755
|
+
),
|
|
756
|
+
(0, 0, 0, 4),
|
|
757
|
+
)
|
|
758
|
+
)
|
|
759
|
+
|
|
760
|
+
self.print()
|
|
761
|
+
|
|
681
762
|
# ---------------------------------------------------------------------
|
|
682
763
|
# Notifications
|
|
683
764
|
# ---------------------------------------------------------------------
|
|
@@ -797,6 +878,20 @@ class TUICommandRenderer:
|
|
|
797
878
|
self.display_error(event)
|
|
798
879
|
case RenderCompactionSummary(summary=summary, kept_items_brief=kept_items_brief):
|
|
799
880
|
self.display_compaction_summary(summary, kept_items_brief)
|
|
881
|
+
case RenderBacktrack(
|
|
882
|
+
checkpoint_id=checkpoint_id,
|
|
883
|
+
note=note,
|
|
884
|
+
rationale=rationale,
|
|
885
|
+
original_user_message=original_user_message,
|
|
886
|
+
messages_discarded=messages_discarded,
|
|
887
|
+
):
|
|
888
|
+
self.display_backtrack(
|
|
889
|
+
checkpoint_id=checkpoint_id,
|
|
890
|
+
note=note,
|
|
891
|
+
rationale=rationale,
|
|
892
|
+
original_user_message=original_user_message,
|
|
893
|
+
messages_discarded=messages_discarded,
|
|
894
|
+
)
|
|
800
895
|
case SpinnerStart():
|
|
801
896
|
self.spinner_start()
|
|
802
897
|
case SpinnerStop():
|
|
@@ -815,6 +910,8 @@ class TUICommandRenderer:
|
|
|
815
910
|
r_status.set_task_start()
|
|
816
911
|
case TaskClockClear():
|
|
817
912
|
r_status.clear_task_start()
|
|
913
|
+
case UpdateTerminalTitlePrefix(prefix=prefix, model_name=model_name):
|
|
914
|
+
update_terminal_title(model_name, prefix=prefix)
|
|
818
915
|
case _:
|
|
819
916
|
continue
|
|
820
917
|
|
klaude_code/tui/runner.py
CHANGED
|
@@ -142,14 +142,19 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
|
|
|
142
142
|
elif detected is False:
|
|
143
143
|
theme = "dark"
|
|
144
144
|
|
|
145
|
-
|
|
145
|
+
tui_display = TUIDisplay(theme=theme)
|
|
146
|
+
display: ui.DisplayABC = tui_display
|
|
146
147
|
if init_config.debug:
|
|
147
148
|
display = ui.DebugEventDisplay(display)
|
|
148
149
|
|
|
150
|
+
def _on_model_change(model_name: str) -> None:
|
|
151
|
+
update_terminal_title(model_name)
|
|
152
|
+
tui_display.set_model_name(model_name)
|
|
153
|
+
|
|
149
154
|
components = await initialize_app_components(
|
|
150
155
|
init_config=init_config,
|
|
151
156
|
display=display,
|
|
152
|
-
on_model_change=
|
|
157
|
+
on_model_change=_on_model_change,
|
|
153
158
|
)
|
|
154
159
|
|
|
155
160
|
def _status_provider() -> REPLStatusSnapshot:
|