klaude-code 1.2.1__py3-none-any.whl → 1.2.3__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/main.py +9 -4
- klaude_code/cli/runtime.py +42 -43
- klaude_code/command/__init__.py +7 -5
- klaude_code/command/clear_cmd.py +6 -29
- klaude_code/command/command_abc.py +44 -8
- klaude_code/command/diff_cmd.py +33 -27
- klaude_code/command/export_cmd.py +18 -26
- klaude_code/command/help_cmd.py +10 -8
- klaude_code/command/model_cmd.py +11 -40
- klaude_code/command/{prompt-update-dev-doc.md → prompt-dev-docs-update.md} +3 -2
- klaude_code/command/{prompt-dev-doc.md → prompt-dev-docs.md} +3 -2
- klaude_code/command/prompt-init.md +2 -5
- klaude_code/command/prompt_command.py +6 -6
- klaude_code/command/refresh_cmd.py +4 -5
- klaude_code/command/registry.py +16 -19
- klaude_code/command/terminal_setup_cmd.py +12 -11
- klaude_code/config/__init__.py +4 -0
- klaude_code/config/config.py +25 -26
- klaude_code/config/list_model.py +8 -3
- klaude_code/config/select_model.py +1 -1
- klaude_code/const/__init__.py +1 -1
- klaude_code/core/__init__.py +0 -3
- klaude_code/core/agent.py +25 -50
- klaude_code/core/executor.py +268 -101
- klaude_code/core/prompt.py +12 -12
- klaude_code/core/{prompt → prompts}/prompt-gemini.md +1 -1
- klaude_code/core/reminders.py +76 -95
- klaude_code/core/task.py +21 -14
- klaude_code/core/tool/__init__.py +45 -11
- klaude_code/core/tool/file/apply_patch.py +5 -1
- klaude_code/core/tool/file/apply_patch_tool.py +11 -13
- klaude_code/core/tool/file/edit_tool.py +27 -23
- klaude_code/core/tool/file/multi_edit_tool.py +15 -17
- klaude_code/core/tool/file/read_tool.py +41 -36
- klaude_code/core/tool/file/write_tool.py +13 -15
- klaude_code/core/tool/memory/memory_tool.py +85 -68
- klaude_code/core/tool/memory/skill_tool.py +10 -12
- klaude_code/core/tool/shell/bash_tool.py +24 -22
- klaude_code/core/tool/shell/command_safety.py +12 -1
- klaude_code/core/tool/sub_agent_tool.py +11 -12
- klaude_code/core/tool/todo/todo_write_tool.py +21 -28
- klaude_code/core/tool/todo/update_plan_tool.py +14 -24
- klaude_code/core/tool/tool_abc.py +3 -4
- klaude_code/core/tool/tool_context.py +7 -7
- klaude_code/core/tool/tool_registry.py +30 -47
- klaude_code/core/tool/tool_runner.py +35 -43
- klaude_code/core/tool/truncation.py +14 -20
- klaude_code/core/tool/web/mermaid_tool.py +12 -14
- klaude_code/core/tool/web/web_fetch_tool.py +15 -17
- klaude_code/core/turn.py +19 -7
- klaude_code/llm/__init__.py +3 -4
- klaude_code/llm/anthropic/client.py +30 -46
- klaude_code/llm/anthropic/input.py +4 -11
- klaude_code/llm/client.py +29 -8
- klaude_code/llm/input_common.py +66 -36
- klaude_code/llm/openai_compatible/client.py +42 -84
- klaude_code/llm/openai_compatible/input.py +11 -16
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +2 -2
- klaude_code/llm/openrouter/client.py +40 -289
- klaude_code/llm/openrouter/input.py +13 -35
- klaude_code/llm/openrouter/reasoning_handler.py +209 -0
- klaude_code/llm/registry.py +5 -75
- klaude_code/llm/responses/client.py +34 -55
- klaude_code/llm/responses/input.py +24 -26
- klaude_code/llm/usage.py +109 -0
- klaude_code/protocol/__init__.py +4 -0
- klaude_code/protocol/events.py +3 -2
- klaude_code/protocol/{llm_parameter.py → llm_param.py} +12 -32
- klaude_code/protocol/model.py +49 -4
- klaude_code/protocol/op.py +18 -16
- klaude_code/protocol/op_handler.py +28 -0
- klaude_code/{core → protocol}/sub_agent.py +7 -0
- klaude_code/session/export.py +150 -70
- klaude_code/session/session.py +28 -14
- klaude_code/session/templates/export_session.html +180 -42
- klaude_code/trace/__init__.py +2 -2
- klaude_code/trace/log.py +11 -5
- klaude_code/ui/__init__.py +91 -8
- klaude_code/ui/core/__init__.py +1 -0
- klaude_code/ui/core/display.py +103 -0
- klaude_code/ui/core/input.py +71 -0
- klaude_code/ui/modes/__init__.py +1 -0
- klaude_code/ui/modes/debug/__init__.py +1 -0
- klaude_code/ui/{base/debug_event_display.py → modes/debug/display.py} +9 -5
- klaude_code/ui/modes/exec/__init__.py +1 -0
- klaude_code/ui/{base/exec_display.py → modes/exec/display.py} +28 -2
- klaude_code/ui/{repl → modes/repl}/__init__.py +5 -6
- klaude_code/ui/modes/repl/clipboard.py +152 -0
- klaude_code/ui/modes/repl/completers.py +429 -0
- klaude_code/ui/modes/repl/display.py +60 -0
- klaude_code/ui/modes/repl/event_handler.py +375 -0
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
- klaude_code/ui/modes/repl/key_bindings.py +170 -0
- klaude_code/ui/{repl → modes/repl}/renderer.py +109 -132
- klaude_code/ui/renderers/assistant.py +21 -0
- klaude_code/ui/renderers/common.py +0 -16
- klaude_code/ui/renderers/developer.py +18 -18
- klaude_code/ui/renderers/diffs.py +36 -14
- klaude_code/ui/renderers/errors.py +1 -1
- klaude_code/ui/renderers/metadata.py +50 -27
- klaude_code/ui/renderers/sub_agent.py +43 -9
- klaude_code/ui/renderers/thinking.py +33 -1
- klaude_code/ui/renderers/tools.py +212 -20
- klaude_code/ui/renderers/user_input.py +19 -23
- klaude_code/ui/rich/__init__.py +1 -0
- klaude_code/ui/{rich_ext → rich}/searchable_text.py +3 -1
- klaude_code/ui/{renderers → rich}/status.py +29 -18
- klaude_code/ui/{base → rich}/theme.py +8 -2
- klaude_code/ui/terminal/__init__.py +1 -0
- klaude_code/ui/{base/terminal_color.py → terminal/color.py} +4 -1
- klaude_code/ui/{base/terminal_control.py → terminal/control.py} +1 -0
- klaude_code/ui/{base/terminal_notifier.py → terminal/notifier.py} +5 -2
- klaude_code/ui/utils/__init__.py +1 -0
- klaude_code/ui/{base/utils.py → utils/common.py} +35 -3
- {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/METADATA +1 -1
- klaude_code-1.2.3.dist-info/RECORD +161 -0
- klaude_code/core/clipboard_manifest.py +0 -124
- klaude_code/llm/openrouter/tool_call_accumulator.py +0 -80
- klaude_code/ui/base/__init__.py +0 -1
- klaude_code/ui/base/display_abc.py +0 -36
- klaude_code/ui/base/input_abc.py +0 -20
- klaude_code/ui/repl/display.py +0 -36
- klaude_code/ui/repl/event_handler.py +0 -247
- klaude_code/ui/repl/input.py +0 -773
- klaude_code/ui/rich_ext/__init__.py +0 -1
- klaude_code-1.2.1.dist-info/RECORD +0 -151
- /klaude_code/core/{prompt → prompts}/prompt-claude-code.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-codex.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent-explore.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent-oracle.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent-webfetch.md +0 -0
- /klaude_code/core/{prompt → prompts}/prompt-subagent.md +0 -0
- /klaude_code/ui/{base → core}/stage_manager.py +0 -0
- /klaude_code/ui/{rich_ext → rich}/live.py +0 -0
- /klaude_code/ui/{rich_ext → rich}/markdown.py +0 -0
- /klaude_code/ui/{rich_ext → rich}/quote.py +0 -0
- /klaude_code/ui/{base → terminal}/progress_bar.py +0 -0
- /klaude_code/ui/{base → utils}/debouncer.py +0 -0
- {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/entry_points.txt +0 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from collections.abc import AsyncIterator
|
|
5
|
+
|
|
6
|
+
from klaude_code.protocol.model import UserInputPayload
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InputProviderABC(ABC):
|
|
10
|
+
"""
|
|
11
|
+
Abstract base class for user input providers.
|
|
12
|
+
|
|
13
|
+
An InputProvider is responsible for collecting user input and yielding it
|
|
14
|
+
to the application. Implementations handle the specifics of input collection,
|
|
15
|
+
such as terminal readline, prompt-toolkit sessions, or other input sources.
|
|
16
|
+
|
|
17
|
+
Lifecycle:
|
|
18
|
+
1. start() is called once before any inputs are requested.
|
|
19
|
+
2. iter_inputs() yields user input strings until the user exits.
|
|
20
|
+
3. stop() is called once when input collection is complete.
|
|
21
|
+
|
|
22
|
+
Typical Usage:
|
|
23
|
+
input_provider = PromptToolkitInput(status_provider=my_status_fn)
|
|
24
|
+
await input_provider.start()
|
|
25
|
+
try:
|
|
26
|
+
async for user_input in input_provider.iter_inputs():
|
|
27
|
+
if user_input.text.strip().lower() in {"exit", "quit"}:
|
|
28
|
+
break
|
|
29
|
+
# Process user_input.text and user_input.images...
|
|
30
|
+
finally:
|
|
31
|
+
await input_provider.stop()
|
|
32
|
+
|
|
33
|
+
Thread Safety:
|
|
34
|
+
Input providers should be used from a single async task.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
@abstractmethod
|
|
38
|
+
async def start(self) -> None:
|
|
39
|
+
"""
|
|
40
|
+
Initialize the input provider before reading inputs.
|
|
41
|
+
|
|
42
|
+
Called once before iter_inputs(). Use this for any setup that needs
|
|
43
|
+
to happen before input collection begins (e.g., configuring terminal
|
|
44
|
+
settings, loading history).
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
@abstractmethod
|
|
48
|
+
async def stop(self) -> None:
|
|
49
|
+
"""
|
|
50
|
+
Clean up the input provider after input collection is complete.
|
|
51
|
+
|
|
52
|
+
Called once after iter_inputs() finishes. Use this for cleanup such
|
|
53
|
+
as saving history, restoring terminal state, or releasing resources.
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
@abstractmethod
|
|
57
|
+
async def iter_inputs(self) -> AsyncIterator[UserInputPayload]:
|
|
58
|
+
"""
|
|
59
|
+
Yield user input payloads asynchronously.
|
|
60
|
+
|
|
61
|
+
This is the main method for collecting user input. Each yield returns
|
|
62
|
+
one complete user input payload containing text and optional images
|
|
63
|
+
(e.g., after the user presses Enter). The iterator completes when the
|
|
64
|
+
user signals end of input (e.g., Ctrl+D) or when the application
|
|
65
|
+
requests shutdown.
|
|
66
|
+
|
|
67
|
+
Yields:
|
|
68
|
+
UserInputPayload with text and optional images.
|
|
69
|
+
"""
|
|
70
|
+
raise NotImplementedError
|
|
71
|
+
yield UserInputPayload(text="") # pyright: ignore[reportUnreachable]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# UI mode implementations
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Debug mode
|
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
from typing import override
|
|
2
2
|
|
|
3
|
-
from klaude_code
|
|
4
|
-
from klaude_code.protocol
|
|
3
|
+
from klaude_code import const
|
|
4
|
+
from klaude_code.protocol import events
|
|
5
5
|
from klaude_code.trace import DebugType, log_debug
|
|
6
|
-
from klaude_code.ui.
|
|
6
|
+
from klaude_code.ui.core.display import DisplayABC
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
class DebugEventDisplay(DisplayABC):
|
|
10
|
-
def __init__(
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
wrapped_display: DisplayABC | None = None,
|
|
13
|
+
log_file: str = const.DEFAULT_DEBUG_LOG_FILE,
|
|
14
|
+
):
|
|
11
15
|
self.wrapped_display = wrapped_display
|
|
12
16
|
self.log_file = log_file
|
|
13
17
|
|
|
14
18
|
@override
|
|
15
|
-
async def consume_event(self, event: Event) -> None:
|
|
19
|
+
async def consume_event(self, event: events.Event) -> None:
|
|
16
20
|
log_debug(
|
|
17
21
|
f"[{event.__class__.__name__}]",
|
|
18
22
|
event.model_dump_json(exclude_none=True),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# Exec mode
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import sys
|
|
1
2
|
from typing import override
|
|
2
3
|
|
|
3
4
|
from klaude_code.protocol import events
|
|
4
|
-
from klaude_code.ui.
|
|
5
|
-
from klaude_code.ui.
|
|
5
|
+
from klaude_code.ui.core.display import DisplayABC
|
|
6
|
+
from klaude_code.ui.terminal.progress_bar import OSC94States, emit_osc94
|
|
6
7
|
|
|
7
8
|
|
|
8
9
|
class ExecDisplay(DisplayABC):
|
|
@@ -35,3 +36,28 @@ class ExecDisplay(DisplayABC):
|
|
|
35
36
|
async def stop(self) -> None:
|
|
36
37
|
"""Do nothing on stop."""
|
|
37
38
|
pass
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class StreamJsonDisplay(DisplayABC):
|
|
42
|
+
"""A display implementation that streams all events as JSON lines."""
|
|
43
|
+
|
|
44
|
+
@override
|
|
45
|
+
async def consume_event(self, event: events.Event) -> None:
|
|
46
|
+
"""Stream each event as a JSON line."""
|
|
47
|
+
if isinstance(event, events.EndEvent):
|
|
48
|
+
return
|
|
49
|
+
event_type = type(event).__name__
|
|
50
|
+
json_data = event.model_dump_json()
|
|
51
|
+
# Output format: {"type": "EventName", "data": {...}}
|
|
52
|
+
print(f'{{"type": "{event_type}", "data": {json_data}}}', flush=True)
|
|
53
|
+
sys.stdout.flush()
|
|
54
|
+
|
|
55
|
+
@override
|
|
56
|
+
async def start(self) -> None:
|
|
57
|
+
"""Do nothing on start."""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
@override
|
|
61
|
+
async def stop(self) -> None:
|
|
62
|
+
"""Do nothing on stop."""
|
|
63
|
+
pass
|
|
@@ -2,8 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
|
-
from klaude_code.protocol
|
|
6
|
-
from klaude_code.ui.repl.
|
|
5
|
+
from klaude_code.protocol import model
|
|
6
|
+
from klaude_code.ui.modes.repl.input_prompt_toolkit import REPLStatusSnapshot
|
|
7
7
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
9
|
from klaude_code.core.agent import Agent
|
|
@@ -30,13 +30,13 @@ def build_repl_status_snapshot(agent: "Agent | None", update_message: str | None
|
|
|
30
30
|
|
|
31
31
|
history = agent.session.conversation_history
|
|
32
32
|
for item in history:
|
|
33
|
-
if isinstance(item, AssistantMessageItem):
|
|
33
|
+
if isinstance(item, model.AssistantMessageItem):
|
|
34
34
|
llm_calls += 1
|
|
35
|
-
elif isinstance(item, ToolCallItem):
|
|
35
|
+
elif isinstance(item, model.ToolCallItem):
|
|
36
36
|
tool_calls += 1
|
|
37
37
|
|
|
38
38
|
for item in reversed(history):
|
|
39
|
-
if isinstance(item, ResponseMetadataItem):
|
|
39
|
+
if isinstance(item, model.ResponseMetadataItem):
|
|
40
40
|
usage = item.usage
|
|
41
41
|
if usage is not None and hasattr(usage, "context_usage_percent"):
|
|
42
42
|
context_usage_percent = usage.context_usage_percent
|
|
@@ -49,4 +49,3 @@ def build_repl_status_snapshot(agent: "Agent | None", update_message: str | None
|
|
|
49
49
|
tool_calls=tool_calls,
|
|
50
50
|
update_message=update_message,
|
|
51
51
|
)
|
|
52
|
-
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Clipboard and image handling for REPL input.
|
|
2
|
+
|
|
3
|
+
This module provides:
|
|
4
|
+
- ClipboardCaptureState: Captures clipboard images and maps tags to file paths
|
|
5
|
+
- capture_clipboard_tag(): Capture clipboard image and return tag
|
|
6
|
+
- extract_images_from_text(): Parse tags and return ImageURLPart list
|
|
7
|
+
- copy_to_clipboard(): Copy text to system clipboard
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import uuid
|
|
17
|
+
from base64 import b64encode
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from PIL import Image, ImageGrab
|
|
21
|
+
|
|
22
|
+
from klaude_code.protocol.model import ImageURLPart
|
|
23
|
+
|
|
24
|
+
# Directory for storing clipboard images
|
|
25
|
+
CLIPBOARD_IMAGES_DIR = Path.home() / ".klaude" / "clipboard" / "images"
|
|
26
|
+
|
|
27
|
+
# Pattern to match [Image #N] tags in user input
|
|
28
|
+
_IMAGE_TAG_RE = re.compile(r"\[Image #(\d+)\]")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class ClipboardCaptureState:
|
|
32
|
+
"""Captures clipboard images and maps tags to file paths in memory."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, images_dir: Path | None = None):
|
|
35
|
+
self._images_dir = images_dir or CLIPBOARD_IMAGES_DIR
|
|
36
|
+
self._pending: dict[str, str] = {} # tag -> path mapping
|
|
37
|
+
self._counter = 1
|
|
38
|
+
|
|
39
|
+
def capture_from_clipboard(self) -> str | None:
|
|
40
|
+
"""Capture image from clipboard, save to disk, and return a tag like [Image #N]."""
|
|
41
|
+
try:
|
|
42
|
+
clipboard_data = ImageGrab.grabclipboard()
|
|
43
|
+
except Exception:
|
|
44
|
+
return None
|
|
45
|
+
if not isinstance(clipboard_data, Image.Image):
|
|
46
|
+
return None
|
|
47
|
+
try:
|
|
48
|
+
self._images_dir.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
except Exception:
|
|
50
|
+
return None
|
|
51
|
+
filename = f"clipboard_{uuid.uuid4().hex[:8]}.png"
|
|
52
|
+
path = self._images_dir / filename
|
|
53
|
+
try:
|
|
54
|
+
clipboard_data.save(path, "PNG")
|
|
55
|
+
except Exception:
|
|
56
|
+
return None
|
|
57
|
+
tag = f"[Image #{self._counter}]"
|
|
58
|
+
self._counter += 1
|
|
59
|
+
self._pending[tag] = str(path)
|
|
60
|
+
return tag
|
|
61
|
+
|
|
62
|
+
def get_pending_images(self) -> dict[str, str]:
|
|
63
|
+
"""Return the current tag-to-path mapping for pending images."""
|
|
64
|
+
return dict(self._pending)
|
|
65
|
+
|
|
66
|
+
def flush(self) -> dict[str, str]:
|
|
67
|
+
"""Flush pending images and return tag-to-path mapping, then reset state."""
|
|
68
|
+
result = dict(self._pending)
|
|
69
|
+
self._pending = {}
|
|
70
|
+
self._counter = 1
|
|
71
|
+
return result
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# Module-level singleton instance
|
|
75
|
+
clipboard_state = ClipboardCaptureState()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def capture_clipboard_tag() -> str | None:
|
|
79
|
+
"""Capture image from clipboard and return tag like [Image #N].
|
|
80
|
+
|
|
81
|
+
Uses the module-level clipboard_state singleton. Returns None if no image
|
|
82
|
+
is available in the clipboard or capture fails.
|
|
83
|
+
"""
|
|
84
|
+
return clipboard_state.capture_from_clipboard()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def extract_images_from_text(text: str) -> list[ImageURLPart]:
|
|
88
|
+
"""Extract images from pending clipboard state based on tags in text.
|
|
89
|
+
|
|
90
|
+
Parses [Image #N] tags in the text, looks up corresponding image paths
|
|
91
|
+
in the clipboard state, and creates ImageURLPart objects from them.
|
|
92
|
+
Flushes the clipboard state after extraction.
|
|
93
|
+
"""
|
|
94
|
+
pending_images = clipboard_state.flush()
|
|
95
|
+
if not pending_images:
|
|
96
|
+
return []
|
|
97
|
+
|
|
98
|
+
# Find all [Image #N] tags in text
|
|
99
|
+
found_tags = set(_IMAGE_TAG_RE.findall(text))
|
|
100
|
+
if not found_tags:
|
|
101
|
+
return []
|
|
102
|
+
|
|
103
|
+
images: list[ImageURLPart] = []
|
|
104
|
+
for tag, path in pending_images.items():
|
|
105
|
+
# Extract the number from the tag and check if it's referenced
|
|
106
|
+
match = _IMAGE_TAG_RE.match(tag)
|
|
107
|
+
if match and match.group(1) in found_tags:
|
|
108
|
+
image_part = _encode_image_file(path)
|
|
109
|
+
if image_part:
|
|
110
|
+
images.append(image_part)
|
|
111
|
+
|
|
112
|
+
return images
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _encode_image_file(file_path: str) -> ImageURLPart | None:
|
|
116
|
+
"""Encode an image file as base64 data URL and create ImageURLPart."""
|
|
117
|
+
try:
|
|
118
|
+
path = Path(file_path)
|
|
119
|
+
if not path.exists():
|
|
120
|
+
return None
|
|
121
|
+
with open(path, "rb") as f:
|
|
122
|
+
encoded = b64encode(f.read()).decode("ascii")
|
|
123
|
+
# Clipboard images are always saved as PNG
|
|
124
|
+
data_url = f"data:image/png;base64,{encoded}"
|
|
125
|
+
return ImageURLPart(image_url=ImageURLPart.ImageURL(url=data_url, id=None))
|
|
126
|
+
except Exception:
|
|
127
|
+
return None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def copy_to_clipboard(text: str) -> None:
|
|
131
|
+
"""Copy text to system clipboard using platform-specific commands."""
|
|
132
|
+
try:
|
|
133
|
+
if sys.platform == "darwin":
|
|
134
|
+
subprocess.run(["pbcopy"], input=text.encode("utf-8"), check=True)
|
|
135
|
+
elif sys.platform == "win32":
|
|
136
|
+
subprocess.run(["clip"], input=text.encode("utf-16"), check=True)
|
|
137
|
+
else:
|
|
138
|
+
# Linux: try xclip first, then xsel
|
|
139
|
+
if shutil.which("xclip"):
|
|
140
|
+
subprocess.run(
|
|
141
|
+
["xclip", "-selection", "clipboard"],
|
|
142
|
+
input=text.encode("utf-8"),
|
|
143
|
+
check=True,
|
|
144
|
+
)
|
|
145
|
+
elif shutil.which("xsel"):
|
|
146
|
+
subprocess.run(
|
|
147
|
+
["xsel", "--clipboard", "--input"],
|
|
148
|
+
input=text.encode("utf-8"),
|
|
149
|
+
check=True,
|
|
150
|
+
)
|
|
151
|
+
except Exception:
|
|
152
|
+
pass
|