klaude-code 1.3.0__py3-none-any.whl → 1.4.1__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/runtime.py +2 -2
- klaude_code/cli/session_cmd.py +24 -9
- klaude_code/command/model_cmd.py +18 -13
- klaude_code/command/thinking_cmd.py +28 -20
- klaude_code/config/select_model.py +40 -30
- klaude_code/core/tool/__init__.py +2 -2
- klaude_code/session/selector.py +64 -47
- klaude_code/ui/modes/repl/event_handler.py +7 -1
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
- klaude_code/ui/modes/repl/key_bindings.py +42 -1
- klaude_code/ui/renderers/developer.py +15 -6
- klaude_code/ui/renderers/errors.py +1 -1
- klaude_code/ui/renderers/tools.py +1 -1
- klaude_code/ui/rich/searchable_text.py +4 -7
- klaude_code/ui/rich/status.py +55 -2
- klaude_code/ui/terminal/selector.py +283 -0
- {klaude_code-1.3.0.dist-info → klaude_code-1.4.1.dist-info}/METADATA +1 -2
- {klaude_code-1.3.0.dist-info → klaude_code-1.4.1.dist-info}/RECORD +20 -19
- {klaude_code-1.3.0.dist-info → klaude_code-1.4.1.dist-info}/WHEEL +0 -0
- {klaude_code-1.3.0.dist-info → klaude_code-1.4.1.dist-info}/entry_points.txt +0 -0
klaude_code/cli/runtime.py
CHANGED
|
@@ -273,7 +273,7 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
|
|
|
273
273
|
display.wrapped_display.renderer.stop_bottom_live()
|
|
274
274
|
|
|
275
275
|
# Pass the pre-detected theme to avoid redundant TTY queries.
|
|
276
|
-
# Querying the terminal background again after
|
|
276
|
+
# Querying the terminal background again after an interactive selection
|
|
277
277
|
# can interfere with prompt_toolkit's terminal state and break history navigation.
|
|
278
278
|
is_light_background: bool | None = None
|
|
279
279
|
if components.theme == "light":
|
|
@@ -352,7 +352,7 @@ async def run_interactive(init_config: AppInitConfig, session_id: str | None = N
|
|
|
352
352
|
op.UserInputOperation(input=user_input, session_id=active_session_id)
|
|
353
353
|
)
|
|
354
354
|
# If it's an interactive command (e.g., /model), avoid starting the ESC monitor
|
|
355
|
-
# to prevent TTY conflicts with interactive
|
|
355
|
+
# to prevent TTY conflicts with interactive prompt_toolkit UIs.
|
|
356
356
|
if has_interactive_command(user_input.text):
|
|
357
357
|
await components.executor.wait_for(submission_id)
|
|
358
358
|
else:
|
klaude_code/cli/session_cmd.py
CHANGED
|
@@ -7,8 +7,11 @@ from klaude_code.trace import log
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def _session_confirm(sessions: list[Session.SessionMetaBrief], message: str) -> bool:
|
|
10
|
-
"""Show session list and confirm deletion using
|
|
11
|
-
|
|
10
|
+
"""Show session list and confirm deletion using prompt_toolkit."""
|
|
11
|
+
|
|
12
|
+
from prompt_toolkit.styles import Style
|
|
13
|
+
|
|
14
|
+
from klaude_code.ui.terminal.selector import SelectItem, select_one
|
|
12
15
|
|
|
13
16
|
def _fmt(ts: float) -> str:
|
|
14
17
|
try:
|
|
@@ -24,14 +27,26 @@ def _session_confirm(sessions: list[Session.SessionMetaBrief], message: str) ->
|
|
|
24
27
|
first_msg += "..."
|
|
25
28
|
log(f" {_fmt(s.updated_at)} {msg_count_display:>3} msgs {first_msg}")
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
items: list[SelectItem[bool]] = [
|
|
31
|
+
SelectItem(title=[("class:text", "No\n")], value=False, search_text="No"),
|
|
32
|
+
SelectItem(title=[("class:text", "Yes\n")], value=True, search_text="Yes"),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
result = select_one(
|
|
36
|
+
message=message,
|
|
37
|
+
items=items,
|
|
38
|
+
pointer="→",
|
|
39
|
+
style=Style(
|
|
40
|
+
[
|
|
41
|
+
("question", "bold"),
|
|
42
|
+
("pointer", "ansigreen"),
|
|
43
|
+
("highlighted", "ansigreen"),
|
|
44
|
+
("text", ""),
|
|
45
|
+
]
|
|
46
|
+
),
|
|
47
|
+
use_search_filter=False,
|
|
34
48
|
)
|
|
49
|
+
return bool(result)
|
|
35
50
|
|
|
36
51
|
|
|
37
52
|
def session_clean(
|
klaude_code/command/model_cmd.py
CHANGED
|
@@ -1,38 +1,43 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
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.config.select_model import select_model_from_config
|
|
7
7
|
from klaude_code.protocol import commands, events, model, op
|
|
8
|
+
from klaude_code.ui.terminal.selector import SelectItem, select_one
|
|
8
9
|
|
|
9
|
-
SELECT_STYLE =
|
|
10
|
+
SELECT_STYLE = Style(
|
|
10
11
|
[
|
|
11
12
|
("instruction", "ansibrightblack"),
|
|
12
|
-
("pointer", "
|
|
13
|
-
("highlighted", "
|
|
13
|
+
("pointer", "ansigreen"),
|
|
14
|
+
("highlighted", "ansigreen"),
|
|
14
15
|
("text", "ansibrightblack"),
|
|
16
|
+
("question", ""),
|
|
15
17
|
]
|
|
16
18
|
)
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
def _confirm_change_default_model_sync(selected_model: str) -> bool:
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
22
|
+
items: list[SelectItem[bool]] = [
|
|
23
|
+
SelectItem(title=[("class:text", "No (session only)\n")], value=False, search_text="No"),
|
|
24
|
+
SelectItem(
|
|
25
|
+
title=[("class:text", "Yes (save as default main_model in ~/.klaude/klaude-config.yaml)\n")],
|
|
26
|
+
value=True,
|
|
27
|
+
search_text="Yes",
|
|
28
|
+
),
|
|
23
29
|
]
|
|
24
30
|
|
|
25
31
|
try:
|
|
26
32
|
# Add a blank line between the model selector and this confirmation prompt.
|
|
27
|
-
|
|
28
|
-
result =
|
|
33
|
+
print("")
|
|
34
|
+
result = select_one(
|
|
29
35
|
message=f"Save '{selected_model}' as default model?",
|
|
30
|
-
|
|
36
|
+
items=items,
|
|
31
37
|
pointer="→",
|
|
32
|
-
instruction="Use arrow keys to move, Enter to select",
|
|
33
|
-
use_jk_keys=False,
|
|
34
38
|
style=SELECT_STYLE,
|
|
35
|
-
|
|
39
|
+
use_search_filter=False,
|
|
40
|
+
)
|
|
36
41
|
except KeyboardInterrupt:
|
|
37
42
|
return False
|
|
38
43
|
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
from typing import Literal, cast
|
|
2
3
|
|
|
3
|
-
import
|
|
4
|
+
from prompt_toolkit.styles import Style
|
|
4
5
|
|
|
5
6
|
from klaude_code.command.command_abc import Agent, CommandABC, CommandResult
|
|
6
7
|
from klaude_code.protocol import commands, events, llm_param, model
|
|
8
|
+
from klaude_code.ui.terminal.selector import SelectItem, select_one
|
|
9
|
+
|
|
10
|
+
ReasoningEffort = Literal["high", "medium", "low", "minimal", "none", "xhigh"]
|
|
7
11
|
|
|
8
12
|
# Thinking level options for different protocols
|
|
9
13
|
RESPONSES_LEVELS = ["low", "medium", "high"]
|
|
@@ -118,12 +122,13 @@ def format_current_thinking(config: llm_param.LLMConfigParameter) -> str:
|
|
|
118
122
|
return "unknown protocol"
|
|
119
123
|
|
|
120
124
|
|
|
121
|
-
SELECT_STYLE =
|
|
125
|
+
SELECT_STYLE = Style(
|
|
122
126
|
[
|
|
123
127
|
("instruction", "ansibrightblack"),
|
|
124
|
-
("pointer", "
|
|
125
|
-
("highlighted", "
|
|
128
|
+
("pointer", "ansigreen"),
|
|
129
|
+
("highlighted", "ansigreen"),
|
|
126
130
|
("text", "ansibrightblack"),
|
|
131
|
+
("question", ""),
|
|
127
132
|
]
|
|
128
133
|
)
|
|
129
134
|
|
|
@@ -131,43 +136,46 @@ SELECT_STYLE = questionary.Style(
|
|
|
131
136
|
def _select_responses_thinking_sync(model_name: str | None) -> llm_param.Thinking | None:
|
|
132
137
|
"""Select thinking level for responses/codex protocol (sync version)."""
|
|
133
138
|
levels = _get_levels_for_responses(model_name)
|
|
134
|
-
|
|
139
|
+
items: list[SelectItem[str]] = [
|
|
140
|
+
SelectItem(title=[("class:text", level + "\n")], value=level, search_text=level) for level in levels
|
|
141
|
+
]
|
|
135
142
|
|
|
136
143
|
try:
|
|
137
|
-
result =
|
|
144
|
+
result = select_one(
|
|
138
145
|
message="Select reasoning effort:",
|
|
139
|
-
|
|
146
|
+
items=items,
|
|
140
147
|
pointer="→",
|
|
141
|
-
instruction="Use arrow keys to move, Enter to select",
|
|
142
|
-
use_jk_keys=False,
|
|
143
148
|
style=SELECT_STYLE,
|
|
144
|
-
|
|
149
|
+
use_search_filter=False,
|
|
150
|
+
)
|
|
145
151
|
|
|
146
152
|
if result is None:
|
|
147
153
|
return None
|
|
148
|
-
return llm_param.Thinking(reasoning_effort=result)
|
|
154
|
+
return llm_param.Thinking(reasoning_effort=cast(ReasoningEffort, result))
|
|
149
155
|
except KeyboardInterrupt:
|
|
150
156
|
return None
|
|
151
157
|
|
|
152
158
|
|
|
153
159
|
def _select_anthropic_thinking_sync() -> llm_param.Thinking | None:
|
|
154
160
|
"""Select thinking level for anthropic/openai_compatible protocol (sync version)."""
|
|
155
|
-
|
|
156
|
-
|
|
161
|
+
items: list[SelectItem[int]] = [
|
|
162
|
+
SelectItem(title=[("class:text", label + "\n")], value=tokens or 0, search_text=label)
|
|
163
|
+
for label, tokens in ANTHROPIC_LEVELS
|
|
157
164
|
]
|
|
158
165
|
|
|
159
166
|
try:
|
|
160
|
-
result =
|
|
167
|
+
result = select_one(
|
|
161
168
|
message="Select thinking level:",
|
|
162
|
-
|
|
169
|
+
items=items,
|
|
163
170
|
pointer="→",
|
|
164
|
-
instruction="Use arrow keys to move, Enter to select",
|
|
165
|
-
use_jk_keys=False,
|
|
166
171
|
style=SELECT_STYLE,
|
|
167
|
-
|
|
168
|
-
|
|
172
|
+
use_search_filter=False,
|
|
173
|
+
)
|
|
174
|
+
if result is None:
|
|
175
|
+
return None
|
|
176
|
+
if result == 0:
|
|
169
177
|
return llm_param.Thinking(type="disabled", budget_tokens=0)
|
|
170
|
-
return llm_param.Thinking(type="enabled", budget_tokens=result
|
|
178
|
+
return llm_param.Thinking(type="enabled", budget_tokens=result)
|
|
171
179
|
except KeyboardInterrupt:
|
|
172
180
|
return None
|
|
173
181
|
|
|
@@ -108,60 +108,70 @@ def select_model_from_config(preferred: str | None = None) -> str | None:
|
|
|
108
108
|
return None
|
|
109
109
|
|
|
110
110
|
try:
|
|
111
|
-
import
|
|
111
|
+
from prompt_toolkit.styles import Style
|
|
112
112
|
|
|
113
|
-
|
|
113
|
+
from klaude_code.ui.terminal.selector import SelectItem, select_one
|
|
114
114
|
|
|
115
115
|
max_model_name_length = max(len(m.model_name) for m in filtered_models)
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
def build_model_id_with_thinking(m: ModelEntry) -> str:
|
|
119
|
-
model_id = m.model_params.model or "N/A"
|
|
117
|
+
def _thinking_info(m: ModelEntry) -> str:
|
|
120
118
|
thinking = m.model_params.thinking
|
|
121
|
-
if thinking:
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
119
|
+
if not thinking:
|
|
120
|
+
return ""
|
|
121
|
+
if thinking.reasoning_effort:
|
|
122
|
+
return f"reasoning {thinking.reasoning_effort}"
|
|
123
|
+
if thinking.budget_tokens:
|
|
124
|
+
return f"thinking budget {thinking.budget_tokens}"
|
|
125
|
+
return "thinking (configured)"
|
|
126
|
+
|
|
127
|
+
items: list[SelectItem[str]] = []
|
|
131
128
|
for m in filtered_models:
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
129
|
+
model_id = m.model_params.model or "N/A"
|
|
130
|
+
first_line_prefix = f"{m.model_name:<{max_model_name_length}} → "
|
|
131
|
+
thinking_info = _thinking_info(m)
|
|
132
|
+
meta_parts: list[str] = [m.provider]
|
|
133
|
+
if thinking_info:
|
|
134
|
+
meta_parts.append(thinking_info)
|
|
135
|
+
if m.model_params.verbosity:
|
|
136
|
+
meta_parts.append(f"verbosity {m.model_params.verbosity}")
|
|
137
|
+
meta_str = " · ".join(meta_parts)
|
|
138
|
+
title = [
|
|
139
|
+
("class:msg", first_line_prefix),
|
|
140
|
+
("class:msg bold", model_id),
|
|
141
|
+
("class:meta", f" {meta_str}\n"),
|
|
142
|
+
]
|
|
143
|
+
search_text = f"{m.model_name} {model_id} {m.provider}"
|
|
144
|
+
items.append(SelectItem(title=title, value=m.model_name, search_text=search_text))
|
|
136
145
|
|
|
137
146
|
try:
|
|
138
147
|
message = f"Select a model (filtered by '{preferred}'):" if preferred else "Select a model:"
|
|
139
|
-
result =
|
|
148
|
+
result = select_one(
|
|
140
149
|
message=message,
|
|
141
|
-
|
|
150
|
+
items=items,
|
|
142
151
|
pointer="→",
|
|
143
|
-
instruction="↑↓ to move • Enter to select",
|
|
144
|
-
use_jk_keys=False,
|
|
145
152
|
use_search_filter=True,
|
|
146
|
-
|
|
153
|
+
initial_value=config.main_model,
|
|
154
|
+
style=Style(
|
|
147
155
|
[
|
|
148
|
-
("
|
|
149
|
-
("
|
|
150
|
-
("
|
|
156
|
+
("pointer", "ansigreen"),
|
|
157
|
+
("highlighted", "ansigreen"),
|
|
158
|
+
("msg", ""),
|
|
159
|
+
("meta", "fg:ansibrightblack"),
|
|
151
160
|
("text", "ansibrightblack"),
|
|
161
|
+
("question", "bold"),
|
|
162
|
+
("search_prefix", "ansibrightblack"),
|
|
152
163
|
# search filter colors at the bottom
|
|
153
164
|
("search_success", "noinherit fg:ansigreen"),
|
|
154
165
|
("search_none", "noinherit fg:ansired"),
|
|
155
|
-
("question-mark", "fg:ansigreen"),
|
|
156
166
|
]
|
|
157
167
|
),
|
|
158
|
-
)
|
|
168
|
+
)
|
|
159
169
|
if isinstance(result, str) and result in names:
|
|
160
170
|
return result
|
|
161
171
|
except KeyboardInterrupt:
|
|
162
172
|
return None
|
|
163
173
|
except Exception as e:
|
|
164
|
-
log((f"Failed to use
|
|
174
|
+
log((f"Failed to use prompt_toolkit for model selection: {e}", "yellow"))
|
|
165
175
|
# Never return an unvalidated model name here.
|
|
166
176
|
# If we can't interactively select, fall back to a known configured model.
|
|
167
177
|
if isinstance(preferred, str) and preferred in names:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
from .file.apply_patch import DiffError, process_patch
|
|
2
2
|
from .file.apply_patch_tool import ApplyPatchTool
|
|
3
|
-
from .file.move_tool import MoveTool
|
|
4
3
|
from .file.edit_tool import EditTool
|
|
4
|
+
from .file.move_tool import MoveTool
|
|
5
5
|
from .file.read_tool import ReadTool
|
|
6
6
|
from .file.write_tool import WriteTool
|
|
7
7
|
from .report_back_tool import ReportBackTool
|
|
@@ -32,11 +32,11 @@ from .web.web_search_tool import WebSearchTool
|
|
|
32
32
|
__all__ = [
|
|
33
33
|
"ApplyPatchTool",
|
|
34
34
|
"BashTool",
|
|
35
|
-
"MoveTool",
|
|
36
35
|
"DiffError",
|
|
37
36
|
"EditTool",
|
|
38
37
|
"FileTracker",
|
|
39
38
|
"MermaidTool",
|
|
39
|
+
"MoveTool",
|
|
40
40
|
"ReadTool",
|
|
41
41
|
"ReportBackTool",
|
|
42
42
|
"SafetyCheckResult",
|
klaude_code/session/selector.py
CHANGED
|
@@ -1,76 +1,93 @@
|
|
|
1
1
|
import time
|
|
2
|
-
from typing import TYPE_CHECKING
|
|
3
2
|
|
|
4
3
|
from klaude_code.trace import log, log_debug
|
|
5
|
-
|
|
6
|
-
if TYPE_CHECKING:
|
|
7
|
-
from questionary import Choice
|
|
4
|
+
from klaude_code.ui.terminal.selector import SelectItem, select_one
|
|
8
5
|
|
|
9
6
|
from .session import Session
|
|
10
7
|
|
|
11
8
|
|
|
9
|
+
def _relative_time(ts: float) -> str:
|
|
10
|
+
"""Format timestamp as relative time like '5 days ago'."""
|
|
11
|
+
now = time.time()
|
|
12
|
+
diff = now - ts
|
|
13
|
+
|
|
14
|
+
if diff < 60:
|
|
15
|
+
return "just now"
|
|
16
|
+
elif diff < 3600:
|
|
17
|
+
mins = int(diff / 60)
|
|
18
|
+
return f"{mins} minute{'s' if mins != 1 else ''} ago"
|
|
19
|
+
elif diff < 86400:
|
|
20
|
+
hours = int(diff / 3600)
|
|
21
|
+
return f"{hours} hour{'s' if hours != 1 else ''} ago"
|
|
22
|
+
elif diff < 604800:
|
|
23
|
+
days = int(diff / 86400)
|
|
24
|
+
return f"{days} day{'s' if days != 1 else ''} ago"
|
|
25
|
+
elif diff < 2592000:
|
|
26
|
+
weeks = int(diff / 604800)
|
|
27
|
+
return f"{weeks} week{'s' if weeks != 1 else ''} ago"
|
|
28
|
+
else:
|
|
29
|
+
months = int(diff / 2592000)
|
|
30
|
+
return f"{months} month{'s' if months != 1 else ''} ago"
|
|
31
|
+
|
|
32
|
+
|
|
12
33
|
def resume_select_session() -> str | None:
|
|
13
|
-
# Column widths
|
|
14
|
-
UPDATED_AT_WIDTH = 16
|
|
15
|
-
MSG_COUNT_WIDTH = 3
|
|
16
|
-
MODEL_WIDTH = 25
|
|
17
|
-
FIRST_MESSAGE_WIDTH = 50
|
|
18
34
|
sessions = Session.list_sessions()
|
|
19
35
|
if not sessions:
|
|
20
36
|
log("No sessions found for this project.")
|
|
21
37
|
return None
|
|
22
38
|
|
|
23
|
-
def _fmt(ts: float) -> str:
|
|
24
|
-
try:
|
|
25
|
-
return time.strftime("%m-%d %H:%M:%S", time.localtime(ts))
|
|
26
|
-
except (ValueError, OSError):
|
|
27
|
-
return str(ts)
|
|
28
|
-
|
|
29
39
|
try:
|
|
30
|
-
import
|
|
40
|
+
from prompt_toolkit.styles import Style
|
|
31
41
|
|
|
32
|
-
|
|
42
|
+
items: list[SelectItem[str]] = []
|
|
33
43
|
for s in sessions:
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
44
|
+
first_msg = s.first_user_message or "N/A"
|
|
45
|
+
first_msg = first_msg.strip().replace("\n", " ")
|
|
46
|
+
|
|
47
|
+
msg_count = "N/A" if s.messages_count == -1 else f"{s.messages_count} messages"
|
|
48
|
+
model = s.model_name or "N/A"
|
|
37
49
|
|
|
38
50
|
title = [
|
|
39
|
-
("class:
|
|
40
|
-
("class:
|
|
41
|
-
(
|
|
42
|
-
"class:t",
|
|
43
|
-
f"{model_display[: MODEL_WIDTH - 1] + '…' if len(model_display) > MODEL_WIDTH else model_display:<{MODEL_WIDTH}} ",
|
|
44
|
-
),
|
|
45
|
-
(
|
|
46
|
-
"class:t",
|
|
47
|
-
f"{first_user_message.strip().replace('\n', ' ↩ '):<{FIRST_MESSAGE_WIDTH}}",
|
|
48
|
-
),
|
|
51
|
+
("class:msg", f"{first_msg}\n"),
|
|
52
|
+
("class:meta", f" {msg_count} · {_relative_time(s.updated_at)} · {model} · {s.id}\n\n"),
|
|
49
53
|
]
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
+
items.append(
|
|
55
|
+
SelectItem(
|
|
56
|
+
title=title,
|
|
57
|
+
value=str(s.id),
|
|
58
|
+
search_text=f"{first_msg} {model} {s.id}",
|
|
59
|
+
)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
return select_one(
|
|
63
|
+
message="Select a session to resume:",
|
|
64
|
+
items=items,
|
|
54
65
|
pointer="→",
|
|
55
|
-
|
|
56
|
-
style=questionary.Style(
|
|
66
|
+
style=Style(
|
|
57
67
|
[
|
|
58
|
-
("
|
|
59
|
-
("
|
|
60
|
-
("
|
|
68
|
+
("msg", ""),
|
|
69
|
+
("meta", "fg:ansibrightblack"),
|
|
70
|
+
("pointer", "bold fg:ansigreen"),
|
|
71
|
+
("highlighted", "fg:ansigreen"),
|
|
72
|
+
("search_prefix", "fg:ansibrightblack"),
|
|
73
|
+
("search_success", "noinherit fg:ansigreen"),
|
|
74
|
+
("search_none", "noinherit fg:ansired"),
|
|
75
|
+
("question", "bold"),
|
|
76
|
+
("text", ""),
|
|
61
77
|
]
|
|
62
78
|
),
|
|
63
|
-
)
|
|
79
|
+
)
|
|
64
80
|
except Exception as e:
|
|
65
|
-
log_debug(f"Failed to use
|
|
81
|
+
log_debug(f"Failed to use prompt_toolkit for session select, {e}")
|
|
66
82
|
|
|
67
83
|
for i, s in enumerate(sessions, 1):
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
)
|
|
84
|
+
first_msg = (s.first_user_message or "N/A").strip().replace("\n", " ")
|
|
85
|
+
if len(first_msg) > 60:
|
|
86
|
+
first_msg = first_msg[:59] + "…"
|
|
87
|
+
msg_count = "N/A" if s.messages_count == -1 else f"{s.messages_count} msgs"
|
|
88
|
+
model = s.model_name or "N/A"
|
|
89
|
+
print(f"{i}. {first_msg}")
|
|
90
|
+
print(f" {_relative_time(s.updated_at)} · {msg_count} · {model}")
|
|
74
91
|
try:
|
|
75
92
|
raw = input("Select a session number: ").strip()
|
|
76
93
|
idx = int(raw)
|
|
@@ -12,6 +12,7 @@ from klaude_code.ui.core.stage_manager import Stage, StageManager
|
|
|
12
12
|
from klaude_code.ui.modes.repl.renderer import REPLRenderer
|
|
13
13
|
from klaude_code.ui.renderers.assistant import ASSISTANT_MESSAGE_MARK
|
|
14
14
|
from klaude_code.ui.renderers.thinking import THINKING_MESSAGE_MARK, normalize_thinking_content
|
|
15
|
+
from klaude_code.ui.rich import status as r_status
|
|
15
16
|
from klaude_code.ui.rich.markdown import MarkdownStream, ThinkingMarkdown
|
|
16
17
|
from klaude_code.ui.rich.theme import ThemeKey
|
|
17
18
|
from klaude_code.ui.terminal.notifier import Notification, NotificationType, TerminalNotifier
|
|
@@ -352,6 +353,8 @@ class DisplayEventHandler:
|
|
|
352
353
|
self.renderer.display_user_message(event)
|
|
353
354
|
|
|
354
355
|
def _on_task_start(self, event: events.TaskStartEvent) -> None:
|
|
356
|
+
if event.sub_agent_state is None:
|
|
357
|
+
r_status.set_task_start()
|
|
355
358
|
self.renderer.spinner_start()
|
|
356
359
|
self.renderer.display_task_start(event)
|
|
357
360
|
emit_osc94(OSC94States.INDETERMINATE)
|
|
@@ -501,6 +504,7 @@ class DisplayEventHandler:
|
|
|
501
504
|
async def _on_task_finish(self, event: events.TaskFinishEvent) -> None:
|
|
502
505
|
self.renderer.display_task_finish(event)
|
|
503
506
|
if not self.renderer.is_sub_agent_session(event.session_id):
|
|
507
|
+
r_status.clear_task_start()
|
|
504
508
|
emit_osc94(OSC94States.HIDDEN)
|
|
505
509
|
self.spinner_status.reset()
|
|
506
510
|
self.renderer.spinner_stop()
|
|
@@ -511,6 +515,7 @@ class DisplayEventHandler:
|
|
|
511
515
|
async def _on_interrupt(self, event: events.InterruptEvent) -> None:
|
|
512
516
|
self.renderer.spinner_stop()
|
|
513
517
|
self.spinner_status.reset()
|
|
518
|
+
r_status.clear_task_start()
|
|
514
519
|
await self.stage_manager.transition_to(Stage.WAITING)
|
|
515
520
|
emit_osc94(OSC94States.HIDDEN)
|
|
516
521
|
self.renderer.display_interrupt()
|
|
@@ -528,6 +533,7 @@ class DisplayEventHandler:
|
|
|
528
533
|
await self.stage_manager.transition_to(Stage.WAITING)
|
|
529
534
|
self.renderer.spinner_stop()
|
|
530
535
|
self.spinner_status.reset()
|
|
536
|
+
r_status.clear_task_start()
|
|
531
537
|
|
|
532
538
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
533
539
|
# Private helper methods
|
|
@@ -619,7 +625,7 @@ class DisplayEventHandler:
|
|
|
619
625
|
# 1 cell for glyph + 1 cell of padding between columns (collapsed).
|
|
620
626
|
spinner_prefix_cells = 2
|
|
621
627
|
|
|
622
|
-
hint_cells = cell_len(
|
|
628
|
+
hint_cells = cell_len(r_status.current_hint_text())
|
|
623
629
|
right_cells = cell_len(right_text.plain) if right_text is not None else 0
|
|
624
630
|
|
|
625
631
|
max_main_cells = terminal_width - spinner_prefix_cells - hint_cells - right_cells - 1
|
|
@@ -55,7 +55,7 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
55
55
|
self._pre_prompt = pre_prompt
|
|
56
56
|
self._post_prompt = post_prompt
|
|
57
57
|
# Use provided value if available to avoid redundant TTY queries that may interfere
|
|
58
|
-
# with prompt_toolkit's terminal state after
|
|
58
|
+
# with prompt_toolkit's terminal state after interactive UIs have been used.
|
|
59
59
|
self._is_light_terminal_background = (
|
|
60
60
|
is_light_background if is_light_background is not None else is_light_terminal_background(timeout=0.2)
|
|
61
61
|
)
|
|
@@ -34,6 +34,47 @@ def create_key_bindings(
|
|
|
34
34
|
"""
|
|
35
35
|
kb = KeyBindings()
|
|
36
36
|
|
|
37
|
+
def _should_submit_instead_of_accepting_completion(buf: Buffer) -> bool:
|
|
38
|
+
"""Return True when Enter should submit even if completions are visible.
|
|
39
|
+
|
|
40
|
+
We show completions proactively for contexts like `/`.
|
|
41
|
+
If the user already typed an exact candidate (e.g. `/clear`), accepting
|
|
42
|
+
a completion often only adds a trailing space and makes Enter require
|
|
43
|
+
two presses. In that case, prefer submitting.
|
|
44
|
+
"""
|
|
45
|
+
state = buf.complete_state
|
|
46
|
+
if state is None or not state.completions:
|
|
47
|
+
return False
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
doc = buf.document # type: ignore[reportUnknownMemberType]
|
|
51
|
+
text = cast(str, doc.text) # type: ignore[reportUnknownMemberType]
|
|
52
|
+
cursor_pos = cast(int, doc.cursor_position) # type: ignore[reportUnknownMemberType]
|
|
53
|
+
except Exception:
|
|
54
|
+
return False
|
|
55
|
+
|
|
56
|
+
# Only apply this heuristic when the caret is at the end of the buffer.
|
|
57
|
+
if cursor_pos != len(text):
|
|
58
|
+
return False
|
|
59
|
+
|
|
60
|
+
for completion in state.completions:
|
|
61
|
+
try:
|
|
62
|
+
start = cursor_pos + completion.start_position
|
|
63
|
+
if start < 0 or start > cursor_pos:
|
|
64
|
+
continue
|
|
65
|
+
|
|
66
|
+
replaced = text[start:cursor_pos]
|
|
67
|
+
inserted = completion.text
|
|
68
|
+
|
|
69
|
+
# If the user already typed an exact candidate, don't force
|
|
70
|
+
# accepting a completion (which often just adds a space).
|
|
71
|
+
if replaced == inserted or replaced == inserted.rstrip():
|
|
72
|
+
return True
|
|
73
|
+
except Exception:
|
|
74
|
+
continue
|
|
75
|
+
|
|
76
|
+
return False
|
|
77
|
+
|
|
37
78
|
def _select_first_completion_if_needed(buf: Buffer) -> None:
|
|
38
79
|
"""Ensure the completion menu has an active selection.
|
|
39
80
|
|
|
@@ -98,7 +139,7 @@ def create_key_bindings(
|
|
|
98
139
|
# When completions are visible, Enter accepts the current selection.
|
|
99
140
|
# This aligns with common TUI completion UX: navigation doesn't modify
|
|
100
141
|
# the buffer, and Enter/Tab inserts the selected option.
|
|
101
|
-
if _accept_current_completion(buf):
|
|
142
|
+
if not _should_submit_instead_of_accepting_completion(buf) and _accept_current_completion(buf):
|
|
102
143
|
return
|
|
103
144
|
|
|
104
145
|
# If the entire buffer is whitespace-only, insert a newline rather than submitting.
|
|
@@ -67,23 +67,32 @@ def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
|
|
|
67
67
|
|
|
68
68
|
if e.item.at_files:
|
|
69
69
|
grid = create_grid()
|
|
70
|
+
# Group at_files by (operation, mentioned_in)
|
|
71
|
+
grouped: dict[tuple[str, str | None], list[str]] = {}
|
|
70
72
|
for at_file in e.item.at_files:
|
|
71
|
-
|
|
73
|
+
key = (at_file.operation, at_file.mentioned_in)
|
|
74
|
+
if key not in grouped:
|
|
75
|
+
grouped[key] = []
|
|
76
|
+
grouped[key].append(at_file.path)
|
|
77
|
+
|
|
78
|
+
for (operation, mentioned_in), paths in grouped.items():
|
|
79
|
+
path_texts = Text(", ", ThemeKey.REMINDER).join(render_path(p, ThemeKey.REMINDER_BOLD) for p in paths)
|
|
80
|
+
if mentioned_in:
|
|
72
81
|
grid.add_row(
|
|
73
82
|
Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
|
|
74
83
|
Text.assemble(
|
|
75
|
-
(f"{
|
|
76
|
-
|
|
84
|
+
(f"{operation} ", ThemeKey.REMINDER),
|
|
85
|
+
path_texts,
|
|
77
86
|
(" mentioned in ", ThemeKey.REMINDER),
|
|
78
|
-
render_path(
|
|
87
|
+
render_path(mentioned_in, ThemeKey.REMINDER_BOLD),
|
|
79
88
|
),
|
|
80
89
|
)
|
|
81
90
|
else:
|
|
82
91
|
grid.add_row(
|
|
83
92
|
Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
|
|
84
93
|
Text.assemble(
|
|
85
|
-
(f"{
|
|
86
|
-
|
|
94
|
+
(f"{operation} ", ThemeKey.REMINDER),
|
|
95
|
+
path_texts,
|
|
87
96
|
),
|
|
88
97
|
)
|
|
89
98
|
parts.append(grid)
|
|
@@ -6,9 +6,7 @@ from collections.abc import Iterable, Sequence
|
|
|
6
6
|
class SearchableFormattedText:
|
|
7
7
|
"""
|
|
8
8
|
Wrapper for prompt_toolkit formatted text that also supports string-like
|
|
9
|
-
methods
|
|
10
|
-
|
|
11
|
-
This allows using ``use_search_filter=True`` with a formatted ``Choice.title``.
|
|
9
|
+
methods commonly expected by search filters (e.g., ``.lower()``).
|
|
12
10
|
|
|
13
11
|
- ``fragments``: A sequence of (style, text) tuples accepted by
|
|
14
12
|
prompt_toolkit's ``to_formatted_text``.
|
|
@@ -32,7 +30,7 @@ class SearchableFormattedText:
|
|
|
32
30
|
def __str__(self) -> str: # pragma: no cover - utility
|
|
33
31
|
return self._plain
|
|
34
32
|
|
|
35
|
-
# Minimal string API
|
|
33
|
+
# Minimal string API for search filtering.
|
|
36
34
|
def lower(self) -> str:
|
|
37
35
|
return self._plain.lower()
|
|
38
36
|
|
|
@@ -47,10 +45,9 @@ class SearchableFormattedText:
|
|
|
47
45
|
|
|
48
46
|
class SearchableFormattedList(list[tuple[str, str]]):
|
|
49
47
|
"""
|
|
50
|
-
List variant compatible with
|
|
48
|
+
List variant compatible with prompt_toolkit formatted-text usage.
|
|
51
49
|
|
|
52
|
-
- Behaves like ``List[Tuple[str, str]]`` for rendering (so ``isinstance(..., list)`` works)
|
|
53
|
-
preserving existing styling behavior in questionary.
|
|
50
|
+
- Behaves like ``List[Tuple[str, str]]`` for rendering (so ``isinstance(..., list)`` works).
|
|
54
51
|
- Provides ``.lower()``/``.upper()`` returning the plain text for search filtering.
|
|
55
52
|
"""
|
|
56
53
|
|
klaude_code/ui/rich/status.py
CHANGED
|
@@ -31,6 +31,7 @@ random.shuffle(BREATHING_SPINNER_GLYPHS)
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
_process_start: float | None = None
|
|
34
|
+
_task_start: float | None = None
|
|
34
35
|
|
|
35
36
|
|
|
36
37
|
def _elapsed_since_start() -> float:
|
|
@@ -42,6 +43,59 @@ def _elapsed_since_start() -> float:
|
|
|
42
43
|
return now - _process_start
|
|
43
44
|
|
|
44
45
|
|
|
46
|
+
def set_task_start(start: float | None = None) -> None:
|
|
47
|
+
"""Set the current task start time (perf_counter seconds)."""
|
|
48
|
+
|
|
49
|
+
global _task_start
|
|
50
|
+
_task_start = time.perf_counter() if start is None else start
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def clear_task_start() -> None:
|
|
54
|
+
"""Clear the current task start time."""
|
|
55
|
+
|
|
56
|
+
global _task_start
|
|
57
|
+
_task_start = None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _task_elapsed_seconds(now: float | None = None) -> float | None:
|
|
61
|
+
if _task_start is None:
|
|
62
|
+
return None
|
|
63
|
+
current = time.perf_counter() if now is None else now
|
|
64
|
+
return max(0.0, current - _task_start)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _format_elapsed_compact(seconds: float) -> str:
|
|
68
|
+
total_seconds = max(0, int(seconds))
|
|
69
|
+
if total_seconds < 60:
|
|
70
|
+
return f"{total_seconds}s"
|
|
71
|
+
|
|
72
|
+
minutes, sec = divmod(total_seconds, 60)
|
|
73
|
+
if minutes < 60:
|
|
74
|
+
return f"{minutes}m{sec:02d}s"
|
|
75
|
+
|
|
76
|
+
hours, minute = divmod(minutes, 60)
|
|
77
|
+
return f"{hours}h{minute:02d}m{sec:02d}s"
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def current_hint_text(*, min_time_width: int = 0) -> str:
|
|
81
|
+
"""Return the full hint string shown on the status line.
|
|
82
|
+
|
|
83
|
+
Includes an optional elapsed time prefix (right-aligned, min width) and
|
|
84
|
+
the constant hint suffix.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
elapsed = _task_elapsed_seconds()
|
|
88
|
+
if elapsed is None:
|
|
89
|
+
return const.STATUS_HINT_TEXT
|
|
90
|
+
time_text = _format_elapsed_compact(elapsed)
|
|
91
|
+
if min_time_width > 0:
|
|
92
|
+
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})"
|
|
97
|
+
|
|
98
|
+
|
|
45
99
|
def _shimmer_profile(main_text: str) -> list[tuple[str, float]]:
|
|
46
100
|
"""Compute per-character shimmer intensity for a horizontal band.
|
|
47
101
|
|
|
@@ -176,7 +230,6 @@ class ShimmerStatusText:
|
|
|
176
230
|
self._main_text = text
|
|
177
231
|
else:
|
|
178
232
|
self._main_text = Text(main_text, style=main_style)
|
|
179
|
-
self._hint_text = Text(const.STATUS_HINT_TEXT)
|
|
180
233
|
self._hint_style = ThemeKey.STATUS_HINT
|
|
181
234
|
self._right_text = right_text
|
|
182
235
|
|
|
@@ -206,7 +259,7 @@ class ShimmerStatusText:
|
|
|
206
259
|
result.append(ch, style=style)
|
|
207
260
|
|
|
208
261
|
# Append hint text without shimmer
|
|
209
|
-
result.append(
|
|
262
|
+
result.append(current_hint_text().strip("\n"), style=hint_style)
|
|
210
263
|
|
|
211
264
|
return result
|
|
212
265
|
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from functools import partial
|
|
7
|
+
|
|
8
|
+
from prompt_toolkit.application import Application
|
|
9
|
+
from prompt_toolkit.application.current import get_app
|
|
10
|
+
from prompt_toolkit.buffer import Buffer
|
|
11
|
+
from prompt_toolkit.filters import Always, Condition
|
|
12
|
+
from prompt_toolkit.key_binding import KeyBindings, KeyPressEvent, merge_key_bindings
|
|
13
|
+
from prompt_toolkit.key_binding.defaults import load_key_bindings
|
|
14
|
+
from prompt_toolkit.keys import Keys
|
|
15
|
+
from prompt_toolkit.layout import ConditionalContainer, Float, FloatContainer, HSplit, Layout, VSplit, Window
|
|
16
|
+
from prompt_toolkit.layout.containers import Container, ScrollOffsets
|
|
17
|
+
from prompt_toolkit.layout.controls import BufferControl, FormattedTextControl
|
|
18
|
+
from prompt_toolkit.styles import Style, merge_styles
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True, slots=True)
|
|
22
|
+
class SelectItem[T]:
|
|
23
|
+
"""One selectable item for terminal selection UI."""
|
|
24
|
+
|
|
25
|
+
title: list[tuple[str, str]]
|
|
26
|
+
value: T
|
|
27
|
+
search_text: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def select_one[T](
|
|
31
|
+
*,
|
|
32
|
+
message: str,
|
|
33
|
+
items: list[SelectItem[T]],
|
|
34
|
+
pointer: str = "→",
|
|
35
|
+
style: Style | None = None,
|
|
36
|
+
use_search_filter: bool = True,
|
|
37
|
+
initial_value: T | None = None,
|
|
38
|
+
search_placeholder: str = "type to search",
|
|
39
|
+
) -> T | None:
|
|
40
|
+
"""Terminal single-choice selector based on prompt_toolkit.
|
|
41
|
+
|
|
42
|
+
Features:
|
|
43
|
+
- Search-as-you-type filter (optional)
|
|
44
|
+
- Multi-line titles (via formatted text fragments)
|
|
45
|
+
- Highlight entire pointed item via `class:highlighted`
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
if not items:
|
|
49
|
+
return None
|
|
50
|
+
|
|
51
|
+
# Non-interactive environments should not enter an interactive prompt.
|
|
52
|
+
if not sys.stdin.isatty() or not sys.stdout.isatty():
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
pointed_at = 0
|
|
56
|
+
|
|
57
|
+
search_buffer: Buffer | None = None
|
|
58
|
+
if use_search_filter:
|
|
59
|
+
search_buffer = Buffer()
|
|
60
|
+
|
|
61
|
+
def visible_indices() -> tuple[list[int], bool]:
|
|
62
|
+
filter_text = search_buffer.text if (use_search_filter and search_buffer is not None) else ""
|
|
63
|
+
if not filter_text:
|
|
64
|
+
return list(range(len(items))), True
|
|
65
|
+
|
|
66
|
+
needle = filter_text.lower()
|
|
67
|
+
matched = [i for i, it in enumerate(items) if needle in it.search_text.lower()]
|
|
68
|
+
if matched:
|
|
69
|
+
return matched, True
|
|
70
|
+
return list(range(len(items))), False
|
|
71
|
+
|
|
72
|
+
def _restyle_title(title: list[tuple[str, str]], cls: str) -> list[tuple[str, str]]:
|
|
73
|
+
# Keep simple text attributes like bold/italic while overriding colors via `cls`.
|
|
74
|
+
keep_attrs = {"bold", "italic", "underline", "reverse", "blink", "strike"}
|
|
75
|
+
restyled: list[tuple[str, str]] = []
|
|
76
|
+
for old_style, text in title:
|
|
77
|
+
attrs = [tok for tok in old_style.split() if tok in keep_attrs]
|
|
78
|
+
style = f"{cls} {' '.join(attrs)}".strip()
|
|
79
|
+
restyled.append((style, text))
|
|
80
|
+
return restyled
|
|
81
|
+
|
|
82
|
+
def get_header_tokens() -> list[tuple[str, str]]:
|
|
83
|
+
return [("class:question", message + " ")]
|
|
84
|
+
|
|
85
|
+
def get_choices_tokens() -> list[tuple[str, str]]:
|
|
86
|
+
nonlocal pointed_at
|
|
87
|
+
indices, _found = visible_indices()
|
|
88
|
+
if not indices:
|
|
89
|
+
return [("class:text", "(no items)\n")]
|
|
90
|
+
|
|
91
|
+
pointed_at %= len(indices)
|
|
92
|
+
tokens: list[tuple[str, str]] = []
|
|
93
|
+
|
|
94
|
+
pointer_pad = " " * (2 + len(pointer))
|
|
95
|
+
pointed_prefix = f" {pointer} "
|
|
96
|
+
|
|
97
|
+
for pos, idx in enumerate(indices):
|
|
98
|
+
is_pointed = pos == pointed_at
|
|
99
|
+
|
|
100
|
+
if is_pointed:
|
|
101
|
+
tokens.append(("class:pointer", pointed_prefix))
|
|
102
|
+
tokens.append(("[SetCursorPosition]", ""))
|
|
103
|
+
else:
|
|
104
|
+
tokens.append(("class:text", pointer_pad))
|
|
105
|
+
|
|
106
|
+
title_tokens = _restyle_title(items[idx].title, "class:highlighted") if is_pointed else items[idx].title
|
|
107
|
+
tokens.extend(title_tokens)
|
|
108
|
+
|
|
109
|
+
return tokens
|
|
110
|
+
|
|
111
|
+
def _on_search_changed(_buf: Buffer) -> None:
|
|
112
|
+
nonlocal pointed_at
|
|
113
|
+
pointed_at = 0
|
|
114
|
+
with contextlib.suppress(Exception):
|
|
115
|
+
get_app().invalidate()
|
|
116
|
+
|
|
117
|
+
kb = KeyBindings()
|
|
118
|
+
|
|
119
|
+
@kb.add(Keys.ControlC, eager=True)
|
|
120
|
+
@kb.add(Keys.ControlQ, eager=True)
|
|
121
|
+
def _cancel(event: KeyPressEvent) -> None:
|
|
122
|
+
event.app.exit(result=None)
|
|
123
|
+
|
|
124
|
+
_ = _cancel # registered via decorator
|
|
125
|
+
|
|
126
|
+
@kb.add(Keys.Down, eager=True)
|
|
127
|
+
def _down(event: KeyPressEvent) -> None:
|
|
128
|
+
nonlocal pointed_at
|
|
129
|
+
pointed_at += 1
|
|
130
|
+
event.app.invalidate()
|
|
131
|
+
|
|
132
|
+
_ = _down # registered via decorator
|
|
133
|
+
|
|
134
|
+
@kb.add(Keys.Up, eager=True)
|
|
135
|
+
def _up(event: KeyPressEvent) -> None:
|
|
136
|
+
nonlocal pointed_at
|
|
137
|
+
pointed_at -= 1
|
|
138
|
+
event.app.invalidate()
|
|
139
|
+
|
|
140
|
+
_ = _up # registered via decorator
|
|
141
|
+
|
|
142
|
+
@kb.add(Keys.Enter, eager=True)
|
|
143
|
+
def _enter(event: KeyPressEvent) -> None:
|
|
144
|
+
indices, _ = visible_indices()
|
|
145
|
+
if not indices:
|
|
146
|
+
event.app.exit(result=None)
|
|
147
|
+
return
|
|
148
|
+
idx = indices[pointed_at % len(indices)]
|
|
149
|
+
event.app.exit(result=items[idx].value)
|
|
150
|
+
|
|
151
|
+
_ = _enter # registered via decorator
|
|
152
|
+
|
|
153
|
+
@kb.add(Keys.Escape, eager=True)
|
|
154
|
+
def _esc(event: KeyPressEvent) -> None:
|
|
155
|
+
nonlocal pointed_at
|
|
156
|
+
if use_search_filter and search_buffer is not None and search_buffer.text:
|
|
157
|
+
search_buffer.reset(append_to_history=False)
|
|
158
|
+
pointed_at = 0
|
|
159
|
+
event.app.invalidate()
|
|
160
|
+
return
|
|
161
|
+
event.app.exit(result=None)
|
|
162
|
+
|
|
163
|
+
_ = _esc # registered via decorator
|
|
164
|
+
|
|
165
|
+
if use_search_filter and search_buffer is not None:
|
|
166
|
+
search_buffer.on_text_changed += _on_search_changed
|
|
167
|
+
|
|
168
|
+
if initial_value is not None:
|
|
169
|
+
try:
|
|
170
|
+
full_index = next(i for i, it in enumerate(items) if it.value == initial_value)
|
|
171
|
+
indices, _ = visible_indices()
|
|
172
|
+
pointed_at = indices.index(full_index) if full_index in indices else 0
|
|
173
|
+
except StopIteration:
|
|
174
|
+
pointed_at = 0
|
|
175
|
+
|
|
176
|
+
header_window = Window(
|
|
177
|
+
FormattedTextControl(get_header_tokens),
|
|
178
|
+
height=1,
|
|
179
|
+
dont_extend_height=Always(),
|
|
180
|
+
always_hide_cursor=Always(),
|
|
181
|
+
)
|
|
182
|
+
spacer_window = Window(
|
|
183
|
+
FormattedTextControl([("", "")]),
|
|
184
|
+
height=1,
|
|
185
|
+
dont_extend_height=Always(),
|
|
186
|
+
always_hide_cursor=Always(),
|
|
187
|
+
)
|
|
188
|
+
list_window = Window(
|
|
189
|
+
FormattedTextControl(get_choices_tokens),
|
|
190
|
+
scroll_offsets=ScrollOffsets(top=0, bottom=2),
|
|
191
|
+
allow_scroll_beyond_bottom=True,
|
|
192
|
+
dont_extend_height=Always(),
|
|
193
|
+
always_hide_cursor=Always(),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
search_container = None
|
|
197
|
+
search_input_window: Window | None = None
|
|
198
|
+
if use_search_filter and search_buffer is not None:
|
|
199
|
+
placeholder_text = f"{search_placeholder} · ↑↓ to select"
|
|
200
|
+
|
|
201
|
+
search_prefix_window = Window(
|
|
202
|
+
FormattedTextControl([("class:search_prefix", "/ ")]),
|
|
203
|
+
width=2,
|
|
204
|
+
height=1,
|
|
205
|
+
dont_extend_height=Always(),
|
|
206
|
+
always_hide_cursor=Always(),
|
|
207
|
+
)
|
|
208
|
+
input_window = Window(
|
|
209
|
+
BufferControl(buffer=search_buffer),
|
|
210
|
+
height=1,
|
|
211
|
+
dont_extend_height=Always(),
|
|
212
|
+
style="class:search_input",
|
|
213
|
+
)
|
|
214
|
+
placeholder_window = ConditionalContainer(
|
|
215
|
+
content=Window(
|
|
216
|
+
FormattedTextControl([("class:search_placeholder", placeholder_text)]),
|
|
217
|
+
height=1,
|
|
218
|
+
dont_extend_height=Always(),
|
|
219
|
+
always_hide_cursor=Always(),
|
|
220
|
+
),
|
|
221
|
+
filter=Condition(lambda: search_buffer.text == ""),
|
|
222
|
+
)
|
|
223
|
+
search_input_window = input_window
|
|
224
|
+
search_input_container = FloatContainer(
|
|
225
|
+
content=input_window,
|
|
226
|
+
floats=[Float(content=placeholder_window, top=0, left=0)],
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
def _rounded_frame(body: Container) -> HSplit:
|
|
230
|
+
border = partial(Window, style="class:frame.border", height=1)
|
|
231
|
+
top = VSplit(
|
|
232
|
+
[
|
|
233
|
+
border(width=1, char="╭"),
|
|
234
|
+
border(char="─"),
|
|
235
|
+
border(width=1, char="╮"),
|
|
236
|
+
],
|
|
237
|
+
height=1,
|
|
238
|
+
padding=0,
|
|
239
|
+
)
|
|
240
|
+
middle = VSplit(
|
|
241
|
+
[
|
|
242
|
+
border(width=1, char="│"),
|
|
243
|
+
body,
|
|
244
|
+
border(width=1, char="│"),
|
|
245
|
+
],
|
|
246
|
+
padding=0,
|
|
247
|
+
)
|
|
248
|
+
bottom = VSplit(
|
|
249
|
+
[
|
|
250
|
+
border(width=1, char="╰"),
|
|
251
|
+
border(char="─"),
|
|
252
|
+
border(width=1, char="╯"),
|
|
253
|
+
],
|
|
254
|
+
height=1,
|
|
255
|
+
padding=0,
|
|
256
|
+
)
|
|
257
|
+
return HSplit([top, middle, bottom], padding=0, style="class:frame")
|
|
258
|
+
|
|
259
|
+
search_container = _rounded_frame(VSplit([search_prefix_window, search_input_container], padding=0))
|
|
260
|
+
|
|
261
|
+
base_style = Style(
|
|
262
|
+
[
|
|
263
|
+
("frame.border", "fg:ansibrightblack"),
|
|
264
|
+
("frame.label", "fg:ansibrightblack italic"),
|
|
265
|
+
("search_prefix", "fg:ansibrightblack"),
|
|
266
|
+
("search_placeholder", "fg:ansibrightblack italic"),
|
|
267
|
+
]
|
|
268
|
+
)
|
|
269
|
+
merged_style = merge_styles([base_style, style] if style is not None else [base_style])
|
|
270
|
+
|
|
271
|
+
root_children: list[Container] = [header_window, spacer_window, list_window]
|
|
272
|
+
if search_container is not None:
|
|
273
|
+
root_children.append(search_container)
|
|
274
|
+
|
|
275
|
+
app: Application[T | None] = Application(
|
|
276
|
+
layout=Layout(HSplit(root_children), focused_element=search_input_window or list_window),
|
|
277
|
+
key_bindings=merge_key_bindings([load_key_bindings(), kb]),
|
|
278
|
+
style=merged_style,
|
|
279
|
+
mouse_support=False,
|
|
280
|
+
full_screen=False,
|
|
281
|
+
erase_when_done=True,
|
|
282
|
+
)
|
|
283
|
+
return app.run()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: klaude-code
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.4.1
|
|
4
4
|
Summary: Minimal code agent CLI
|
|
5
5
|
Requires-Dist: anthropic>=0.66.0
|
|
6
6
|
Requires-Dist: chardet>=5.2.0
|
|
@@ -12,7 +12,6 @@ Requires-Dist: pillow>=12.0.0
|
|
|
12
12
|
Requires-Dist: prompt-toolkit>=3.0.52
|
|
13
13
|
Requires-Dist: pydantic>=2.11.7
|
|
14
14
|
Requires-Dist: pyyaml>=6.0.2
|
|
15
|
-
Requires-Dist: questionary>=2.1.1
|
|
16
15
|
Requires-Dist: rich>=14.1.0
|
|
17
16
|
Requires-Dist: trafilatura>=2.0.0
|
|
18
17
|
Requires-Dist: typer>=0.17.3
|
|
@@ -11,9 +11,9 @@ klaude_code/cli/config_cmd.py,sha256=hlvslLNgdRHkokq1Pnam0XOdR3jqO3K0vNLqtWnPa6Q
|
|
|
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
13
|
klaude_code/cli/main.py,sha256=td_nMg0AyFDdyh3TLi7WCpi9DyAehduI4jZSOAaCNXI,12857
|
|
14
|
-
klaude_code/cli/runtime.py,sha256=
|
|
14
|
+
klaude_code/cli/runtime.py,sha256=9PbyCiyaSm3wQZViIMOgJRNnjgLLIkCkQrDcn8yMn1E,15190
|
|
15
15
|
klaude_code/cli/self_update.py,sha256=iGuj0i869Zi0M70W52-VVLxZp90ISr30fQpZkHGMK2o,8059
|
|
16
|
-
klaude_code/cli/session_cmd.py,sha256=
|
|
16
|
+
klaude_code/cli/session_cmd.py,sha256=q2YarlV6KARkFnbm_36ZUvBh8Noj8B7TlMg1RIlt1GE,3154
|
|
17
17
|
klaude_code/command/__init__.py,sha256=hN239WtGddSsln7qF-VMJZw1FNOTb1gBikWZrzfoC9M,3366
|
|
18
18
|
klaude_code/command/clear_cmd.py,sha256=3Ru6pFmOwru06XTLTuEGNUhKgy3COOaNe22Dk0TpGrQ,719
|
|
19
19
|
klaude_code/command/command_abc.py,sha256=wZl_azY6Dpd4OvjtkSEPI3ilXaygLIVkO7NCgNlrofQ,2536
|
|
@@ -22,7 +22,7 @@ klaude_code/command/export_cmd.py,sha256=Cs7YXWtos-ZfN9OEppIl8Xrb017kDG7R6hGiilq
|
|
|
22
22
|
klaude_code/command/export_online_cmd.py,sha256=RYYLnkLtg6edsgysmhsfTw16ncFRIT6PqeTdWhWXLHE,6094
|
|
23
23
|
klaude_code/command/fork_session_cmd.py,sha256=T3o0mOVcqL2Ts39Ijl4t2Sl0GYbvh8zyd9z21M-AThU,1682
|
|
24
24
|
klaude_code/command/help_cmd.py,sha256=yQJnVtj6sgXQdGsi4u9aS7EcjJLSrXccUA-v_bqmsRw,1633
|
|
25
|
-
klaude_code/command/model_cmd.py,sha256=
|
|
25
|
+
klaude_code/command/model_cmd.py,sha256=Ff-2tkvpdnnSoCN98FUjHMtoA2qk1PNNkhzZlW22HAk,3053
|
|
26
26
|
klaude_code/command/prompt-init.md,sha256=a4_FQ3gKizqs2vl9oEY5jtG6HNhv3f-1b5RSCFq0A18,1873
|
|
27
27
|
klaude_code/command/prompt-jj-describe.md,sha256=n-7hiXU8oodCMR3ipNyRR86pAUzXMz6seloU9a6QQnY,974
|
|
28
28
|
klaude_code/command/prompt_command.py,sha256=rMi-ZRLpUSt1t0IQVtwnzIYqcrXK-MwZrabbZ8dc8U4,2774
|
|
@@ -32,13 +32,13 @@ klaude_code/command/release_notes_cmd.py,sha256=FIrBRfKTlXEp8mBh15buNjgOrl_GMX7F
|
|
|
32
32
|
klaude_code/command/resume_cmd.py,sha256=z5PNVZ_jzH-8YWPd0Qjj8mFwE8kjPSpM_GohExXL_QA,1970
|
|
33
33
|
klaude_code/command/status_cmd.py,sha256=95cp4-Qg7ju4TZhKIV6_dfv1rrjcyNO6816NHtfk6v0,5413
|
|
34
34
|
klaude_code/command/terminal_setup_cmd.py,sha256=SivM1gX_anGY_8DCQNFZ5VblFqt4sVgCMEWPRlo6K5w,10911
|
|
35
|
-
klaude_code/command/thinking_cmd.py,sha256=
|
|
35
|
+
klaude_code/command/thinking_cmd.py,sha256=uip1Cvam2f9j44yyT57j8H5LjPeLP7mkRK3A5bmuAMU,9327
|
|
36
36
|
klaude_code/config/__init__.py,sha256=Qe1BeMekBfO2-Zd30x33lB70hdM1QQZGrp4DbWSQ-II,353
|
|
37
37
|
klaude_code/config/assets/__init__.py,sha256=uMUfmXT3I-gYiI-HVr1DrE60mx5cY1o8V7SYuGqOmvY,32
|
|
38
38
|
klaude_code/config/assets/builtin_config.yaml,sha256=9sfpcVqY3uWVGSdyteH3P_B8ZDwPhfJAoT2Q5o7I1bk,5605
|
|
39
39
|
klaude_code/config/builtin_config.py,sha256=RgbawLV4yKk1IhJsQn04RkitDyLLhCwTqQ3nJkdvHGI,1113
|
|
40
40
|
klaude_code/config/config.py,sha256=rTMU-7IYl_fmthB4LsrCaSgUVttNSw7Agif8XRCmU1g,16331
|
|
41
|
-
klaude_code/config/select_model.py,sha256=
|
|
41
|
+
klaude_code/config/select_model.py,sha256=0ON5O2ITBvyBNU80uEFQr65z2wbhdHtlzYLVTYx0AjA,7303
|
|
42
42
|
klaude_code/const.py,sha256=Xc6UKku2sGQE05mvPNCpBbKK205vJrS9CaNAeKvu1AA,4612
|
|
43
43
|
klaude_code/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
44
44
|
klaude_code/core/agent.py,sha256=bWm-UFX_0-KAy5j_YHH8X8o3MJT4-40Ni2EaDP2SL5k,5819
|
|
@@ -60,7 +60,7 @@ klaude_code/core/prompts/prompt-sub-agent-web.md,sha256=ewS7-h8_u4QZftFpqrZWpht9
|
|
|
60
60
|
klaude_code/core/prompts/prompt-sub-agent.md,sha256=dmmdsOenbAOfqG6FmdR88spOLZkXmntDBs-cmZ9DN_g,897
|
|
61
61
|
klaude_code/core/reminders.py,sha256=M_YPlOuZ2TpTjoxfEp1FbswB4yuk9_XUgSGb9MoMBCA,24741
|
|
62
62
|
klaude_code/core/task.py,sha256=V0z-QSDSxB4d4mcqXl6z_KydG_yT0JhD7274AQbNGKM,11858
|
|
63
|
-
klaude_code/core/tool/__init__.py,sha256=
|
|
63
|
+
klaude_code/core/tool/__init__.py,sha256=Y1r-xka9Zk5e5SrB0kcweFp4LyH0aafQ-BDiwYCAqFY,1992
|
|
64
64
|
klaude_code/core/tool/file/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
65
65
|
klaude_code/core/tool/file/_utils.py,sha256=OG4BE9WyJqzH8ilVCL3D9yvAcHk-r-L9snd-E8gO_io,967
|
|
66
66
|
klaude_code/core/tool/file/apply_patch.py,sha256=LZd3pYQ9ow_TxiFnqYuzD216HmvkLX6lW6BoMd9iQRs,17080
|
|
@@ -141,7 +141,7 @@ klaude_code/protocol/tools.py,sha256=ejhMCBBMz1ODbPEiynhzjB-aLbIRKL-wipPFv-nEz4g
|
|
|
141
141
|
klaude_code/session/__init__.py,sha256=oXcDA5w-gJCbzmlF8yuWy3ezIW9DgFBNUs-gJHUJ-Rc,121
|
|
142
142
|
klaude_code/session/codec.py,sha256=ummbqT7t6uHHXtaS9lOkyhi1h0YpMk7SNSms8DyGAHU,2015
|
|
143
143
|
klaude_code/session/export.py,sha256=dj-IRUNtXL8uONDj9bsEXcEHKyeHY7lIcXv80yP88h4,31022
|
|
144
|
-
klaude_code/session/selector.py,sha256=
|
|
144
|
+
klaude_code/session/selector.py,sha256=T4CUAJ6trrN14W9jXm1rnVMuoetKtn3FZoQhftnxBoo,3409
|
|
145
145
|
klaude_code/session/session.py,sha256=otWpPnCk5LGS5IW_zTdeXBtLdxbBlEK2jH5FnrOIpF4,16969
|
|
146
146
|
klaude_code/session/store.py,sha256=-e-lInCB3N1nFLlet7bipkmPk1PXmGthuMxv5z3hg5o,6953
|
|
147
147
|
klaude_code/session/templates/export_session.html,sha256=bA27AkcC7DQRoWmcMBeaR8WOx1z76hezEDf0aYH-0HQ,119780
|
|
@@ -171,22 +171,22 @@ klaude_code/ui/modes/repl/__init__.py,sha256=_0II73jlz5JUtvJsZ9sGRJzeHIQyJJpaI0e
|
|
|
171
171
|
klaude_code/ui/modes/repl/clipboard.py,sha256=ZCpk7kRSXGhh0Q_BWtUUuSYT7ZOqRjAoRcg9T9n48Wo,5137
|
|
172
172
|
klaude_code/ui/modes/repl/completers.py,sha256=AElBFnWculNsSadom7ScnKf-P_vilC4V5AUn2qpRkXE,30005
|
|
173
173
|
klaude_code/ui/modes/repl/display.py,sha256=06wawOHWO2ItEA9EIEh97p3GDID7TJhAtpaA03nPQXs,2335
|
|
174
|
-
klaude_code/ui/modes/repl/event_handler.py,sha256=
|
|
175
|
-
klaude_code/ui/modes/repl/input_prompt_toolkit.py,sha256=
|
|
176
|
-
klaude_code/ui/modes/repl/key_bindings.py,sha256=
|
|
174
|
+
klaude_code/ui/modes/repl/event_handler.py,sha256=pXjiLGilSzrsrr9lsk19NeiRGFjVq91Rtfp1PNLK36A,26026
|
|
175
|
+
klaude_code/ui/modes/repl/input_prompt_toolkit.py,sha256=P8L7GhupoWTQNDkRXs8JpNiv5JRaw9VjXTG060h4DIw,8767
|
|
176
|
+
klaude_code/ui/modes/repl/key_bindings.py,sha256=NKllb1W0LfJAZEqDjlW2aedBQ2PBEOOddoTiRNY1_mU,12121
|
|
177
177
|
klaude_code/ui/modes/repl/renderer.py,sha256=7cum6SuKSuuBePDSyk4UvWs6q5dwgLA0NrJZ3eD9tHw,15902
|
|
178
178
|
klaude_code/ui/renderers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
179
179
|
klaude_code/ui/renderers/assistant.py,sha256=7iu5zlHR7JGviHs2eA25Dsbd7ZkzCR2_0XzkqMPVxDI,862
|
|
180
180
|
klaude_code/ui/renderers/bash_syntax.py,sha256=VcX_tuojOtS58s_Ff-Zmhw_6LBRn2wsvR5UBtEr_qQU,5923
|
|
181
181
|
klaude_code/ui/renderers/common.py,sha256=l9V7yuiejowyw3FdZ2n3VJ2OO_K1rEUINmFz-mC2xlw,2648
|
|
182
|
-
klaude_code/ui/renderers/developer.py,sha256=
|
|
182
|
+
klaude_code/ui/renderers/developer.py,sha256=J6hBko8fiRxF67scI1NQ_Ztz3Y47VF3uxXblhIqUciE,8138
|
|
183
183
|
klaude_code/ui/renderers/diffs.py,sha256=uLpgYTudH38wucozoUw4xbPWMC6uYTQTaDTHcg-0zvM,10418
|
|
184
|
-
klaude_code/ui/renderers/errors.py,sha256
|
|
184
|
+
klaude_code/ui/renderers/errors.py,sha256=MavmYOQ7lyjA_VpuUpDVFCuY9W7XrMVdLsg2lCOn4GY,655
|
|
185
185
|
klaude_code/ui/renderers/mermaid_viewer.py,sha256=TIUFLtTqdG-iFD4Mgm8OdzU_9UO14niftTJ11f4makc,1691
|
|
186
186
|
klaude_code/ui/renderers/metadata.py,sha256=jE_yxNIwsBMEiaiwd0b3czyKXkFbQZWQc5MYI-_2k5c,8280
|
|
187
187
|
klaude_code/ui/renderers/sub_agent.py,sha256=g8QCFXTtFX_w8oTaGMYGuy6u5KqbFMlvzWofER0hGKk,5946
|
|
188
188
|
klaude_code/ui/renderers/thinking.py,sha256=TbQxkjR6MuDXzASBK_rMaxxqvSdhfwDtVwXhOExuvlM,1946
|
|
189
|
-
klaude_code/ui/renderers/tools.py,sha256=
|
|
189
|
+
klaude_code/ui/renderers/tools.py,sha256=lebQHccj2tkJIjO-JB0TvCIixx-BKXHfD-egXSxBV7Y,27891
|
|
190
190
|
klaude_code/ui/renderers/user_input.py,sha256=e2hZS7UUnzQuQ6UqzSKRDkFJMkKTLUoub1JclHMX40g,3941
|
|
191
191
|
klaude_code/ui/rich/__init__.py,sha256=zEZjnHR3Fnv_sFMxwIMjoJfwDoC4GRGv3lHJzAGRq_o,236
|
|
192
192
|
klaude_code/ui/rich/cjk_wrap.py,sha256=ncmifgTwF6q95iayHQyazGbntt7BRQb_Ed7aXc8JU6Y,7551
|
|
@@ -194,17 +194,18 @@ klaude_code/ui/rich/code_panel.py,sha256=ZKuJHh-kh-hIkBXSGLERLaDbJ7I9hvtvmYKocJn
|
|
|
194
194
|
klaude_code/ui/rich/live.py,sha256=qiBLPSE4KW_Dpemy5MZ5BKhkFWEN2fjXBiQHmhJrPSM,2722
|
|
195
195
|
klaude_code/ui/rich/markdown.py,sha256=ltcm4qVX6fsqUNkPWeOwX636FsQ6-gST6uLLcXAl9yA,15397
|
|
196
196
|
klaude_code/ui/rich/quote.py,sha256=tZcxN73SfDBHF_qk0Jkh9gWBqPBn8VLp9RF36YRdKEM,1123
|
|
197
|
-
klaude_code/ui/rich/searchable_text.py,sha256=
|
|
198
|
-
klaude_code/ui/rich/status.py,sha256=
|
|
197
|
+
klaude_code/ui/rich/searchable_text.py,sha256=PUe6MotKxSBY4FlPeojVjVQgxCsx_jiQ41bCzLp8WvE,2271
|
|
198
|
+
klaude_code/ui/rich/status.py,sha256=GKX_kMT42epQE_xIw2TvudprcMUNF8OpH9QvVjocdhk,10812
|
|
199
199
|
klaude_code/ui/rich/theme.py,sha256=-jsQ5vB2kIM6hZD_YZZejS1OJQE4b48hJur-Hcnbhs4,14439
|
|
200
200
|
klaude_code/ui/terminal/__init__.py,sha256=GIMnsEcIAGT_vBHvTlWEdyNmAEpruyscUA6M_j3GQZU,1412
|
|
201
201
|
klaude_code/ui/terminal/color.py,sha256=jvVbuysf5pnI0uAjUVeyW2HwU58dutTg2msykbu2w4Y,7197
|
|
202
202
|
klaude_code/ui/terminal/control.py,sha256=WhkqEWdtzUO4iWULp-iI9VazAWmzzW52qTQXk-4Dr4s,4922
|
|
203
203
|
klaude_code/ui/terminal/notifier.py,sha256=wkRM66d98Oh6PujnN4bB7NiQxIYEHqQXverMKU43H5E,3187
|
|
204
204
|
klaude_code/ui/terminal/progress_bar.py,sha256=MDnhPbqCnN4GDgLOlxxOEVZPDwVC_XL2NM5sl1MFNcQ,2133
|
|
205
|
+
klaude_code/ui/terminal/selector.py,sha256=v0akaR4Jw5U_3fgK4INqf0R2nIhRZC-JqhECsF3z22I,9633
|
|
205
206
|
klaude_code/ui/utils/__init__.py,sha256=YEsCLjbCPaPza-UXTPUMTJTrc9BmNBUP5CbFWlshyOQ,15
|
|
206
207
|
klaude_code/ui/utils/common.py,sha256=tqHqwgLtAyP805kwRFyoAL4EgMutcNb3Y-GAXJ4IeuM,2263
|
|
207
|
-
klaude_code-1.
|
|
208
|
-
klaude_code-1.
|
|
209
|
-
klaude_code-1.
|
|
210
|
-
klaude_code-1.
|
|
208
|
+
klaude_code-1.4.1.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
209
|
+
klaude_code-1.4.1.dist-info/entry_points.txt,sha256=kkXIXedaTOtjXPr2rVjRVVXZYlFUcBHELaqmyVlWUFA,92
|
|
210
|
+
klaude_code-1.4.1.dist-info/METADATA,sha256=5cZHqchLcz-Jw9wBBudFwewS-S97SSJS3b8bJ3iEo4Y,9091
|
|
211
|
+
klaude_code-1.4.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|