klaude-code 2.8.1__py3-none-any.whl → 2.9.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- klaude_code/app/runtime.py +2 -1
- klaude_code/auth/antigravity/oauth.py +33 -38
- klaude_code/auth/antigravity/token_manager.py +0 -18
- klaude_code/auth/base.py +53 -0
- klaude_code/auth/claude/oauth.py +34 -49
- 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 +25 -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 +11 -56
- 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 +1 -5
- 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 +27 -3
- klaude_code/core/tool/offload.py +0 -35
- klaude_code/core/tool/shell/bash_tool.py +1 -1
- 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/bedrock_anthropic/__init__.py +3 -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 +32 -6
- klaude_code/llm/json_stable.py +37 -0
- klaude_code/llm/{codex → openai_codex}/__init__.py +1 -1
- klaude_code/llm/{codex → openai_codex}/client.py +24 -2
- klaude_code/llm/openai_codex/prompt_sync.py +237 -0
- klaude_code/llm/openai_compatible/client.py +3 -1
- klaude_code/llm/openai_compatible/input.py +0 -10
- klaude_code/llm/openai_compatible/stream.py +35 -10
- klaude_code/llm/{responses → openai_responses}/client.py +1 -1
- klaude_code/llm/{responses → openai_responses}/input.py +15 -5
- klaude_code/llm/registry.py +3 -8
- klaude_code/llm/stream_parts.py +3 -1
- klaude_code/llm/usage.py +1 -9
- klaude_code/protocol/events.py +2 -2
- klaude_code/protocol/message.py +3 -2
- klaude_code/protocol/model.py +34 -2
- 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 +80 -22
- 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/bash_syntax.py +4 -0
- klaude_code/tui/components/command_output.py +3 -1
- klaude_code/tui/components/developer.py +3 -0
- klaude_code/tui/components/diffs.py +4 -209
- klaude_code/tui/components/errors.py +4 -0
- klaude_code/tui/components/mermaid_viewer.py +2 -2
- klaude_code/tui/components/metadata.py +0 -3
- klaude_code/tui/components/rich/markdown.py +120 -87
- klaude_code/tui/components/rich/status.py +2 -2
- klaude_code/tui/components/rich/theme.py +11 -6
- klaude_code/tui/components/sub_agent.py +2 -46
- klaude_code/tui/components/thinking.py +0 -33
- klaude_code/tui/components/tools.py +65 -21
- klaude_code/tui/components/user_input.py +2 -0
- 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 +29 -47
- klaude_code/tui/renderer.py +48 -33
- klaude_code/tui/runner.py +2 -1
- klaude_code/tui/terminal/image.py +27 -34
- klaude_code/ui/common.py +0 -70
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/METADATA +3 -6
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/RECORD +103 -99
- klaude_code/core/tool/sub_agent_tool.py +0 -126
- klaude_code/llm/bedrock/__init__.py +0 -3
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -108
- klaude_code/tui/components/rich/searchable_text.py +0 -68
- /klaude_code/llm/{bedrock → bedrock_anthropic}/client.py +0 -0
- /klaude_code/llm/{responses → openai_responses}/__init__.py +0 -0
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/WHEEL +0 -0
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/entry_points.txt +0 -0
|
@@ -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
|
|
@@ -10,8 +10,10 @@ from rich.text import Text
|
|
|
10
10
|
|
|
11
11
|
from klaude_code.const import (
|
|
12
12
|
BASH_OUTPUT_PANEL_THRESHOLD,
|
|
13
|
+
DIFF_PREFIX_WIDTH,
|
|
13
14
|
INVALID_TOOL_CALL_MAX_LENGTH,
|
|
14
15
|
QUERY_DISPLAY_TRUNCATE_LENGTH,
|
|
16
|
+
TAB_EXPAND_WIDTH,
|
|
15
17
|
URL_TRUNCATE_MAX_LENGTH,
|
|
16
18
|
WEB_SEARCH_DEFAULT_MAX_RESULTS,
|
|
17
19
|
)
|
|
@@ -48,6 +50,33 @@ def is_sub_agent_tool(tool_name: str) -> bool:
|
|
|
48
50
|
return _is_sub_agent_tool(tool_name)
|
|
49
51
|
|
|
50
52
|
|
|
53
|
+
def get_task_active_form(arguments: str) -> str:
|
|
54
|
+
"""Return active form text for Task tool based on its arguments."""
|
|
55
|
+
import json
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
parsed = json.loads(arguments)
|
|
59
|
+
except json.JSONDecodeError:
|
|
60
|
+
return "Tasking"
|
|
61
|
+
|
|
62
|
+
if not isinstance(parsed, dict):
|
|
63
|
+
return "Tasking"
|
|
64
|
+
|
|
65
|
+
args = cast(dict[str, Any], parsed)
|
|
66
|
+
|
|
67
|
+
type_raw = args.get("type")
|
|
68
|
+
if not isinstance(type_raw, str):
|
|
69
|
+
return "Tasking"
|
|
70
|
+
|
|
71
|
+
match type_raw.strip():
|
|
72
|
+
case "explore":
|
|
73
|
+
return "Exploring"
|
|
74
|
+
case "web":
|
|
75
|
+
return "Surfing"
|
|
76
|
+
case _:
|
|
77
|
+
return "Tasking"
|
|
78
|
+
|
|
79
|
+
|
|
51
80
|
def render_path(path: str, style: str, is_directory: bool = False) -> Text:
|
|
52
81
|
if path.startswith(str(Path().cwd())):
|
|
53
82
|
path = path.replace(str(Path().cwd()), "").lstrip("/")
|
|
@@ -173,7 +202,7 @@ def render_bash_tool_call(arguments: str) -> RenderableType:
|
|
|
173
202
|
|
|
174
203
|
|
|
175
204
|
def render_update_plan_tool_call(arguments: str) -> RenderableType:
|
|
176
|
-
tool_name = "
|
|
205
|
+
tool_name = "Plan"
|
|
177
206
|
details: RenderableType | None = None
|
|
178
207
|
|
|
179
208
|
if arguments:
|
|
@@ -273,7 +302,7 @@ def render_write_tool_call(arguments: str) -> RenderableType:
|
|
|
273
302
|
|
|
274
303
|
|
|
275
304
|
def render_apply_patch_tool_call(arguments: str) -> RenderableType:
|
|
276
|
-
tool_name = "
|
|
305
|
+
tool_name = "Patch"
|
|
277
306
|
|
|
278
307
|
try:
|
|
279
308
|
payload = json.loads(arguments)
|
|
@@ -299,21 +328,22 @@ def render_apply_patch_tool_call(arguments: str) -> RenderableType:
|
|
|
299
328
|
elif line.startswith("*** Delete File:"):
|
|
300
329
|
delete_files.append(line[len("*** Delete File:") :].strip())
|
|
301
330
|
|
|
302
|
-
|
|
331
|
+
details = Text("", ThemeKey.TOOL_PARAM)
|
|
303
332
|
if update_files:
|
|
304
|
-
|
|
333
|
+
details.append(f"Edit × {len(update_files)}")
|
|
305
334
|
if add_files:
|
|
335
|
+
if details.plain:
|
|
336
|
+
details.append(", ")
|
|
306
337
|
# For single .md file addition, show filename in parentheses
|
|
307
338
|
if len(add_files) == 1 and add_files[0].endswith(".md"):
|
|
308
|
-
|
|
309
|
-
|
|
339
|
+
details.append("Create ")
|
|
340
|
+
details.append_text(render_path(add_files[0], ThemeKey.TOOL_PARAM_FILE_PATH))
|
|
310
341
|
else:
|
|
311
|
-
|
|
342
|
+
details.append(f"Create × {len(add_files)}")
|
|
312
343
|
if delete_files:
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
details = Text(", ".join(parts), ThemeKey.TOOL_PARAM)
|
|
344
|
+
if details.plain:
|
|
345
|
+
details.append(", ")
|
|
346
|
+
details.append(f"Delete × {len(delete_files)}")
|
|
317
347
|
else:
|
|
318
348
|
details = Text(
|
|
319
349
|
str(patch_content)[:INVALID_TOOL_CALL_MAX_LENGTH],
|
|
@@ -359,6 +389,24 @@ def render_generic_tool_result(result: str, *, is_error: bool = False) -> Render
|
|
|
359
389
|
return text
|
|
360
390
|
|
|
361
391
|
|
|
392
|
+
def render_read_preview(ui_extra: model.ReadPreviewUIExtra) -> RenderableType:
|
|
393
|
+
"""Render read preview with line numbers aligned to diff style."""
|
|
394
|
+
grid = create_grid()
|
|
395
|
+
grid.padding = (0, 0)
|
|
396
|
+
|
|
397
|
+
for line in ui_extra.lines:
|
|
398
|
+
prefix = f"{line.line_no:>{DIFF_PREFIX_WIDTH}} "
|
|
399
|
+
content = line.content.expandtabs(TAB_EXPAND_WIDTH)
|
|
400
|
+
grid.add_row(Text(prefix, ThemeKey.TOOL_RESULT), Text(content, ThemeKey.TOOL_RESULT))
|
|
401
|
+
|
|
402
|
+
if ui_extra.remaining_lines > 0:
|
|
403
|
+
remaining_prefix = f"{'⋮':>{DIFF_PREFIX_WIDTH}} "
|
|
404
|
+
remaining_text = Text(f"(more {ui_extra.remaining_lines} lines)", ThemeKey.TOOL_RESULT_TRUNCATED)
|
|
405
|
+
grid.add_row(Text(remaining_prefix, ThemeKey.TOOL_RESULT_TRUNCATED), remaining_text)
|
|
406
|
+
|
|
407
|
+
return grid
|
|
408
|
+
|
|
409
|
+
|
|
362
410
|
def _extract_mermaid_link(
|
|
363
411
|
ui_extra: model.ToolResultUIExtra | None,
|
|
364
412
|
) -> model.MermaidLinkUIExtra | None:
|
|
@@ -434,7 +482,7 @@ def _render_mermaid_viewer_link(
|
|
|
434
482
|
|
|
435
483
|
|
|
436
484
|
def render_web_fetch_tool_call(arguments: str) -> RenderableType:
|
|
437
|
-
tool_name = "Fetch"
|
|
485
|
+
tool_name = "Fetch Web"
|
|
438
486
|
|
|
439
487
|
try:
|
|
440
488
|
payload: dict[str, str] = json.loads(arguments)
|
|
@@ -452,7 +500,7 @@ def render_web_fetch_tool_call(arguments: str) -> RenderableType:
|
|
|
452
500
|
|
|
453
501
|
|
|
454
502
|
def render_web_search_tool_call(arguments: str) -> RenderableType:
|
|
455
|
-
tool_name = "Web
|
|
503
|
+
tool_name = "Search Web"
|
|
456
504
|
|
|
457
505
|
try:
|
|
458
506
|
payload: dict[str, Any] = json.loads(arguments)
|
|
@@ -516,6 +564,7 @@ _TOOL_ACTIVE_FORM: dict[str, str] = {
|
|
|
516
564
|
tools.WEB_FETCH: "Fetching Web",
|
|
517
565
|
tools.WEB_SEARCH: "Searching Web",
|
|
518
566
|
tools.REPORT_BACK: "Reporting",
|
|
567
|
+
tools.IMAGE_GEN: "Generating Image",
|
|
519
568
|
}
|
|
520
569
|
|
|
521
570
|
|
|
@@ -527,13 +576,6 @@ def get_tool_active_form(tool_name: str) -> str:
|
|
|
527
576
|
if tool_name in _TOOL_ACTIVE_FORM:
|
|
528
577
|
return _TOOL_ACTIVE_FORM[tool_name]
|
|
529
578
|
|
|
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
579
|
return f"Calling {tool_name}"
|
|
538
580
|
|
|
539
581
|
|
|
@@ -558,7 +600,7 @@ def render_tool_call(e: events.ToolCallEvent) -> RenderableType | None:
|
|
|
558
600
|
case tools.APPLY_PATCH:
|
|
559
601
|
return render_apply_patch_tool_call(e.arguments)
|
|
560
602
|
case tools.TODO_WRITE:
|
|
561
|
-
return render_generic_tool_call("Update
|
|
603
|
+
return render_generic_tool_call("Update To-Dos", "", MARK_PLAN)
|
|
562
604
|
case tools.UPDATE_PLAN:
|
|
563
605
|
return render_update_plan_tool_call(e.arguments)
|
|
564
606
|
case tools.MERMAID:
|
|
@@ -656,6 +698,8 @@ def render_tool_result(
|
|
|
656
698
|
|
|
657
699
|
match e.tool_name:
|
|
658
700
|
case tools.READ:
|
|
701
|
+
if isinstance(e.ui_extra, model.ReadPreviewUIExtra):
|
|
702
|
+
return wrap(render_read_preview(e.ui_extra))
|
|
659
703
|
return None
|
|
660
704
|
case tools.EDIT:
|
|
661
705
|
return wrap(r_diffs.render_structured_diff(diff_ui) if diff_ui else Text(""))
|
|
@@ -3,6 +3,7 @@ import re
|
|
|
3
3
|
from rich.console import Group, RenderableType
|
|
4
4
|
from rich.text import Text
|
|
5
5
|
|
|
6
|
+
from klaude_code.const import TAB_EXPAND_WIDTH
|
|
6
7
|
from klaude_code.skill import get_available_skills
|
|
7
8
|
from klaude_code.tui.components.common import create_grid
|
|
8
9
|
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
@@ -82,6 +83,7 @@ def render_user_input(content: str) -> RenderableType:
|
|
|
82
83
|
lines = content.strip().split("\n")
|
|
83
84
|
renderables: list[RenderableType] = []
|
|
84
85
|
for i, line in enumerate(lines):
|
|
86
|
+
line = line.expandtabs(TAB_EXPAND_WIDTH)
|
|
85
87
|
# Handle slash command on first line
|
|
86
88
|
if i == 0 and line.startswith("/"):
|
|
87
89
|
splits = line.split(" ", maxsplit=1)
|
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:
|