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 +3 -56
- klaude_code/command/fork_session_cmd.py +220 -2
- klaude_code/command/refresh_cmd.py +4 -4
- klaude_code/command/resume_cmd.py +21 -11
- klaude_code/llm/usage.py +1 -1
- klaude_code/session/__init__.py +2 -2
- klaude_code/session/selector.py +32 -4
- klaude_code/session/session.py +18 -12
- klaude_code/ui/modes/repl/event_handler.py +22 -32
- klaude_code/ui/modes/repl/renderer.py +1 -1
- klaude_code/ui/renderers/developer.py +2 -2
- klaude_code/ui/renderers/metadata.py +8 -0
- klaude_code/ui/rich/markdown.py +41 -9
- klaude_code/ui/rich/status.py +83 -22
- klaude_code/ui/terminal/selector.py +72 -3
- {klaude_code-1.5.0.dist-info → klaude_code-1.6.0.dist-info}/METADATA +1 -1
- {klaude_code-1.5.0.dist-info → klaude_code-1.6.0.dist-info}/RECORD +19 -19
- {klaude_code-1.5.0.dist-info → klaude_code-1.6.0.dist-info}/WHEEL +0 -0
- {klaude_code-1.5.0.dist-info → klaude_code-1.6.0.dist-info}/entry_points.txt +0 -0
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.
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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", "
|
|
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
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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=
|
|
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(
|
|
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.
|
|
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
|
|
klaude_code/session/__init__.py
CHANGED
|
@@ -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"]
|
klaude_code/session/selector.py
CHANGED
|
@@ -33,12 +33,39 @@ class SessionSelectOption:
|
|
|
33
33
|
"""Option data for session selection UI."""
|
|
34
34
|
|
|
35
35
|
session_id: str
|
|
36
|
-
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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,
|
klaude_code/session/session.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
370
|
+
continue
|
|
364
371
|
data = cast(dict[str, Any], data_raw)
|
|
365
372
|
content = data.get("content")
|
|
366
373
|
if isinstance(content, str):
|
|
367
|
-
|
|
368
|
-
return None
|
|
374
|
+
messages.append(content)
|
|
369
375
|
except (OSError, json.JSONDecodeError):
|
|
370
|
-
|
|
371
|
-
return
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
269
|
-
"""Get
|
|
270
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
)
|
klaude_code/ui/rich/markdown.py
CHANGED
|
@@ -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
|
|
258
|
-
stable ANSI already ends with
|
|
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
|
-
|
|
263
|
-
|
|
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
|
-
|
|
268
|
-
|
|
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
|
|
404
|
-
|
|
405
|
-
|
|
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)
|
klaude_code/ui/rich/status.py
CHANGED
|
@@ -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
|
-
|
|
84
|
-
|
|
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
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
314
|
+
main_text.append(ch, style=style)
|
|
260
315
|
|
|
261
|
-
#
|
|
262
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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),
|
|
@@ -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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
|
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=
|
|
147
|
-
klaude_code/session/session.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
211
|
-
klaude_code-1.
|
|
212
|
-
klaude_code-1.
|
|
213
|
-
klaude_code-1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|