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.
Files changed (140) hide show
  1. klaude_code/cli/main.py +9 -4
  2. klaude_code/cli/runtime.py +42 -43
  3. klaude_code/command/__init__.py +7 -5
  4. klaude_code/command/clear_cmd.py +6 -29
  5. klaude_code/command/command_abc.py +44 -8
  6. klaude_code/command/diff_cmd.py +33 -27
  7. klaude_code/command/export_cmd.py +18 -26
  8. klaude_code/command/help_cmd.py +10 -8
  9. klaude_code/command/model_cmd.py +11 -40
  10. klaude_code/command/{prompt-update-dev-doc.md → prompt-dev-docs-update.md} +3 -2
  11. klaude_code/command/{prompt-dev-doc.md → prompt-dev-docs.md} +3 -2
  12. klaude_code/command/prompt-init.md +2 -5
  13. klaude_code/command/prompt_command.py +6 -6
  14. klaude_code/command/refresh_cmd.py +4 -5
  15. klaude_code/command/registry.py +16 -19
  16. klaude_code/command/terminal_setup_cmd.py +12 -11
  17. klaude_code/config/__init__.py +4 -0
  18. klaude_code/config/config.py +25 -26
  19. klaude_code/config/list_model.py +8 -3
  20. klaude_code/config/select_model.py +1 -1
  21. klaude_code/const/__init__.py +1 -1
  22. klaude_code/core/__init__.py +0 -3
  23. klaude_code/core/agent.py +25 -50
  24. klaude_code/core/executor.py +268 -101
  25. klaude_code/core/prompt.py +12 -12
  26. klaude_code/core/{prompt → prompts}/prompt-gemini.md +1 -1
  27. klaude_code/core/reminders.py +76 -95
  28. klaude_code/core/task.py +21 -14
  29. klaude_code/core/tool/__init__.py +45 -11
  30. klaude_code/core/tool/file/apply_patch.py +5 -1
  31. klaude_code/core/tool/file/apply_patch_tool.py +11 -13
  32. klaude_code/core/tool/file/edit_tool.py +27 -23
  33. klaude_code/core/tool/file/multi_edit_tool.py +15 -17
  34. klaude_code/core/tool/file/read_tool.py +41 -36
  35. klaude_code/core/tool/file/write_tool.py +13 -15
  36. klaude_code/core/tool/memory/memory_tool.py +85 -68
  37. klaude_code/core/tool/memory/skill_tool.py +10 -12
  38. klaude_code/core/tool/shell/bash_tool.py +24 -22
  39. klaude_code/core/tool/shell/command_safety.py +12 -1
  40. klaude_code/core/tool/sub_agent_tool.py +11 -12
  41. klaude_code/core/tool/todo/todo_write_tool.py +21 -28
  42. klaude_code/core/tool/todo/update_plan_tool.py +14 -24
  43. klaude_code/core/tool/tool_abc.py +3 -4
  44. klaude_code/core/tool/tool_context.py +7 -7
  45. klaude_code/core/tool/tool_registry.py +30 -47
  46. klaude_code/core/tool/tool_runner.py +35 -43
  47. klaude_code/core/tool/truncation.py +14 -20
  48. klaude_code/core/tool/web/mermaid_tool.py +12 -14
  49. klaude_code/core/tool/web/web_fetch_tool.py +15 -17
  50. klaude_code/core/turn.py +19 -7
  51. klaude_code/llm/__init__.py +3 -4
  52. klaude_code/llm/anthropic/client.py +30 -46
  53. klaude_code/llm/anthropic/input.py +4 -11
  54. klaude_code/llm/client.py +29 -8
  55. klaude_code/llm/input_common.py +66 -36
  56. klaude_code/llm/openai_compatible/client.py +42 -84
  57. klaude_code/llm/openai_compatible/input.py +11 -16
  58. klaude_code/llm/openai_compatible/tool_call_accumulator.py +2 -2
  59. klaude_code/llm/openrouter/client.py +40 -289
  60. klaude_code/llm/openrouter/input.py +13 -35
  61. klaude_code/llm/openrouter/reasoning_handler.py +209 -0
  62. klaude_code/llm/registry.py +5 -75
  63. klaude_code/llm/responses/client.py +34 -55
  64. klaude_code/llm/responses/input.py +24 -26
  65. klaude_code/llm/usage.py +109 -0
  66. klaude_code/protocol/__init__.py +4 -0
  67. klaude_code/protocol/events.py +3 -2
  68. klaude_code/protocol/{llm_parameter.py → llm_param.py} +12 -32
  69. klaude_code/protocol/model.py +49 -4
  70. klaude_code/protocol/op.py +18 -16
  71. klaude_code/protocol/op_handler.py +28 -0
  72. klaude_code/{core → protocol}/sub_agent.py +7 -0
  73. klaude_code/session/export.py +150 -70
  74. klaude_code/session/session.py +28 -14
  75. klaude_code/session/templates/export_session.html +180 -42
  76. klaude_code/trace/__init__.py +2 -2
  77. klaude_code/trace/log.py +11 -5
  78. klaude_code/ui/__init__.py +91 -8
  79. klaude_code/ui/core/__init__.py +1 -0
  80. klaude_code/ui/core/display.py +103 -0
  81. klaude_code/ui/core/input.py +71 -0
  82. klaude_code/ui/modes/__init__.py +1 -0
  83. klaude_code/ui/modes/debug/__init__.py +1 -0
  84. klaude_code/ui/{base/debug_event_display.py → modes/debug/display.py} +9 -5
  85. klaude_code/ui/modes/exec/__init__.py +1 -0
  86. klaude_code/ui/{base/exec_display.py → modes/exec/display.py} +28 -2
  87. klaude_code/ui/{repl → modes/repl}/__init__.py +5 -6
  88. klaude_code/ui/modes/repl/clipboard.py +152 -0
  89. klaude_code/ui/modes/repl/completers.py +429 -0
  90. klaude_code/ui/modes/repl/display.py +60 -0
  91. klaude_code/ui/modes/repl/event_handler.py +375 -0
  92. klaude_code/ui/modes/repl/input_prompt_toolkit.py +198 -0
  93. klaude_code/ui/modes/repl/key_bindings.py +170 -0
  94. klaude_code/ui/{repl → modes/repl}/renderer.py +109 -132
  95. klaude_code/ui/renderers/assistant.py +21 -0
  96. klaude_code/ui/renderers/common.py +0 -16
  97. klaude_code/ui/renderers/developer.py +18 -18
  98. klaude_code/ui/renderers/diffs.py +36 -14
  99. klaude_code/ui/renderers/errors.py +1 -1
  100. klaude_code/ui/renderers/metadata.py +50 -27
  101. klaude_code/ui/renderers/sub_agent.py +43 -9
  102. klaude_code/ui/renderers/thinking.py +33 -1
  103. klaude_code/ui/renderers/tools.py +212 -20
  104. klaude_code/ui/renderers/user_input.py +19 -23
  105. klaude_code/ui/rich/__init__.py +1 -0
  106. klaude_code/ui/{rich_ext → rich}/searchable_text.py +3 -1
  107. klaude_code/ui/{renderers → rich}/status.py +29 -18
  108. klaude_code/ui/{base → rich}/theme.py +8 -2
  109. klaude_code/ui/terminal/__init__.py +1 -0
  110. klaude_code/ui/{base/terminal_color.py → terminal/color.py} +4 -1
  111. klaude_code/ui/{base/terminal_control.py → terminal/control.py} +1 -0
  112. klaude_code/ui/{base/terminal_notifier.py → terminal/notifier.py} +5 -2
  113. klaude_code/ui/utils/__init__.py +1 -0
  114. klaude_code/ui/{base/utils.py → utils/common.py} +35 -3
  115. {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/METADATA +1 -1
  116. klaude_code-1.2.3.dist-info/RECORD +161 -0
  117. klaude_code/core/clipboard_manifest.py +0 -124
  118. klaude_code/llm/openrouter/tool_call_accumulator.py +0 -80
  119. klaude_code/ui/base/__init__.py +0 -1
  120. klaude_code/ui/base/display_abc.py +0 -36
  121. klaude_code/ui/base/input_abc.py +0 -20
  122. klaude_code/ui/repl/display.py +0 -36
  123. klaude_code/ui/repl/event_handler.py +0 -247
  124. klaude_code/ui/repl/input.py +0 -773
  125. klaude_code/ui/rich_ext/__init__.py +0 -1
  126. klaude_code-1.2.1.dist-info/RECORD +0 -151
  127. /klaude_code/core/{prompt → prompts}/prompt-claude-code.md +0 -0
  128. /klaude_code/core/{prompt → prompts}/prompt-codex.md +0 -0
  129. /klaude_code/core/{prompt → prompts}/prompt-subagent-explore.md +0 -0
  130. /klaude_code/core/{prompt → prompts}/prompt-subagent-oracle.md +0 -0
  131. /klaude_code/core/{prompt → prompts}/prompt-subagent-webfetch.md +0 -0
  132. /klaude_code/core/{prompt → prompts}/prompt-subagent.md +0 -0
  133. /klaude_code/ui/{base → core}/stage_manager.py +0 -0
  134. /klaude_code/ui/{rich_ext → rich}/live.py +0 -0
  135. /klaude_code/ui/{rich_ext → rich}/markdown.py +0 -0
  136. /klaude_code/ui/{rich_ext → rich}/quote.py +0 -0
  137. /klaude_code/ui/{base → terminal}/progress_bar.py +0 -0
  138. /klaude_code/ui/{base → utils}/debouncer.py +0 -0
  139. {klaude_code-1.2.1.dist-info → klaude_code-1.2.3.dist-info}/WHEEL +0 -0
  140. {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.const import DEFAULT_DEBUG_LOG_FILE
4
- from klaude_code.protocol.events import Event
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.base.display_abc import DisplayABC
6
+ from klaude_code.ui.core.display import DisplayABC
7
7
 
8
8
 
9
9
  class DebugEventDisplay(DisplayABC):
10
- def __init__(self, wrapped_display: DisplayABC | None = None, log_file: str = DEFAULT_DEBUG_LOG_FILE):
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.base.display_abc import DisplayABC
5
- from klaude_code.ui.base.progress_bar import OSC94States, emit_osc94
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.model import AssistantMessageItem, ResponseMetadataItem, ToolCallItem
6
- from klaude_code.ui.repl.input import REPLStatusSnapshot
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