klaude-code 2.8.1__py3-none-any.whl → 2.9.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/app/runtime.py +2 -1
- klaude_code/auth/antigravity/oauth.py +0 -9
- klaude_code/auth/antigravity/token_manager.py +0 -18
- klaude_code/auth/base.py +53 -0
- klaude_code/auth/codex/exceptions.py +0 -4
- klaude_code/auth/codex/oauth.py +32 -28
- klaude_code/auth/codex/token_manager.py +0 -18
- klaude_code/cli/cost_cmd.py +128 -39
- klaude_code/cli/list_model.py +27 -10
- klaude_code/cli/main.py +14 -3
- klaude_code/config/assets/builtin_config.yaml +8 -24
- klaude_code/config/config.py +47 -25
- klaude_code/config/sub_agent_model_helper.py +18 -13
- klaude_code/config/thinking.py +0 -8
- klaude_code/const.py +1 -1
- klaude_code/core/agent_profile.py +10 -52
- klaude_code/core/compaction/overflow.py +0 -4
- klaude_code/core/executor.py +33 -5
- klaude_code/core/manager/llm_clients.py +9 -1
- klaude_code/core/prompts/prompt-claude-code.md +4 -4
- klaude_code/core/reminders.py +21 -23
- klaude_code/core/task.py +0 -4
- klaude_code/core/tool/__init__.py +3 -2
- klaude_code/core/tool/file/apply_patch.py +0 -27
- klaude_code/core/tool/file/read_tool.md +3 -2
- klaude_code/core/tool/file/read_tool.py +15 -2
- klaude_code/core/tool/offload.py +0 -35
- klaude_code/core/tool/sub_agent/__init__.py +6 -0
- klaude_code/core/tool/sub_agent/image_gen.md +16 -0
- klaude_code/core/tool/sub_agent/image_gen.py +146 -0
- klaude_code/core/tool/sub_agent/task.md +20 -0
- klaude_code/core/tool/sub_agent/task.py +205 -0
- klaude_code/core/tool/tool_registry.py +0 -16
- klaude_code/core/turn.py +1 -1
- klaude_code/llm/anthropic/input.py +6 -5
- klaude_code/llm/antigravity/input.py +14 -7
- klaude_code/llm/codex/client.py +22 -0
- klaude_code/llm/codex/prompt_sync.py +237 -0
- klaude_code/llm/google/client.py +8 -6
- klaude_code/llm/google/input.py +20 -12
- klaude_code/llm/image.py +18 -11
- klaude_code/llm/input_common.py +14 -6
- klaude_code/llm/json_stable.py +37 -0
- klaude_code/llm/openai_compatible/input.py +0 -10
- klaude_code/llm/openai_compatible/stream.py +16 -1
- klaude_code/llm/registry.py +0 -5
- klaude_code/llm/responses/input.py +15 -5
- klaude_code/llm/usage.py +0 -8
- klaude_code/protocol/events.py +2 -1
- klaude_code/protocol/message.py +2 -2
- klaude_code/protocol/model.py +20 -1
- klaude_code/protocol/op.py +13 -0
- klaude_code/protocol/op_handler.py +5 -0
- klaude_code/protocol/sub_agent/AGENTS.md +5 -5
- klaude_code/protocol/sub_agent/__init__.py +13 -34
- klaude_code/protocol/sub_agent/explore.py +7 -34
- klaude_code/protocol/sub_agent/image_gen.py +3 -74
- klaude_code/protocol/sub_agent/task.py +3 -47
- klaude_code/protocol/sub_agent/web.py +8 -52
- klaude_code/protocol/tools.py +2 -0
- klaude_code/session/session.py +58 -21
- klaude_code/session/store.py +0 -4
- klaude_code/skill/assets/deslop/SKILL.md +9 -0
- klaude_code/skill/system_skills.py +0 -20
- klaude_code/tui/command/fork_session_cmd.py +5 -2
- klaude_code/tui/command/resume_cmd.py +9 -2
- klaude_code/tui/command/sub_agent_model_cmd.py +85 -18
- klaude_code/tui/components/assistant.py +0 -26
- klaude_code/tui/components/command_output.py +3 -1
- klaude_code/tui/components/developer.py +3 -0
- klaude_code/tui/components/diffs.py +2 -208
- klaude_code/tui/components/errors.py +4 -0
- klaude_code/tui/components/mermaid_viewer.py +2 -2
- klaude_code/tui/components/rich/markdown.py +0 -54
- klaude_code/tui/components/rich/theme.py +2 -0
- klaude_code/tui/components/sub_agent.py +2 -46
- klaude_code/tui/components/thinking.py +0 -33
- klaude_code/tui/components/tools.py +43 -21
- klaude_code/tui/input/images.py +21 -18
- klaude_code/tui/input/key_bindings.py +2 -2
- klaude_code/tui/input/prompt_toolkit.py +49 -49
- klaude_code/tui/machine.py +15 -11
- klaude_code/tui/renderer.py +11 -20
- klaude_code/tui/runner.py +2 -1
- klaude_code/tui/terminal/image.py +6 -34
- klaude_code/ui/common.py +0 -70
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.0.dist-info}/METADATA +3 -6
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.0.dist-info}/RECORD +90 -86
- klaude_code/core/tool/sub_agent_tool.py +0 -126
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -108
- klaude_code/tui/components/rich/searchable_text.py +0 -68
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.0.dist-info}/entry_points.txt +0 -0
|
@@ -9,6 +9,8 @@ def render_error(error_msg: Text) -> RenderableType:
|
|
|
9
9
|
"""Render error with X mark for error events."""
|
|
10
10
|
grid = create_grid()
|
|
11
11
|
error_msg.style = ThemeKey.ERROR
|
|
12
|
+
error_msg.overflow = "ellipsis"
|
|
13
|
+
error_msg.no_wrap = True
|
|
12
14
|
grid.add_row(Text("✘", style=ThemeKey.ERROR_BOLD), error_msg)
|
|
13
15
|
return grid
|
|
14
16
|
|
|
@@ -17,5 +19,7 @@ def render_tool_error(error_msg: Text) -> RenderableType:
|
|
|
17
19
|
"""Render error with indent for tool results."""
|
|
18
20
|
grid = create_grid()
|
|
19
21
|
error_msg.style = ThemeKey.ERROR
|
|
22
|
+
error_msg.overflow = "ellipsis"
|
|
23
|
+
error_msg.no_wrap = True
|
|
20
24
|
grid.add_row(Text(" "), error_msg)
|
|
21
25
|
return grid
|
|
@@ -16,7 +16,7 @@ _MERMAID_DEFAULT_PNG_SCALE = 2
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
def artifacts_dir() -> Path:
|
|
19
|
-
return Path(TOOL_OUTPUT_TRUNCATION_DIR)
|
|
19
|
+
return Path(TOOL_OUTPUT_TRUNCATION_DIR)
|
|
20
20
|
|
|
21
21
|
|
|
22
22
|
def _extract_pako_from_link(link: str) -> str | None:
|
|
@@ -72,7 +72,7 @@ def ensure_viewer_file(*, code: str, link: str, tool_call_id: str) -> Path | Non
|
|
|
72
72
|
return None
|
|
73
73
|
|
|
74
74
|
safe_id = tool_call_id.replace("/", "_")
|
|
75
|
-
path = artifacts_dir() / f"mermaid-
|
|
75
|
+
path = artifacts_dir() / f"klaude-mermaid-{safe_id}.html"
|
|
76
76
|
if path.exists():
|
|
77
77
|
return path
|
|
78
78
|
|
|
@@ -332,11 +332,6 @@ class MarkdownStream:
|
|
|
332
332
|
self.right_margin: int = max(right_margin, 0)
|
|
333
333
|
self.markdown_class: Callable[..., Markdown] = markdown_class or NoInsetMarkdown
|
|
334
334
|
|
|
335
|
-
@property
|
|
336
|
-
def _live_started(self) -> bool:
|
|
337
|
-
"""Check if Live display has been started (derived from self.live)."""
|
|
338
|
-
return self._live_sink is not None
|
|
339
|
-
|
|
340
335
|
def _get_base_width(self) -> int:
|
|
341
336
|
return self.console.options.max_width
|
|
342
337
|
|
|
@@ -450,14 +445,6 @@ class MarkdownStream:
|
|
|
450
445
|
return "", text, 0
|
|
451
446
|
return stable_source, live_source, stable_line
|
|
452
447
|
|
|
453
|
-
def render_ansi(self, text: str, *, apply_mark: bool) -> str:
|
|
454
|
-
"""Render markdown source to an ANSI string.
|
|
455
|
-
|
|
456
|
-
This is primarily intended for internal debugging and tests.
|
|
457
|
-
"""
|
|
458
|
-
lines, _ = self._render_markdown_to_lines(text, apply_mark=apply_mark)
|
|
459
|
-
return "".join(lines)
|
|
460
|
-
|
|
461
448
|
def render_stable_ansi(self, stable_source: str, *, has_live_suffix: bool, final: bool) -> tuple[str, list[str]]:
|
|
462
449
|
"""Render stable prefix to ANSI, preserving inter-block spacing.
|
|
463
450
|
|
|
@@ -474,47 +461,6 @@ class MarkdownStream:
|
|
|
474
461
|
lines, images = self._render_markdown_to_lines(render_source, apply_mark=True)
|
|
475
462
|
return "".join(lines), images
|
|
476
463
|
|
|
477
|
-
@staticmethod
|
|
478
|
-
def normalize_live_ansi_for_boundary(*, stable_ansi: str, live_ansi: str) -> str:
|
|
479
|
-
"""Normalize whitespace at the stable/live boundary.
|
|
480
|
-
|
|
481
|
-
Some Rich Markdown blocks (e.g. lists) render with a leading blank line.
|
|
482
|
-
If the stable prefix already renders a trailing blank line, rendering the
|
|
483
|
-
live suffix separately may introduce an extra blank line that wouldn't
|
|
484
|
-
appear when rendering the full document.
|
|
485
|
-
|
|
486
|
-
This function removes *overlapping* blank lines from the live ANSI when
|
|
487
|
-
the stable ANSI already ends with one or more blank lines.
|
|
488
|
-
|
|
489
|
-
Important: don't remove *all* leading blank lines from the live suffix.
|
|
490
|
-
In some incomplete-block cases, the live render may begin with multiple
|
|
491
|
-
blank lines while the full-document render would keep one of them.
|
|
492
|
-
"""
|
|
493
|
-
|
|
494
|
-
stable_lines = stable_ansi.splitlines(keepends=True)
|
|
495
|
-
if not stable_lines:
|
|
496
|
-
return live_ansi
|
|
497
|
-
|
|
498
|
-
stable_trailing_blank = 0
|
|
499
|
-
for line in reversed(stable_lines):
|
|
500
|
-
if line.strip():
|
|
501
|
-
break
|
|
502
|
-
stable_trailing_blank += 1
|
|
503
|
-
if stable_trailing_blank <= 0:
|
|
504
|
-
return live_ansi
|
|
505
|
-
|
|
506
|
-
live_lines = live_ansi.splitlines(keepends=True)
|
|
507
|
-
live_leading_blank = 0
|
|
508
|
-
for line in live_lines:
|
|
509
|
-
if line.strip():
|
|
510
|
-
break
|
|
511
|
-
live_leading_blank += 1
|
|
512
|
-
|
|
513
|
-
drop = min(stable_trailing_blank, live_leading_blank)
|
|
514
|
-
if drop > 0:
|
|
515
|
-
live_lines = live_lines[drop:]
|
|
516
|
-
return "".join(live_lines)
|
|
517
|
-
|
|
518
464
|
def _append_nonfinal_sentinel(self, stable_source: str) -> str:
|
|
519
465
|
"""Make Rich render stable content as if it isn't the last block.
|
|
520
466
|
|
|
@@ -109,6 +109,7 @@ DARK_PALETTE = Palette(
|
|
|
109
109
|
|
|
110
110
|
class ThemeKey(str, Enum):
|
|
111
111
|
LINES = "lines"
|
|
112
|
+
LINES_DIM = "lines.dim"
|
|
112
113
|
|
|
113
114
|
# CODE
|
|
114
115
|
CODE_BACKGROUND = "code_background"
|
|
@@ -233,6 +234,7 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
233
234
|
app_theme=Theme(
|
|
234
235
|
styles={
|
|
235
236
|
ThemeKey.LINES.value: palette.grey3,
|
|
237
|
+
ThemeKey.LINES_DIM.value: "dim " + palette.grey3,
|
|
236
238
|
# CODE
|
|
237
239
|
ThemeKey.CODE_BACKGROUND.value: f"on {palette.code_background}",
|
|
238
240
|
# PANEL
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
-
from typing import Any
|
|
2
|
+
from typing import Any
|
|
3
3
|
|
|
4
4
|
from rich.console import Group, RenderableType
|
|
5
5
|
from rich.json import JSON
|
|
@@ -7,8 +7,7 @@ from rich.style import Style
|
|
|
7
7
|
from rich.text import Text
|
|
8
8
|
|
|
9
9
|
from klaude_code.const import SUB_AGENT_RESULT_MAX_LINES
|
|
10
|
-
from klaude_code.protocol import
|
|
11
|
-
from klaude_code.protocol.sub_agent import get_sub_agent_profile_by_tool
|
|
10
|
+
from klaude_code.protocol import model
|
|
12
11
|
from klaude_code.tui.components.common import truncate_head
|
|
13
12
|
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
14
13
|
|
|
@@ -125,46 +124,3 @@ def render_sub_agent_result(
|
|
|
125
124
|
elements.append(Text(agent_id_footer, style=ThemeKey.SUB_AGENT_FOOTER))
|
|
126
125
|
|
|
127
126
|
return Group(*elements)
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
def build_sub_agent_state_from_tool_call(e: events.ToolCallEvent) -> model.SubAgentState | None:
|
|
131
|
-
"""Build SubAgentState from a tool call event for replay rendering."""
|
|
132
|
-
profile = get_sub_agent_profile_by_tool(e.tool_name)
|
|
133
|
-
if profile is None:
|
|
134
|
-
return None
|
|
135
|
-
description = profile.name
|
|
136
|
-
prompt = ""
|
|
137
|
-
output_schema: dict[str, Any] | None = None
|
|
138
|
-
generation: dict[str, Any] | None = None
|
|
139
|
-
resume: str | None = None
|
|
140
|
-
if e.arguments:
|
|
141
|
-
try:
|
|
142
|
-
payload: dict[str, object] = json.loads(e.arguments)
|
|
143
|
-
except json.JSONDecodeError:
|
|
144
|
-
payload = {}
|
|
145
|
-
desc_value = payload.get("description")
|
|
146
|
-
if isinstance(desc_value, str) and desc_value.strip():
|
|
147
|
-
description = desc_value.strip()
|
|
148
|
-
prompt_value = payload.get("prompt") or payload.get("task")
|
|
149
|
-
if isinstance(prompt_value, str):
|
|
150
|
-
prompt = prompt_value.strip()
|
|
151
|
-
resume_value = payload.get("resume")
|
|
152
|
-
if isinstance(resume_value, str) and resume_value.strip():
|
|
153
|
-
resume = resume_value.strip()
|
|
154
|
-
# Extract output_schema if profile supports it
|
|
155
|
-
if profile.output_schema_arg:
|
|
156
|
-
schema_value = payload.get(profile.output_schema_arg)
|
|
157
|
-
if isinstance(schema_value, dict):
|
|
158
|
-
output_schema = cast(dict[str, Any], schema_value)
|
|
159
|
-
# Extract generation config for ImageGen
|
|
160
|
-
generation_value = payload.get("generation")
|
|
161
|
-
if isinstance(generation_value, dict):
|
|
162
|
-
generation = cast(dict[str, Any], generation_value)
|
|
163
|
-
return model.SubAgentState(
|
|
164
|
-
sub_agent_type=profile.name,
|
|
165
|
-
sub_agent_desc=description,
|
|
166
|
-
sub_agent_prompt=prompt,
|
|
167
|
-
resume=resume,
|
|
168
|
-
output_schema=output_schema,
|
|
169
|
-
generation=generation,
|
|
170
|
-
)
|
|
@@ -1,14 +1,5 @@
|
|
|
1
1
|
import re
|
|
2
2
|
|
|
3
|
-
from rich.console import RenderableType
|
|
4
|
-
from rich.padding import Padding
|
|
5
|
-
from rich.text import Text
|
|
6
|
-
|
|
7
|
-
from klaude_code.const import MARKDOWN_RIGHT_MARGIN
|
|
8
|
-
from klaude_code.tui.components.common import create_grid
|
|
9
|
-
from klaude_code.tui.components.rich.markdown import ThinkingMarkdown
|
|
10
|
-
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
11
|
-
|
|
12
3
|
# UI markers
|
|
13
4
|
THINKING_MESSAGE_MARK = "∴"
|
|
14
5
|
|
|
@@ -70,27 +61,3 @@ def extract_last_bold_header(text: str) -> str | None:
|
|
|
70
61
|
i = end + 2
|
|
71
62
|
|
|
72
63
|
return last
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def render_thinking(content: str, *, code_theme: str, style: str) -> RenderableType | None:
|
|
76
|
-
"""Render thinking content as markdown with left mark.
|
|
77
|
-
|
|
78
|
-
Returns None if content is empty.
|
|
79
|
-
Note: Caller should push thinking_markdown_theme before printing.
|
|
80
|
-
"""
|
|
81
|
-
if len(content.strip()) == 0:
|
|
82
|
-
return None
|
|
83
|
-
|
|
84
|
-
grid = create_grid()
|
|
85
|
-
grid.add_row(
|
|
86
|
-
Text(THINKING_MESSAGE_MARK, style=ThemeKey.THINKING),
|
|
87
|
-
Padding(
|
|
88
|
-
ThinkingMarkdown(
|
|
89
|
-
normalize_thinking_content(content),
|
|
90
|
-
code_theme=code_theme,
|
|
91
|
-
style=style,
|
|
92
|
-
),
|
|
93
|
-
(0, MARKDOWN_RIGHT_MARGIN, 0, 0),
|
|
94
|
-
),
|
|
95
|
-
)
|
|
96
|
-
return grid
|
|
@@ -48,6 +48,33 @@ def is_sub_agent_tool(tool_name: str) -> bool:
|
|
|
48
48
|
return _is_sub_agent_tool(tool_name)
|
|
49
49
|
|
|
50
50
|
|
|
51
|
+
def get_task_active_form(arguments: str) -> str:
|
|
52
|
+
"""Return active form text for Task tool based on its arguments."""
|
|
53
|
+
import json
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
parsed = json.loads(arguments)
|
|
57
|
+
except json.JSONDecodeError:
|
|
58
|
+
return "Tasking"
|
|
59
|
+
|
|
60
|
+
if not isinstance(parsed, dict):
|
|
61
|
+
return "Tasking"
|
|
62
|
+
|
|
63
|
+
args = cast(dict[str, Any], parsed)
|
|
64
|
+
|
|
65
|
+
type_raw = args.get("type")
|
|
66
|
+
if not isinstance(type_raw, str):
|
|
67
|
+
return "Tasking"
|
|
68
|
+
|
|
69
|
+
match type_raw.strip():
|
|
70
|
+
case "explore":
|
|
71
|
+
return "Exploring"
|
|
72
|
+
case "web":
|
|
73
|
+
return "Surfing"
|
|
74
|
+
case _:
|
|
75
|
+
return "Tasking"
|
|
76
|
+
|
|
77
|
+
|
|
51
78
|
def render_path(path: str, style: str, is_directory: bool = False) -> Text:
|
|
52
79
|
if path.startswith(str(Path().cwd())):
|
|
53
80
|
path = path.replace(str(Path().cwd()), "").lstrip("/")
|
|
@@ -173,7 +200,7 @@ def render_bash_tool_call(arguments: str) -> RenderableType:
|
|
|
173
200
|
|
|
174
201
|
|
|
175
202
|
def render_update_plan_tool_call(arguments: str) -> RenderableType:
|
|
176
|
-
tool_name = "
|
|
203
|
+
tool_name = "Plan"
|
|
177
204
|
details: RenderableType | None = None
|
|
178
205
|
|
|
179
206
|
if arguments:
|
|
@@ -273,7 +300,7 @@ def render_write_tool_call(arguments: str) -> RenderableType:
|
|
|
273
300
|
|
|
274
301
|
|
|
275
302
|
def render_apply_patch_tool_call(arguments: str) -> RenderableType:
|
|
276
|
-
tool_name = "
|
|
303
|
+
tool_name = "Patch"
|
|
277
304
|
|
|
278
305
|
try:
|
|
279
306
|
payload = json.loads(arguments)
|
|
@@ -299,21 +326,22 @@ def render_apply_patch_tool_call(arguments: str) -> RenderableType:
|
|
|
299
326
|
elif line.startswith("*** Delete File:"):
|
|
300
327
|
delete_files.append(line[len("*** Delete File:") :].strip())
|
|
301
328
|
|
|
302
|
-
|
|
329
|
+
details = Text("", ThemeKey.TOOL_PARAM)
|
|
303
330
|
if update_files:
|
|
304
|
-
|
|
331
|
+
details.append(f"Edit × {len(update_files)}")
|
|
305
332
|
if add_files:
|
|
333
|
+
if details.plain:
|
|
334
|
+
details.append(", ")
|
|
306
335
|
# For single .md file addition, show filename in parentheses
|
|
307
336
|
if len(add_files) == 1 and add_files[0].endswith(".md"):
|
|
308
|
-
|
|
309
|
-
|
|
337
|
+
details.append("Create ")
|
|
338
|
+
details.append_text(render_path(add_files[0], ThemeKey.TOOL_PARAM_FILE_PATH))
|
|
310
339
|
else:
|
|
311
|
-
|
|
340
|
+
details.append(f"Create × {len(add_files)}")
|
|
312
341
|
if delete_files:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
details = Text(", ".join(parts), ThemeKey.TOOL_PARAM)
|
|
342
|
+
if details.plain:
|
|
343
|
+
details.append(", ")
|
|
344
|
+
details.append(f"Delete × {len(delete_files)}")
|
|
317
345
|
else:
|
|
318
346
|
details = Text(
|
|
319
347
|
str(patch_content)[:INVALID_TOOL_CALL_MAX_LENGTH],
|
|
@@ -434,7 +462,7 @@ def _render_mermaid_viewer_link(
|
|
|
434
462
|
|
|
435
463
|
|
|
436
464
|
def render_web_fetch_tool_call(arguments: str) -> RenderableType:
|
|
437
|
-
tool_name = "Fetch"
|
|
465
|
+
tool_name = "Fetch Web"
|
|
438
466
|
|
|
439
467
|
try:
|
|
440
468
|
payload: dict[str, str] = json.loads(arguments)
|
|
@@ -452,7 +480,7 @@ def render_web_fetch_tool_call(arguments: str) -> RenderableType:
|
|
|
452
480
|
|
|
453
481
|
|
|
454
482
|
def render_web_search_tool_call(arguments: str) -> RenderableType:
|
|
455
|
-
tool_name = "Web
|
|
483
|
+
tool_name = "Search Web"
|
|
456
484
|
|
|
457
485
|
try:
|
|
458
486
|
payload: dict[str, Any] = json.loads(arguments)
|
|
@@ -516,6 +544,7 @@ _TOOL_ACTIVE_FORM: dict[str, str] = {
|
|
|
516
544
|
tools.WEB_FETCH: "Fetching Web",
|
|
517
545
|
tools.WEB_SEARCH: "Searching Web",
|
|
518
546
|
tools.REPORT_BACK: "Reporting",
|
|
547
|
+
tools.IMAGE_GEN: "Generating Image",
|
|
519
548
|
}
|
|
520
549
|
|
|
521
550
|
|
|
@@ -527,13 +556,6 @@ def get_tool_active_form(tool_name: str) -> str:
|
|
|
527
556
|
if tool_name in _TOOL_ACTIVE_FORM:
|
|
528
557
|
return _TOOL_ACTIVE_FORM[tool_name]
|
|
529
558
|
|
|
530
|
-
# Check sub agent profiles
|
|
531
|
-
from klaude_code.protocol.sub_agent import get_sub_agent_profile_by_tool
|
|
532
|
-
|
|
533
|
-
profile = get_sub_agent_profile_by_tool(tool_name)
|
|
534
|
-
if profile and profile.active_form:
|
|
535
|
-
return profile.active_form
|
|
536
|
-
|
|
537
559
|
return f"Calling {tool_name}"
|
|
538
560
|
|
|
539
561
|
|
|
@@ -558,7 +580,7 @@ def render_tool_call(e: events.ToolCallEvent) -> RenderableType | None:
|
|
|
558
580
|
case tools.APPLY_PATCH:
|
|
559
581
|
return render_apply_patch_tool_call(e.arguments)
|
|
560
582
|
case tools.TODO_WRITE:
|
|
561
|
-
return render_generic_tool_call("Update
|
|
583
|
+
return render_generic_tool_call("Update To-Dos", "", MARK_PLAN)
|
|
562
584
|
case tools.UPDATE_PLAN:
|
|
563
585
|
return render_update_plan_tool_call(e.arguments)
|
|
564
586
|
case tools.MERMAID:
|
klaude_code/tui/input/images.py
CHANGED
|
@@ -17,11 +17,10 @@ import shutil
|
|
|
17
17
|
import subprocess
|
|
18
18
|
import sys
|
|
19
19
|
import uuid
|
|
20
|
-
from base64 import b64encode
|
|
21
20
|
from pathlib import Path
|
|
22
21
|
|
|
23
22
|
from klaude_code.const import get_system_temp
|
|
24
|
-
from klaude_code.protocol.message import
|
|
23
|
+
from klaude_code.protocol.message import ImageFilePart
|
|
25
24
|
|
|
26
25
|
# ---------------------------------------------------------------------------
|
|
27
26
|
# Constants and marker syntax
|
|
@@ -183,36 +182,40 @@ def capture_clipboard_tag() -> str | None:
|
|
|
183
182
|
# ---------------------------------------------------------------------------
|
|
184
183
|
|
|
185
184
|
|
|
186
|
-
|
|
187
|
-
"""
|
|
185
|
+
_MIME_TYPES: dict[str, str] = {
|
|
186
|
+
".png": "image/png",
|
|
187
|
+
".jpg": "image/jpeg",
|
|
188
|
+
".jpeg": "image/jpeg",
|
|
189
|
+
".gif": "image/gif",
|
|
190
|
+
".webp": "image/webp",
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _create_image_file_part(file_path: str) -> ImageFilePart | None:
|
|
195
|
+
"""Create an ImageFilePart from a file path."""
|
|
188
196
|
try:
|
|
189
197
|
path = Path(file_path)
|
|
190
198
|
if not path.exists():
|
|
191
199
|
return None
|
|
192
|
-
with open(path, "rb") as f:
|
|
193
|
-
encoded = b64encode(f.read()).decode("ascii")
|
|
194
200
|
|
|
195
201
|
suffix = path.suffix.lower()
|
|
196
|
-
mime =
|
|
197
|
-
".png": "image/png",
|
|
198
|
-
".jpg": "image/jpeg",
|
|
199
|
-
".jpeg": "image/jpeg",
|
|
200
|
-
".gif": "image/gif",
|
|
201
|
-
".webp": "image/webp",
|
|
202
|
-
}.get(suffix)
|
|
202
|
+
mime = _MIME_TYPES.get(suffix)
|
|
203
203
|
if mime is None:
|
|
204
204
|
return None
|
|
205
205
|
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
return ImageFilePart(
|
|
207
|
+
file_path=str(path),
|
|
208
|
+
mime_type=mime,
|
|
209
|
+
byte_size=path.stat().st_size,
|
|
210
|
+
)
|
|
208
211
|
except OSError:
|
|
209
212
|
return None
|
|
210
213
|
|
|
211
214
|
|
|
212
|
-
def extract_images_from_text(text: str) -> list[
|
|
215
|
+
def extract_images_from_text(text: str) -> list[ImageFilePart]:
|
|
213
216
|
"""Extract images referenced by [image ...] markers in text."""
|
|
214
217
|
|
|
215
|
-
images: list[
|
|
218
|
+
images: list[ImageFilePart] = []
|
|
216
219
|
for m in IMAGE_MARKER_RE.finditer(text):
|
|
217
220
|
raw = m.group("path")
|
|
218
221
|
path_str = parse_image_marker_path(raw)
|
|
@@ -221,7 +224,7 @@ def extract_images_from_text(text: str) -> list[ImageURLPart]:
|
|
|
221
224
|
p = Path(path_str).expanduser()
|
|
222
225
|
if not p.is_absolute():
|
|
223
226
|
p = (Path.cwd() / p).resolve()
|
|
224
|
-
image_part =
|
|
227
|
+
image_part = _create_image_file_part(str(p))
|
|
225
228
|
if image_part:
|
|
226
229
|
images.append(image_part)
|
|
227
230
|
return images
|
|
@@ -19,7 +19,7 @@ from typing import cast
|
|
|
19
19
|
from prompt_toolkit.application.current import get_app
|
|
20
20
|
from prompt_toolkit.buffer import Buffer
|
|
21
21
|
from prompt_toolkit.filters import Always, Condition, Filter
|
|
22
|
-
from prompt_toolkit.filters.app import has_completions
|
|
22
|
+
from prompt_toolkit.filters.app import has_completions, is_searching
|
|
23
23
|
from prompt_toolkit.key_binding import KeyBindings
|
|
24
24
|
from prompt_toolkit.key_binding.key_processor import KeyPressEvent
|
|
25
25
|
from prompt_toolkit.keys import Keys
|
|
@@ -367,7 +367,7 @@ def create_key_bindings(
|
|
|
367
367
|
|
|
368
368
|
_insert_newline(event)
|
|
369
369
|
|
|
370
|
-
@kb.add("enter", filter=enabled)
|
|
370
|
+
@kb.add("enter", filter=enabled & ~is_searching)
|
|
371
371
|
def _(event: KeyPressEvent) -> None:
|
|
372
372
|
nonlocal swallow_next_control_j
|
|
373
373
|
|
|
@@ -20,6 +20,7 @@ from prompt_toolkit.key_binding import merge_key_bindings
|
|
|
20
20
|
from prompt_toolkit.layout import Float
|
|
21
21
|
from prompt_toolkit.layout.containers import Container, FloatContainer, Window
|
|
22
22
|
from prompt_toolkit.layout.controls import BufferControl, UIContent
|
|
23
|
+
from prompt_toolkit.layout.dimension import Dimension
|
|
23
24
|
from prompt_toolkit.layout.menus import CompletionsMenu, MultiColumnCompletionsMenu
|
|
24
25
|
from prompt_toolkit.patch_stdout import patch_stdout
|
|
25
26
|
from prompt_toolkit.styles import Style
|
|
@@ -61,9 +62,9 @@ COMPLETION_SELECTED_LIGHT_BG = "ansigreen"
|
|
|
61
62
|
COMPLETION_SELECTED_UNKNOWN_BG = "ansigreen"
|
|
62
63
|
COMPLETION_MENU = "ansibrightblack"
|
|
63
64
|
INPUT_PROMPT_STYLE = "ansimagenta bold"
|
|
64
|
-
PLACEHOLDER_TEXT_STYLE_DARK_BG = "fg:#5a5a5a
|
|
65
|
-
PLACEHOLDER_TEXT_STYLE_LIGHT_BG = "fg:#7a7a7a
|
|
66
|
-
PLACEHOLDER_TEXT_STYLE_UNKNOWN_BG = "fg:#8a8a8a
|
|
65
|
+
PLACEHOLDER_TEXT_STYLE_DARK_BG = "fg:#5a5a5a"
|
|
66
|
+
PLACEHOLDER_TEXT_STYLE_LIGHT_BG = "fg:#7a7a7a"
|
|
67
|
+
PLACEHOLDER_TEXT_STYLE_UNKNOWN_BG = "fg:#8a8a8a"
|
|
67
68
|
PLACEHOLDER_SYMBOL_STYLE_DARK_BG = "bg:#2a2a2a fg:#5a5a5a"
|
|
68
69
|
PLACEHOLDER_SYMBOL_STYLE_LIGHT_BG = "bg:#e6e6e6 fg:#7a7a7a"
|
|
69
70
|
PLACEHOLDER_SYMBOL_STYLE_UNKNOWN_BG = "bg:#2a2a2a fg:#8a8a8a"
|
|
@@ -81,6 +82,9 @@ def _left_align_completion_menus(container: Container) -> None:
|
|
|
81
82
|
cursor (`xcursor=True`). That makes the popup indent as the caret moves.
|
|
82
83
|
We walk the layout tree and rewrite the Float positioning for completion menus
|
|
83
84
|
to keep them fixed at the left edge.
|
|
85
|
+
|
|
86
|
+
Note: We intentionally keep Y positioning (ycursor) unchanged so that the
|
|
87
|
+
completion menu stays near the cursor/input line.
|
|
84
88
|
"""
|
|
85
89
|
if isinstance(container, FloatContainer):
|
|
86
90
|
for flt in container.floats:
|
|
@@ -300,6 +304,10 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
300
304
|
key_bindings=kb,
|
|
301
305
|
completer=ThreadedCompleter(create_repl_completer(command_info_provider=self._command_info_provider)),
|
|
302
306
|
complete_while_typing=True,
|
|
307
|
+
# Keep the bottom toolbar stable while completion menus open/close.
|
|
308
|
+
# Reserving space dynamically can make the non-fullscreen prompt
|
|
309
|
+
# "jump" by printing extra lines.
|
|
310
|
+
reserve_space_for_menu=0,
|
|
303
311
|
erase_when_done=True,
|
|
304
312
|
mouse_support=False,
|
|
305
313
|
style=Style.from_dict(
|
|
@@ -417,41 +425,40 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
417
425
|
|
|
418
426
|
original_height = input_window.height
|
|
419
427
|
|
|
428
|
+
# Keep a comfortable multiline editing area even when no completion
|
|
429
|
+
# space is reserved. (We set reserve_space_for_menu=0 to avoid the
|
|
430
|
+
# bottom toolbar jumping when completions open/close.)
|
|
431
|
+
base_rows = 10
|
|
432
|
+
|
|
420
433
|
def _height(): # type: ignore[no-untyped-def]
|
|
421
434
|
picker_open = (self._model_picker is not None and self._model_picker.is_open) or (
|
|
422
435
|
self._thinking_picker is not None and self._thinking_picker.is_open
|
|
423
436
|
)
|
|
424
437
|
|
|
425
|
-
try:
|
|
426
|
-
complete_state = self._session.default_buffer.complete_state
|
|
427
|
-
completion_open = complete_state is not None and bool(complete_state.completions)
|
|
428
|
-
except Exception:
|
|
429
|
-
completion_open = False
|
|
430
|
-
|
|
431
438
|
try:
|
|
432
439
|
original_height_value = original_height() if callable(original_height) else original_height
|
|
433
440
|
except Exception:
|
|
434
441
|
original_height_value = None
|
|
435
|
-
|
|
442
|
+
original_min = 0
|
|
443
|
+
if isinstance(original_height_value, Dimension):
|
|
444
|
+
original_min = int(original_height_value.min)
|
|
445
|
+
elif isinstance(original_height_value, int):
|
|
446
|
+
original_min = int(original_height_value)
|
|
436
447
|
|
|
437
|
-
if picker_open
|
|
438
|
-
target_rows = 24 if picker_open else 14
|
|
448
|
+
target_rows = 24 if picker_open else base_rows
|
|
439
449
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
450
|
+
# Cap to the current terminal size.
|
|
451
|
+
# Leave a small buffer to avoid triggering "Window too small".
|
|
452
|
+
try:
|
|
453
|
+
rows = get_app().output.get_size().rows
|
|
454
|
+
except Exception:
|
|
455
|
+
rows = 0
|
|
446
456
|
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
return expanded
|
|
457
|
+
desired = max(original_min, target_rows)
|
|
458
|
+
if rows > 0:
|
|
459
|
+
desired = max(3, min(desired, rows - 2))
|
|
451
460
|
|
|
452
|
-
|
|
453
|
-
return original_height()
|
|
454
|
-
return original_height
|
|
461
|
+
return Dimension(min=desired, preferred=desired)
|
|
455
462
|
|
|
456
463
|
input_window.height = _height
|
|
457
464
|
|
|
@@ -583,7 +590,7 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
583
590
|
except (AttributeError, RuntimeError):
|
|
584
591
|
pass
|
|
585
592
|
|
|
586
|
-
# Priority: update_message > debug_log_path
|
|
593
|
+
# Priority: update_message > debug_log_path > shortcut hints
|
|
587
594
|
display_text: str | None = None
|
|
588
595
|
text_style: str = ""
|
|
589
596
|
if update_message:
|
|
@@ -593,31 +600,25 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
593
600
|
display_text = f"Debug log: {debug_log_path}"
|
|
594
601
|
text_style = "fg:ansibrightblack"
|
|
595
602
|
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
# will still reserve the toolbar line.)
|
|
599
|
-
if not display_text:
|
|
603
|
+
if display_text:
|
|
604
|
+
left_text = " " + display_text
|
|
600
605
|
try:
|
|
601
606
|
terminal_width = shutil.get_terminal_size().columns
|
|
607
|
+
padding = " " * max(0, terminal_width - len(left_text))
|
|
602
608
|
except (OSError, ValueError):
|
|
603
|
-
|
|
604
|
-
return FormattedText([("", " " * max(0, terminal_width))])
|
|
609
|
+
padding = ""
|
|
605
610
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
terminal_width = shutil.get_terminal_size().columns
|
|
609
|
-
padding = " " * max(0, terminal_width - len(left_text))
|
|
610
|
-
except (OSError, ValueError):
|
|
611
|
-
padding = ""
|
|
611
|
+
toolbar_text = left_text + padding
|
|
612
|
+
return FormattedText([(text_style, toolbar_text)])
|
|
612
613
|
|
|
613
|
-
|
|
614
|
-
return
|
|
614
|
+
# Show shortcut hints when nothing else to display
|
|
615
|
+
return self._render_shortcut_hints()
|
|
615
616
|
|
|
616
617
|
# -------------------------------------------------------------------------
|
|
617
|
-
#
|
|
618
|
+
# Shortcut hints (bottom toolbar)
|
|
618
619
|
# -------------------------------------------------------------------------
|
|
619
620
|
|
|
620
|
-
def
|
|
621
|
+
def _render_shortcut_hints(self) -> FormattedText:
|
|
621
622
|
if self._is_light_terminal_background is True:
|
|
622
623
|
text_style = PLACEHOLDER_TEXT_STYLE_LIGHT_BG
|
|
623
624
|
symbol_style = PLACEHOLDER_SYMBOL_STYLE_LIGHT_BG
|
|
@@ -630,27 +631,27 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
630
631
|
|
|
631
632
|
return FormattedText(
|
|
632
633
|
[
|
|
633
|
-
(text_style, " "
|
|
634
|
+
(text_style, " "),
|
|
634
635
|
(symbol_style, " @ "),
|
|
635
636
|
(text_style, " "),
|
|
636
637
|
(text_style, "files"),
|
|
637
|
-
(text_style, "
|
|
638
|
+
(text_style, " • "),
|
|
638
639
|
(symbol_style, " $ "),
|
|
639
640
|
(text_style, " "),
|
|
640
641
|
(text_style, "skills"),
|
|
641
|
-
(text_style, "
|
|
642
|
+
(text_style, " • "),
|
|
642
643
|
(symbol_style, " / "),
|
|
643
644
|
(text_style, " "),
|
|
644
645
|
(text_style, "commands"),
|
|
645
|
-
(text_style, "
|
|
646
|
+
(text_style, " • "),
|
|
646
647
|
(symbol_style, " ctrl-l "),
|
|
647
648
|
(text_style, " "),
|
|
648
649
|
(text_style, "models"),
|
|
649
|
-
(text_style, "
|
|
650
|
+
(text_style, " • "),
|
|
650
651
|
(symbol_style, " ctrl-t "),
|
|
651
652
|
(text_style, " "),
|
|
652
653
|
(text_style, "think"),
|
|
653
|
-
(text_style, "
|
|
654
|
+
(text_style, " • "),
|
|
654
655
|
(symbol_style, " ctrl-v "),
|
|
655
656
|
(text_style, " "),
|
|
656
657
|
(text_style, "paste image"),
|
|
@@ -679,7 +680,6 @@ class PromptToolkitInput(InputProviderABC):
|
|
|
679
680
|
# proper styling instead of showing raw escape codes.
|
|
680
681
|
with patch_stdout(raw=True):
|
|
681
682
|
line: str = await self._session.prompt_async(
|
|
682
|
-
placeholder=self._render_input_placeholder(),
|
|
683
683
|
bottom_toolbar=self._get_bottom_toolbar,
|
|
684
684
|
)
|
|
685
685
|
if self._post_prompt is not None:
|