klaude-code 1.2.6__py3-none-any.whl → 1.8.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/auth/__init__.py +24 -0
- klaude_code/auth/codex/__init__.py +20 -0
- klaude_code/auth/codex/exceptions.py +17 -0
- klaude_code/auth/codex/jwt_utils.py +45 -0
- klaude_code/auth/codex/oauth.py +229 -0
- klaude_code/auth/codex/token_manager.py +84 -0
- klaude_code/cli/auth_cmd.py +73 -0
- klaude_code/cli/config_cmd.py +91 -0
- klaude_code/cli/cost_cmd.py +338 -0
- klaude_code/cli/debug.py +78 -0
- klaude_code/cli/list_model.py +307 -0
- klaude_code/cli/main.py +233 -134
- klaude_code/cli/runtime.py +309 -117
- klaude_code/{version.py → cli/self_update.py} +114 -5
- klaude_code/cli/session_cmd.py +37 -21
- klaude_code/command/__init__.py +88 -27
- klaude_code/command/clear_cmd.py +8 -7
- klaude_code/command/command_abc.py +31 -31
- klaude_code/command/debug_cmd.py +79 -0
- klaude_code/command/export_cmd.py +19 -53
- klaude_code/command/export_online_cmd.py +154 -0
- klaude_code/command/fork_session_cmd.py +267 -0
- klaude_code/command/help_cmd.py +7 -8
- klaude_code/command/model_cmd.py +60 -10
- klaude_code/command/model_select.py +84 -0
- klaude_code/command/prompt-jj-describe.md +32 -0
- klaude_code/command/prompt_command.py +19 -11
- klaude_code/command/refresh_cmd.py +8 -10
- klaude_code/command/registry.py +139 -40
- klaude_code/command/release_notes_cmd.py +84 -0
- klaude_code/command/resume_cmd.py +111 -0
- klaude_code/command/status_cmd.py +104 -60
- klaude_code/command/terminal_setup_cmd.py +7 -9
- klaude_code/command/thinking_cmd.py +98 -0
- klaude_code/config/__init__.py +14 -6
- klaude_code/config/assets/__init__.py +1 -0
- klaude_code/config/assets/builtin_config.yaml +303 -0
- klaude_code/config/builtin_config.py +38 -0
- klaude_code/config/config.py +378 -109
- klaude_code/config/select_model.py +117 -53
- klaude_code/config/thinking.py +269 -0
- klaude_code/{const/__init__.py → const.py} +50 -19
- klaude_code/core/agent.py +20 -28
- klaude_code/core/executor.py +327 -112
- klaude_code/core/manager/__init__.py +2 -4
- klaude_code/core/manager/llm_clients.py +1 -15
- klaude_code/core/manager/llm_clients_builder.py +10 -11
- klaude_code/core/manager/sub_agent_manager.py +37 -6
- klaude_code/core/prompt.py +63 -44
- klaude_code/core/prompts/prompt-claude-code.md +2 -13
- klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +117 -0
- klaude_code/core/prompts/prompt-codex-gpt-5-2-codex.md +117 -0
- klaude_code/core/prompts/prompt-codex.md +9 -42
- klaude_code/core/prompts/prompt-minimal.md +12 -0
- klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +16 -3
- klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -2
- klaude_code/core/prompts/prompt-sub-agent-web.md +51 -0
- klaude_code/core/reminders.py +283 -95
- klaude_code/core/task.py +113 -75
- klaude_code/core/tool/__init__.py +24 -31
- klaude_code/core/tool/file/_utils.py +36 -0
- klaude_code/core/tool/file/apply_patch.py +17 -25
- klaude_code/core/tool/file/apply_patch_tool.py +57 -77
- klaude_code/core/tool/file/diff_builder.py +151 -0
- klaude_code/core/tool/file/edit_tool.py +50 -63
- klaude_code/core/tool/file/move_tool.md +41 -0
- klaude_code/core/tool/file/move_tool.py +435 -0
- klaude_code/core/tool/file/read_tool.md +1 -1
- klaude_code/core/tool/file/read_tool.py +86 -86
- klaude_code/core/tool/file/write_tool.py +59 -69
- klaude_code/core/tool/report_back_tool.py +84 -0
- klaude_code/core/tool/shell/bash_tool.py +265 -22
- klaude_code/core/tool/shell/command_safety.py +3 -6
- klaude_code/core/tool/{memory → skill}/skill_tool.py +16 -26
- klaude_code/core/tool/sub_agent_tool.py +13 -2
- klaude_code/core/tool/todo/todo_write_tool.md +0 -157
- klaude_code/core/tool/todo/todo_write_tool.py +1 -1
- klaude_code/core/tool/todo/todo_write_tool_raw.md +182 -0
- klaude_code/core/tool/todo/update_plan_tool.py +1 -1
- klaude_code/core/tool/tool_abc.py +18 -0
- klaude_code/core/tool/tool_context.py +27 -12
- klaude_code/core/tool/tool_registry.py +7 -7
- klaude_code/core/tool/tool_runner.py +44 -36
- klaude_code/core/tool/truncation.py +29 -14
- klaude_code/core/tool/web/mermaid_tool.md +43 -0
- klaude_code/core/tool/web/mermaid_tool.py +2 -5
- klaude_code/core/tool/web/web_fetch_tool.md +1 -1
- klaude_code/core/tool/web/web_fetch_tool.py +112 -22
- klaude_code/core/tool/web/web_search_tool.md +23 -0
- klaude_code/core/tool/web/web_search_tool.py +130 -0
- klaude_code/core/turn.py +168 -66
- klaude_code/llm/__init__.py +2 -10
- klaude_code/llm/anthropic/client.py +190 -178
- klaude_code/llm/anthropic/input.py +39 -15
- klaude_code/llm/bedrock/__init__.py +3 -0
- klaude_code/llm/bedrock/client.py +60 -0
- klaude_code/llm/client.py +7 -21
- klaude_code/llm/codex/__init__.py +5 -0
- klaude_code/llm/codex/client.py +149 -0
- klaude_code/llm/google/__init__.py +3 -0
- klaude_code/llm/google/client.py +309 -0
- klaude_code/llm/google/input.py +215 -0
- klaude_code/llm/input_common.py +3 -9
- klaude_code/llm/openai_compatible/client.py +72 -164
- klaude_code/llm/openai_compatible/input.py +6 -4
- klaude_code/llm/openai_compatible/stream.py +273 -0
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +17 -1
- klaude_code/llm/openrouter/client.py +89 -160
- klaude_code/llm/openrouter/input.py +18 -30
- klaude_code/llm/openrouter/reasoning.py +118 -0
- klaude_code/llm/registry.py +39 -7
- klaude_code/llm/responses/client.py +184 -171
- klaude_code/llm/responses/input.py +20 -1
- klaude_code/llm/usage.py +17 -12
- klaude_code/protocol/commands.py +17 -1
- klaude_code/protocol/events.py +31 -4
- klaude_code/protocol/llm_param.py +13 -10
- klaude_code/protocol/model.py +232 -29
- klaude_code/protocol/op.py +90 -1
- klaude_code/protocol/op_handler.py +35 -1
- klaude_code/protocol/sub_agent/__init__.py +117 -0
- klaude_code/protocol/sub_agent/explore.py +63 -0
- klaude_code/protocol/sub_agent/oracle.py +91 -0
- klaude_code/protocol/sub_agent/task.py +61 -0
- klaude_code/protocol/sub_agent/web.py +79 -0
- klaude_code/protocol/tools.py +4 -2
- klaude_code/session/__init__.py +2 -2
- klaude_code/session/codec.py +71 -0
- klaude_code/session/export.py +293 -86
- klaude_code/session/selector.py +89 -67
- klaude_code/session/session.py +320 -309
- klaude_code/session/store.py +220 -0
- klaude_code/session/templates/export_session.html +595 -83
- klaude_code/session/templates/mermaid_viewer.html +926 -0
- klaude_code/skill/__init__.py +27 -0
- klaude_code/skill/assets/deslop/SKILL.md +17 -0
- klaude_code/skill/assets/dev-docs/SKILL.md +108 -0
- klaude_code/skill/assets/handoff/SKILL.md +39 -0
- klaude_code/skill/assets/jj-workspace/SKILL.md +20 -0
- klaude_code/skill/assets/skill-creator/SKILL.md +139 -0
- klaude_code/{core/tool/memory/skill_loader.py → skill/loader.py} +55 -15
- klaude_code/skill/manager.py +70 -0
- klaude_code/skill/system_skills.py +192 -0
- klaude_code/trace/__init__.py +20 -2
- klaude_code/trace/log.py +150 -5
- klaude_code/ui/__init__.py +4 -9
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/core/stage_manager.py +7 -7
- klaude_code/ui/modes/debug/display.py +2 -1
- klaude_code/ui/modes/repl/__init__.py +3 -48
- klaude_code/ui/modes/repl/clipboard.py +5 -5
- klaude_code/ui/modes/repl/completers.py +487 -123
- klaude_code/ui/modes/repl/display.py +5 -4
- klaude_code/ui/modes/repl/event_handler.py +370 -117
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +552 -105
- klaude_code/ui/modes/repl/key_bindings.py +146 -23
- klaude_code/ui/modes/repl/renderer.py +189 -99
- klaude_code/ui/renderers/assistant.py +9 -2
- klaude_code/ui/renderers/bash_syntax.py +178 -0
- klaude_code/ui/renderers/common.py +78 -0
- klaude_code/ui/renderers/developer.py +104 -48
- klaude_code/ui/renderers/diffs.py +87 -6
- klaude_code/ui/renderers/errors.py +11 -6
- klaude_code/ui/renderers/mermaid_viewer.py +57 -0
- klaude_code/ui/renderers/metadata.py +112 -76
- klaude_code/ui/renderers/sub_agent.py +92 -7
- klaude_code/ui/renderers/thinking.py +40 -18
- klaude_code/ui/renderers/tools.py +405 -227
- klaude_code/ui/renderers/user_input.py +73 -13
- klaude_code/ui/rich/__init__.py +10 -1
- klaude_code/ui/rich/cjk_wrap.py +228 -0
- klaude_code/ui/rich/code_panel.py +131 -0
- klaude_code/ui/rich/live.py +17 -0
- klaude_code/ui/rich/markdown.py +305 -170
- klaude_code/ui/rich/searchable_text.py +10 -13
- klaude_code/ui/rich/status.py +190 -49
- klaude_code/ui/rich/theme.py +135 -39
- klaude_code/ui/terminal/__init__.py +55 -0
- klaude_code/ui/terminal/color.py +1 -1
- klaude_code/ui/terminal/control.py +13 -22
- klaude_code/ui/terminal/notifier.py +44 -4
- klaude_code/ui/terminal/selector.py +658 -0
- klaude_code/ui/utils/common.py +0 -18
- klaude_code-1.8.0.dist-info/METADATA +377 -0
- klaude_code-1.8.0.dist-info/RECORD +219 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/entry_points.txt +1 -0
- klaude_code/command/diff_cmd.py +0 -138
- klaude_code/command/prompt-dev-docs-update.md +0 -56
- klaude_code/command/prompt-dev-docs.md +0 -46
- klaude_code/config/list_model.py +0 -162
- klaude_code/core/manager/agent_manager.py +0 -127
- klaude_code/core/prompts/prompt-subagent-webfetch.md +0 -46
- klaude_code/core/tool/file/multi_edit_tool.md +0 -42
- klaude_code/core/tool/file/multi_edit_tool.py +0 -199
- klaude_code/core/tool/memory/memory_tool.md +0 -16
- klaude_code/core/tool/memory/memory_tool.py +0 -462
- klaude_code/llm/openrouter/reasoning_handler.py +0 -209
- klaude_code/protocol/sub_agent.py +0 -348
- klaude_code/ui/utils/debouncer.py +0 -42
- klaude_code-1.2.6.dist-info/METADATA +0 -178
- klaude_code-1.2.6.dist-info/RECORD +0 -167
- /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
- /klaude_code/core/tool/{memory → skill}/__init__.py +0 -0
- /klaude_code/core/tool/{memory → skill}/skill_tool.md +0 -0
- {klaude_code-1.2.6.dist-info → klaude_code-1.8.0.dist-info}/WHEEL +0 -0
|
@@ -6,17 +6,26 @@ with dependencies injected to avoid circular imports.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
import contextlib
|
|
9
10
|
import re
|
|
10
11
|
from collections.abc import Callable
|
|
11
12
|
from typing import cast
|
|
12
13
|
|
|
14
|
+
from prompt_toolkit.buffer import Buffer
|
|
15
|
+
from prompt_toolkit.filters import Always, Filter
|
|
16
|
+
from prompt_toolkit.filters.app import has_completions
|
|
13
17
|
from prompt_toolkit.key_binding import KeyBindings
|
|
18
|
+
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
|
|
14
19
|
|
|
15
20
|
|
|
16
21
|
def create_key_bindings(
|
|
17
22
|
capture_clipboard_tag: Callable[[], str | None],
|
|
18
23
|
copy_to_clipboard: Callable[[str], None],
|
|
19
24
|
at_token_pattern: re.Pattern[str],
|
|
25
|
+
*,
|
|
26
|
+
input_enabled: Filter | None = None,
|
|
27
|
+
open_model_picker: Callable[[], None] | None = None,
|
|
28
|
+
open_thinking_picker: Callable[[], None] | None = None,
|
|
20
29
|
) -> KeyBindings:
|
|
21
30
|
"""Create REPL key bindings with injected dependencies.
|
|
22
31
|
|
|
@@ -29,20 +38,96 @@ def create_key_bindings(
|
|
|
29
38
|
KeyBindings instance with all REPL handlers configured
|
|
30
39
|
"""
|
|
31
40
|
kb = KeyBindings()
|
|
41
|
+
enabled = input_enabled if input_enabled is not None else Always()
|
|
32
42
|
|
|
33
|
-
|
|
34
|
-
|
|
43
|
+
def _should_submit_instead_of_accepting_completion(buf: Buffer) -> bool:
|
|
44
|
+
"""Return True when Enter should submit even if completions are visible.
|
|
45
|
+
|
|
46
|
+
We show completions proactively for contexts like `/`.
|
|
47
|
+
If the user already typed an exact candidate (e.g. `/clear`), accepting
|
|
48
|
+
a completion often only adds a trailing space and makes Enter require
|
|
49
|
+
two presses. In that case, prefer submitting.
|
|
50
|
+
"""
|
|
51
|
+
state = buf.complete_state
|
|
52
|
+
if state is None or not state.completions:
|
|
53
|
+
return False
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
doc = buf.document # type: ignore[reportUnknownMemberType]
|
|
57
|
+
text = cast(str, doc.text) # type: ignore[reportUnknownMemberType]
|
|
58
|
+
cursor_pos = cast(int, doc.cursor_position) # type: ignore[reportUnknownMemberType]
|
|
59
|
+
except Exception:
|
|
60
|
+
return False
|
|
61
|
+
|
|
62
|
+
# Only apply this heuristic when the caret is at the end of the buffer.
|
|
63
|
+
if cursor_pos != len(text):
|
|
64
|
+
return False
|
|
65
|
+
|
|
66
|
+
for completion in state.completions:
|
|
67
|
+
try:
|
|
68
|
+
start = cursor_pos + completion.start_position
|
|
69
|
+
if start < 0 or start > cursor_pos:
|
|
70
|
+
continue
|
|
71
|
+
|
|
72
|
+
replaced = text[start:cursor_pos]
|
|
73
|
+
inserted = completion.text
|
|
74
|
+
|
|
75
|
+
# If the user already typed an exact candidate, don't force
|
|
76
|
+
# accepting a completion (which often just adds a space).
|
|
77
|
+
if replaced == inserted or replaced == inserted.rstrip():
|
|
78
|
+
return True
|
|
79
|
+
except Exception:
|
|
80
|
+
continue
|
|
81
|
+
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
def _select_first_completion_if_needed(buf: Buffer) -> None:
|
|
85
|
+
"""Ensure the completion menu has an active selection.
|
|
86
|
+
|
|
87
|
+
prompt_toolkit's default behavior keeps `complete_index=None` until the
|
|
88
|
+
user explicitly selects an item. We want the first item to be selected
|
|
89
|
+
by default, without modifying the buffer text.
|
|
90
|
+
"""
|
|
91
|
+
state = buf.complete_state
|
|
92
|
+
if state is None or not state.completions:
|
|
93
|
+
return
|
|
94
|
+
if state.complete_index is None:
|
|
95
|
+
state.complete_index = 0
|
|
96
|
+
|
|
97
|
+
def _cycle_completion(buf: Buffer, *, delta: int) -> None:
|
|
98
|
+
state = buf.complete_state
|
|
99
|
+
if state is None or not state.completions:
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
_select_first_completion_if_needed(buf)
|
|
103
|
+
idx = state.complete_index or 0
|
|
104
|
+
state.complete_index = (idx + delta) % len(state.completions)
|
|
105
|
+
|
|
106
|
+
def _accept_current_completion(buf: Buffer) -> bool:
|
|
107
|
+
"""Apply the currently selected completion, if any.
|
|
108
|
+
|
|
109
|
+
Returns True when a completion was applied.
|
|
110
|
+
"""
|
|
111
|
+
state = buf.complete_state
|
|
112
|
+
if state is None or not state.completions:
|
|
113
|
+
return False
|
|
114
|
+
|
|
115
|
+
_select_first_completion_if_needed(buf)
|
|
116
|
+
completion = state.current_completion or state.completions[0]
|
|
117
|
+
buf.apply_completion(completion)
|
|
118
|
+
return True
|
|
119
|
+
|
|
120
|
+
@kb.add("c-v", filter=enabled)
|
|
121
|
+
def _(event: KeyPressEvent) -> None:
|
|
35
122
|
"""Paste image from clipboard as [Image #N]."""
|
|
36
123
|
tag = capture_clipboard_tag()
|
|
37
124
|
if tag:
|
|
38
|
-
|
|
125
|
+
with contextlib.suppress(Exception):
|
|
39
126
|
event.current_buffer.insert_text(tag) # pyright: ignore[reportUnknownMemberType]
|
|
40
|
-
except Exception:
|
|
41
|
-
pass
|
|
42
127
|
|
|
43
|
-
@kb.add("enter")
|
|
44
|
-
def _(event
|
|
45
|
-
buf = event.current_buffer
|
|
128
|
+
@kb.add("enter", filter=enabled)
|
|
129
|
+
def _(event: KeyPressEvent) -> None:
|
|
130
|
+
buf = event.current_buffer
|
|
46
131
|
doc = buf.document # type: ignore
|
|
47
132
|
|
|
48
133
|
# If VS Code/Windsurf/Cursor sent a "\\" sentinel before Enter (Shift+Enter mapping),
|
|
@@ -53,10 +138,16 @@ def create_key_bindings(
|
|
|
53
138
|
buf.delete_before_cursor() # remove the sentinel backslash # type: ignore[reportUnknownMemberType]
|
|
54
139
|
buf.insert_text("\n") # type: ignore[reportUnknownMemberType]
|
|
55
140
|
return
|
|
56
|
-
except
|
|
141
|
+
except (AttributeError, TypeError):
|
|
57
142
|
# Fall through to default behavior if anything goes wrong
|
|
58
143
|
pass
|
|
59
144
|
|
|
145
|
+
# When completions are visible, Enter accepts the current selection.
|
|
146
|
+
# This aligns with common TUI completion UX: navigation doesn't modify
|
|
147
|
+
# the buffer, and Enter/Tab inserts the selected option.
|
|
148
|
+
if not _should_submit_instead_of_accepting_completion(buf) and _accept_current_completion(buf):
|
|
149
|
+
return
|
|
150
|
+
|
|
60
151
|
# If the entire buffer is whitespace-only, insert a newline rather than submitting.
|
|
61
152
|
if len(buf.text.strip()) == 0: # type: ignore
|
|
62
153
|
buf.insert_text("\n") # type: ignore
|
|
@@ -65,12 +156,30 @@ def create_key_bindings(
|
|
|
65
156
|
# No need to persist manifest anymore - iter_inputs will handle image extraction
|
|
66
157
|
buf.validate_and_handle() # type: ignore
|
|
67
158
|
|
|
68
|
-
@kb.add("
|
|
69
|
-
def _(event
|
|
159
|
+
@kb.add("tab", filter=enabled & has_completions)
|
|
160
|
+
def _(event: KeyPressEvent) -> None:
|
|
161
|
+
buf = event.current_buffer
|
|
162
|
+
if _accept_current_completion(buf):
|
|
163
|
+
event.app.invalidate() # type: ignore[reportUnknownMemberType]
|
|
164
|
+
|
|
165
|
+
@kb.add("down", filter=enabled & has_completions)
|
|
166
|
+
def _(event: KeyPressEvent) -> None:
|
|
167
|
+
buf = event.current_buffer
|
|
168
|
+
_cycle_completion(buf, delta=1)
|
|
169
|
+
event.app.invalidate() # type: ignore[reportUnknownMemberType]
|
|
170
|
+
|
|
171
|
+
@kb.add("up", filter=enabled & has_completions)
|
|
172
|
+
def _(event: KeyPressEvent) -> None:
|
|
173
|
+
buf = event.current_buffer
|
|
174
|
+
_cycle_completion(buf, delta=-1)
|
|
175
|
+
event.app.invalidate() # type: ignore[reportUnknownMemberType]
|
|
176
|
+
|
|
177
|
+
@kb.add("c-j", filter=enabled)
|
|
178
|
+
def _(event: KeyPressEvent) -> None:
|
|
70
179
|
event.current_buffer.insert_text("\n") # type: ignore
|
|
71
180
|
|
|
72
|
-
@kb.add("c")
|
|
73
|
-
def _(event
|
|
181
|
+
@kb.add("c", filter=enabled)
|
|
182
|
+
def _(event: KeyPressEvent) -> None:
|
|
74
183
|
"""Copy selected text to system clipboard, or insert 'c' if no selection."""
|
|
75
184
|
buf = event.current_buffer # type: ignore
|
|
76
185
|
if buf.selection_state: # type: ignore[reportUnknownMemberType]
|
|
@@ -84,8 +193,8 @@ def create_key_bindings(
|
|
|
84
193
|
else:
|
|
85
194
|
buf.insert_text("c") # type: ignore[reportUnknownMemberType]
|
|
86
195
|
|
|
87
|
-
@kb.add("backspace")
|
|
88
|
-
def _(event
|
|
196
|
+
@kb.add("backspace", filter=enabled)
|
|
197
|
+
def _(event: KeyPressEvent) -> None:
|
|
89
198
|
"""Ensure completions refresh on backspace when editing an @token.
|
|
90
199
|
|
|
91
200
|
We delete the character before cursor (default behavior), then explicitly
|
|
@@ -106,17 +215,17 @@ def create_key_bindings(
|
|
|
106
215
|
should_refresh = True
|
|
107
216
|
elif buf.document.cursor_position_row == 0: # type: ignore[reportUnknownMemberType]
|
|
108
217
|
# Check for slash command pattern without accessing protected attribute
|
|
109
|
-
text_before_str =
|
|
218
|
+
text_before_str = text_before or ""
|
|
110
219
|
if text_before_str.strip().startswith("/") and " " not in text_before_str:
|
|
111
220
|
should_refresh = True
|
|
112
221
|
|
|
113
222
|
if should_refresh:
|
|
114
223
|
buf.start_completion(select_first=False) # type: ignore[reportUnknownMemberType]
|
|
115
|
-
except
|
|
224
|
+
except (AttributeError, TypeError):
|
|
116
225
|
pass
|
|
117
226
|
|
|
118
|
-
@kb.add("left")
|
|
119
|
-
def _(event
|
|
227
|
+
@kb.add("left", filter=enabled)
|
|
228
|
+
def _(event: KeyPressEvent) -> None:
|
|
120
229
|
"""Support wrapping to previous line when pressing left at column 0."""
|
|
121
230
|
buf = event.current_buffer # type: ignore
|
|
122
231
|
try:
|
|
@@ -137,11 +246,11 @@ def create_key_bindings(
|
|
|
137
246
|
# Default behavior: move one character left when possible.
|
|
138
247
|
if doc.cursor_position > 0: # type: ignore[reportUnknownMemberType]
|
|
139
248
|
buf.cursor_left() # type: ignore[reportUnknownMemberType]
|
|
140
|
-
except
|
|
249
|
+
except (AttributeError, IndexError, TypeError):
|
|
141
250
|
pass
|
|
142
251
|
|
|
143
|
-
@kb.add("right")
|
|
144
|
-
def _(event
|
|
252
|
+
@kb.add("right", filter=enabled)
|
|
253
|
+
def _(event: KeyPressEvent) -> None:
|
|
145
254
|
"""Support wrapping to next line when pressing right at line end."""
|
|
146
255
|
buf = event.current_buffer # type: ignore
|
|
147
256
|
try:
|
|
@@ -164,7 +273,21 @@ def create_key_bindings(
|
|
|
164
273
|
# Default behavior: move one character right when possible.
|
|
165
274
|
if doc.cursor_position < len(doc.text): # type: ignore[reportUnknownMemberType]
|
|
166
275
|
buf.cursor_right() # type: ignore[reportUnknownMemberType]
|
|
167
|
-
except
|
|
276
|
+
except (AttributeError, IndexError, TypeError):
|
|
168
277
|
pass
|
|
169
278
|
|
|
279
|
+
@kb.add("c-l", filter=enabled, eager=True)
|
|
280
|
+
def _(event: KeyPressEvent) -> None:
|
|
281
|
+
del event
|
|
282
|
+
if open_model_picker is not None:
|
|
283
|
+
with contextlib.suppress(Exception):
|
|
284
|
+
open_model_picker()
|
|
285
|
+
|
|
286
|
+
@kb.add("c-t", filter=enabled, eager=True)
|
|
287
|
+
def _(event: KeyPressEvent) -> None:
|
|
288
|
+
del event
|
|
289
|
+
if open_thinking_picker is not None:
|
|
290
|
+
with contextlib.suppress(Exception):
|
|
291
|
+
open_thinking_picker()
|
|
292
|
+
|
|
170
293
|
return kb
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
4
|
+
from collections.abc import Iterator
|
|
3
5
|
from contextlib import contextmanager
|
|
4
6
|
from dataclasses import dataclass
|
|
5
|
-
from typing import Any
|
|
7
|
+
from typing import Any
|
|
6
8
|
|
|
7
|
-
from rich import
|
|
8
|
-
from rich.
|
|
9
|
-
from rich.console import Console
|
|
9
|
+
from rich.console import Console, Group, RenderableType
|
|
10
|
+
from rich.padding import Padding
|
|
10
11
|
from rich.spinner import Spinner
|
|
11
|
-
from rich.status import Status
|
|
12
12
|
from rich.style import Style, StyleType
|
|
13
13
|
from rich.text import Text
|
|
14
14
|
|
|
15
|
+
from klaude_code import const
|
|
15
16
|
from klaude_code.protocol import events, model
|
|
16
17
|
from klaude_code.ui.renderers import assistant as r_assistant
|
|
17
18
|
from klaude_code.ui.renderers import developer as r_developer
|
|
@@ -21,16 +22,18 @@ from klaude_code.ui.renderers import sub_agent as r_sub_agent
|
|
|
21
22
|
from klaude_code.ui.renderers import thinking as r_thinking
|
|
22
23
|
from klaude_code.ui.renderers import tools as r_tools
|
|
23
24
|
from klaude_code.ui.renderers import user_input as r_user_input
|
|
25
|
+
from klaude_code.ui.renderers.common import truncate_display
|
|
24
26
|
from klaude_code.ui.rich import status as r_status
|
|
27
|
+
from klaude_code.ui.rich.live import CropAboveLive, SingleLine
|
|
25
28
|
from klaude_code.ui.rich.quote import Quote
|
|
26
|
-
from klaude_code.ui.rich.status import ShimmerStatusText
|
|
29
|
+
from klaude_code.ui.rich.status import BreathingSpinner, ShimmerStatusText
|
|
27
30
|
from klaude_code.ui.rich.theme import ThemeKey, get_theme
|
|
28
|
-
from klaude_code.ui.utils.common import truncate_display
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
@dataclass
|
|
32
34
|
class SessionStatus:
|
|
33
35
|
color: Style | None = None
|
|
36
|
+
color_index: int | None = None
|
|
34
37
|
sub_agent_state: model.SubAgentState | None = None
|
|
35
38
|
|
|
36
39
|
|
|
@@ -41,22 +44,32 @@ class REPLRenderer:
|
|
|
41
44
|
self.themes = get_theme(theme)
|
|
42
45
|
self.console: Console = Console(theme=self.themes.app_theme)
|
|
43
46
|
self.console.push_theme(self.themes.markdown_theme)
|
|
44
|
-
self.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
self._bottom_live: CropAboveLive | None = None
|
|
48
|
+
self._stream_renderable: RenderableType | None = None
|
|
49
|
+
self._stream_max_height: int = 0
|
|
50
|
+
self._stream_last_height: int = 0
|
|
51
|
+
self._stream_last_width: int = 0
|
|
52
|
+
self._spinner_visible: bool = False
|
|
53
|
+
|
|
54
|
+
self._status_text: ShimmerStatusText = ShimmerStatusText(const.STATUS_DEFAULT_TEXT)
|
|
55
|
+
self._status_spinner: Spinner = BreathingSpinner(
|
|
56
|
+
r_status.spinner_name(),
|
|
57
|
+
text=SingleLine(self._status_text),
|
|
58
|
+
style=ThemeKey.STATUS_SPINNER,
|
|
48
59
|
)
|
|
49
60
|
|
|
50
61
|
self.session_map: dict[str, SessionStatus] = {}
|
|
51
62
|
self.current_sub_agent_color: Style | None = None
|
|
52
|
-
self.
|
|
63
|
+
self.sub_agent_color_index = 0
|
|
53
64
|
|
|
54
65
|
def register_session(self, session_id: str, sub_agent_state: model.SubAgentState | None = None) -> None:
|
|
55
66
|
session_status = SessionStatus(
|
|
56
67
|
sub_agent_state=sub_agent_state,
|
|
57
68
|
)
|
|
58
69
|
if sub_agent_state is not None:
|
|
59
|
-
|
|
70
|
+
color, color_index = self.pick_sub_agent_color()
|
|
71
|
+
session_status.color = color
|
|
72
|
+
session_status.color_index = color_index
|
|
60
73
|
self.session_map[session_id] = session_status
|
|
61
74
|
|
|
62
75
|
def is_sub_agent_session(self, session_id: str) -> bool:
|
|
@@ -65,16 +78,16 @@ class REPLRenderer:
|
|
|
65
78
|
def _advance_sub_agent_color_index(self) -> None:
|
|
66
79
|
palette_size = len(self.themes.sub_agent_colors)
|
|
67
80
|
if palette_size == 0:
|
|
68
|
-
self.
|
|
81
|
+
self.sub_agent_color_index = 0
|
|
69
82
|
return
|
|
70
|
-
self.
|
|
83
|
+
self.sub_agent_color_index = (self.sub_agent_color_index + 1) % palette_size
|
|
71
84
|
|
|
72
|
-
def pick_sub_agent_color(self) -> Style:
|
|
85
|
+
def pick_sub_agent_color(self) -> tuple[Style, int]:
|
|
73
86
|
self._advance_sub_agent_color_index()
|
|
74
87
|
palette = self.themes.sub_agent_colors
|
|
75
88
|
if not palette:
|
|
76
|
-
return Style()
|
|
77
|
-
return palette[self.
|
|
89
|
+
return Style(), 0
|
|
90
|
+
return palette[self.sub_agent_color_index], self.sub_agent_color_index
|
|
78
91
|
|
|
79
92
|
def get_session_sub_agent_color(self, session_id: str) -> Style:
|
|
80
93
|
status = self.session_map.get(session_id)
|
|
@@ -82,8 +95,12 @@ class REPLRenderer:
|
|
|
82
95
|
return status.color
|
|
83
96
|
return Style()
|
|
84
97
|
|
|
85
|
-
def
|
|
86
|
-
|
|
98
|
+
def get_session_sub_agent_background(self, session_id: str) -> Style:
|
|
99
|
+
status = self.session_map.get(session_id)
|
|
100
|
+
backgrounds = self.themes.sub_agent_backgrounds
|
|
101
|
+
if status and status.color_index is not None and backgrounds:
|
|
102
|
+
return backgrounds[status.color_index]
|
|
103
|
+
return Style()
|
|
87
104
|
|
|
88
105
|
@contextmanager
|
|
89
106
|
def session_print_context(self, session_id: str) -> Iterator[None]:
|
|
@@ -98,49 +115,22 @@ class REPLRenderer:
|
|
|
98
115
|
def print(self, *objects: Any, style: StyleType | None = None, end: str = "\n") -> None:
|
|
99
116
|
if self.current_sub_agent_color:
|
|
100
117
|
if objects:
|
|
101
|
-
|
|
118
|
+
content = objects[0] if len(objects) == 1 else objects
|
|
119
|
+
self.console.print(Quote(content, style=self.current_sub_agent_color), overflow="ellipsis")
|
|
102
120
|
return
|
|
103
|
-
self.console.print(*objects, style=style, end=end)
|
|
121
|
+
self.console.print(*objects, style=style, end=end, overflow="ellipsis")
|
|
104
122
|
|
|
105
123
|
def display_tool_call(self, e: events.ToolCallEvent) -> None:
|
|
106
|
-
# Handle sub-agent tool calls in replay mode
|
|
107
124
|
if r_tools.is_sub_agent_tool(e.tool_name):
|
|
108
|
-
if e.is_replay:
|
|
109
|
-
state = r_sub_agent.build_sub_agent_state_from_tool_call(e)
|
|
110
|
-
if state is not None:
|
|
111
|
-
sub_agent_default_style = (
|
|
112
|
-
self.themes.sub_agent_colors[0] if self.themes.sub_agent_colors else Style()
|
|
113
|
-
)
|
|
114
|
-
self.print(
|
|
115
|
-
Quote(
|
|
116
|
-
r_sub_agent.render_sub_agent_call(state, sub_agent_default_style),
|
|
117
|
-
style=sub_agent_default_style,
|
|
118
|
-
)
|
|
119
|
-
)
|
|
120
125
|
return
|
|
121
|
-
|
|
122
126
|
renderable = r_tools.render_tool_call(e)
|
|
123
127
|
if renderable is not None:
|
|
124
128
|
self.print(renderable)
|
|
125
129
|
|
|
126
130
|
def display_tool_call_result(self, e: events.ToolResultEvent) -> None:
|
|
127
|
-
# Handle sub-agent tool results in replay mode
|
|
128
131
|
if r_tools.is_sub_agent_tool(e.tool_name):
|
|
129
|
-
if e.is_replay:
|
|
130
|
-
sub_agent_default_style = self.themes.sub_agent_colors[0] if self.themes.sub_agent_colors else Style()
|
|
131
|
-
self.print(
|
|
132
|
-
Quote(
|
|
133
|
-
r_sub_agent.render_sub_agent_result(
|
|
134
|
-
e.result,
|
|
135
|
-
code_theme=self.themes.code_theme,
|
|
136
|
-
style=sub_agent_default_style,
|
|
137
|
-
),
|
|
138
|
-
style=sub_agent_default_style,
|
|
139
|
-
)
|
|
140
|
-
)
|
|
141
132
|
return
|
|
142
|
-
|
|
143
|
-
renderable = r_tools.render_tool_result(e)
|
|
133
|
+
renderable = r_tools.render_tool_result(e, code_theme=self.themes.code_theme)
|
|
144
134
|
if renderable is not None:
|
|
145
135
|
self.print(renderable)
|
|
146
136
|
|
|
@@ -159,37 +149,53 @@ class REPLRenderer:
|
|
|
159
149
|
async def replay_history(self, history_events: events.ReplayHistoryEvent) -> None:
|
|
160
150
|
tool_call_dict: dict[str, events.ToolCallEvent] = {}
|
|
161
151
|
for event in history_events.events:
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
self.print(
|
|
152
|
+
event_session_id = getattr(event, "session_id", history_events.session_id)
|
|
153
|
+
is_sub_agent = self.is_sub_agent_session(event_session_id)
|
|
154
|
+
|
|
155
|
+
with self.session_print_context(event_session_id):
|
|
156
|
+
match event:
|
|
157
|
+
case events.TaskStartEvent() as e:
|
|
158
|
+
self.display_task_start(e)
|
|
159
|
+
case events.TurnStartEvent():
|
|
160
|
+
self.print()
|
|
161
|
+
case events.AssistantMessageEvent() as e:
|
|
162
|
+
if is_sub_agent:
|
|
163
|
+
continue
|
|
164
|
+
renderable = r_assistant.render_assistant_message(e.content, code_theme=self.themes.code_theme)
|
|
165
|
+
if renderable is not None:
|
|
166
|
+
self.print(renderable)
|
|
167
|
+
self.print()
|
|
168
|
+
case events.ThinkingEvent() as e:
|
|
169
|
+
if is_sub_agent:
|
|
170
|
+
continue
|
|
171
|
+
self.display_thinking(e.content)
|
|
172
|
+
case events.DeveloperMessageEvent() as e:
|
|
173
|
+
self.display_developer_message(e)
|
|
174
|
+
self.display_command_output(e)
|
|
175
|
+
case events.UserMessageEvent() as e:
|
|
176
|
+
if is_sub_agent:
|
|
177
|
+
continue
|
|
178
|
+
self.print(r_user_input.render_user_input(e.content))
|
|
179
|
+
case events.ToolCallEvent() as e:
|
|
180
|
+
tool_call_dict[e.tool_call_id] = e
|
|
181
|
+
case events.ToolResultEvent() as e:
|
|
182
|
+
tool_call_event = tool_call_dict.get(e.tool_call_id)
|
|
183
|
+
if tool_call_event is not None:
|
|
184
|
+
self.display_tool_call(tool_call_event)
|
|
185
|
+
tool_call_dict.pop(e.tool_call_id, None)
|
|
186
|
+
if is_sub_agent:
|
|
187
|
+
continue
|
|
188
|
+
self.display_tool_call_result(e)
|
|
189
|
+
case events.TaskMetadataEvent() as e:
|
|
190
|
+
self.print(r_metadata.render_task_metadata(e))
|
|
171
191
|
self.print()
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
case events.ToolCallEvent() as tool_call_event:
|
|
180
|
-
tool_call_dict[tool_call_event.tool_call_id] = tool_call_event
|
|
181
|
-
case events.ToolResultEvent() as tool_result_event:
|
|
182
|
-
tool_call_event = tool_call_dict.get(tool_result_event.tool_call_id)
|
|
183
|
-
if tool_call_event is not None:
|
|
184
|
-
self.display_tool_call(tool_call_event)
|
|
185
|
-
tool_call_dict.pop(tool_result_event.tool_call_id, None)
|
|
186
|
-
self.display_tool_call_result(tool_result_event)
|
|
187
|
-
case events.ResponseMetadataEvent() as metadata_event:
|
|
188
|
-
self.print(r_metadata.render_response_metadata(metadata_event))
|
|
189
|
-
self.print()
|
|
190
|
-
case events.InterruptEvent():
|
|
191
|
-
self.print()
|
|
192
|
-
self.print(r_user_input.render_interrupt())
|
|
192
|
+
case events.InterruptEvent():
|
|
193
|
+
self.print()
|
|
194
|
+
self.print(r_user_input.render_interrupt())
|
|
195
|
+
case events.ErrorEvent() as e:
|
|
196
|
+
self.display_error(e)
|
|
197
|
+
case events.TaskFinishEvent() as e:
|
|
198
|
+
self.display_task_finish(e)
|
|
193
199
|
|
|
194
200
|
def display_developer_message(self, e: events.DeveloperMessageEvent) -> None:
|
|
195
201
|
if not r_developer.need_render_developer_message(e):
|
|
@@ -205,7 +211,7 @@ class REPLRenderer:
|
|
|
205
211
|
self.print()
|
|
206
212
|
|
|
207
213
|
def display_welcome(self, event: events.WelcomeEvent) -> None:
|
|
208
|
-
self.print(r_metadata.render_welcome(event
|
|
214
|
+
self.print(r_metadata.render_welcome(event))
|
|
209
215
|
|
|
210
216
|
def display_user_message(self, event: events.UserMessageEvent) -> None:
|
|
211
217
|
self.print(r_user_input.render_user_input(event.content))
|
|
@@ -231,18 +237,28 @@ class REPLRenderer:
|
|
|
231
237
|
self.print(renderable)
|
|
232
238
|
self.print()
|
|
233
239
|
|
|
234
|
-
def
|
|
240
|
+
def display_task_metadata(self, event: events.TaskMetadataEvent) -> None:
|
|
235
241
|
with self.session_print_context(event.session_id):
|
|
236
|
-
self.print(r_metadata.
|
|
242
|
+
self.print(r_metadata.render_task_metadata(event))
|
|
237
243
|
self.print()
|
|
238
244
|
|
|
239
245
|
def display_task_finish(self, event: events.TaskFinishEvent) -> None:
|
|
240
246
|
if self.is_sub_agent_session(event.session_id):
|
|
247
|
+
session_status = self.session_map.get(event.session_id)
|
|
248
|
+
description = (
|
|
249
|
+
session_status.sub_agent_state.sub_agent_desc
|
|
250
|
+
if session_status and session_status.sub_agent_state
|
|
251
|
+
else None
|
|
252
|
+
)
|
|
253
|
+
panel_style = self.get_session_sub_agent_background(event.session_id)
|
|
241
254
|
with self.session_print_context(event.session_id):
|
|
242
255
|
self.print(
|
|
243
256
|
r_sub_agent.render_sub_agent_result(
|
|
244
257
|
event.task_result,
|
|
245
258
|
code_theme=self.themes.code_theme,
|
|
259
|
+
has_structured_output=event.has_structured_output,
|
|
260
|
+
description=description,
|
|
261
|
+
panel_style=panel_style,
|
|
246
262
|
)
|
|
247
263
|
)
|
|
248
264
|
|
|
@@ -250,15 +266,11 @@ class REPLRenderer:
|
|
|
250
266
|
self.print(r_user_input.render_interrupt())
|
|
251
267
|
|
|
252
268
|
def display_error(self, event: events.ErrorEvent) -> None:
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
self.
|
|
256
|
-
|
|
257
|
-
)
|
|
258
|
-
)
|
|
259
|
-
|
|
260
|
-
def display_thinking_prefix(self) -> None:
|
|
261
|
-
self.print(r_thinking.thinking_prefix())
|
|
269
|
+
if event.session_id:
|
|
270
|
+
with self.session_print_context(event.session_id):
|
|
271
|
+
self.print(r_errors.render_error(truncate_display(event.error_message)))
|
|
272
|
+
else:
|
|
273
|
+
self.print(r_errors.render_error(truncate_display(event.error_message)))
|
|
262
274
|
|
|
263
275
|
# -------------------------------------------------------------------------
|
|
264
276
|
# Spinner control methods
|
|
@@ -266,16 +278,94 @@ class REPLRenderer:
|
|
|
266
278
|
|
|
267
279
|
def spinner_start(self) -> None:
|
|
268
280
|
"""Start the spinner animation."""
|
|
269
|
-
self.
|
|
281
|
+
self._spinner_visible = True
|
|
282
|
+
self._ensure_bottom_live_started()
|
|
283
|
+
self._refresh_bottom_live()
|
|
270
284
|
|
|
271
285
|
def spinner_stop(self) -> None:
|
|
272
286
|
"""Stop the spinner animation."""
|
|
273
|
-
self.
|
|
287
|
+
self._spinner_visible = False
|
|
288
|
+
self._refresh_bottom_live()
|
|
274
289
|
|
|
275
|
-
def spinner_update(self, status_text: str | Text) -> None:
|
|
276
|
-
"""Update the spinner status text."""
|
|
277
|
-
self.
|
|
290
|
+
def spinner_update(self, status_text: str | Text, right_text: RenderableType | None = None) -> None:
|
|
291
|
+
"""Update the spinner status text with optional right-aligned text."""
|
|
292
|
+
self._status_text = ShimmerStatusText(status_text, right_text)
|
|
293
|
+
self._status_spinner.update(text=SingleLine(self._status_text), style=ThemeKey.STATUS_SPINNER)
|
|
294
|
+
self._refresh_bottom_live()
|
|
278
295
|
|
|
279
296
|
def spinner_renderable(self) -> Spinner:
|
|
280
297
|
"""Return the spinner's renderable for embedding in other components."""
|
|
281
|
-
return self.
|
|
298
|
+
return self._status_spinner
|
|
299
|
+
|
|
300
|
+
def set_stream_renderable(self, renderable: RenderableType | None) -> None:
|
|
301
|
+
"""Set the current streaming renderable displayed above the status line."""
|
|
302
|
+
|
|
303
|
+
if renderable is None:
|
|
304
|
+
self._stream_renderable = None
|
|
305
|
+
self._stream_max_height = 0
|
|
306
|
+
self._stream_last_height = 0
|
|
307
|
+
self._stream_last_width = 0
|
|
308
|
+
self._refresh_bottom_live()
|
|
309
|
+
return
|
|
310
|
+
|
|
311
|
+
self._ensure_bottom_live_started()
|
|
312
|
+
self._stream_renderable = renderable
|
|
313
|
+
|
|
314
|
+
height = len(self.console.render_lines(renderable, self.console.options, pad=False))
|
|
315
|
+
self._stream_last_height = height
|
|
316
|
+
self._stream_last_width = self.console.size.width
|
|
317
|
+
self._stream_max_height = max(self._stream_max_height, height)
|
|
318
|
+
self._refresh_bottom_live()
|
|
319
|
+
|
|
320
|
+
def _ensure_bottom_live_started(self) -> None:
|
|
321
|
+
if self._bottom_live is not None:
|
|
322
|
+
return
|
|
323
|
+
self._bottom_live = CropAboveLive(
|
|
324
|
+
Text(""),
|
|
325
|
+
console=self.console,
|
|
326
|
+
refresh_per_second=30,
|
|
327
|
+
transient=True,
|
|
328
|
+
redirect_stdout=False,
|
|
329
|
+
redirect_stderr=False,
|
|
330
|
+
)
|
|
331
|
+
self._bottom_live.start()
|
|
332
|
+
|
|
333
|
+
def _bottom_renderable(self) -> RenderableType:
|
|
334
|
+
stream_part: RenderableType = Group()
|
|
335
|
+
gap_part: RenderableType = Group()
|
|
336
|
+
|
|
337
|
+
if const.MARKDOWN_STREAM_LIVE_REPAINT_ENABLED:
|
|
338
|
+
stream = self._stream_renderable
|
|
339
|
+
if stream is not None:
|
|
340
|
+
current_width = self.console.size.width
|
|
341
|
+
if self._stream_last_width != current_width:
|
|
342
|
+
height = len(self.console.render_lines(stream, self.console.options, pad=False))
|
|
343
|
+
self._stream_last_height = height
|
|
344
|
+
self._stream_last_width = current_width
|
|
345
|
+
self._stream_max_height = max(self._stream_max_height, height)
|
|
346
|
+
else:
|
|
347
|
+
height = self._stream_last_height
|
|
348
|
+
|
|
349
|
+
pad_lines = max(self._stream_max_height - height, 0)
|
|
350
|
+
if pad_lines:
|
|
351
|
+
stream = Padding(stream, (0, 0, pad_lines, 0))
|
|
352
|
+
stream_part = stream
|
|
353
|
+
|
|
354
|
+
gap_part = Text("") if self._spinner_visible else Group()
|
|
355
|
+
|
|
356
|
+
status_part: RenderableType = SingleLine(self._status_spinner) if self._spinner_visible else Group()
|
|
357
|
+
return Group(stream_part, gap_part, status_part)
|
|
358
|
+
|
|
359
|
+
def _refresh_bottom_live(self) -> None:
|
|
360
|
+
if self._bottom_live is None:
|
|
361
|
+
return
|
|
362
|
+
self._bottom_live.update(self._bottom_renderable(), refresh=True)
|
|
363
|
+
|
|
364
|
+
def stop_bottom_live(self) -> None:
|
|
365
|
+
if self._bottom_live is None:
|
|
366
|
+
return
|
|
367
|
+
with contextlib.suppress(Exception):
|
|
368
|
+
# Avoid cursor restore when stopping right before prompt_toolkit.
|
|
369
|
+
self._bottom_live.transient = False
|
|
370
|
+
self._bottom_live.stop()
|
|
371
|
+
self._bottom_live = None
|