klaude-code 2.10.3__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.
Files changed (56) hide show
  1. klaude_code/auth/AGENTS.md +4 -24
  2. klaude_code/auth/__init__.py +1 -17
  3. klaude_code/cli/auth_cmd.py +3 -53
  4. klaude_code/cli/list_model.py +0 -50
  5. klaude_code/config/assets/builtin_config.yaml +0 -28
  6. klaude_code/config/config.py +5 -42
  7. klaude_code/const.py +5 -2
  8. klaude_code/core/agent_profile.py +2 -10
  9. klaude_code/core/backtrack/__init__.py +3 -0
  10. klaude_code/core/backtrack/manager.py +48 -0
  11. klaude_code/core/memory.py +25 -9
  12. klaude_code/core/task.py +53 -7
  13. klaude_code/core/tool/__init__.py +2 -0
  14. klaude_code/core/tool/backtrack/__init__.py +3 -0
  15. klaude_code/core/tool/backtrack/backtrack_tool.md +17 -0
  16. klaude_code/core/tool/backtrack/backtrack_tool.py +65 -0
  17. klaude_code/core/tool/context.py +5 -0
  18. klaude_code/core/turn.py +3 -0
  19. klaude_code/llm/input_common.py +70 -1
  20. klaude_code/llm/openai_compatible/input.py +5 -2
  21. klaude_code/llm/openrouter/input.py +5 -2
  22. klaude_code/llm/registry.py +0 -1
  23. klaude_code/protocol/events.py +10 -0
  24. klaude_code/protocol/llm_param.py +0 -1
  25. klaude_code/protocol/message.py +10 -1
  26. klaude_code/protocol/tools.py +1 -0
  27. klaude_code/session/session.py +111 -2
  28. klaude_code/session/store.py +2 -0
  29. klaude_code/skill/assets/executing-plans/SKILL.md +84 -0
  30. klaude_code/skill/assets/writing-plans/SKILL.md +116 -0
  31. klaude_code/tui/commands.py +15 -0
  32. klaude_code/tui/components/developer.py +1 -1
  33. klaude_code/tui/components/rich/status.py +7 -76
  34. klaude_code/tui/components/rich/theme.py +10 -0
  35. klaude_code/tui/components/tools.py +31 -18
  36. klaude_code/tui/display.py +4 -0
  37. klaude_code/tui/input/prompt_toolkit.py +15 -1
  38. klaude_code/tui/machine.py +26 -8
  39. klaude_code/tui/renderer.py +97 -0
  40. klaude_code/tui/runner.py +7 -2
  41. klaude_code/tui/terminal/image.py +28 -12
  42. klaude_code/ui/terminal/title.py +8 -3
  43. {klaude_code-2.10.3.dist-info → klaude_code-2.10.4.dist-info}/METADATA +1 -1
  44. {klaude_code-2.10.3.dist-info → klaude_code-2.10.4.dist-info}/RECORD +46 -49
  45. klaude_code/auth/antigravity/__init__.py +0 -20
  46. klaude_code/auth/antigravity/exceptions.py +0 -17
  47. klaude_code/auth/antigravity/oauth.py +0 -315
  48. klaude_code/auth/antigravity/pkce.py +0 -25
  49. klaude_code/auth/antigravity/token_manager.py +0 -27
  50. klaude_code/core/prompts/prompt-antigravity.md +0 -80
  51. klaude_code/llm/antigravity/__init__.py +0 -3
  52. klaude_code/llm/antigravity/client.py +0 -558
  53. klaude_code/llm/antigravity/input.py +0 -268
  54. klaude_code/skill/assets/create-plan/SKILL.md +0 -74
  55. {klaude_code-2.10.3.dist-info → klaude_code-2.10.4.dist-info}/WHEEL +0 -0
  56. {klaude_code-2.10.3.dist-info → klaude_code-2.10.4.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,116 @@
1
+ ---
2
+ name: writing-plans
3
+ description: Use when you have a spec or requirements for a multi-step task, before touching code
4
+ ---
5
+
6
+ # Writing Plans
7
+
8
+ ## Overview
9
+
10
+ Write comprehensive implementation plans assuming the engineer has zero context for our codebase and questionable taste. Document everything they need to know: which files to touch for each task, code, testing, docs they might need to check, how to test it. Give them the whole plan as bite-sized tasks. DRY. YAGNI. TDD. Frequent commits.
11
+
12
+ Assume they are a skilled developer, but know almost nothing about our toolset or problem domain. Assume they don't know good test design very well.
13
+
14
+ **Announce at start:** "I'm using the writing-plans skill to create the implementation plan."
15
+
16
+ **Context:** This should be run in a dedicated worktree (created by brainstorming skill).
17
+
18
+ **Save plans to:** `docs/plans/YYYY-MM-DD-<feature-name>.md`
19
+
20
+ ## Bite-Sized Task Granularity
21
+
22
+ **Each step is one action (2-5 minutes):**
23
+ - "Write the failing test" - step
24
+ - "Run it to make sure it fails" - step
25
+ - "Implement the minimal code to make the test pass" - step
26
+ - "Run the tests and make sure they pass" - step
27
+ - "Commit" - step
28
+
29
+ ## Plan Document Header
30
+
31
+ **Every plan MUST start with this header:**
32
+
33
+ ```markdown
34
+ # [Feature Name] Implementation Plan
35
+
36
+ > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
37
+
38
+ **Goal:** [One sentence describing what this builds]
39
+
40
+ **Architecture:** [2-3 sentences about approach]
41
+
42
+ **Tech Stack:** [Key technologies/libraries]
43
+
44
+ ---
45
+ ```
46
+
47
+ ## Task Structure
48
+
49
+ ```markdown
50
+ ### Task N: [Component Name]
51
+
52
+ **Files:**
53
+ - Create: `exact/path/to/file.py`
54
+ - Modify: `exact/path/to/existing.py:123-145`
55
+ - Test: `tests/exact/path/to/test.py`
56
+
57
+ **Step 1: Write the failing test**
58
+
59
+ ```python
60
+ def test_specific_behavior():
61
+ result = function(input)
62
+ assert result == expected
63
+ ```
64
+
65
+ **Step 2: Run test to verify it fails**
66
+
67
+ Run: `pytest tests/path/test.py::test_name -v`
68
+ Expected: FAIL with "function not defined"
69
+
70
+ **Step 3: Write minimal implementation**
71
+
72
+ ```python
73
+ def function(input):
74
+ return expected
75
+ ```
76
+
77
+ **Step 4: Run test to verify it passes**
78
+
79
+ Run: `pytest tests/path/test.py::test_name -v`
80
+ Expected: PASS
81
+
82
+ **Step 5: Commit**
83
+
84
+ ```bash
85
+ git add tests/path/test.py src/path/file.py
86
+ git commit -m "feat: add specific feature"
87
+ ```
88
+ ```
89
+
90
+ ## Remember
91
+ - Exact file paths always
92
+ - Complete code in plan (not "add validation")
93
+ - Exact commands with expected output
94
+ - Reference relevant skills with @ syntax
95
+ - DRY, YAGNI, TDD, frequent commits
96
+
97
+ ## Execution Handoff
98
+
99
+ After saving the plan, offer execution choice:
100
+
101
+ **"Plan complete and saved to `docs/plans/<filename>.md`. Two execution options:**
102
+
103
+ **1. Subagent-Driven (this session)** - I dispatch fresh subagent per task, review between tasks, fast iteration
104
+
105
+ **2. Parallel Session (separate)** - Open new session with executing-plans, batch execution with checkpoints
106
+
107
+ **Which approach?"**
108
+
109
+ **If Subagent-Driven chosen:**
110
+ - **REQUIRED SUB-SKILL:** Use superpowers:subagent-driven-development
111
+ - Stay in this session
112
+ - Fresh subagent per task + code review
113
+
114
+ **If Parallel Session chosen:**
115
+ - Guide them to open new session in worktree
116
+ - **REQUIRED SUB-SKILL:** New session uses superpowers:executing-plans
@@ -183,3 +183,18 @@ class TaskClockClear(RenderCommand):
183
183
  class RenderCompactionSummary(RenderCommand):
184
184
  summary: str
185
185
  kept_items_brief: tuple[tuple[str, int, str], ...] = () # (item_type, count, preview)
186
+
187
+
188
+ @dataclass(frozen=True, slots=True)
189
+ class RenderBacktrack(RenderCommand):
190
+ checkpoint_id: int
191
+ note: str
192
+ rationale: str
193
+ original_user_message: str
194
+ messages_discarded: int | None = None
195
+
196
+
197
+ @dataclass(frozen=True, slots=True)
198
+ class UpdateTerminalTitlePrefix(RenderCommand):
199
+ prefix: str | None
200
+ model_name: str | None
@@ -6,7 +6,7 @@ from klaude_code.tui.components.common import create_grid
6
6
  from klaude_code.tui.components.rich.theme import ThemeKey
7
7
  from klaude_code.tui.components.tools import render_path
8
8
 
9
- REMINDER_BULLET = " +"
9
+ REMINDER_BULLET = "+"
10
10
 
11
11
 
12
12
  def need_render_developer_message(e: events.DeveloperMessageEvent) -> bool:
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import contextlib
4
4
  import math
5
- import random
6
5
  import time
7
6
  from collections.abc import Callable
8
7
 
@@ -21,23 +20,14 @@ from klaude_code.const import (
21
20
  STATUS_HINT_TEXT,
22
21
  STATUS_SHIMMER_ALPHA_SCALE,
23
22
  STATUS_SHIMMER_BAND_HALF_WIDTH,
23
+ STATUS_SHIMMER_ENABLED,
24
24
  STATUS_SHIMMER_PADDING,
25
25
  )
26
26
  from klaude_code.tui.components.rich.theme import ThemeKey
27
- from klaude_code.tui.terminal.color import get_last_terminal_background_rgb
28
27
 
29
28
  # Use an existing Rich spinner name; BreathingSpinner overrides its rendering
30
29
  BREATHING_SPINNER_NAME = "dots"
31
30
 
32
- # Alternating glyphs for the breathing spinner - switches at each "transparent" point
33
- _BREATHING_SPINNER_GLYPHS_BASE = [
34
- "✦",
35
- ]
36
-
37
- # Shuffle glyphs on module load for variety across sessions
38
- BREATHING_SPINNER_GLYPHS = _BREATHING_SPINNER_GLYPHS_BASE.copy()
39
- random.shuffle(BREATHING_SPINNER_GLYPHS)
40
-
41
31
 
42
32
  _process_start: float | None = None
43
33
  _task_start: float | None = None
@@ -158,6 +148,9 @@ def _shimmer_profile(main_text: str) -> list[tuple[str, float]]:
158
148
  if not chars:
159
149
  return []
160
150
 
151
+ if not STATUS_SHIMMER_ENABLED:
152
+ return [(ch, 0.0) for ch in chars]
153
+
161
154
  padding = STATUS_SHIMMER_PADDING
162
155
  char_count = len(chars)
163
156
  period = char_count + padding * 2
@@ -211,58 +204,6 @@ def _shimmer_style(console: Console, base_style: Style, intensity: float) -> Sty
211
204
  return base_style + Style(color=shimmer_color)
212
205
 
213
206
 
214
- def _breathing_intensity() -> float:
215
- """Compute breathing intensity in [0, 1] for the spinner.
216
-
217
- Intensity follows a smooth cosine curve over the configured period, starting
218
- from 0 (fully blended into background), rising to 1 (full style color),
219
- then returning to 0, giving a subtle "breathing" effect.
220
- """
221
-
222
- period = max(SPINNER_BREATH_PERIOD_SECONDS, 0.1)
223
- elapsed = _elapsed_since_start()
224
- phase = (elapsed % period) / period
225
- return 0.5 * (1.0 - math.cos(2.0 * math.pi * phase))
226
-
227
-
228
- def _breathing_glyph() -> str:
229
- """Get the current glyph for the breathing spinner.
230
-
231
- Alternates between glyphs at each breath cycle (when intensity reaches 0).
232
- """
233
- period = max(SPINNER_BREATH_PERIOD_SECONDS, 0.1)
234
- elapsed = _elapsed_since_start()
235
- cycle = int(elapsed / period)
236
- return BREATHING_SPINNER_GLYPHS[cycle % len(BREATHING_SPINNER_GLYPHS)]
237
-
238
-
239
- def _breathing_style(console: Console, base_style: Style, intensity: float) -> Style:
240
- """Blend a base style's foreground color toward terminal background.
241
-
242
- When intensity is 0, the color matches the background (effectively
243
- "transparent"); when intensity is 1, the color is the base style color.
244
- """
245
-
246
- base_color = base_style.color or Color.default()
247
- base_triplet = base_color.get_truecolor()
248
- base_r, base_g, base_b = base_triplet
249
-
250
- cached_bg = get_last_terminal_background_rgb()
251
- if cached_bg is not None:
252
- bg_r, bg_g, bg_b = cached_bg
253
- else:
254
- bg_triplet = Color.default().get_truecolor(foreground=False)
255
- bg_r, bg_g, bg_b = bg_triplet
256
-
257
- intensity_clamped = max(0.0, min(1.0, intensity))
258
- r = int(bg_r * (1.0 - intensity_clamped) + base_r * intensity_clamped)
259
- g = int(bg_g * (1.0 - intensity_clamped) + base_g * intensity_clamped)
260
- b = int(bg_b * (1.0 - intensity_clamped) + base_b * intensity_clamped)
261
-
262
- breathing_color = Color.from_rgb(r, g, b)
263
- return base_style + Style(color=breathing_color)
264
-
265
-
266
207
  def truncate_left(text: Text, max_cells: int, *, console: Console, ellipsis: str = "…") -> Text:
267
208
  """Left-truncate Text to fit within max_cells.
268
209
 
@@ -409,21 +350,11 @@ class BreathingSpinner(RichSpinner):
409
350
  return console.get_style(style_name)
410
351
 
411
352
  def _render_breathing(self, console: Console) -> RenderableType:
412
- base_style = self._resolve_base_style(console)
413
- intensity = _breathing_intensity()
414
- style = _breathing_style(console, base_style, intensity)
415
-
416
- glyph = _breathing_glyph()
417
- frame = Text(glyph, style=style)
418
-
419
353
  if not self.text:
420
- return frame
354
+ return Text()
421
355
  if isinstance(self.text, (str, Text)):
422
- return Text.assemble(frame, " ", self.text)
423
-
424
- table = Table.grid(padding=1)
425
- table.add_row(frame, self.text)
426
- return table
356
+ return self.text if isinstance(self.text, Text) else Text(self.text)
357
+ return self.text
427
358
 
428
359
 
429
360
  # Monkey-patch Rich's Status module to use the breathing spinner implementation
@@ -189,6 +189,11 @@ class ThemeKey(str, Enum):
189
189
  THINKING_BOLD = "thinking.bold"
190
190
  # COMPACTION
191
191
  COMPACTION_SUMMARY = "compaction.summary"
192
+ # BACKTRACK
193
+ BACKTRACK = "backtrack"
194
+ BACKTRACK_INFO = "backtrack.info"
195
+ BACKTRACK_USER_MESSAGE = "backtrack.user_message"
196
+ BACKTRACK_NOTE = "backtrack.note"
192
197
  # TODO_ITEM
193
198
  TODO_EXPLANATION = "todo.explanation"
194
199
  TODO_PENDING_MARK = "todo.pending.mark"
@@ -311,6 +316,11 @@ def get_theme(theme: str | None = None) -> Themes:
311
316
  ThemeKey.THINKING_BOLD.value: "italic " + palette.grey1,
312
317
  # COMPACTION
313
318
  ThemeKey.COMPACTION_SUMMARY.value: palette.grey1,
319
+ # BACKTRACK
320
+ ThemeKey.BACKTRACK.value: palette.orange,
321
+ ThemeKey.BACKTRACK_INFO.value: "dim " + palette.grey2,
322
+ ThemeKey.BACKTRACK_USER_MESSAGE.value: palette.cyan,
323
+ ThemeKey.BACKTRACK_NOTE.value: palette.grey1,
314
324
  # TODO_ITEM
315
325
  ThemeKey.TODO_EXPLANATION.value: palette.grey1 + " italic",
316
326
  ThemeKey.TODO_PENDING_MARK.value: "bold " + palette.grey1,
@@ -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:
@@ -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)
@@ -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
- target_rows = 24 if picker_open else base_rows
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".
@@ -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.STATUS_TEXT_BOLD))
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.STATUS_TEXT_BOLD_ITALIC)
244
+ activity_text = Text(extra_reasoning, style=ThemeKey.STATUS_TEXT)
242
245
  else:
243
- prefixed = Text(extra_reasoning, style=ThemeKey.STATUS_TEXT_BOLD_ITALIC)
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=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=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: