klaude-code 1.2.25__py3-none-any.whl → 1.2.27__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/config_cmd.py +1 -5
- klaude_code/cli/list_model.py +170 -129
- klaude_code/cli/main.py +37 -5
- klaude_code/cli/runtime.py +4 -6
- klaude_code/cli/self_update.py +2 -1
- klaude_code/cli/session_cmd.py +1 -1
- klaude_code/config/__init__.py +3 -1
- klaude_code/config/assets/__init__.py +1 -0
- klaude_code/config/assets/builtin_config.yaml +233 -0
- klaude_code/config/builtin_config.py +37 -0
- klaude_code/config/config.py +332 -112
- klaude_code/config/select_model.py +45 -8
- klaude_code/const.py +5 -1
- klaude_code/core/executor.py +4 -2
- klaude_code/core/manager/llm_clients_builder.py +4 -1
- klaude_code/core/tool/file/apply_patch_tool.py +26 -3
- klaude_code/core/tool/file/edit_tool.py +4 -4
- klaude_code/core/tool/file/write_tool.py +4 -4
- klaude_code/core/tool/shell/bash_tool.py +2 -2
- klaude_code/llm/openai_compatible/stream.py +2 -1
- klaude_code/protocol/model.py +24 -1
- klaude_code/session/export.py +1 -1
- klaude_code/session/selector.py +2 -2
- klaude_code/session/session.py +4 -4
- klaude_code/ui/modes/repl/completers.py +4 -4
- klaude_code/ui/modes/repl/event_handler.py +23 -4
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +4 -4
- klaude_code/ui/modes/repl/key_bindings.py +4 -4
- klaude_code/ui/modes/repl/renderer.py +22 -17
- klaude_code/ui/renderers/diffs.py +1 -1
- klaude_code/ui/renderers/metadata.py +2 -2
- klaude_code/ui/renderers/sub_agent.py +14 -12
- klaude_code/ui/renderers/thinking.py +1 -1
- klaude_code/ui/renderers/tools.py +27 -3
- klaude_code/ui/rich/markdown.py +35 -15
- klaude_code/ui/rich/theme.py +2 -5
- klaude_code/ui/terminal/color.py +1 -1
- klaude_code/ui/terminal/control.py +4 -4
- {klaude_code-1.2.25.dist-info → klaude_code-1.2.27.dist-info}/METADATA +121 -127
- {klaude_code-1.2.25.dist-info → klaude_code-1.2.27.dist-info}/RECORD +42 -39
- {klaude_code-1.2.25.dist-info → klaude_code-1.2.27.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.25.dist-info → klaude_code-1.2.27.dist-info}/entry_points.txt +0 -0
|
@@ -20,7 +20,7 @@ class ApplyPatchHandler:
|
|
|
20
20
|
@classmethod
|
|
21
21
|
async def handle_apply_patch(cls, patch_text: str) -> model.ToolResultItem:
|
|
22
22
|
try:
|
|
23
|
-
output,
|
|
23
|
+
output, ui_extra = await asyncio.to_thread(cls._apply_patch_in_thread, patch_text)
|
|
24
24
|
except apply_patch_module.DiffError as error:
|
|
25
25
|
return model.ToolResultItem(status="error", output=str(error))
|
|
26
26
|
except Exception as error: # pragma: no cover # unexpected errors bubbled to tool result
|
|
@@ -28,11 +28,11 @@ class ApplyPatchHandler:
|
|
|
28
28
|
return model.ToolResultItem(
|
|
29
29
|
status="success",
|
|
30
30
|
output=output,
|
|
31
|
-
ui_extra=
|
|
31
|
+
ui_extra=ui_extra,
|
|
32
32
|
)
|
|
33
33
|
|
|
34
34
|
@staticmethod
|
|
35
|
-
def _apply_patch_in_thread(patch_text: str) -> tuple[str, model.
|
|
35
|
+
def _apply_patch_in_thread(patch_text: str) -> tuple[str, model.ToolResultUIExtra]:
|
|
36
36
|
ap = apply_patch_module
|
|
37
37
|
normalized_start = patch_text.lstrip()
|
|
38
38
|
if not normalized_start.startswith("*** Begin Patch"):
|
|
@@ -69,6 +69,16 @@ class ApplyPatchHandler:
|
|
|
69
69
|
commit = ap.patch_to_commit(patch, orig)
|
|
70
70
|
diff_ui = ApplyPatchHandler._commit_to_structured_diff(commit)
|
|
71
71
|
|
|
72
|
+
md_items: list[model.MarkdownDocUIExtra] = []
|
|
73
|
+
for change_path, change in commit.changes.items():
|
|
74
|
+
if change.type == apply_patch_module.ActionType.ADD and change_path.endswith(".md"):
|
|
75
|
+
md_items.append(
|
|
76
|
+
model.MarkdownDocUIExtra(
|
|
77
|
+
file_path=resolve_path(change_path),
|
|
78
|
+
content=change.new_content or "",
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
|
|
72
82
|
def write_fn(path: str, content: str) -> None:
|
|
73
83
|
resolved = resolve_path(path)
|
|
74
84
|
if os.path.isdir(resolved):
|
|
@@ -102,6 +112,16 @@ class ApplyPatchHandler:
|
|
|
102
112
|
file_tracker.pop(resolved, None)
|
|
103
113
|
|
|
104
114
|
ap.apply_commit(commit, write_fn, remove_fn)
|
|
115
|
+
|
|
116
|
+
# apply_patch can include multiple operations. If we added markdown files,
|
|
117
|
+
# return a MultiUIExtra so UI can render markdown previews (without showing a diff for those markdown adds).
|
|
118
|
+
if md_items:
|
|
119
|
+
items: list[model.MultiUIExtraItem] = []
|
|
120
|
+
items.extend(md_items)
|
|
121
|
+
if diff_ui.files:
|
|
122
|
+
items.append(diff_ui)
|
|
123
|
+
return "Done!", model.MultiUIExtra(items=items)
|
|
124
|
+
|
|
105
125
|
return "Done!", diff_ui
|
|
106
126
|
|
|
107
127
|
@staticmethod
|
|
@@ -110,6 +130,9 @@ class ApplyPatchHandler:
|
|
|
110
130
|
for path in sorted(commit.changes):
|
|
111
131
|
change = commit.changes[path]
|
|
112
132
|
if change.type == apply_patch_module.ActionType.ADD:
|
|
133
|
+
# For markdown files created via Add File, we render content via MarkdownDocUIExtra instead of a diff.
|
|
134
|
+
if path.endswith(".md"):
|
|
135
|
+
continue
|
|
113
136
|
files.append(build_structured_file_diff("", change.new_content or "", file_path=path))
|
|
114
137
|
elif change.type == apply_patch_module.ActionType.DELETE:
|
|
115
138
|
files.append(build_structured_file_diff(change.old_content or "", "", file_path=path))
|
|
@@ -88,7 +88,7 @@ class EditTool(ToolABC):
|
|
|
88
88
|
async def call(cls, arguments: str) -> model.ToolResultItem:
|
|
89
89
|
try:
|
|
90
90
|
args = EditTool.EditArguments.model_validate_json(arguments)
|
|
91
|
-
except
|
|
91
|
+
except ValueError as e: # pragma: no cover - defensive
|
|
92
92
|
return model.ToolResultItem(status="error", output=f"Invalid arguments: {e}")
|
|
93
93
|
|
|
94
94
|
file_path = os.path.abspath(args.file_path)
|
|
@@ -150,7 +150,7 @@ class EditTool(ToolABC):
|
|
|
150
150
|
# Backward-compat: old sessions only stored mtime.
|
|
151
151
|
try:
|
|
152
152
|
current_mtime = Path(file_path).stat().st_mtime
|
|
153
|
-
except
|
|
153
|
+
except OSError:
|
|
154
154
|
current_mtime = tracked_status.mtime
|
|
155
155
|
if current_mtime != tracked_status.mtime:
|
|
156
156
|
return model.ToolResultItem(
|
|
@@ -188,7 +188,7 @@ class EditTool(ToolABC):
|
|
|
188
188
|
# Write back
|
|
189
189
|
try:
|
|
190
190
|
await asyncio.to_thread(write_text, file_path, after)
|
|
191
|
-
except
|
|
191
|
+
except (OSError, UnicodeError) as e: # pragma: no cover
|
|
192
192
|
return model.ToolResultItem(status="error", output=f"<tool_use_error>{e}</tool_use_error>")
|
|
193
193
|
|
|
194
194
|
# Prepare UI extra: unified diff with 3 context lines
|
|
@@ -233,7 +233,7 @@ class EditTool(ToolABC):
|
|
|
233
233
|
plus_range = plus.split(" ")[0]
|
|
234
234
|
start = int(plus_range.split(",")[0]) if "," in plus_range else int(plus_range)
|
|
235
235
|
after_line_no = start - 1
|
|
236
|
-
except
|
|
236
|
+
except (ValueError, IndexError):
|
|
237
237
|
after_line_no = 0
|
|
238
238
|
continue
|
|
239
239
|
if line.startswith(" ") or (line.startswith("+") and not line.startswith("+++ ")):
|
|
@@ -49,7 +49,7 @@ class WriteTool(ToolABC):
|
|
|
49
49
|
async def call(cls, arguments: str) -> model.ToolResultItem:
|
|
50
50
|
try:
|
|
51
51
|
args = WriteArguments.model_validate_json(arguments)
|
|
52
|
-
except
|
|
52
|
+
except ValueError as e: # pragma: no cover - defensive
|
|
53
53
|
return model.ToolResultItem(status="error", output=f"Invalid arguments: {e}")
|
|
54
54
|
|
|
55
55
|
file_path = os.path.abspath(args.file_path)
|
|
@@ -79,7 +79,7 @@ class WriteTool(ToolABC):
|
|
|
79
79
|
try:
|
|
80
80
|
before = await asyncio.to_thread(read_text, file_path)
|
|
81
81
|
before_read_ok = True
|
|
82
|
-
except
|
|
82
|
+
except OSError:
|
|
83
83
|
before = ""
|
|
84
84
|
before_read_ok = False
|
|
85
85
|
|
|
@@ -98,7 +98,7 @@ class WriteTool(ToolABC):
|
|
|
98
98
|
# Backward-compat: old sessions only stored mtime, or we couldn't hash.
|
|
99
99
|
try:
|
|
100
100
|
current_mtime = Path(file_path).stat().st_mtime
|
|
101
|
-
except
|
|
101
|
+
except OSError:
|
|
102
102
|
current_mtime = tracked_status.mtime
|
|
103
103
|
if current_mtime != tracked_status.mtime:
|
|
104
104
|
return model.ToolResultItem(
|
|
@@ -111,7 +111,7 @@ class WriteTool(ToolABC):
|
|
|
111
111
|
|
|
112
112
|
try:
|
|
113
113
|
await asyncio.to_thread(write_text, file_path, args.content)
|
|
114
|
-
except
|
|
114
|
+
except (OSError, UnicodeError) as e: # pragma: no cover
|
|
115
115
|
return model.ToolResultItem(status="error", output=f"<tool_use_error>{e}</tool_use_error>")
|
|
116
116
|
|
|
117
117
|
if file_tracker is not None:
|
|
@@ -274,7 +274,7 @@ class BashTool(ToolABC):
|
|
|
274
274
|
proc.terminate()
|
|
275
275
|
except ProcessLookupError:
|
|
276
276
|
return
|
|
277
|
-
except
|
|
277
|
+
except OSError:
|
|
278
278
|
# Fall back to kill below.
|
|
279
279
|
pass
|
|
280
280
|
|
|
@@ -356,7 +356,7 @@ class BashTool(ToolABC):
|
|
|
356
356
|
except asyncio.CancelledError:
|
|
357
357
|
# Propagate cooperative cancellation so outer layers can handle interrupts correctly.
|
|
358
358
|
raise
|
|
359
|
-
except
|
|
359
|
+
except OSError as e: # safeguard: catch remaining OS-level errors (permissions, resources, etc.)
|
|
360
360
|
return model.ToolResultItem(
|
|
361
361
|
status="error",
|
|
362
362
|
output=f"Execution error: {e}",
|
|
@@ -20,6 +20,7 @@ from typing import Any, Literal, cast
|
|
|
20
20
|
import httpx
|
|
21
21
|
import openai
|
|
22
22
|
import openai.types
|
|
23
|
+
import pydantic
|
|
23
24
|
from openai import AsyncStream
|
|
24
25
|
from openai.types.chat.chat_completion_chunk import ChatCompletionChunk
|
|
25
26
|
|
|
@@ -204,7 +205,7 @@ async def parse_chat_completions_stream(
|
|
|
204
205
|
try:
|
|
205
206
|
usage = openai.types.CompletionUsage.model_validate(choice_usage)
|
|
206
207
|
metadata_tracker.set_usage(convert_usage(usage, param.context_limit, param.max_tokens))
|
|
207
|
-
except
|
|
208
|
+
except pydantic.ValidationError:
|
|
208
209
|
pass
|
|
209
210
|
|
|
210
211
|
delta = cast(Any, getattr(choice0, "delta", None))
|
klaude_code/protocol/model.py
CHANGED
|
@@ -151,6 +151,28 @@ class SessionStatusUIExtra(BaseModel):
|
|
|
151
151
|
by_model: list["TaskMetadata"] = []
|
|
152
152
|
|
|
153
153
|
|
|
154
|
+
MultiUIExtraItem = (
|
|
155
|
+
DiffUIExtra
|
|
156
|
+
| TodoListUIExtra
|
|
157
|
+
| SessionIdUIExtra
|
|
158
|
+
| MermaidLinkUIExtra
|
|
159
|
+
| TruncationUIExtra
|
|
160
|
+
| MarkdownDocUIExtra
|
|
161
|
+
| SessionStatusUIExtra
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class MultiUIExtra(BaseModel):
|
|
166
|
+
"""A container UIExtra that can render multiple UI blocks for a single tool result.
|
|
167
|
+
|
|
168
|
+
This is primarily used by tools like apply_patch which can perform multiple
|
|
169
|
+
operations in one invocation.
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
type: Literal["multi"] = "multi"
|
|
173
|
+
items: list[MultiUIExtraItem]
|
|
174
|
+
|
|
175
|
+
|
|
154
176
|
ToolResultUIExtra = Annotated[
|
|
155
177
|
DiffUIExtra
|
|
156
178
|
| TodoListUIExtra
|
|
@@ -158,7 +180,8 @@ ToolResultUIExtra = Annotated[
|
|
|
158
180
|
| MermaidLinkUIExtra
|
|
159
181
|
| TruncationUIExtra
|
|
160
182
|
| MarkdownDocUIExtra
|
|
161
|
-
| SessionStatusUIExtra
|
|
183
|
+
| SessionStatusUIExtra
|
|
184
|
+
| MultiUIExtra,
|
|
162
185
|
Field(discriminator="type"),
|
|
163
186
|
]
|
|
164
187
|
|
klaude_code/session/export.py
CHANGED
klaude_code/session/selector.py
CHANGED
|
@@ -23,7 +23,7 @@ def resume_select_session() -> str | None:
|
|
|
23
23
|
def _fmt(ts: float) -> str:
|
|
24
24
|
try:
|
|
25
25
|
return time.strftime("%m-%d %H:%M:%S", time.localtime(ts))
|
|
26
|
-
except
|
|
26
|
+
except (ValueError, OSError):
|
|
27
27
|
return str(ts)
|
|
28
28
|
|
|
29
29
|
try:
|
|
@@ -76,6 +76,6 @@ def resume_select_session() -> str | None:
|
|
|
76
76
|
idx = int(raw)
|
|
77
77
|
if 1 <= idx <= len(sessions):
|
|
78
78
|
return str(sessions[idx - 1].id)
|
|
79
|
-
except
|
|
79
|
+
except (ValueError, EOFError):
|
|
80
80
|
return None
|
|
81
81
|
return None
|
klaude_code/session/session.py
CHANGED
|
@@ -7,7 +7,7 @@ from collections.abc import Iterable, Sequence
|
|
|
7
7
|
from pathlib import Path
|
|
8
8
|
from typing import Any, cast
|
|
9
9
|
|
|
10
|
-
from pydantic import BaseModel, Field, PrivateAttr
|
|
10
|
+
from pydantic import BaseModel, Field, PrivateAttr, ValidationError
|
|
11
11
|
|
|
12
12
|
from klaude_code.protocol import events, llm_param, model, tools
|
|
13
13
|
from klaude_code.session.store import JsonlSessionStore, ProjectPaths, build_meta_snapshot
|
|
@@ -124,7 +124,7 @@ class Session(BaseModel):
|
|
|
124
124
|
if isinstance(k, str) and isinstance(v, dict):
|
|
125
125
|
try:
|
|
126
126
|
file_tracker[k] = model.FileStatus.model_validate(v)
|
|
127
|
-
except
|
|
127
|
+
except ValidationError:
|
|
128
128
|
continue
|
|
129
129
|
|
|
130
130
|
todos_raw = raw.get("todos")
|
|
@@ -135,7 +135,7 @@ class Session(BaseModel):
|
|
|
135
135
|
continue
|
|
136
136
|
try:
|
|
137
137
|
todos.append(model.TodoItem.model_validate(todo_raw))
|
|
138
|
-
except
|
|
138
|
+
except ValidationError:
|
|
139
139
|
continue
|
|
140
140
|
|
|
141
141
|
created_at = float(raw.get("created_at", time.time()))
|
|
@@ -306,7 +306,7 @@ class Session(BaseModel):
|
|
|
306
306
|
seen_sub_agent_sessions.add(session_id)
|
|
307
307
|
try:
|
|
308
308
|
sub_session = Session.load(session_id)
|
|
309
|
-
except
|
|
309
|
+
except (OSError, json.JSONDecodeError, ValueError):
|
|
310
310
|
return
|
|
311
311
|
yield from sub_session.get_history_item()
|
|
312
312
|
|
|
@@ -204,7 +204,7 @@ class _SkillCompleter(Completer):
|
|
|
204
204
|
from klaude_code.skill import get_available_skills
|
|
205
205
|
|
|
206
206
|
return get_available_skills()
|
|
207
|
-
except
|
|
207
|
+
except (ImportError, RuntimeError):
|
|
208
208
|
return []
|
|
209
209
|
|
|
210
210
|
def is_skill_context(self, document: Document) -> bool:
|
|
@@ -497,7 +497,7 @@ class _AtFilesCompleter(Completer):
|
|
|
497
497
|
try:
|
|
498
498
|
if (cwd / s).is_dir():
|
|
499
499
|
uniq[idx] = f"{s}/"
|
|
500
|
-
except
|
|
500
|
+
except OSError:
|
|
501
501
|
continue
|
|
502
502
|
return uniq
|
|
503
503
|
|
|
@@ -530,7 +530,7 @@ class _AtFilesCompleter(Completer):
|
|
|
530
530
|
if tag != "search":
|
|
531
531
|
return root, None
|
|
532
532
|
return root, kw
|
|
533
|
-
except
|
|
533
|
+
except ValueError:
|
|
534
534
|
return None, None
|
|
535
535
|
|
|
536
536
|
# ---- Utilities ----
|
|
@@ -680,7 +680,7 @@ class _AtFilesCompleter(Completer):
|
|
|
680
680
|
if p.is_dir() and not rel.endswith("/"):
|
|
681
681
|
rel += "/"
|
|
682
682
|
items.append(rel)
|
|
683
|
-
except
|
|
683
|
+
except OSError:
|
|
684
684
|
return []
|
|
685
685
|
return items[: min(self._max_results, 100)]
|
|
686
686
|
|
|
@@ -122,6 +122,7 @@ class ActivityState:
|
|
|
122
122
|
|
|
123
123
|
def __init__(self) -> None:
|
|
124
124
|
self._composing: bool = False
|
|
125
|
+
self._buffer_length: int = 0
|
|
125
126
|
self._tool_calls: dict[str, int] = {}
|
|
126
127
|
|
|
127
128
|
@property
|
|
@@ -134,6 +135,11 @@ class ActivityState:
|
|
|
134
135
|
|
|
135
136
|
def set_composing(self, composing: bool) -> None:
|
|
136
137
|
self._composing = composing
|
|
138
|
+
if not composing:
|
|
139
|
+
self._buffer_length = 0
|
|
140
|
+
|
|
141
|
+
def set_buffer_length(self, length: int) -> None:
|
|
142
|
+
self._buffer_length = length
|
|
137
143
|
|
|
138
144
|
def add_tool_call(self, tool_name: str) -> None:
|
|
139
145
|
self._tool_calls[tool_name] = self._tool_calls.get(tool_name, 0) + 1
|
|
@@ -143,6 +149,7 @@ class ActivityState:
|
|
|
143
149
|
|
|
144
150
|
def reset(self) -> None:
|
|
145
151
|
self._composing = False
|
|
152
|
+
self._buffer_length = 0
|
|
146
153
|
self._tool_calls = {}
|
|
147
154
|
|
|
148
155
|
def get_activity_text(self) -> Text | None:
|
|
@@ -159,7 +166,12 @@ class ActivityState:
|
|
|
159
166
|
first = False
|
|
160
167
|
return activity_text
|
|
161
168
|
if self._composing:
|
|
162
|
-
|
|
169
|
+
# Main status text with creative verb
|
|
170
|
+
text = Text.assemble(
|
|
171
|
+
("Composing ", ThemeKey.STATUS_TEXT_BOLD),
|
|
172
|
+
(f"({self._buffer_length:,})", ThemeKey.STATUS_TEXT),
|
|
173
|
+
)
|
|
174
|
+
return text
|
|
163
175
|
return None
|
|
164
176
|
|
|
165
177
|
|
|
@@ -206,6 +218,10 @@ class SpinnerStatusState:
|
|
|
206
218
|
self._reasoning_status = None
|
|
207
219
|
self._activity.set_composing(composing)
|
|
208
220
|
|
|
221
|
+
def set_buffer_length(self, length: int) -> None:
|
|
222
|
+
"""Set buffer length for composing state display."""
|
|
223
|
+
self._activity.set_buffer_length(length)
|
|
224
|
+
|
|
209
225
|
def add_tool_call(self, tool_name: str) -> None:
|
|
210
226
|
"""Add a tool call to the accumulator."""
|
|
211
227
|
self._activity.add_tool_call(tool_name)
|
|
@@ -230,7 +246,7 @@ class SpinnerStatusState:
|
|
|
230
246
|
"""Get current spinner status as rich Text (without context)."""
|
|
231
247
|
activity_text = self._activity.get_activity_text()
|
|
232
248
|
|
|
233
|
-
base_status = self.
|
|
249
|
+
base_status = self._reasoning_status or self._todo_status
|
|
234
250
|
|
|
235
251
|
if base_status:
|
|
236
252
|
result = Text(base_status, style=ThemeKey.STATUS_TEXT_BOLD)
|
|
@@ -375,7 +391,7 @@ class DisplayEventHandler:
|
|
|
375
391
|
},
|
|
376
392
|
theme=self.renderer.themes.thinking_markdown_theme,
|
|
377
393
|
console=self.renderer.console,
|
|
378
|
-
live_sink=self.renderer.set_stream_renderable,
|
|
394
|
+
live_sink=self.renderer.set_stream_renderable if const.MARKDOWN_STREAM_LIVE_REPAINT_ENABLED else None,
|
|
379
395
|
mark=THINKING_MESSAGE_MARK,
|
|
380
396
|
mark_style=ThemeKey.THINKING,
|
|
381
397
|
left_margin=const.MARKDOWN_LEFT_MARGIN,
|
|
@@ -412,12 +428,15 @@ class DisplayEventHandler:
|
|
|
412
428
|
mdargs={"code_theme": self.renderer.themes.code_theme},
|
|
413
429
|
theme=self.renderer.themes.markdown_theme,
|
|
414
430
|
console=self.renderer.console,
|
|
415
|
-
live_sink=self.renderer.set_stream_renderable,
|
|
431
|
+
live_sink=self.renderer.set_stream_renderable if const.MARKDOWN_STREAM_LIVE_REPAINT_ENABLED else None,
|
|
416
432
|
mark=ASSISTANT_MESSAGE_MARK,
|
|
417
433
|
left_margin=const.MARKDOWN_LEFT_MARGIN,
|
|
418
434
|
)
|
|
419
435
|
self.assistant_stream.start(mdstream)
|
|
420
436
|
self.assistant_stream.append(event.content)
|
|
437
|
+
self.spinner_status.set_buffer_length(len(self.assistant_stream.buffer))
|
|
438
|
+
if not first_delta:
|
|
439
|
+
self._update_spinner()
|
|
421
440
|
if first_delta and self.assistant_stream.mdstream is not None:
|
|
422
441
|
self.assistant_stream.mdstream.update(self.assistant_stream.buffer)
|
|
423
442
|
await self.stage_manager.transition_to(Stage.ASSISTANT)
|
|
@@ -118,7 +118,7 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
118
118
|
try:
|
|
119
119
|
status = self._status_provider()
|
|
120
120
|
update_message = status.update_message
|
|
121
|
-
except
|
|
121
|
+
except (AttributeError, RuntimeError):
|
|
122
122
|
pass
|
|
123
123
|
|
|
124
124
|
# If update available, show only the update message
|
|
@@ -127,7 +127,7 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
127
127
|
try:
|
|
128
128
|
terminal_width = shutil.get_terminal_size().columns
|
|
129
129
|
padding = " " * max(0, terminal_width - len(left_text))
|
|
130
|
-
except
|
|
130
|
+
except (OSError, ValueError):
|
|
131
131
|
padding = ""
|
|
132
132
|
toolbar_text = left_text + padding
|
|
133
133
|
return FormattedText([("#ansiyellow", toolbar_text)])
|
|
@@ -151,7 +151,7 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
151
151
|
# Add context if available
|
|
152
152
|
if status.context_usage_percent is not None:
|
|
153
153
|
right_parts.append(f"context {status.context_usage_percent:.1f}%")
|
|
154
|
-
except
|
|
154
|
+
except (AttributeError, RuntimeError):
|
|
155
155
|
pass
|
|
156
156
|
|
|
157
157
|
# Build left and right text with borders
|
|
@@ -163,7 +163,7 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
163
163
|
terminal_width = shutil.get_terminal_size().columns
|
|
164
164
|
used_width = len(left_text) + len(right_text)
|
|
165
165
|
padding = " " * max(0, terminal_width - used_width)
|
|
166
|
-
except
|
|
166
|
+
except (OSError, ValueError):
|
|
167
167
|
padding = ""
|
|
168
168
|
|
|
169
169
|
# Build result with style
|
|
@@ -52,7 +52,7 @@ def create_key_bindings(
|
|
|
52
52
|
buf.delete_before_cursor() # remove the sentinel backslash # type: ignore[reportUnknownMemberType]
|
|
53
53
|
buf.insert_text("\n") # type: ignore[reportUnknownMemberType]
|
|
54
54
|
return
|
|
55
|
-
except
|
|
55
|
+
except (AttributeError, TypeError):
|
|
56
56
|
# Fall through to default behavior if anything goes wrong
|
|
57
57
|
pass
|
|
58
58
|
|
|
@@ -111,7 +111,7 @@ def create_key_bindings(
|
|
|
111
111
|
|
|
112
112
|
if should_refresh:
|
|
113
113
|
buf.start_completion(select_first=False) # type: ignore[reportUnknownMemberType]
|
|
114
|
-
except
|
|
114
|
+
except (AttributeError, TypeError):
|
|
115
115
|
pass
|
|
116
116
|
|
|
117
117
|
@kb.add("left")
|
|
@@ -136,7 +136,7 @@ def create_key_bindings(
|
|
|
136
136
|
# Default behavior: move one character left when possible.
|
|
137
137
|
if doc.cursor_position > 0: # type: ignore[reportUnknownMemberType]
|
|
138
138
|
buf.cursor_left() # type: ignore[reportUnknownMemberType]
|
|
139
|
-
except
|
|
139
|
+
except (AttributeError, IndexError, TypeError):
|
|
140
140
|
pass
|
|
141
141
|
|
|
142
142
|
@kb.add("right")
|
|
@@ -163,7 +163,7 @@ def create_key_bindings(
|
|
|
163
163
|
# Default behavior: move one character right when possible.
|
|
164
164
|
if doc.cursor_position < len(doc.text): # type: ignore[reportUnknownMemberType]
|
|
165
165
|
buf.cursor_right() # type: ignore[reportUnknownMemberType]
|
|
166
|
-
except
|
|
166
|
+
except (AttributeError, IndexError, TypeError):
|
|
167
167
|
pass
|
|
168
168
|
|
|
169
169
|
return kb
|
|
@@ -332,23 +332,28 @@ class REPLRenderer:
|
|
|
332
332
|
self._bottom_live.start()
|
|
333
333
|
|
|
334
334
|
def _bottom_renderable(self) -> RenderableType:
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
self.
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
335
|
+
stream_part: RenderableType = Group()
|
|
336
|
+
gap_part: RenderableType = Group()
|
|
337
|
+
|
|
338
|
+
if const.MARKDOWN_STREAM_LIVE_REPAINT_ENABLED:
|
|
339
|
+
stream = self._stream_renderable
|
|
340
|
+
if stream is not None:
|
|
341
|
+
current_width = self.console.size.width
|
|
342
|
+
if self._stream_last_width != current_width:
|
|
343
|
+
height = len(self.console.render_lines(stream, self.console.options, pad=False))
|
|
344
|
+
self._stream_last_height = height
|
|
345
|
+
self._stream_last_width = current_width
|
|
346
|
+
self._stream_max_height = max(self._stream_max_height, height)
|
|
347
|
+
else:
|
|
348
|
+
height = self._stream_last_height
|
|
349
|
+
|
|
350
|
+
pad_lines = max(self._stream_max_height - height, 0)
|
|
351
|
+
if pad_lines:
|
|
352
|
+
stream = Padding(stream, (0, 0, pad_lines, 0))
|
|
353
|
+
stream_part = stream
|
|
354
|
+
|
|
355
|
+
gap_part = Text("") if self._spinner_visible else Group()
|
|
356
|
+
|
|
352
357
|
status_part: RenderableType = SingleLine(self._status_spinner) if self._spinner_visible else Group()
|
|
353
358
|
return Group(stream_part, gap_part, status_part)
|
|
354
359
|
|
|
@@ -148,7 +148,7 @@ def render_diff(diff_text: str, show_file_name: bool = False) -> RenderableType:
|
|
|
148
148
|
plus = parts[2] # like '+12,4'
|
|
149
149
|
new_start = int(plus[1:].split(",")[0])
|
|
150
150
|
new_ln = new_start
|
|
151
|
-
except
|
|
151
|
+
except (IndexError, ValueError):
|
|
152
152
|
new_ln = None
|
|
153
153
|
if has_rendered_diff_content:
|
|
154
154
|
grid.add_row(Text(f"{'⋮':>{const.DIFF_PREFIX_WIDTH}}", style=ThemeKey.TOOL_RESULT), "")
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from importlib.metadata import version
|
|
1
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
2
2
|
|
|
3
3
|
from rich import box
|
|
4
4
|
from rich.console import Group, RenderableType
|
|
@@ -17,7 +17,7 @@ def _get_version() -> str:
|
|
|
17
17
|
"""Get the current version of klaude-code."""
|
|
18
18
|
try:
|
|
19
19
|
return version("klaude-code")
|
|
20
|
-
except
|
|
20
|
+
except PackageNotFoundError:
|
|
21
21
|
return "unknown"
|
|
22
22
|
|
|
23
23
|
|
|
@@ -68,24 +68,22 @@ def render_sub_agent_result(
|
|
|
68
68
|
panel_style: Style | None = None,
|
|
69
69
|
) -> RenderableType:
|
|
70
70
|
stripped_result = result.strip()
|
|
71
|
-
|
|
72
|
-
# Add markdown heading if description is provided
|
|
73
|
-
if description:
|
|
74
|
-
stripped_result = f"# {description}\n\n{stripped_result}"
|
|
75
|
-
|
|
76
71
|
result_panel_style = panel_style or ThemeKey.SUB_AGENT_RESULT_PANEL
|
|
77
72
|
|
|
78
73
|
# Use rich JSON for structured output
|
|
79
74
|
if has_structured_output:
|
|
80
75
|
try:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
style=ThemeKey.TOOL_RESULT,
|
|
86
|
-
),
|
|
87
|
-
JSON(stripped_result),
|
|
76
|
+
group_elements: list[RenderableType] = [
|
|
77
|
+
Text(
|
|
78
|
+
"use /export to view full output",
|
|
79
|
+
style=ThemeKey.TOOL_RESULT,
|
|
88
80
|
),
|
|
81
|
+
JSON(stripped_result),
|
|
82
|
+
]
|
|
83
|
+
if description:
|
|
84
|
+
group_elements.insert(0, NoInsetMarkdown(f"# {description}", code_theme=code_theme, style=style or ""))
|
|
85
|
+
return Panel.fit(
|
|
86
|
+
Group(*group_elements),
|
|
89
87
|
box=box.SIMPLE,
|
|
90
88
|
border_style=ThemeKey.LINES,
|
|
91
89
|
style=result_panel_style,
|
|
@@ -94,6 +92,10 @@ def render_sub_agent_result(
|
|
|
94
92
|
# Fall back to markdown if not valid JSON
|
|
95
93
|
pass
|
|
96
94
|
|
|
95
|
+
# Add markdown heading if description is provided for non-structured output
|
|
96
|
+
if description:
|
|
97
|
+
stripped_result = f"# {description}\n\n{stripped_result}"
|
|
98
|
+
|
|
97
99
|
lines = stripped_result.splitlines()
|
|
98
100
|
if len(lines) > const.SUB_AGENT_RESULT_MAX_LINES:
|
|
99
101
|
hidden_count = len(lines) - const.SUB_AGENT_RESULT_MAX_LINES
|