klaude-code 1.5.0__py3-none-any.whl → 1.6.0__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/main.py CHANGED
@@ -11,64 +11,11 @@ from klaude_code.cli.config_cmd import register_config_commands
11
11
  from klaude_code.cli.debug import DEBUG_FILTER_HELP, open_log_file_in_editor, resolve_debug_settings
12
12
  from klaude_code.cli.self_update import register_self_update_commands, version_option_callback
13
13
  from klaude_code.cli.session_cmd import register_session_commands
14
- from klaude_code.session import Session, build_session_select_options
14
+ from klaude_code.command.resume_cmd import select_session_sync
15
+ from klaude_code.session import Session
15
16
  from klaude_code.trace import DebugType, prepare_debug_log_file
16
17
 
17
18
 
18
- def select_session_interactive() -> str | None:
19
- """Interactive session selection for CLI.
20
-
21
- Returns:
22
- Selected session ID, or None if no session selected or no sessions exist.
23
- """
24
- from klaude_code.trace import log
25
-
26
- options = build_session_select_options()
27
- if not options:
28
- log("No sessions found for this project.")
29
- return None
30
-
31
- from prompt_toolkit.styles import Style
32
-
33
- from klaude_code.ui.terminal.selector import SelectItem, select_one
34
-
35
- items: list[SelectItem[str]] = []
36
- for opt in options:
37
- title = [
38
- ("class:msg", f"{opt.first_user_message}\n"),
39
- ("class:meta", f" {opt.messages_count} · {opt.relative_time} · {opt.model_name} · {opt.session_id}\n\n"),
40
- ]
41
- items.append(
42
- SelectItem(
43
- title=title,
44
- value=opt.session_id,
45
- search_text=f"{opt.first_user_message} {opt.model_name} {opt.session_id}",
46
- )
47
- )
48
-
49
- try:
50
- return select_one(
51
- message="Select a session to resume:",
52
- items=items,
53
- pointer="→",
54
- style=Style(
55
- [
56
- ("msg", ""),
57
- ("meta", "fg:ansibrightblack"),
58
- ("pointer", "bold fg:ansigreen"),
59
- ("highlighted", "fg:ansigreen"),
60
- ("search_prefix", "fg:ansibrightblack"),
61
- ("search_success", "noinherit fg:ansigreen"),
62
- ("search_none", "noinherit fg:ansired"),
63
- ("question", "bold"),
64
- ("text", ""),
65
- ]
66
- ),
67
- )
68
- except KeyboardInterrupt:
69
- return None
70
-
71
-
72
19
  def set_terminal_title(title: str) -> None:
73
20
  """Set terminal window title using ANSI escape sequence."""
74
21
  # Never write terminal control sequences when stdout is not a TTY (pipes/CI/redirects).
@@ -361,7 +308,7 @@ def main_callback(
361
308
  session_id: str | None = None
362
309
 
363
310
  if resume:
364
- session_id = select_session_interactive()
311
+ session_id = select_session_sync()
365
312
  if session_id is None:
366
313
  return
367
314
  # If user didn't pick, allow fallback to --continue
@@ -1,5 +1,182 @@
1
+ import asyncio
2
+ import sys
3
+ from dataclasses import dataclass
4
+ from typing import Literal
5
+
6
+ from prompt_toolkit.styles import Style
7
+
1
8
  from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
2
9
  from klaude_code.protocol import commands, events, model
10
+ from klaude_code.ui.terminal.selector import SelectItem, select_one
11
+
12
+ FORK_SELECT_STYLE = Style(
13
+ [
14
+ ("msg", ""),
15
+ ("meta", "fg:ansibrightblack"),
16
+ ("separator", "fg:ansibrightblack"),
17
+ ("assistant", "fg:ansiblue"),
18
+ ("pointer", "bold fg:ansigreen"),
19
+ ("search_prefix", "fg:ansibrightblack"),
20
+ ("search_success", "noinherit fg:ansigreen"),
21
+ ("search_none", "noinherit fg:ansired"),
22
+ ("question", "bold"),
23
+ ("text", ""),
24
+ ]
25
+ )
26
+
27
+
28
+ @dataclass
29
+ class ForkPoint:
30
+ """A fork point in conversation history."""
31
+
32
+ history_index: int | None # None means fork entire conversation
33
+ user_message: str
34
+ tool_call_stats: dict[str, int] # tool_name -> count
35
+ last_assistant_summary: str
36
+
37
+
38
+ def _truncate(text: str, max_len: int = 60) -> str:
39
+ """Truncate text to max_len, adding ellipsis if needed."""
40
+ text = text.replace("\n", " ").strip()
41
+ if len(text) <= max_len:
42
+ return text
43
+ return text[: max_len - 3] + "..."
44
+
45
+
46
+ def _build_fork_points(conversation_history: list[model.ConversationItem]) -> list[ForkPoint]:
47
+ """Build list of fork points from conversation history.
48
+
49
+ Fork points are:
50
+ - Each UserMessageItem position (for UI display, including first which would be empty session)
51
+ - The end of the conversation (fork entire conversation)
52
+ """
53
+ fork_points: list[ForkPoint] = []
54
+ user_indices: list[int] = []
55
+
56
+ for i, item in enumerate(conversation_history):
57
+ if isinstance(item, model.UserMessageItem):
58
+ user_indices.append(i)
59
+
60
+ # For each UserMessageItem, create a fork point at that position
61
+ for i, user_idx in enumerate(user_indices):
62
+ user_item = conversation_history[user_idx]
63
+ assert isinstance(user_item, model.UserMessageItem)
64
+
65
+ # Find the end of this "task" (next UserMessageItem or end of history)
66
+ next_user_idx = user_indices[i + 1] if i + 1 < len(user_indices) else len(conversation_history)
67
+
68
+ # Count tool calls by name and find last assistant message in this segment
69
+ tool_stats: dict[str, int] = {}
70
+ last_assistant_content = ""
71
+ for j in range(user_idx, next_user_idx):
72
+ item = conversation_history[j]
73
+ if isinstance(item, model.ToolCallItem):
74
+ tool_stats[item.name] = tool_stats.get(item.name, 0) + 1
75
+ elif isinstance(item, model.AssistantMessageItem) and item.content:
76
+ last_assistant_content = item.content
77
+
78
+ fork_points.append(
79
+ ForkPoint(
80
+ history_index=user_idx,
81
+ user_message=user_item.content or "(empty)",
82
+ tool_call_stats=tool_stats,
83
+ last_assistant_summary=_truncate(last_assistant_content) if last_assistant_content else "",
84
+ )
85
+ )
86
+
87
+ # Add the "fork entire conversation" option at the end
88
+ if user_indices:
89
+ fork_points.append(
90
+ ForkPoint(
91
+ history_index=None, # None means fork entire conversation
92
+ user_message="", # No specific message, this represents the end
93
+ tool_call_stats={},
94
+ last_assistant_summary="",
95
+ )
96
+ )
97
+
98
+ return fork_points
99
+
100
+
101
+ def _build_select_items(fork_points: list[ForkPoint]) -> list[SelectItem[int | None]]:
102
+ """Build SelectItem list from fork points."""
103
+ items: list[SelectItem[int | None]] = []
104
+
105
+ for i, fp in enumerate(fork_points):
106
+ is_first = i == 0
107
+ is_last = i == len(fork_points) - 1
108
+
109
+ # Build the title
110
+ title_parts: list[tuple[str, str]] = []
111
+
112
+ # First line: separator (with special markers for first/last fork points)
113
+ if is_first and not is_last:
114
+ title_parts.append(("class:separator", "----- fork from here (empty session) -----\n\n"))
115
+ elif is_last:
116
+ title_parts.append(("class:separator", "----- fork from here (entire session) -----\n\n"))
117
+ else:
118
+ title_parts.append(("class:separator", "----- fork from here -----\n\n"))
119
+
120
+ if not is_last:
121
+ # Second line: user message
122
+ title_parts.append(("class:msg", f"user: {_truncate(fp.user_message, 70)}\n"))
123
+
124
+ # Third line: tool call stats (if any)
125
+ if fp.tool_call_stats:
126
+ tool_parts = [f"{name} × {count}" for name, count in fp.tool_call_stats.items()]
127
+ title_parts.append(("class:meta", f"tools: {', '.join(tool_parts)}\n"))
128
+
129
+ # Fourth line: last assistant message summary (if any)
130
+ if fp.last_assistant_summary:
131
+ title_parts.append(("class:assistant", f"ai: {fp.last_assistant_summary}\n"))
132
+
133
+ # Empty line at the end
134
+ title_parts.append(("class:text", "\n"))
135
+
136
+ items.append(
137
+ SelectItem(
138
+ title=title_parts,
139
+ value=fp.history_index,
140
+ search_text=fp.user_message if not is_last else "fork entire conversation",
141
+ )
142
+ )
143
+
144
+ return items
145
+
146
+
147
+ def _select_fork_point_sync(fork_points: list[ForkPoint]) -> int | None | Literal["cancelled"]:
148
+ """Interactive fork point selection (sync version for asyncio.to_thread).
149
+
150
+ Returns:
151
+ - int: history index to fork at (exclusive)
152
+ - None: fork entire conversation
153
+ - "cancelled": user cancelled selection
154
+ """
155
+ items = _build_select_items(fork_points)
156
+ if not items:
157
+ return None
158
+
159
+ # Default to the last option (fork entire conversation)
160
+ last_value = items[-1].value
161
+
162
+ # Non-interactive environments default to forking entire conversation
163
+ if not sys.stdin.isatty() or not sys.stdout.isatty():
164
+ return last_value
165
+
166
+ try:
167
+ result = select_one(
168
+ message="Select fork point (messages before this point will be included):",
169
+ items=items,
170
+ pointer="→",
171
+ style=FORK_SELECT_STYLE,
172
+ initial_value=last_value,
173
+ highlight_pointed_item=False,
174
+ )
175
+ if result is None:
176
+ return "cancelled"
177
+ return result
178
+ except KeyboardInterrupt:
179
+ return "cancelled"
3
180
 
4
181
 
5
182
  class ForkSessionCommand(CommandABC):
@@ -13,6 +190,10 @@ class ForkSessionCommand(CommandABC):
13
190
  def summary(self) -> str:
14
191
  return "Fork the current session and show a resume-by-id command"
15
192
 
193
+ @property
194
+ def is_interactive(self) -> bool:
195
+ return True
196
+
16
197
  async def run(self, agent: Agent, user_input: model.UserInputPayload) -> CommandResult:
17
198
  del user_input # unused
18
199
 
@@ -26,13 +207,50 @@ class ForkSessionCommand(CommandABC):
26
207
  )
27
208
  return CommandResult(events=[event], persist_user_input=False, persist_events=False)
28
209
 
29
- new_session = agent.session.fork()
210
+ # Build fork points from conversation history
211
+ fork_points = _build_fork_points(agent.session.conversation_history)
212
+
213
+ if not fork_points:
214
+ # Only one user message, just fork entirely
215
+ new_session = agent.session.fork()
216
+ await new_session.wait_for_flush()
217
+
218
+ event = events.DeveloperMessageEvent(
219
+ session_id=agent.session.id,
220
+ item=model.DeveloperMessageItem(
221
+ content=f"Session forked successfully. New session id: {new_session.id}",
222
+ command_output=model.CommandOutput(
223
+ command_name=self.name,
224
+ ui_extra=model.SessionIdUIExtra(session_id=new_session.id),
225
+ ),
226
+ ),
227
+ )
228
+ return CommandResult(events=[event], persist_user_input=False, persist_events=False)
229
+
230
+ # Interactive selection
231
+ selected = await asyncio.to_thread(_select_fork_point_sync, fork_points)
232
+
233
+ if selected == "cancelled":
234
+ event = events.DeveloperMessageEvent(
235
+ session_id=agent.session.id,
236
+ item=model.DeveloperMessageItem(
237
+ content="(fork cancelled)",
238
+ command_output=model.CommandOutput(command_name=self.name),
239
+ ),
240
+ )
241
+ return CommandResult(events=[event], persist_user_input=False, persist_events=False)
242
+
243
+ # Perform the fork
244
+ new_session = agent.session.fork(until_index=selected)
30
245
  await new_session.wait_for_flush()
31
246
 
247
+ # Build result message
248
+ fork_description = "entire conversation" if selected is None else f"up to message index {selected}"
249
+
32
250
  event = events.DeveloperMessageEvent(
33
251
  session_id=agent.session.id,
34
252
  item=model.DeveloperMessageItem(
35
- content=f"Session forked successfully. New session id: {new_session.id}",
253
+ content=f"Session forked ({fork_description}). New session id: {new_session.id}",
36
254
  command_output=model.CommandOutput(
37
255
  command_name=self.name,
38
256
  ui_extra=model.SessionIdUIExtra(session_id=new_session.id),
@@ -23,7 +23,7 @@ class RefreshTerminalCommand(CommandABC):
23
23
 
24
24
  os.system("cls" if os.name == "nt" else "clear")
25
25
 
26
- result = CommandResult(
26
+ return CommandResult(
27
27
  events=[
28
28
  events.WelcomeEvent(
29
29
  work_dir=str(agent.session.work_dir),
@@ -35,7 +35,7 @@ class RefreshTerminalCommand(CommandABC):
35
35
  updated_at=agent.session.updated_at,
36
36
  is_load=False,
37
37
  ),
38
- ]
38
+ ],
39
+ persist_user_input=False,
40
+ persist_events=False,
39
41
  )
40
-
41
- return result
@@ -4,14 +4,14 @@ from prompt_toolkit.styles import Style
4
4
 
5
5
  from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
6
6
  from klaude_code.protocol import commands, events, model, op
7
- from klaude_code.session.selector import build_session_select_options
7
+ from klaude_code.session.selector import build_session_select_options, format_user_messages_display
8
8
  from klaude_code.trace import log
9
9
  from klaude_code.ui.terminal.selector import SelectItem, select_one
10
10
 
11
11
  SESSION_SELECT_STYLE = Style(
12
12
  [
13
- ("msg", ""),
14
- ("meta", "fg:ansibrightblack"),
13
+ ("msg", "fg:ansibrightblack"),
14
+ ("meta", ""),
15
15
  ("pointer", "bold fg:ansigreen"),
16
16
  ("highlighted", "fg:ansigreen"),
17
17
  ("search_prefix", "fg:ansibrightblack"),
@@ -23,7 +23,7 @@ SESSION_SELECT_STYLE = Style(
23
23
  )
24
24
 
25
25
 
26
- def _select_session_sync() -> str | None:
26
+ def select_session_sync() -> str | None:
27
27
  """Interactive session selection (sync version for asyncio.to_thread)."""
28
28
  options = build_session_select_options()
29
29
  if not options:
@@ -31,16 +31,26 @@ def _select_session_sync() -> str | None:
31
31
  return None
32
32
 
33
33
  items: list[SelectItem[str]] = []
34
- for opt in options:
35
- title = [
36
- ("class:msg", f"{opt.first_user_message}\n"),
37
- ("class:meta", f" {opt.messages_count} · {opt.relative_time} · {opt.model_name} · {opt.session_id}\n\n"),
38
- ]
34
+ for idx, opt in enumerate(options, 1):
35
+ display_msgs = format_user_messages_display(opt.user_messages)
36
+ title: list[tuple[str, str]] = []
37
+ title.append(("fg:ansibrightblack", f"{idx:2}. "))
38
+ title.append(
39
+ ("class:meta", f"{opt.relative_time} · {opt.messages_count} · {opt.model_name} · {opt.session_id}\n")
40
+ )
41
+ for msg in display_msgs:
42
+ if msg == "⋮":
43
+ title.append(("class:msg", f" {msg}\n"))
44
+ else:
45
+ title.append(("class:msg", f" > {msg}\n"))
46
+ title.append(("", "\n"))
47
+
48
+ search_text = " ".join(opt.user_messages) + f" {opt.model_name} {opt.session_id}"
39
49
  items.append(
40
50
  SelectItem(
41
51
  title=title,
42
52
  value=opt.session_id,
43
- search_text=f"{opt.first_user_message} {opt.model_name} {opt.session_id}",
53
+ search_text=search_text,
44
54
  )
45
55
  )
46
56
 
@@ -83,7 +93,7 @@ class ResumeCommand(CommandABC):
83
93
  )
84
94
  return CommandResult(events=[event], persist_user_input=False, persist_events=False)
85
95
 
86
- selected_session_id = await asyncio.to_thread(_select_session_sync)
96
+ selected_session_id = await asyncio.to_thread(select_session_sync)
87
97
  if selected_session_id is None:
88
98
  event = events.DeveloperMessageEvent(
89
99
  session_id=agent.session.id,
klaude_code/llm/usage.py CHANGED
@@ -81,7 +81,7 @@ class MetadataTracker:
81
81
  ) * 1000
82
82
 
83
83
  if self._last_token_time is not None and self._metadata_item.usage.output_tokens > 0:
84
- time_duration = self._last_token_time - self._first_token_time
84
+ time_duration = self._last_token_time - self._request_start_time
85
85
  if time_duration >= 0.15:
86
86
  self._metadata_item.usage.throughput_tps = self._metadata_item.usage.output_tokens / time_duration
87
87
 
@@ -1,4 +1,4 @@
1
- from .selector import SessionSelectOption, build_session_select_options
1
+ from .selector import SessionSelectOption, build_session_select_options, format_user_messages_display
2
2
  from .session import Session
3
3
 
4
- __all__ = ["Session", "SessionSelectOption", "build_session_select_options"]
4
+ __all__ = ["Session", "SessionSelectOption", "build_session_select_options", "format_user_messages_display"]
@@ -33,12 +33,39 @@ class SessionSelectOption:
33
33
  """Option data for session selection UI."""
34
34
 
35
35
  session_id: str
36
- first_user_message: str
36
+ user_messages: list[str]
37
37
  messages_count: str
38
38
  relative_time: str
39
39
  model_name: str
40
40
 
41
41
 
42
+ def _format_message(msg: str) -> str:
43
+ """Format a user message for display (strip and collapse newlines)."""
44
+ return msg.strip().replace("\n", " ")
45
+
46
+
47
+ def format_user_messages_display(messages: list[str]) -> list[str]:
48
+ """Format user messages for display in session selection.
49
+
50
+ Shows up to 6 messages. If more than 6, shows first 3 and last 3 with ellipsis.
51
+ Each message is on its own line.
52
+
53
+ Args:
54
+ messages: List of user messages.
55
+
56
+ Returns:
57
+ List of formatted message lines for display.
58
+ """
59
+ if len(messages) <= 6:
60
+ return messages
61
+
62
+ # More than 6: show first 3, ellipsis, last 3
63
+ result = messages[:3]
64
+ result.append("⋮")
65
+ result.extend(messages[-3:])
66
+ return result
67
+
68
+
42
69
  def build_session_select_options() -> list[SessionSelectOption]:
43
70
  """Build session selection options data.
44
71
 
@@ -51,8 +78,9 @@ def build_session_select_options() -> list[SessionSelectOption]:
51
78
 
52
79
  options: list[SessionSelectOption] = []
53
80
  for s in sessions:
54
- first_msg = s.first_user_message or "N/A"
55
- first_msg = first_msg.strip().replace("\n", " ")
81
+ user_messages = [_format_message(m) for m in s.user_messages if m.strip()]
82
+ if not user_messages:
83
+ user_messages = ["N/A"]
56
84
 
57
85
  msg_count = "N/A" if s.messages_count == -1 else f"{s.messages_count} messages"
58
86
  model = s.model_name or "N/A"
@@ -60,7 +88,7 @@ def build_session_select_options() -> list[SessionSelectOption]:
60
88
  options.append(
61
89
  SessionSelectOption(
62
90
  session_id=str(s.id),
63
- first_user_message=first_msg,
91
+ user_messages=user_messages,
64
92
  messages_count=msg_count,
65
93
  relative_time=_relative_time(s.updated_at),
66
94
  model_name=model,
@@ -197,11 +197,16 @@ class Session(BaseModel):
197
197
  )
198
198
  self._store.append_and_flush(session_id=self.id, items=items, meta=meta)
199
199
 
200
- def fork(self, *, new_id: str | None = None) -> Session:
200
+ def fork(self, *, new_id: str | None = None, until_index: int | None = None) -> Session:
201
201
  """Create a new session as a fork of the current session.
202
202
 
203
203
  The forked session copies metadata and conversation history, but does not
204
204
  modify the current session.
205
+
206
+ Args:
207
+ new_id: Optional ID for the forked session.
208
+ until_index: If provided, only copy conversation history up to (but not including) this index.
209
+ If None, copy all history.
205
210
  """
206
211
 
207
212
  forked = Session.create(id=new_id, work_dir=self.work_dir)
@@ -213,7 +218,8 @@ class Session(BaseModel):
213
218
  forked.file_tracker = {k: v.model_copy(deep=True) for k, v in self.file_tracker.items()}
214
219
  forked.todos = [todo.model_copy(deep=True) for todo in self.todos]
215
220
 
216
- items = [it.model_copy(deep=True) for it in self.conversation_history]
221
+ history_to_copy = self.conversation_history[:until_index] if until_index is not None else self.conversation_history
222
+ items = [it.model_copy(deep=True) for it in history_to_copy]
217
223
  if items:
218
224
  forked.append_history(items)
219
225
 
@@ -338,7 +344,7 @@ class Session(BaseModel):
338
344
  updated_at: float
339
345
  work_dir: str
340
346
  path: str
341
- first_user_message: str | None = None
347
+ user_messages: list[str] = []
342
348
  messages_count: int = -1
343
349
  model_name: str | None = None
344
350
 
@@ -346,10 +352,11 @@ class Session(BaseModel):
346
352
  def list_sessions(cls) -> list[SessionMetaBrief]:
347
353
  store = get_default_store()
348
354
 
349
- def _get_first_user_message(session_id: str) -> str | None:
355
+ def _get_user_messages(session_id: str) -> list[str]:
350
356
  events_path = store.paths.events_file(session_id)
351
357
  if not events_path.exists():
352
- return None
358
+ return []
359
+ messages: list[str] = []
353
360
  try:
354
361
  for line in events_path.read_text(encoding="utf-8").splitlines():
355
362
  obj_raw = json.loads(line)
@@ -360,15 +367,14 @@ class Session(BaseModel):
360
367
  continue
361
368
  data_raw = obj.get("data")
362
369
  if not isinstance(data_raw, dict):
363
- return None
370
+ continue
364
371
  data = cast(dict[str, Any], data_raw)
365
372
  content = data.get("content")
366
373
  if isinstance(content, str):
367
- return content
368
- return None
374
+ messages.append(content)
369
375
  except (OSError, json.JSONDecodeError):
370
- return None
371
- return None
376
+ pass
377
+ return messages
372
378
 
373
379
  items: list[Session.SessionMetaBrief] = []
374
380
  for meta_path in store.iter_meta_files():
@@ -382,7 +388,7 @@ class Session(BaseModel):
382
388
  created = float(data.get("created_at", meta_path.stat().st_mtime))
383
389
  updated = float(data.get("updated_at", meta_path.stat().st_mtime))
384
390
  work_dir = str(data.get("work_dir", ""))
385
- first_user_message = _get_first_user_message(sid)
391
+ user_messages = _get_user_messages(sid)
386
392
  messages_count = int(data.get("messages_count", -1))
387
393
  model_name = data.get("model_name") if isinstance(data.get("model_name"), str) else None
388
394
 
@@ -393,7 +399,7 @@ class Session(BaseModel):
393
399
  updated_at=updated,
394
400
  work_dir=work_dir,
395
401
  path=str(meta_path),
396
- first_user_message=first_user_message,
402
+ user_messages=user_messages,
397
403
  messages_count=messages_count,
398
404
  model_name=model_name,
399
405
  )
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
4
 
5
- from rich.cells import cell_len
6
5
  from rich.rule import Rule
7
6
  from rich.text import Text
8
7
 
@@ -265,11 +264,27 @@ class SpinnerStatusState:
265
264
 
266
265
  return result
267
266
 
268
- def get_context_text(self) -> Text | None:
269
- """Get context usage text for right-aligned display."""
270
- if self._context_percent is None:
267
+ def get_right_text(self) -> r_status.DynamicText | None:
268
+ """Get right-aligned status text (elapsed time and optional context %)."""
269
+
270
+ elapsed_text = r_status.current_elapsed_text()
271
+ has_context = self._context_percent is not None
272
+
273
+ if elapsed_text is None and not has_context:
271
274
  return None
272
- return Text(f"{self._context_percent:.1f}%", style=ThemeKey.METADATA_DIM)
275
+
276
+ def _render() -> Text:
277
+ parts: list[str] = []
278
+ if self._context_percent is not None:
279
+ parts.append(f"{self._context_percent:.1f}%")
280
+ current_elapsed = r_status.current_elapsed_text()
281
+ if current_elapsed is not None:
282
+ if parts:
283
+ parts.append(" · ")
284
+ parts.append(current_elapsed)
285
+ return Text("".join(parts), style=ThemeKey.METADATA_DIM)
286
+
287
+ return r_status.DynamicText(_render)
273
288
 
274
289
 
275
290
  class DisplayEventHandler:
@@ -550,11 +565,10 @@ class DisplayEventHandler:
550
565
  def _update_spinner(self) -> None:
551
566
  """Update spinner text from current status state."""
552
567
  status_text = self.spinner_status.get_status()
553
- context_text = self.spinner_status.get_context_text()
554
- status_text = self._truncate_spinner_status_text(status_text, right_text=context_text)
568
+ right_text = self.spinner_status.get_right_text()
555
569
  self.renderer.spinner_update(
556
570
  status_text,
557
- context_text,
571
+ right_text,
558
572
  )
559
573
 
560
574
  async def _flush_assistant_buffer(self, state: StreamState) -> None:
@@ -612,27 +626,3 @@ class DisplayEventHandler:
612
626
  if len(todo.content) > 0:
613
627
  status_text = todo.content
614
628
  return status_text.replace("\n", " ").strip()
615
-
616
- def _truncate_spinner_status_text(self, status_text: Text, *, right_text: Text | None) -> Text:
617
- """Truncate spinner status to a single line based on terminal width.
618
-
619
- Rich wraps based on terminal cell width (CJK chars count as 2). Use
620
- cell-aware truncation to prevent the status from wrapping into two lines.
621
- """
622
-
623
- terminal_width = self.renderer.console.size.width
624
-
625
- # BreathingSpinner renders as a 2-column Table.grid(padding=1):
626
- # 1 cell for glyph + 1 cell of padding between columns (collapsed).
627
- spinner_prefix_cells = 2
628
-
629
- hint_cells = cell_len(r_status.current_hint_text())
630
- right_cells = cell_len(right_text.plain) if right_text is not None else 0
631
-
632
- max_main_cells = terminal_width - spinner_prefix_cells - hint_cells - right_cells - 1
633
- # rich.text.Text.truncate behaves unexpectedly for 0; clamp to at least 1.
634
- max_main_cells = max(1, max_main_cells)
635
-
636
- truncated = status_text.copy()
637
- truncated.truncate(max_main_cells, overflow="ellipsis", pad=False)
638
- return truncated
@@ -283,7 +283,7 @@ class REPLRenderer:
283
283
  self._spinner_visible = False
284
284
  self._refresh_bottom_live()
285
285
 
286
- def spinner_update(self, status_text: str | Text, right_text: Text | None = None) -> None:
286
+ def spinner_update(self, status_text: str | Text, right_text: RenderableType | None = None) -> None:
287
287
  """Update the spinner status text with optional right-aligned text."""
288
288
  self._status_text = ShimmerStatusText(status_text, right_text)
289
289
  self._status_spinner.update(text=SingleLine(self._status_text), style=ThemeKey.STATUS_SPINNER)
@@ -161,10 +161,10 @@ def _format_cost(cost: float | None, currency: str = "USD") -> str:
161
161
  def _render_fork_session_output(command_output: model.CommandOutput) -> RenderableType:
162
162
  """Render fork session output with usage instructions."""
163
163
  if not isinstance(command_output.ui_extra, model.SessionIdUIExtra):
164
- return Text("(no session id)", style=ThemeKey.METADATA)
164
+ return Padding.indent(Text("(no session id)", style=ThemeKey.METADATA), level=2)
165
165
 
166
- session_id = command_output.ui_extra.session_id
167
166
  grid = Table.grid(padding=(0, 1))
167
+ session_id = command_output.ui_extra.session_id
168
168
  grid.add_column(style=ThemeKey.METADATA, overflow="fold")
169
169
 
170
170
  grid.add_row(Text("Session forked. To continue in a new conversation:", style=ThemeKey.METADATA))
@@ -6,6 +6,7 @@ from rich.padding import Padding
6
6
  from rich.panel import Panel
7
7
  from rich.text import Text
8
8
 
9
+ from klaude_code import const
9
10
  from klaude_code.protocol import events, model
10
11
  from klaude_code.trace import is_debug_enabled
11
12
  from klaude_code.ui.renderers.common import create_grid
@@ -95,10 +96,17 @@ def _render_task_metadata_block(
95
96
  # Context (only for main agent)
96
97
  if show_context_and_time and metadata.usage.context_usage_percent is not None:
97
98
  context_size = format_number(metadata.usage.context_size or 0)
99
+ # Calculate effective limit (same as Usage.context_usage_percent)
100
+ effective_limit = (metadata.usage.context_limit or 0) - (
101
+ metadata.usage.max_tokens or const.DEFAULT_MAX_TOKENS
102
+ )
103
+ effective_limit_str = format_number(effective_limit) if effective_limit > 0 else "?"
98
104
  parts.append(
99
105
  Text.assemble(
100
106
  ("context ", ThemeKey.METADATA_DIM),
101
107
  (context_size, ThemeKey.METADATA),
108
+ ("/", ThemeKey.METADATA_DIM),
109
+ (effective_limit_str, ThemeKey.METADATA),
102
110
  (f" ({metadata.usage.context_usage_percent:.1f}%)", ThemeKey.METADATA_DIM),
103
111
  )
104
112
  )
@@ -254,18 +254,36 @@ class MarkdownStream:
254
254
  live suffix separately may introduce an extra blank line that wouldn't
255
255
  appear when rendering the full document.
256
256
 
257
- This function removes leading blank lines from the live ANSI when the
258
- stable ANSI already ends with a blank line.
257
+ This function removes *overlapping* blank lines from the live ANSI when
258
+ the stable ANSI already ends with one or more blank lines.
259
+
260
+ Important: don't remove *all* leading blank lines from the live suffix.
261
+ In some incomplete-block cases, the live render may begin with multiple
262
+ blank lines while the full-document render would keep one of them.
259
263
  """
260
264
 
261
265
  stable_lines = stable_ansi.splitlines(keepends=True)
262
- stable_ends_blank = bool(stable_lines) and not stable_lines[-1].strip()
263
- if not stable_ends_blank:
266
+ if not stable_lines:
267
+ return live_ansi
268
+
269
+ stable_trailing_blank = 0
270
+ for line in reversed(stable_lines):
271
+ if line.strip():
272
+ break
273
+ stable_trailing_blank += 1
274
+ if stable_trailing_blank <= 0:
264
275
  return live_ansi
265
276
 
266
277
  live_lines = live_ansi.splitlines(keepends=True)
267
- while live_lines and not live_lines[0].strip():
268
- live_lines.pop(0)
278
+ live_leading_blank = 0
279
+ for line in live_lines:
280
+ if line.strip():
281
+ break
282
+ live_leading_blank += 1
283
+
284
+ drop = min(stable_trailing_blank, live_leading_blank)
285
+ if drop > 0:
286
+ live_lines = live_lines[drop:]
269
287
  return "".join(live_lines)
270
288
 
271
289
  def _append_nonfinal_sentinel(self, stable_source: str) -> str:
@@ -400,9 +418,23 @@ class MarkdownStream:
400
418
  apply_mark_live = self._stable_source_line_count == 0
401
419
  live_lines = self._render_markdown_to_lines(live_source, apply_mark=apply_mark_live)
402
420
 
403
- if self._stable_rendered_lines and not self._stable_rendered_lines[-1].strip():
404
- while live_lines and not live_lines[0].strip():
405
- live_lines.pop(0)
421
+ if self._stable_rendered_lines:
422
+ stable_trailing_blank = 0
423
+ for line in reversed(self._stable_rendered_lines):
424
+ if line.strip():
425
+ break
426
+ stable_trailing_blank += 1
427
+
428
+ if stable_trailing_blank > 0:
429
+ live_leading_blank = 0
430
+ for line in live_lines:
431
+ if line.strip():
432
+ break
433
+ live_leading_blank += 1
434
+
435
+ drop = min(stable_trailing_blank, live_leading_blank)
436
+ if drop > 0:
437
+ live_lines = live_lines[drop:]
406
438
 
407
439
  live_text = Text.from_ansi("".join(live_lines))
408
440
  self._live_sink(live_text)
@@ -4,10 +4,13 @@ import contextlib
4
4
  import math
5
5
  import random
6
6
  import time
7
+ from collections.abc import Callable
7
8
 
8
9
  import rich.status as rich_status
10
+ from rich.cells import cell_len
9
11
  from rich.color import Color
10
12
  from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
13
+ from rich.measure import Measurement
11
14
  from rich.spinner import Spinner as RichSpinner
12
15
  from rich.style import Style
13
16
  from rich.table import Table
@@ -80,20 +83,63 @@ def _format_elapsed_compact(seconds: float) -> str:
80
83
  def current_hint_text(*, min_time_width: int = 0) -> str:
81
84
  """Return the full hint string shown on the status line.
82
85
 
83
- Includes an optional elapsed time prefix (right-aligned, min width) and
84
- the constant hint suffix.
86
+ The hint is the constant suffix shown after the main status text.
87
+
88
+ The elapsed task time is rendered on the right side of the status line
89
+ (near context usage), not inside the hint.
85
90
  """
86
91
 
92
+ # Keep the signature stable; min_time_width is intentionally ignored.
93
+ _ = min_time_width
94
+ return const.STATUS_HINT_TEXT
95
+
96
+
97
+ def current_elapsed_text(*, min_time_width: int = 0) -> str | None:
98
+ """Return the current task elapsed time text (e.g. "11s", "1m02s")."""
99
+
87
100
  elapsed = _task_elapsed_seconds()
88
101
  if elapsed is None:
89
- return const.STATUS_HINT_TEXT
102
+ return None
90
103
  time_text = _format_elapsed_compact(elapsed)
91
104
  if min_time_width > 0:
92
105
  time_text = time_text.rjust(min_time_width)
93
- suffix = const.STATUS_HINT_TEXT.strip()
94
- if suffix.startswith("(") and suffix.endswith(")"):
95
- suffix = suffix[1:-1]
96
- return f" ({time_text} · {suffix})"
106
+ return time_text
107
+
108
+
109
+ class DynamicText:
110
+ """Renderable that materializes a Text instance at render time.
111
+
112
+ This is useful for status line elements that should refresh without
113
+ requiring explicit spinner_update calls (e.g. elapsed time).
114
+ """
115
+
116
+ def __init__(
117
+ self,
118
+ factory: Callable[[], Text],
119
+ *,
120
+ min_width_cells: int = 0,
121
+ ) -> None:
122
+ self._factory = factory
123
+ self.min_width_cells = min_width_cells
124
+
125
+ @property
126
+ def plain(self) -> str:
127
+ return self._factory().plain
128
+
129
+ def __rich_measure__(self, console: Console, options: ConsoleOptions) -> Measurement:
130
+ # Ensure Table/grid layout allocates a stable width for this renderable.
131
+ text = self._factory()
132
+ measured = Measurement.get(console, options, text)
133
+ min_width = max(measured.minimum, self.min_width_cells)
134
+ max_width = max(measured.maximum, self.min_width_cells)
135
+
136
+ limit = getattr(options, "max_width", options.size.width)
137
+ max_width = min(max_width, limit)
138
+ min_width = min(min_width, max_width)
139
+ return Measurement(min_width, max_width)
140
+
141
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
142
+ yield self._factory()
97
143
 
98
144
 
99
145
  def _shimmer_profile(main_text: str) -> list[tuple[str, float]]:
@@ -220,7 +266,7 @@ class ShimmerStatusText:
220
266
  def __init__(
221
267
  self,
222
268
  main_text: str | Text,
223
- right_text: Text | None = None,
269
+ right_text: RenderableType | None = None,
224
270
  main_style: ThemeKey = ThemeKey.STATUS_TEXT,
225
271
  ) -> None:
226
272
  if isinstance(main_text, Text):
@@ -234,34 +280,49 @@ class ShimmerStatusText:
234
280
  self._right_text = right_text
235
281
 
236
282
  def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
237
- left_text = self._render_left_text(console)
283
+ left_text = _StatusLeftText(main=self._main_text, hint_style=self._hint_style)
238
284
 
239
285
  if self._right_text is None:
240
286
  yield left_text
241
287
  return
242
288
 
243
- # Use Table.grid to create left-right aligned layout
244
- table = Table.grid(expand=True)
289
+ # Use Table.grid to create left-right aligned layout with a stable gap.
290
+ table = Table.grid(expand=True, padding=(0, 1, 0, 0), collapse_padding=True, pad_edge=False)
245
291
  table.add_column(justify="left", ratio=1)
246
292
  table.add_column(justify="right")
247
293
  table.add_row(left_text, self._right_text)
248
294
  yield table
249
295
 
250
- def _render_left_text(self, console: Console) -> Text:
251
- """Render the left part with shimmer effect on main text only."""
252
- result = Text()
253
- hint_style = console.get_style(str(self._hint_style))
254
296
 
255
- # Apply shimmer only to main text
256
- for index, (ch, intensity) in enumerate(_shimmer_profile(self._main_text.plain)):
257
- base_style = self._main_text.get_style_at_offset(console, index)
297
+ class _StatusLeftText:
298
+ def __init__(self, *, main: Text, hint_style: ThemeKey) -> None:
299
+ self._main = main
300
+ self._hint_style = hint_style
301
+
302
+ def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
303
+ max_width = getattr(options, "max_width", options.size.width)
304
+
305
+ # Keep the hint visually attached to the status text, while truncating only
306
+ # the main status segment when space is tight.
307
+ hint_text = Text(current_hint_text().strip("\n"), style=console.get_style(str(self._hint_style)))
308
+ hint_cells = cell_len(hint_text.plain)
309
+
310
+ main_text = Text()
311
+ for index, (ch, intensity) in enumerate(_shimmer_profile(self._main.plain)):
312
+ base_style = self._main.get_style_at_offset(console, index)
258
313
  style = _shimmer_style(console, base_style, intensity)
259
- result.append(ch, style=style)
314
+ main_text.append(ch, style=style)
260
315
 
261
- # Append hint text without shimmer
262
- result.append(current_hint_text().strip("\n"), style=hint_style)
316
+ # If the hint itself can't fit, fall back to truncating the combined text.
317
+ if max_width <= hint_cells:
318
+ combined = Text.assemble(main_text, hint_text)
319
+ combined.truncate(max(1, max_width), overflow="ellipsis", pad=False)
320
+ yield combined
321
+ return
263
322
 
264
- return result
323
+ main_budget = max_width - hint_cells
324
+ main_text.truncate(max(1, main_budget), overflow="ellipsis", pad=False)
325
+ yield Text.assemble(main_text, hint_text)
265
326
 
266
327
 
267
328
  def spinner_name() -> str:
@@ -98,6 +98,53 @@ def _restyle_title(title: list[tuple[str, str]], cls: str) -> list[tuple[str, st
98
98
  return restyled
99
99
 
100
100
 
101
+ def _indent_multiline_tokens(
102
+ tokens: list[tuple[str, str]],
103
+ indent: str,
104
+ *,
105
+ indent_style: str = "class:text",
106
+ ) -> list[tuple[str, str]]:
107
+ """Indent continuation lines inside formatted tokens.
108
+
109
+ This is needed when an item's title contains embedded newlines. The selector
110
+ prefixes each *item* with the pointer padding, but continuation lines inside
111
+ a single item would otherwise start at column 0.
112
+ """
113
+ if not tokens or all("\n" not in text for _style, text in tokens):
114
+ return tokens
115
+
116
+ def _has_non_newline_text(s: str) -> bool:
117
+ return bool(s.replace("\n", ""))
118
+
119
+ has_text_after_token: list[bool] = [False] * len(tokens)
120
+ remaining = False
121
+ for i in range(len(tokens) - 1, -1, -1):
122
+ has_text_after_token[i] = remaining
123
+ remaining = remaining or _has_non_newline_text(tokens[i][1])
124
+
125
+ out: list[tuple[str, str]] = []
126
+ for token_index, (style, text) in enumerate(tokens):
127
+ if "\n" not in text:
128
+ out.append((style, text))
129
+ continue
130
+
131
+ parts = text.split("\n")
132
+ for part_index, part in enumerate(parts):
133
+ if part:
134
+ out.append((style, part))
135
+
136
+ # If this was a newline, re-add it.
137
+ if part_index < len(parts) - 1:
138
+ out.append((style, "\n"))
139
+
140
+ # Only indent when there is more text remaining within this item.
141
+ has_text_later_in_token = any(p for p in parts[part_index + 1 :])
142
+ if has_text_later_in_token or has_text_after_token[token_index]:
143
+ out.append((indent_style, indent))
144
+
145
+ return out
146
+
147
+
101
148
  def _filter_items[T](
102
149
  items: list[SelectItem[T]],
103
150
  filter_text: str,
@@ -120,6 +167,8 @@ def _build_choices_tokens[T](
120
167
  visible_indices: list[int],
121
168
  pointed_at: int,
122
169
  pointer: str,
170
+ *,
171
+ highlight_pointed_item: bool = True,
123
172
  ) -> list[tuple[str, str]]:
124
173
  """Build formatted tokens for the choice list."""
125
174
  if not visible_indices:
@@ -137,7 +186,12 @@ def _build_choices_tokens[T](
137
186
  else:
138
187
  tokens.append(("class:text", pointer_pad))
139
188
 
140
- title_tokens = _restyle_title(items[idx].title, "class:highlighted") if is_pointed else items[idx].title
189
+ if is_pointed and highlight_pointed_item:
190
+ title_tokens = _restyle_title(items[idx].title, "class:highlighted")
191
+ else:
192
+ title_tokens = items[idx].title
193
+
194
+ title_tokens = _indent_multiline_tokens(title_tokens, pointer_pad)
141
195
  tokens.extend(title_tokens)
142
196
 
143
197
  return tokens
@@ -226,6 +280,7 @@ def select_one[T](
226
280
  use_search_filter: bool = True,
227
281
  initial_value: T | None = None,
228
282
  search_placeholder: str = "type to search",
283
+ highlight_pointed_item: bool = True,
229
284
  ) -> T | None:
230
285
  """Terminal single-choice selector based on prompt_toolkit."""
231
286
  if not items:
@@ -250,7 +305,13 @@ def select_one[T](
250
305
  indices, _ = _filter_items(items, get_filter_text())
251
306
  if indices:
252
307
  pointed_at %= len(indices)
253
- return _build_choices_tokens(items, indices, pointed_at, pointer)
308
+ return _build_choices_tokens(
309
+ items,
310
+ indices,
311
+ pointed_at,
312
+ pointer,
313
+ highlight_pointed_item=highlight_pointed_item,
314
+ )
254
315
 
255
316
  def on_search_changed(_buf: Buffer) -> None:
256
317
  nonlocal pointed_at
@@ -376,6 +437,7 @@ class SelectOverlay[T]:
376
437
  use_search_filter: bool = True,
377
438
  search_placeholder: str = "type to search",
378
439
  list_height: int = 8,
440
+ highlight_pointed_item: bool = True,
379
441
  on_select: Callable[[T], Coroutine[Any, Any, None] | None] | None = None,
380
442
  on_cancel: Callable[[], Coroutine[Any, Any, None] | None] | None = None,
381
443
  ) -> None:
@@ -383,6 +445,7 @@ class SelectOverlay[T]:
383
445
  self._use_search_filter = use_search_filter
384
446
  self._search_placeholder = search_placeholder
385
447
  self._list_height = max(1, list_height)
448
+ self._highlight_pointed_item = highlight_pointed_item
386
449
  self._on_select = on_select
387
450
  self._on_cancel = on_cancel
388
451
 
@@ -482,7 +545,13 @@ class SelectOverlay[T]:
482
545
  indices, _ = self._get_visible_indices()
483
546
  if indices:
484
547
  self._pointed_at %= len(indices)
485
- return _build_choices_tokens(self._items, indices, self._pointed_at, self._pointer)
548
+ return _build_choices_tokens(
549
+ self._items,
550
+ indices,
551
+ self._pointed_at,
552
+ self._pointer,
553
+ highlight_pointed_item=self._highlight_pointed_item,
554
+ )
486
555
 
487
556
  header_window = Window(
488
557
  FormattedTextControl(get_header_tokens),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: klaude-code
3
- Version: 1.5.0
3
+ Version: 1.6.0
4
4
  Summary: Minimal code agent CLI
5
5
  Requires-Dist: anthropic>=0.66.0
6
6
  Requires-Dist: chardet>=5.2.0
@@ -10,7 +10,7 @@ klaude_code/cli/auth_cmd.py,sha256=UWMHjn9xZp2o8OZc-x8y9MnkZgRWOkFXk05iKJYcySE,2
10
10
  klaude_code/cli/config_cmd.py,sha256=hlvslLNgdRHkokq1Pnam0XOdR3jqO3K0vNLqtWnPa6Q,3261
11
11
  klaude_code/cli/debug.py,sha256=cPQ7cgATcJTyBIboleW_Q4Pa_t-tGG6x-Hj3woeeuHE,2669
12
12
  klaude_code/cli/list_model.py,sha256=uA0PNR1RjUK7BCKu2Q0Sh2xB9j9Gpwp_bsWhroTW6JY,9260
13
- klaude_code/cli/main.py,sha256=vTnKdSDJBeXy8fxxyrzNDIaa-2XxH0Zi9BvO1qIOIbM,14935
13
+ klaude_code/cli/main.py,sha256=uNZl0RjeLRITbfHerma4_kq2f0hF166dFZqAHLBu580,13236
14
14
  klaude_code/cli/runtime.py,sha256=6CtsQa8UcC9ppnNm2AvsF3yxgncyEYwpIIX0bb-3NN0,19826
15
15
  klaude_code/cli/self_update.py,sha256=iGuj0i869Zi0M70W52-VVLxZp90ISr30fQpZkHGMK2o,8059
16
16
  klaude_code/cli/session_cmd.py,sha256=q2YarlV6KARkFnbm_36ZUvBh8Noj8B7TlMg1RIlt1GE,3154
@@ -20,17 +20,17 @@ klaude_code/command/command_abc.py,sha256=wZl_azY6Dpd4OvjtkSEPI3ilXaygLIVkO7NCgN
20
20
  klaude_code/command/debug_cmd.py,sha256=9sBIAwHz28QoI-tHsU3ksQlDObF1ilIbtAAEAVMR0v0,2734
21
21
  klaude_code/command/export_cmd.py,sha256=Cs7YXWtos-ZfN9OEppIl8Xrb017kDG7R6hGiilqt2bM,1623
22
22
  klaude_code/command/export_online_cmd.py,sha256=RYYLnkLtg6edsgysmhsfTw16ncFRIT6PqeTdWhWXLHE,6094
23
- klaude_code/command/fork_session_cmd.py,sha256=T3o0mOVcqL2Ts39Ijl4t2Sl0GYbvh8zyd9z21M-AThU,1682
23
+ klaude_code/command/fork_session_cmd.py,sha256=ocVg1YZw99RWmoCu67L6zOyIzz49CgTQRA36NFjgt-k,9672
24
24
  klaude_code/command/help_cmd.py,sha256=wtmOoi4DVaMJPCXLlNKJ4s-kNycNKuYk0MZkZijXLcQ,1666
25
25
  klaude_code/command/model_cmd.py,sha256=h3jUi9YOhT9rN87yfCxxU-yN3UiUzwI7Xf2UsRjjP5I,2956
26
26
  klaude_code/command/model_select.py,sha256=_TquYw8zDQHkEaRCqOCIcD2XWt8Jg-3WfGhHFSsjFw0,3189
27
27
  klaude_code/command/prompt-init.md,sha256=a4_FQ3gKizqs2vl9oEY5jtG6HNhv3f-1b5RSCFq0A18,1873
28
28
  klaude_code/command/prompt-jj-describe.md,sha256=n-7hiXU8oodCMR3ipNyRR86pAUzXMz6seloU9a6QQnY,974
29
29
  klaude_code/command/prompt_command.py,sha256=rMi-ZRLpUSt1t0IQVtwnzIYqcrXK-MwZrabbZ8dc8U4,2774
30
- klaude_code/command/refresh_cmd.py,sha256=575eJ5IsOc1e_7CulMxvTu5GQ6BaXTG1k8IsAqzrwdQ,1244
30
+ klaude_code/command/refresh_cmd.py,sha256=Gd5-Vg8VRMOAMgXOzR2Y8wtVQudL0TWEdkut_fyQjQs,1292
31
31
  klaude_code/command/registry.py,sha256=IeOGJ_TMWb_EE5c2JnWYR6XdDfwYfQDFAikI4u_5tXw,6904
32
32
  klaude_code/command/release_notes_cmd.py,sha256=FIrBRfKTlXEp8mBh15buNjgOrl_GMX7FeeMWxYYBn1o,2674
33
- klaude_code/command/resume_cmd.py,sha256=dOvq4OoaNU2MCXfJW9eRYZLZnGkTMef5H8LvhX60yqI,3510
33
+ klaude_code/command/resume_cmd.py,sha256=YiIj4jbabf8QGTZoIkTjGWmrBof24r98zwNRd-NuROU,3945
34
34
  klaude_code/command/status_cmd.py,sha256=95cp4-Qg7ju4TZhKIV6_dfv1rrjcyNO6816NHtfk6v0,5413
35
35
  klaude_code/command/terminal_setup_cmd.py,sha256=SivM1gX_anGY_8DCQNFZ5VblFqt4sVgCMEWPRlo6K5w,10911
36
36
  klaude_code/command/thinking_cmd.py,sha256=NPejWmx6HDxoWzAJVLEENCr3Wi6sQSbT8A8LRh1-2Nk,3059
@@ -126,7 +126,7 @@ klaude_code/llm/registry.py,sha256=grgHetTd-lSxTXiY689QW_Zd6voaid7qBqSnulpg_fE,1
126
126
  klaude_code/llm/responses/__init__.py,sha256=WsiyvnNiIytaYcaAqNiB8GI-5zcpjjeODPbMlteeFjA,67
127
127
  klaude_code/llm/responses/client.py,sha256=XEsVehevQJ0WFbEVxIkI-su7VwIcaeq0P9eSrIRcGug,10184
128
128
  klaude_code/llm/responses/input.py,sha256=qr61LmQJdcb_f-ofrAz06WpK_k4PEcI36XsyuZAXbKk,6805
129
- klaude_code/llm/usage.py,sha256=cq6yZNSKBhRVVjFqBYJQrK3mw9ZSLXaTpbDeal-BjBQ,4205
129
+ klaude_code/llm/usage.py,sha256=ohQ6EBsWXZj6B4aJ4lDPqfhXRyd0LUAM1nXEJ_elD7A,4207
130
130
  klaude_code/protocol/__init__.py,sha256=aGUgzhYqvhuT3Mk2vj7lrHGriH4h9TSbqV1RsRFAZjQ,194
131
131
  klaude_code/protocol/commands.py,sha256=4tFt98CD_KvS9C-XEaHLN-S-QFsbDxQb_kGKnPkQlrk,958
132
132
  klaude_code/protocol/events.py,sha256=KUMf1rLNdHQO9cZiQ9Pa1VsKkP1PTMbUkp18bu_jGy8,3935
@@ -140,11 +140,11 @@ klaude_code/protocol/sub_agent/oracle.py,sha256=0cbuutKQcvwaM--Q15mbkCdbpZMF4Yjx
140
140
  klaude_code/protocol/sub_agent/task.py,sha256=3RYXTfK0vqSevv0MG3DYUpExrCS2F3Cvszl_Zz9d8-g,3620
141
141
  klaude_code/protocol/sub_agent/web.py,sha256=Z5vUM367kz8CIexN6UVPG4XxzVOaaRek-Ga64NvcZdk,3043
142
142
  klaude_code/protocol/tools.py,sha256=ejhMCBBMz1ODbPEiynhzjB-aLbIRKL-wipPFv-nEz4g,373
143
- klaude_code/session/__init__.py,sha256=-iKgJA4Pl0Yg6tqStEpGh8eT3SzWl2iRg-QfefuR4Ow,179
143
+ klaude_code/session/__init__.py,sha256=4sw81uQvEd3YUOOjamKk1KqGmxeb4Ic9T1Tee5zztyU,241
144
144
  klaude_code/session/codec.py,sha256=ummbqT7t6uHHXtaS9lOkyhi1h0YpMk7SNSms8DyGAHU,2015
145
145
  klaude_code/session/export.py,sha256=dj-IRUNtXL8uONDj9bsEXcEHKyeHY7lIcXv80yP88h4,31022
146
- klaude_code/session/selector.py,sha256=xxu28og7Qvk2gBb3O4zC1o_lP8sVmMlzd511-kZtfeM,1986
147
- klaude_code/session/session.py,sha256=otWpPnCk5LGS5IW_zTdeXBtLdxbBlEK2jH5FnrOIpF4,16969
146
+ klaude_code/session/selector.py,sha256=FpKpGs06fM-LdV-yVUqEY-FJsFn2OtGK-0paXjsZVTg,2770
147
+ klaude_code/session/session.py,sha256=CUbIBpPgl_m2lr3h5jZNktJG0S5eYGZkL37seWdZ7uw,17318
148
148
  klaude_code/session/store.py,sha256=-e-lInCB3N1nFLlet7bipkmPk1PXmGthuMxv5z3hg5o,6953
149
149
  klaude_code/session/templates/export_session.html,sha256=bA27AkcC7DQRoWmcMBeaR8WOx1z76hezEDf0aYH-0HQ,119780
150
150
  klaude_code/session/templates/mermaid_viewer.html,sha256=lOkETxlctX1C1WJtS1wFw6KhNQmemxwJZFpXDSjlMOk,27842
@@ -173,19 +173,19 @@ klaude_code/ui/modes/repl/__init__.py,sha256=_0II73jlz5JUtvJsZ9sGRJzeHIQyJJpaI0e
173
173
  klaude_code/ui/modes/repl/clipboard.py,sha256=ZCpk7kRSXGhh0Q_BWtUUuSYT7ZOqRjAoRcg9T9n48Wo,5137
174
174
  klaude_code/ui/modes/repl/completers.py,sha256=zH5zslovTKJwH1Gu8ZufvMDGkSd342F6fHE1hjlxHgM,31849
175
175
  klaude_code/ui/modes/repl/display.py,sha256=06wawOHWO2ItEA9EIEh97p3GDID7TJhAtpaA03nPQXs,2335
176
- klaude_code/ui/modes/repl/event_handler.py,sha256=wDMkG4MpV0SEGmQaX-rOCJyomEdy0JYIDK0VB7R_RXo,26127
176
+ klaude_code/ui/modes/repl/event_handler.py,sha256=O8yDr4xNMAqgXEiT90KWBoQX-2pIPjVf591QJ0ftjIo,25482
177
177
  klaude_code/ui/modes/repl/input_prompt_toolkit.py,sha256=40PfnhMCeOHEkb8MQFpyA4qIkWhYmhjosb3NAeFxyqM,28434
178
178
  klaude_code/ui/modes/repl/key_bindings.py,sha256=tZV0ILMWpHCPcVFpf9bnbTSXgnnqsW0-6cCMMVTRciA,13023
179
- klaude_code/ui/modes/repl/renderer.py,sha256=7cum6SuKSuuBePDSyk4UvWs6q5dwgLA0NrJZ3eD9tHw,15902
179
+ klaude_code/ui/modes/repl/renderer.py,sha256=kdJKRGMGEQFskHeibkI-heoFZP6ucHOK_x7brXPhNCI,15912
180
180
  klaude_code/ui/renderers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
181
181
  klaude_code/ui/renderers/assistant.py,sha256=7iu5zlHR7JGviHs2eA25Dsbd7ZkzCR2_0XzkqMPVxDI,862
182
182
  klaude_code/ui/renderers/bash_syntax.py,sha256=VcX_tuojOtS58s_Ff-Zmhw_6LBRn2wsvR5UBtEr_qQU,5923
183
183
  klaude_code/ui/renderers/common.py,sha256=l9V7yuiejowyw3FdZ2n3VJ2OO_K1rEUINmFz-mC2xlw,2648
184
- klaude_code/ui/renderers/developer.py,sha256=J6hBko8fiRxF67scI1NQ_Ztz3Y47VF3uxXblhIqUciE,8138
184
+ klaude_code/ui/renderers/developer.py,sha256=JB8NZ6blur8U6Gn4uUMg6dOTmaMNvTcxxaOk3P9toHo,8163
185
185
  klaude_code/ui/renderers/diffs.py,sha256=uLpgYTudH38wucozoUw4xbPWMC6uYTQTaDTHcg-0zvM,10418
186
186
  klaude_code/ui/renderers/errors.py,sha256=MavmYOQ7lyjA_VpuUpDVFCuY9W7XrMVdLsg2lCOn4GY,655
187
187
  klaude_code/ui/renderers/mermaid_viewer.py,sha256=TIUFLtTqdG-iFD4Mgm8OdzU_9UO14niftTJ11f4makc,1691
188
- klaude_code/ui/renderers/metadata.py,sha256=hB0l9gkt9JY_4c_JBcUB3BxHTl0sz4_zmggZesU0C3o,8105
188
+ klaude_code/ui/renderers/metadata.py,sha256=EWxh5UTSZG_vRVf6taKI_E1YkR_56U1Gs9soDuZcpq4,8576
189
189
  klaude_code/ui/renderers/sub_agent.py,sha256=g8QCFXTtFX_w8oTaGMYGuy6u5KqbFMlvzWofER0hGKk,5946
190
190
  klaude_code/ui/renderers/thinking.py,sha256=TbQxkjR6MuDXzASBK_rMaxxqvSdhfwDtVwXhOExuvlM,1946
191
191
  klaude_code/ui/renderers/tools.py,sha256=lebQHccj2tkJIjO-JB0TvCIixx-BKXHfD-egXSxBV7Y,27891
@@ -194,20 +194,20 @@ klaude_code/ui/rich/__init__.py,sha256=zEZjnHR3Fnv_sFMxwIMjoJfwDoC4GRGv3lHJzAGRq
194
194
  klaude_code/ui/rich/cjk_wrap.py,sha256=ncmifgTwF6q95iayHQyazGbntt7BRQb_Ed7aXc8JU6Y,7551
195
195
  klaude_code/ui/rich/code_panel.py,sha256=ZKuJHh-kh-hIkBXSGLERLaDbJ7I9hvtvmYKocJn39_w,4744
196
196
  klaude_code/ui/rich/live.py,sha256=qiBLPSE4KW_Dpemy5MZ5BKhkFWEN2fjXBiQHmhJrPSM,2722
197
- klaude_code/ui/rich/markdown.py,sha256=ltcm4qVX6fsqUNkPWeOwX636FsQ6-gST6uLLcXAl9yA,15397
197
+ klaude_code/ui/rich/markdown.py,sha256=0tU4i1QTsyfjTf8sL-s-Ui-MMXyq-U4f2n465qJc_Xs,16486
198
198
  klaude_code/ui/rich/quote.py,sha256=tZcxN73SfDBHF_qk0Jkh9gWBqPBn8VLp9RF36YRdKEM,1123
199
199
  klaude_code/ui/rich/searchable_text.py,sha256=PUe6MotKxSBY4FlPeojVjVQgxCsx_jiQ41bCzLp8WvE,2271
200
- klaude_code/ui/rich/status.py,sha256=GKX_kMT42epQE_xIw2TvudprcMUNF8OpH9QvVjocdhk,10812
200
+ klaude_code/ui/rich/status.py,sha256=M_pPNbZUYJ7uaxSGoSaNGrzPhHDZfcV2gjUeu8XmYNg,13192
201
201
  klaude_code/ui/rich/theme.py,sha256=5R2TpedF21iQHgF4sVwlWcCBjNJ-sOcneHScDBH3hHI,14439
202
202
  klaude_code/ui/terminal/__init__.py,sha256=GIMnsEcIAGT_vBHvTlWEdyNmAEpruyscUA6M_j3GQZU,1412
203
203
  klaude_code/ui/terminal/color.py,sha256=jvVbuysf5pnI0uAjUVeyW2HwU58dutTg2msykbu2w4Y,7197
204
204
  klaude_code/ui/terminal/control.py,sha256=WhkqEWdtzUO4iWULp-iI9VazAWmzzW52qTQXk-4Dr4s,4922
205
205
  klaude_code/ui/terminal/notifier.py,sha256=Mi7UlpAYgNj4mhsGLSdQxa2tQNfE3c6jCcT3U_v_vQ4,4577
206
206
  klaude_code/ui/terminal/progress_bar.py,sha256=MDnhPbqCnN4GDgLOlxxOEVZPDwVC_XL2NM5sl1MFNcQ,2133
207
- klaude_code/ui/terminal/selector.py,sha256=8fIgquPkLwKicBXhqbSpNrXphPNDOhH9ku0HCgIzPXc,19886
207
+ klaude_code/ui/terminal/selector.py,sha256=NblhWxUp0AW2OyepG4DHNy4yKE947Oi0OiqlafvBCEE,22144
208
208
  klaude_code/ui/utils/__init__.py,sha256=YEsCLjbCPaPza-UXTPUMTJTrc9BmNBUP5CbFWlshyOQ,15
209
209
  klaude_code/ui/utils/common.py,sha256=tqHqwgLtAyP805kwRFyoAL4EgMutcNb3Y-GAXJ4IeuM,2263
210
- klaude_code-1.5.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
211
- klaude_code-1.5.0.dist-info/entry_points.txt,sha256=kkXIXedaTOtjXPr2rVjRVVXZYlFUcBHELaqmyVlWUFA,92
212
- klaude_code-1.5.0.dist-info/METADATA,sha256=4r92SCibfnKrzXtU40YNrRIQPn7Tzdq6tBh4yOhQbUM,9091
213
- klaude_code-1.5.0.dist-info/RECORD,,
210
+ klaude_code-1.6.0.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
211
+ klaude_code-1.6.0.dist-info/entry_points.txt,sha256=kkXIXedaTOtjXPr2rVjRVVXZYlFUcBHELaqmyVlWUFA,92
212
+ klaude_code-1.6.0.dist-info/METADATA,sha256=h5IGANYxmMyc3H-4lRuaTKpumksBz0NEnJBAwbSZMuU,9091
213
+ klaude_code-1.6.0.dist-info/RECORD,,