klaude-code 1.8.0__py3-none-any.whl → 2.0.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/auth/base.py +97 -0
- klaude_code/auth/claude/__init__.py +6 -0
- klaude_code/auth/claude/exceptions.py +9 -0
- klaude_code/auth/claude/oauth.py +172 -0
- klaude_code/auth/claude/token_manager.py +26 -0
- klaude_code/auth/codex/token_manager.py +10 -50
- klaude_code/cli/auth_cmd.py +127 -46
- klaude_code/cli/config_cmd.py +4 -2
- klaude_code/cli/cost_cmd.py +14 -9
- klaude_code/cli/list_model.py +248 -200
- klaude_code/cli/main.py +1 -1
- klaude_code/cli/runtime.py +7 -5
- klaude_code/cli/self_update.py +1 -1
- klaude_code/cli/session_cmd.py +1 -1
- klaude_code/command/clear_cmd.py +6 -2
- klaude_code/command/command_abc.py +2 -2
- klaude_code/command/debug_cmd.py +4 -4
- klaude_code/command/export_cmd.py +2 -2
- klaude_code/command/export_online_cmd.py +12 -12
- klaude_code/command/fork_session_cmd.py +29 -23
- klaude_code/command/help_cmd.py +4 -4
- klaude_code/command/model_cmd.py +4 -4
- klaude_code/command/model_select.py +1 -1
- klaude_code/command/prompt-commit.md +82 -0
- klaude_code/command/prompt_command.py +3 -3
- klaude_code/command/refresh_cmd.py +2 -2
- klaude_code/command/registry.py +7 -5
- klaude_code/command/release_notes_cmd.py +4 -4
- klaude_code/command/resume_cmd.py +15 -11
- klaude_code/command/status_cmd.py +4 -4
- klaude_code/command/terminal_setup_cmd.py +8 -8
- klaude_code/command/thinking_cmd.py +4 -4
- klaude_code/config/assets/builtin_config.yaml +52 -3
- klaude_code/config/builtin_config.py +16 -5
- klaude_code/config/config.py +31 -7
- klaude_code/config/thinking.py +4 -4
- klaude_code/const.py +146 -91
- klaude_code/core/agent.py +3 -12
- klaude_code/core/executor.py +21 -13
- klaude_code/core/manager/sub_agent_manager.py +71 -7
- klaude_code/core/prompt.py +1 -1
- klaude_code/core/prompts/prompt-sub-agent-image-gen.md +1 -0
- klaude_code/core/prompts/prompt-sub-agent-web.md +27 -1
- klaude_code/core/reminders.py +88 -69
- klaude_code/core/task.py +44 -45
- klaude_code/core/tool/file/apply_patch_tool.py +9 -9
- klaude_code/core/tool/file/diff_builder.py +3 -5
- klaude_code/core/tool/file/edit_tool.py +23 -23
- klaude_code/core/tool/file/move_tool.py +43 -43
- klaude_code/core/tool/file/read_tool.py +44 -39
- klaude_code/core/tool/file/write_tool.py +14 -14
- klaude_code/core/tool/report_back_tool.py +4 -4
- klaude_code/core/tool/shell/bash_tool.py +23 -23
- klaude_code/core/tool/skill/skill_tool.py +7 -7
- klaude_code/core/tool/sub_agent_tool.py +38 -9
- klaude_code/core/tool/todo/todo_write_tool.py +8 -8
- klaude_code/core/tool/todo/update_plan_tool.py +6 -6
- klaude_code/core/tool/tool_abc.py +2 -2
- klaude_code/core/tool/tool_context.py +27 -0
- klaude_code/core/tool/tool_runner.py +88 -42
- klaude_code/core/tool/truncation.py +38 -20
- klaude_code/core/tool/web/mermaid_tool.py +6 -7
- klaude_code/core/tool/web/web_fetch_tool.py +68 -30
- klaude_code/core/tool/web/web_search_tool.py +15 -17
- klaude_code/core/turn.py +120 -73
- klaude_code/llm/anthropic/client.py +104 -44
- klaude_code/llm/anthropic/input.py +116 -108
- klaude_code/llm/bedrock/client.py +8 -5
- klaude_code/llm/claude/__init__.py +3 -0
- klaude_code/llm/claude/client.py +105 -0
- klaude_code/llm/client.py +4 -3
- klaude_code/llm/codex/client.py +16 -10
- klaude_code/llm/google/client.py +122 -60
- klaude_code/llm/google/input.py +94 -108
- klaude_code/llm/image.py +123 -0
- klaude_code/llm/input_common.py +136 -189
- klaude_code/llm/openai_compatible/client.py +17 -7
- klaude_code/llm/openai_compatible/input.py +36 -66
- klaude_code/llm/openai_compatible/stream.py +119 -67
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +23 -11
- klaude_code/llm/openrouter/client.py +34 -9
- klaude_code/llm/openrouter/input.py +63 -64
- klaude_code/llm/openrouter/reasoning.py +22 -24
- klaude_code/llm/registry.py +20 -15
- klaude_code/llm/responses/client.py +107 -45
- klaude_code/llm/responses/input.py +115 -98
- klaude_code/llm/usage.py +52 -25
- klaude_code/protocol/__init__.py +1 -0
- klaude_code/protocol/events.py +16 -12
- klaude_code/protocol/llm_param.py +22 -3
- klaude_code/protocol/message.py +250 -0
- klaude_code/protocol/model.py +94 -281
- klaude_code/protocol/op.py +2 -2
- klaude_code/protocol/sub_agent/__init__.py +2 -2
- klaude_code/protocol/sub_agent/explore.py +10 -0
- klaude_code/protocol/sub_agent/image_gen.py +119 -0
- klaude_code/protocol/sub_agent/task.py +10 -0
- klaude_code/protocol/sub_agent/web.py +10 -0
- klaude_code/session/codec.py +6 -6
- klaude_code/session/export.py +261 -62
- klaude_code/session/selector.py +7 -24
- klaude_code/session/session.py +125 -53
- klaude_code/session/store.py +5 -32
- klaude_code/session/templates/export_session.html +1 -1
- klaude_code/session/templates/mermaid_viewer.html +1 -1
- klaude_code/trace/log.py +11 -6
- klaude_code/ui/core/input.py +1 -1
- klaude_code/ui/core/stage_manager.py +1 -8
- klaude_code/ui/modes/debug/display.py +2 -2
- klaude_code/ui/modes/repl/clipboard.py +2 -2
- klaude_code/ui/modes/repl/completers.py +18 -10
- klaude_code/ui/modes/repl/event_handler.py +136 -127
- klaude_code/ui/modes/repl/input_prompt_toolkit.py +1 -1
- klaude_code/ui/modes/repl/key_bindings.py +1 -1
- klaude_code/ui/modes/repl/renderer.py +107 -15
- klaude_code/ui/renderers/assistant.py +2 -2
- klaude_code/ui/renderers/common.py +65 -7
- klaude_code/ui/renderers/developer.py +7 -6
- klaude_code/ui/renderers/diffs.py +11 -11
- klaude_code/ui/renderers/mermaid_viewer.py +49 -2
- klaude_code/ui/renderers/metadata.py +39 -31
- klaude_code/ui/renderers/sub_agent.py +57 -16
- klaude_code/ui/renderers/thinking.py +37 -2
- klaude_code/ui/renderers/tools.py +180 -165
- klaude_code/ui/rich/live.py +3 -1
- klaude_code/ui/rich/markdown.py +39 -7
- klaude_code/ui/rich/quote.py +76 -1
- klaude_code/ui/rich/status.py +14 -8
- klaude_code/ui/rich/theme.py +13 -6
- klaude_code/ui/terminal/image.py +34 -0
- klaude_code/ui/terminal/notifier.py +2 -1
- klaude_code/ui/terminal/progress_bar.py +4 -4
- klaude_code/ui/terminal/selector.py +22 -4
- klaude_code/ui/utils/common.py +55 -0
- {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.dist-info}/METADATA +28 -6
- klaude_code-2.0.0.dist-info/RECORD +229 -0
- klaude_code/command/prompt-jj-describe.md +0 -32
- klaude_code/core/prompts/prompt-sub-agent-oracle.md +0 -22
- klaude_code/protocol/sub_agent/oracle.py +0 -91
- klaude_code-1.8.0.dist-info/RECORD +0 -219
- {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.dist-info}/WHEEL +0 -0
- {klaude_code-1.8.0.dist-info → klaude_code-2.0.0.dist-info}/entry_points.txt +0 -0
klaude_code/ui/rich/live.py
CHANGED
|
@@ -7,6 +7,8 @@ from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
|
|
|
7
7
|
from rich.live import Live
|
|
8
8
|
from rich.segment import Segment
|
|
9
9
|
|
|
10
|
+
from klaude_code.const import CROP_ABOVE_LIVE_REFRESH_PER_SECOND
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
class CropAbove:
|
|
12
14
|
def __init__(self, renderable: RenderableType, style: str = "") -> None:
|
|
@@ -33,7 +35,7 @@ class CropAboveLive(Live):
|
|
|
33
35
|
renderable: RenderableType | None = None,
|
|
34
36
|
*,
|
|
35
37
|
console: Console | None = None,
|
|
36
|
-
refresh_per_second: float =
|
|
38
|
+
refresh_per_second: float = CROP_ABOVE_LIVE_REFRESH_PER_SECOND,
|
|
37
39
|
transient: bool = False,
|
|
38
40
|
get_renderable: Any | None = None,
|
|
39
41
|
style: str = "",
|
klaude_code/ui/rich/markdown.py
CHANGED
|
@@ -18,7 +18,7 @@ from rich.table import Table
|
|
|
18
18
|
from rich.text import Text
|
|
19
19
|
from rich.theme import Theme
|
|
20
20
|
|
|
21
|
-
from klaude_code import
|
|
21
|
+
from klaude_code.const import MARKDOWN_RIGHT_MARGIN, MARKDOWN_STREAM_LIVE_REPAINT_ENABLED, UI_REFRESH_RATE_FPS
|
|
22
22
|
from klaude_code.ui.rich.code_panel import CodePanel
|
|
23
23
|
|
|
24
24
|
|
|
@@ -55,8 +55,6 @@ class Divider(MarkdownElement):
|
|
|
55
55
|
|
|
56
56
|
|
|
57
57
|
class MarkdownTable(TableElement):
|
|
58
|
-
"""A table element with MINIMAL_HEAVY_HEAD box style."""
|
|
59
|
-
|
|
60
58
|
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
61
59
|
table = Table(box=box.MARKDOWN, border_style=console.get_style("markdown.table.border"))
|
|
62
60
|
|
|
@@ -69,7 +67,27 @@ class MarkdownTable(TableElement):
|
|
|
69
67
|
row_content = [element.content for element in row.cells]
|
|
70
68
|
table.add_row(*row_content)
|
|
71
69
|
|
|
72
|
-
|
|
70
|
+
# Render table and strip top/bottom blank lines that MARKDOWN box adds
|
|
71
|
+
segments = list(console.render(table, options))
|
|
72
|
+
|
|
73
|
+
# Skip leading blank line (before first newline)
|
|
74
|
+
first_newline_idx = next((i for i, s in enumerate(segments) if s.text == "\n"), None)
|
|
75
|
+
if first_newline_idx is not None:
|
|
76
|
+
first_line = "".join(s.text for s in segments[:first_newline_idx])
|
|
77
|
+
if not first_line.strip():
|
|
78
|
+
segments = segments[first_newline_idx + 1 :]
|
|
79
|
+
|
|
80
|
+
# Skip trailing blank line (after last newline)
|
|
81
|
+
while len(segments) >= 2 and segments[-1].text == "\n":
|
|
82
|
+
prev_newline = next((i for i in range(len(segments) - 2, -1, -1) if segments[i].text == "\n"), None)
|
|
83
|
+
if prev_newline is not None:
|
|
84
|
+
between = "".join(s.text for s in segments[prev_newline + 1 : -1])
|
|
85
|
+
if not between.strip():
|
|
86
|
+
segments = segments[: prev_newline + 1]
|
|
87
|
+
continue
|
|
88
|
+
break
|
|
89
|
+
|
|
90
|
+
yield from segments
|
|
73
91
|
|
|
74
92
|
|
|
75
93
|
class LeftHeading(Heading):
|
|
@@ -135,7 +153,7 @@ class MarkdownStream:
|
|
|
135
153
|
mark: str | None = None,
|
|
136
154
|
mark_style: StyleType | None = None,
|
|
137
155
|
left_margin: int = 0,
|
|
138
|
-
right_margin: int =
|
|
156
|
+
right_margin: int = MARKDOWN_RIGHT_MARGIN,
|
|
139
157
|
markdown_class: Callable[..., Markdown] | None = None,
|
|
140
158
|
) -> None:
|
|
141
159
|
"""Initialize the markdown stream.
|
|
@@ -162,7 +180,7 @@ class MarkdownStream:
|
|
|
162
180
|
|
|
163
181
|
# Streaming control
|
|
164
182
|
self.when: float = 0.0 # Timestamp of last update
|
|
165
|
-
self.min_delay: float = 1.0 /
|
|
183
|
+
self.min_delay: float = 1.0 / UI_REFRESH_RATE_FPS
|
|
166
184
|
self._parser: MarkdownIt = MarkdownIt("commonmark")
|
|
167
185
|
|
|
168
186
|
self.theme = theme
|
|
@@ -201,6 +219,20 @@ class MarkdownStream:
|
|
|
201
219
|
|
|
202
220
|
last = top_level[-1]
|
|
203
221
|
assert last.map is not None
|
|
222
|
+
|
|
223
|
+
# When the buffer ends mid-line, markdown-it-py can temporarily classify
|
|
224
|
+
# some lines as a thematic break (hr). For example, a trailing "- --"
|
|
225
|
+
# parses as an hr, but appending a non-hr character ("- --0") turns it
|
|
226
|
+
# into a list item, which should belong to the previous list block.
|
|
227
|
+
#
|
|
228
|
+
# Because stable_line is clamped to be monotonic, advancing to the hr's
|
|
229
|
+
# start line would be irreversible and can split a list across
|
|
230
|
+
# stable/live, producing a render mismatch.
|
|
231
|
+
if last.type == "hr" and not text.endswith("\n"):
|
|
232
|
+
prev = top_level[-2]
|
|
233
|
+
assert prev.map is not None
|
|
234
|
+
return max(prev.map[0], 0)
|
|
235
|
+
|
|
204
236
|
start_line = last.map[0]
|
|
205
237
|
return max(start_line, 0)
|
|
206
238
|
|
|
@@ -414,7 +446,7 @@ class MarkdownStream:
|
|
|
414
446
|
self._live_sink(None)
|
|
415
447
|
return
|
|
416
448
|
|
|
417
|
-
if
|
|
449
|
+
if MARKDOWN_STREAM_LIVE_REPAINT_ENABLED and self._live_sink is not None:
|
|
418
450
|
apply_mark_live = self._stable_source_line_count == 0
|
|
419
451
|
live_lines = self._render_markdown_to_lines(live_source, apply_mark=apply_mark_live)
|
|
420
452
|
|
klaude_code/ui/rich/quote.py
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
from typing import Any
|
|
1
|
+
from typing import TYPE_CHECKING, Any, Self
|
|
2
2
|
|
|
3
3
|
from rich.console import Console, ConsoleOptions, RenderResult
|
|
4
4
|
from rich.segment import Segment
|
|
5
5
|
from rich.style import Style
|
|
6
6
|
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from rich.console import RenderableType
|
|
9
|
+
|
|
7
10
|
|
|
8
11
|
class Quote:
|
|
9
12
|
"""Wrapper to add quote prefix to any content"""
|
|
@@ -32,3 +35,75 @@ class Quote:
|
|
|
32
35
|
yield prefix_segment
|
|
33
36
|
yield from line
|
|
34
37
|
yield new_line
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class TreeQuote:
|
|
41
|
+
"""Wrapper to add a tree-style prefix to any content."""
|
|
42
|
+
|
|
43
|
+
def __init__(
|
|
44
|
+
self,
|
|
45
|
+
content: Any,
|
|
46
|
+
*,
|
|
47
|
+
prefix_first: str | None = None,
|
|
48
|
+
prefix_middle: str = "│ ",
|
|
49
|
+
prefix_last: str = "└ ",
|
|
50
|
+
style: str | Style = "magenta",
|
|
51
|
+
style_first: str | Style | None = None,
|
|
52
|
+
):
|
|
53
|
+
self.content = content
|
|
54
|
+
self.prefix_first = prefix_first
|
|
55
|
+
self.prefix_middle = prefix_middle
|
|
56
|
+
self.prefix_last = prefix_last
|
|
57
|
+
self.style = style
|
|
58
|
+
self.style_first = style_first
|
|
59
|
+
|
|
60
|
+
@classmethod
|
|
61
|
+
def for_tool_call(cls, content: "RenderableType", *, mark: str, style: str, style_first: str) -> Self:
|
|
62
|
+
"""Create a tree quote for tool call display.
|
|
63
|
+
|
|
64
|
+
The mark appears on the first line, with continuation lines using "│ ".
|
|
65
|
+
"""
|
|
66
|
+
return cls(
|
|
67
|
+
content,
|
|
68
|
+
prefix_first=f"{mark} ",
|
|
69
|
+
prefix_middle="│ ",
|
|
70
|
+
prefix_last="│ ",
|
|
71
|
+
style=style,
|
|
72
|
+
style_first=style_first,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def for_tool_result(
|
|
77
|
+
cls, content: "RenderableType", *, is_last: bool, style: str = "tool.result.tree_prefix"
|
|
78
|
+
) -> Self:
|
|
79
|
+
"""Create a tree quote for tool result display.
|
|
80
|
+
|
|
81
|
+
Uses "└ " for the last result in a turn, "│ " otherwise.
|
|
82
|
+
"""
|
|
83
|
+
return cls(content, prefix_last="└ " if is_last else "│ ", style=style)
|
|
84
|
+
|
|
85
|
+
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
86
|
+
# Reduce width to leave space for prefix
|
|
87
|
+
prefix_width = max(
|
|
88
|
+
len(self.prefix_middle),
|
|
89
|
+
len(self.prefix_last),
|
|
90
|
+
len(self.prefix_first) if self.prefix_first is not None else 0,
|
|
91
|
+
)
|
|
92
|
+
render_options = options.update(width=options.max_width - prefix_width)
|
|
93
|
+
|
|
94
|
+
quote_style = console.get_style(self.style) if isinstance(self.style, str) else self.style
|
|
95
|
+
first_style = console.get_style(self.style_first) if isinstance(self.style_first, str) else self.style_first
|
|
96
|
+
|
|
97
|
+
new_line = Segment("\n")
|
|
98
|
+
lines = console.render_lines(self.content, render_options)
|
|
99
|
+
line_count = len(lines)
|
|
100
|
+
|
|
101
|
+
for idx, line in enumerate(lines):
|
|
102
|
+
if idx == 0 and self.prefix_first is not None:
|
|
103
|
+
yield Segment(self.prefix_first, first_style or quote_style)
|
|
104
|
+
else:
|
|
105
|
+
is_last = idx == line_count - 1
|
|
106
|
+
prefix = self.prefix_last if is_last else self.prefix_middle
|
|
107
|
+
yield Segment(prefix, quote_style)
|
|
108
|
+
yield from line
|
|
109
|
+
yield new_line
|
klaude_code/ui/rich/status.py
CHANGED
|
@@ -16,7 +16,13 @@ from rich.style import Style
|
|
|
16
16
|
from rich.table import Table
|
|
17
17
|
from rich.text import Text
|
|
18
18
|
|
|
19
|
-
from klaude_code import
|
|
19
|
+
from klaude_code.const import (
|
|
20
|
+
SPINNER_BREATH_PERIOD_SECONDS,
|
|
21
|
+
STATUS_HINT_TEXT,
|
|
22
|
+
STATUS_SHIMMER_ALPHA_SCALE,
|
|
23
|
+
STATUS_SHIMMER_BAND_HALF_WIDTH,
|
|
24
|
+
STATUS_SHIMMER_PADDING,
|
|
25
|
+
)
|
|
20
26
|
from klaude_code.ui.rich.theme import ThemeKey
|
|
21
27
|
from klaude_code.ui.terminal.color import get_last_terminal_background_rgb
|
|
22
28
|
|
|
@@ -91,7 +97,7 @@ def current_hint_text(*, min_time_width: int = 0) -> str:
|
|
|
91
97
|
|
|
92
98
|
# Keep the signature stable; min_time_width is intentionally ignored.
|
|
93
99
|
_ = min_time_width
|
|
94
|
-
return
|
|
100
|
+
return STATUS_HINT_TEXT
|
|
95
101
|
|
|
96
102
|
|
|
97
103
|
def current_elapsed_text(*, min_time_width: int = 0) -> str | None:
|
|
@@ -152,18 +158,18 @@ def _shimmer_profile(main_text: str) -> list[tuple[str, float]]:
|
|
|
152
158
|
if not chars:
|
|
153
159
|
return []
|
|
154
160
|
|
|
155
|
-
padding =
|
|
161
|
+
padding = STATUS_SHIMMER_PADDING
|
|
156
162
|
char_count = len(chars)
|
|
157
163
|
period = char_count + padding * 2
|
|
158
164
|
|
|
159
165
|
# Use same period as breathing spinner for visual consistency
|
|
160
|
-
sweep_seconds = max(
|
|
166
|
+
sweep_seconds = max(SPINNER_BREATH_PERIOD_SECONDS, 0.1)
|
|
161
167
|
|
|
162
168
|
elapsed = _elapsed_since_start()
|
|
163
169
|
# Complete one full sweep in sweep_seconds, regardless of text length
|
|
164
170
|
pos_f = (elapsed / sweep_seconds % 1.0) * period
|
|
165
171
|
pos = int(pos_f)
|
|
166
|
-
band_half_width =
|
|
172
|
+
band_half_width = STATUS_SHIMMER_BAND_HALF_WIDTH
|
|
167
173
|
|
|
168
174
|
profile: list[tuple[str, float]] = []
|
|
169
175
|
for index, ch in enumerate(chars):
|
|
@@ -189,7 +195,7 @@ def _shimmer_style(console: Console, base_style: Style, intensity: float) -> Sty
|
|
|
189
195
|
if intensity <= 0.0:
|
|
190
196
|
return base_style
|
|
191
197
|
|
|
192
|
-
alpha = max(0.0, min(1.0, intensity *
|
|
198
|
+
alpha = max(0.0, min(1.0, intensity * STATUS_SHIMMER_ALPHA_SCALE))
|
|
193
199
|
|
|
194
200
|
base_color = base_style.color or Color.default()
|
|
195
201
|
base_triplet = base_color.get_truecolor()
|
|
@@ -213,7 +219,7 @@ def _breathing_intensity() -> float:
|
|
|
213
219
|
then returning to 0, giving a subtle "breathing" effect.
|
|
214
220
|
"""
|
|
215
221
|
|
|
216
|
-
period = max(
|
|
222
|
+
period = max(SPINNER_BREATH_PERIOD_SECONDS, 0.1)
|
|
217
223
|
elapsed = _elapsed_since_start()
|
|
218
224
|
phase = (elapsed % period) / period
|
|
219
225
|
return 0.5 * (1.0 - math.cos(2.0 * math.pi * phase))
|
|
@@ -224,7 +230,7 @@ def _breathing_glyph() -> str:
|
|
|
224
230
|
|
|
225
231
|
Alternates between glyphs at each breath cycle (when intensity reaches 0).
|
|
226
232
|
"""
|
|
227
|
-
period = max(
|
|
233
|
+
period = max(SPINNER_BREATH_PERIOD_SECONDS, 0.1)
|
|
228
234
|
elapsed = _elapsed_since_start()
|
|
229
235
|
cycle = int(elapsed / period)
|
|
230
236
|
return BREATHING_SPINNER_GLYPHS[cycle % len(BREATHING_SPINNER_GLYPHS)]
|
klaude_code/ui/rich/theme.py
CHANGED
|
@@ -124,6 +124,7 @@ class ThemeKey(str, Enum):
|
|
|
124
124
|
# ERROR
|
|
125
125
|
ERROR = "error"
|
|
126
126
|
ERROR_BOLD = "error.bold"
|
|
127
|
+
ERROR_DIM = "error.dim"
|
|
127
128
|
INTERRUPT = "interrupt"
|
|
128
129
|
# METADATA
|
|
129
130
|
METADATA = "metadata"
|
|
@@ -154,6 +155,7 @@ class ThemeKey(str, Enum):
|
|
|
154
155
|
TOOL_PARAM = "tool.param"
|
|
155
156
|
TOOL_PARAM_BOLD = "tool.param.bold"
|
|
156
157
|
TOOL_RESULT = "tool.result"
|
|
158
|
+
TOOL_RESULT_TREE_PREFIX = "tool.result.tree_prefix"
|
|
157
159
|
TOOL_RESULT_TRUNCATED = "tool.result.truncated"
|
|
158
160
|
TOOL_RESULT_BOLD = "tool.result.bold"
|
|
159
161
|
TOOL_MARK = "tool.mark"
|
|
@@ -161,6 +163,7 @@ class ThemeKey(str, Enum):
|
|
|
161
163
|
TOOL_REJECTED = "tool.rejected"
|
|
162
164
|
TOOL_TIMEOUT = "tool.timeout"
|
|
163
165
|
TOOL_RESULT_MERMAID = "tool.result.mermaid"
|
|
166
|
+
SUB_AGENT_FOOTER = "sub_agent.footer"
|
|
164
167
|
# BASH SYNTAX
|
|
165
168
|
BASH_COMMAND = "bash.command"
|
|
166
169
|
BASH_ARGUMENT = "bash.argument"
|
|
@@ -197,8 +200,8 @@ class ThemeKey(str, Enum):
|
|
|
197
200
|
CONFIG_STATUS_PRIMARY = "config.status.primary"
|
|
198
201
|
CONFIG_STATUS_ERROR = "config.status.error"
|
|
199
202
|
CONFIG_ITEM_NAME = "config.item.name"
|
|
203
|
+
CONFIG_MODEL_ID = "config.model.id"
|
|
200
204
|
CONFIG_PARAM_LABEL = "config.param.label"
|
|
201
|
-
CONFIG_PANEL_BORDER = "config.panel.border"
|
|
202
205
|
|
|
203
206
|
def __str__(self) -> str:
|
|
204
207
|
return self.value
|
|
@@ -216,6 +219,7 @@ class Themes:
|
|
|
216
219
|
|
|
217
220
|
def get_theme(theme: str | None = None) -> Themes:
|
|
218
221
|
palette = LIGHT_PALETTE if theme == "light" else DARK_PALETTE
|
|
222
|
+
|
|
219
223
|
return Themes(
|
|
220
224
|
app_theme=Theme(
|
|
221
225
|
styles={
|
|
@@ -234,6 +238,7 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
234
238
|
# ERROR
|
|
235
239
|
ThemeKey.ERROR.value: palette.red,
|
|
236
240
|
ThemeKey.ERROR_BOLD.value: "bold " + palette.red,
|
|
241
|
+
ThemeKey.ERROR_DIM.value: "dim " + palette.red,
|
|
237
242
|
ThemeKey.INTERRUPT.value: "reverse bold " + palette.red,
|
|
238
243
|
# USER_INPUT
|
|
239
244
|
ThemeKey.USER_INPUT.value: palette.magenta,
|
|
@@ -263,13 +268,15 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
263
268
|
ThemeKey.TOOL_PARAM.value: palette.green,
|
|
264
269
|
ThemeKey.TOOL_PARAM_BOLD.value: "bold " + palette.green,
|
|
265
270
|
ThemeKey.TOOL_RESULT.value: palette.grey_green,
|
|
271
|
+
ThemeKey.TOOL_RESULT_TREE_PREFIX.value: palette.grey_green + " dim",
|
|
266
272
|
ThemeKey.TOOL_RESULT_BOLD.value: "bold " + palette.grey_green,
|
|
267
|
-
ThemeKey.TOOL_RESULT_TRUNCATED.value: palette.yellow,
|
|
273
|
+
ThemeKey.TOOL_RESULT_TRUNCATED.value: palette.yellow + " dim",
|
|
268
274
|
ThemeKey.TOOL_MARK.value: "bold",
|
|
269
275
|
ThemeKey.TOOL_APPROVED.value: palette.green + " bold reverse",
|
|
270
276
|
ThemeKey.TOOL_REJECTED.value: palette.red + " bold reverse",
|
|
271
277
|
ThemeKey.TOOL_TIMEOUT.value: palette.yellow,
|
|
272
278
|
ThemeKey.TOOL_RESULT_MERMAID: palette.blue + " underline",
|
|
279
|
+
ThemeKey.SUB_AGENT_FOOTER.value: "dim " + palette.grey2,
|
|
273
280
|
# BASH SYNTAX
|
|
274
281
|
ThemeKey.BASH_COMMAND.value: "bold " + palette.green,
|
|
275
282
|
ThemeKey.BASH_ARGUMENT.value: palette.green,
|
|
@@ -300,13 +307,13 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
300
307
|
ThemeKey.RESUME_FLAG.value: "bold reverse " + palette.green,
|
|
301
308
|
ThemeKey.RESUME_INFO.value: palette.green,
|
|
302
309
|
# CONFIGURATION DISPLAY
|
|
303
|
-
ThemeKey.CONFIG_TABLE_HEADER.value: palette.grey1,
|
|
310
|
+
ThemeKey.CONFIG_TABLE_HEADER.value: "bold " + palette.grey1,
|
|
304
311
|
ThemeKey.CONFIG_STATUS_OK.value: palette.green,
|
|
305
|
-
ThemeKey.CONFIG_STATUS_PRIMARY.value: palette.yellow,
|
|
312
|
+
ThemeKey.CONFIG_STATUS_PRIMARY.value: "bold " + palette.yellow,
|
|
306
313
|
ThemeKey.CONFIG_STATUS_ERROR.value: palette.red,
|
|
307
314
|
ThemeKey.CONFIG_ITEM_NAME.value: palette.cyan,
|
|
308
|
-
ThemeKey.
|
|
309
|
-
ThemeKey.
|
|
315
|
+
ThemeKey.CONFIG_MODEL_ID.value: palette.blue,
|
|
316
|
+
ThemeKey.CONFIG_PARAM_LABEL.value: "dim",
|
|
310
317
|
ThemeKey.CONFIG_PROVIDER.value: palette.cyan + " bold",
|
|
311
318
|
}
|
|
312
319
|
),
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import IO
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def print_kitty_image(file_path: str | Path, *, height: int | None = None, file: IO[str] | None = None) -> None:
|
|
9
|
+
"""Print an image to the terminal using Kitty graphics protocol.
|
|
10
|
+
|
|
11
|
+
This intentionally bypasses Rich rendering to avoid interleaving Live refreshes
|
|
12
|
+
with raw escape sequences.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
path = Path(file_path) if isinstance(file_path, str) else file_path
|
|
16
|
+
if not path.exists():
|
|
17
|
+
print(f"Image not found: {path}", file=file or sys.stdout, flush=True)
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
try:
|
|
21
|
+
from term_image.image import KittyImage # type: ignore[import-untyped]
|
|
22
|
+
|
|
23
|
+
KittyImage.forced_support = True # type: ignore[reportUnknownMemberType]
|
|
24
|
+
img = KittyImage.from_file(path) # type: ignore[reportUnknownMemberType]
|
|
25
|
+
if height is not None:
|
|
26
|
+
img.height = height # type: ignore[reportUnknownMemberType]
|
|
27
|
+
|
|
28
|
+
out = file or sys.stdout
|
|
29
|
+
print("", file=out)
|
|
30
|
+
print(str(img), file=out)
|
|
31
|
+
print("", file=out)
|
|
32
|
+
out.flush()
|
|
33
|
+
except Exception:
|
|
34
|
+
print(f"Saved image: {path}", file=file or sys.stdout, flush=True)
|
|
@@ -7,6 +7,7 @@ from dataclasses import dataclass
|
|
|
7
7
|
from enum import Enum
|
|
8
8
|
from typing import TextIO, cast
|
|
9
9
|
|
|
10
|
+
from klaude_code.const import NOTIFY_COMPACT_LIMIT
|
|
10
11
|
from klaude_code.trace import DebugType, log_debug
|
|
11
12
|
|
|
12
13
|
# Environment variable for tmux test signal channel
|
|
@@ -102,7 +103,7 @@ class TerminalNotifier:
|
|
|
102
103
|
return term.lower() not in {"", "dumb"}
|
|
103
104
|
|
|
104
105
|
|
|
105
|
-
def _compact(text: str, limit: int =
|
|
106
|
+
def _compact(text: str, limit: int = NOTIFY_COMPACT_LIMIT) -> str:
|
|
106
107
|
squashed = " ".join(text.split())
|
|
107
108
|
if len(squashed) > limit:
|
|
108
109
|
return squashed[: limit - 3] + "…"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"""
|
|
2
|
-
Use OSC 9;4
|
|
2
|
+
Use OSC 9;4;… to control progress bar in terminal like Ghostty
|
|
3
3
|
States:
|
|
4
4
|
0/hidden
|
|
5
5
|
1/normal
|
|
@@ -71,17 +71,17 @@ if __name__ == "__main__":
|
|
|
71
71
|
# Clear progress bar
|
|
72
72
|
emit_osc94(OSC94States.HIDDEN)
|
|
73
73
|
|
|
74
|
-
print("Waiting
|
|
74
|
+
print("Waiting…")
|
|
75
75
|
# Indeterminate
|
|
76
76
|
emit_osc94(OSC94States.INDETERMINATE)
|
|
77
77
|
|
|
78
78
|
time.sleep(2)
|
|
79
|
-
print("Error
|
|
79
|
+
print("Error…")
|
|
80
80
|
# Error
|
|
81
81
|
emit_osc94(OSC94States.ERROR)
|
|
82
82
|
|
|
83
83
|
time.sleep(2)
|
|
84
|
-
print("Warning
|
|
84
|
+
print("Warning…")
|
|
85
85
|
# Warning
|
|
86
86
|
emit_osc94(OSC94States.WARNING)
|
|
87
87
|
time.sleep(2)
|
|
@@ -88,12 +88,30 @@ def build_model_select_items(models: list[Any]) -> list[SelectItem[str]]:
|
|
|
88
88
|
|
|
89
89
|
|
|
90
90
|
def _restyle_title(title: list[tuple[str, str]], cls: str) -> list[tuple[str, str]]:
|
|
91
|
-
"""Re-apply a style class while keeping
|
|
92
|
-
|
|
91
|
+
"""Re-apply a style class while keeping existing style tokens.
|
|
92
|
+
|
|
93
|
+
This is used to highlight the currently-pointed item. We want to:
|
|
94
|
+
- preserve explicit colors (e.g. `fg:ansibrightblack`) defined by callers
|
|
95
|
+
- preserve existing classes (e.g. `class:msg`, `class:meta`) so their
|
|
96
|
+
non-color attributes remain in effect
|
|
97
|
+
- preserve text attributes like bold/italic/dim
|
|
98
|
+
"""
|
|
99
|
+
|
|
100
|
+
keep_attrs = {"bold", "italic", "underline", "reverse", "blink", "strike", "dim"}
|
|
93
101
|
restyled: list[tuple[str, str]] = []
|
|
94
102
|
for old_style, text in title:
|
|
95
|
-
|
|
96
|
-
|
|
103
|
+
tokens = old_style.split()
|
|
104
|
+
attrs = [tok for tok in tokens if tok in keep_attrs]
|
|
105
|
+
style_tokens = [tok for tok in tokens if tok not in keep_attrs]
|
|
106
|
+
|
|
107
|
+
if cls in style_tokens:
|
|
108
|
+
style_tokens = [tok for tok in style_tokens if tok != cls]
|
|
109
|
+
|
|
110
|
+
# Place the highlight class first, so existing per-token styles (classes
|
|
111
|
+
# or explicit fg/bg) keep their precedence. This prevents highlight from
|
|
112
|
+
# accidentally overriding caller-defined colors.
|
|
113
|
+
combined = [cls, *style_tokens, *attrs]
|
|
114
|
+
style = " ".join(tok for tok in combined if tok)
|
|
97
115
|
restyled.append((style, text))
|
|
98
116
|
return restyled
|
|
99
117
|
|
klaude_code/ui/utils/common.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import re
|
|
2
2
|
import subprocess
|
|
3
3
|
from pathlib import Path
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
if TYPE_CHECKING:
|
|
7
|
+
from klaude_code.protocol.llm_param import LLMConfigModelParameter, OpenRouterProviderRouting
|
|
4
8
|
|
|
5
9
|
LEADING_NEWLINES_REGEX = re.compile(r"^\n{2,}")
|
|
6
10
|
|
|
@@ -88,3 +92,54 @@ def show_path_with_tilde(path: Path | None = None):
|
|
|
88
92
|
return f"~/{relative_path}"
|
|
89
93
|
except ValueError:
|
|
90
94
|
return str(path)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def format_model_params(model_params: "LLMConfigModelParameter") -> list[str]:
|
|
98
|
+
"""Format model parameters in a concise style.
|
|
99
|
+
|
|
100
|
+
Returns a list of formatted parameter strings like:
|
|
101
|
+
- "reasoning medium"
|
|
102
|
+
- "thinking budget 10000"
|
|
103
|
+
- "verbosity 2"
|
|
104
|
+
- "provider-routing: {…}"
|
|
105
|
+
"""
|
|
106
|
+
parts: list[str] = []
|
|
107
|
+
|
|
108
|
+
if model_params.thinking:
|
|
109
|
+
if model_params.thinking.reasoning_effort:
|
|
110
|
+
parts.append(f"reasoning {model_params.thinking.reasoning_effort}")
|
|
111
|
+
if model_params.thinking.reasoning_summary:
|
|
112
|
+
parts.append(f"summary {model_params.thinking.reasoning_summary}")
|
|
113
|
+
if model_params.thinking.budget_tokens:
|
|
114
|
+
parts.append(f"thinking budget {model_params.thinking.budget_tokens}")
|
|
115
|
+
|
|
116
|
+
if model_params.verbosity:
|
|
117
|
+
parts.append(f"verbosity {model_params.verbosity}")
|
|
118
|
+
|
|
119
|
+
if model_params.provider_routing:
|
|
120
|
+
parts.append(f"provider routing {_format_provider_routing(model_params.provider_routing)}")
|
|
121
|
+
|
|
122
|
+
if model_params.modalities:
|
|
123
|
+
parts.append(f"modalities {','.join(model_params.modalities)}")
|
|
124
|
+
|
|
125
|
+
if model_params.image_config:
|
|
126
|
+
if model_params.image_config.aspect_ratio:
|
|
127
|
+
parts.append(f"image aspect {model_params.image_config.aspect_ratio}")
|
|
128
|
+
if model_params.image_config.image_size:
|
|
129
|
+
parts.append(f"image size {model_params.image_config.image_size}")
|
|
130
|
+
|
|
131
|
+
return parts
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _format_provider_routing(pr: "OpenRouterProviderRouting") -> str:
|
|
135
|
+
"""Format provider routing settings concisely."""
|
|
136
|
+
items: list[str] = []
|
|
137
|
+
if pr.sort:
|
|
138
|
+
items.append(pr.sort)
|
|
139
|
+
if pr.only:
|
|
140
|
+
items.append(">".join(pr.only))
|
|
141
|
+
if pr.order:
|
|
142
|
+
items.append(">".join(pr.order))
|
|
143
|
+
if pr.ignore:
|
|
144
|
+
items.append(f"ignore {'>'.join(pr.ignore)}")
|
|
145
|
+
return " · ".join(items) if items else ""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: klaude-code
|
|
3
|
-
Version:
|
|
3
|
+
Version: 2.0.0
|
|
4
4
|
Summary: Minimal code agent CLI
|
|
5
5
|
Requires-Dist: anthropic>=0.66.0
|
|
6
6
|
Requires-Dist: chardet>=5.2.0
|
|
@@ -9,11 +9,12 @@ Requires-Dist: diff-match-patch>=20241021
|
|
|
9
9
|
Requires-Dist: google-genai>=1.56.0
|
|
10
10
|
Requires-Dist: markdown-it-py>=4.0.0
|
|
11
11
|
Requires-Dist: openai>=1.102.0
|
|
12
|
-
Requires-Dist: pillow>=
|
|
12
|
+
Requires-Dist: pillow>=9.1,<11.0
|
|
13
13
|
Requires-Dist: prompt-toolkit>=3.0.52
|
|
14
14
|
Requires-Dist: pydantic>=2.11.7
|
|
15
15
|
Requires-Dist: pyyaml>=6.0.2
|
|
16
16
|
Requires-Dist: rich>=14.1.0
|
|
17
|
+
Requires-Dist: term-image>=0.7.2
|
|
17
18
|
Requires-Dist: trafilatura>=2.0.0
|
|
18
19
|
Requires-Dist: typer>=0.17.3
|
|
19
20
|
Requires-Python: >=3.13
|
|
@@ -120,11 +121,12 @@ On first run, you'll be prompted to select a model. Your choice is saved as `mai
|
|
|
120
121
|
| Provider | Env Variable | Models |
|
|
121
122
|
|-------------|-----------------------|-------------------------------------------------------------------------------|
|
|
122
123
|
| anthropic | `ANTHROPIC_API_KEY` | sonnet, opus |
|
|
124
|
+
| claude | N/A (OAuth) | sonnet@claude, opus@claude (requires Claude Pro/Max subscription) |
|
|
123
125
|
| openai | `OPENAI_API_KEY` | gpt-5.2 |
|
|
124
126
|
| openrouter | `OPENROUTER_API_KEY` | gpt-5.2, gpt-5.2-fast, gpt-5.1-codex-max, sonnet, opus, haiku, kimi, gemini-* |
|
|
125
127
|
| deepseek | `DEEPSEEK_API_KEY` | deepseek |
|
|
126
128
|
| moonshot | `MOONSHOT_API_KEY` | kimi@moonshot |
|
|
127
|
-
| codex | N/A (OAuth) | gpt-5.2-codex
|
|
129
|
+
| codex | N/A (OAuth) | gpt-5.2-codex (requires ChatGPT Pro subscription) |
|
|
128
130
|
|
|
129
131
|
List all configured providers and models:
|
|
130
132
|
|
|
@@ -134,6 +136,26 @@ klaude list
|
|
|
134
136
|
|
|
135
137
|
Models from providers without a valid API key are shown as dimmed/unavailable.
|
|
136
138
|
|
|
139
|
+
#### OAuth Login
|
|
140
|
+
|
|
141
|
+
For subscription-based providers (Claude Pro/Max, ChatGPT Pro), use the login command:
|
|
142
|
+
|
|
143
|
+
```bash
|
|
144
|
+
# Interactive provider selection
|
|
145
|
+
klaude login
|
|
146
|
+
|
|
147
|
+
# Or specify provider directly
|
|
148
|
+
klaude login claude # Claude Pro/Max subscription
|
|
149
|
+
klaude login codex # ChatGPT Pro subscription
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
To logout:
|
|
153
|
+
|
|
154
|
+
```bash
|
|
155
|
+
klaude logout claude
|
|
156
|
+
klaude logout codex
|
|
157
|
+
```
|
|
158
|
+
|
|
137
159
|
#### Custom Configuration
|
|
138
160
|
|
|
139
161
|
User config file: `~/.klaude/klaude-config.yaml`
|
|
@@ -240,7 +262,6 @@ provider_list:
|
|
|
240
262
|
main_model: opus
|
|
241
263
|
|
|
242
264
|
sub_agent_models:
|
|
243
|
-
oracle: gpt-4.1
|
|
244
265
|
explore: sonnet
|
|
245
266
|
task: opus
|
|
246
267
|
webagent: sonnet
|
|
@@ -269,12 +290,13 @@ provider_list:
|
|
|
269
290
|
##### Supported Protocols
|
|
270
291
|
|
|
271
292
|
- `anthropic` - Anthropic Claude API
|
|
293
|
+
- `claude_oauth` - Claude OAuth (for Claude Pro/Max subscribers)
|
|
272
294
|
- `openai` - OpenAI-compatible API
|
|
273
295
|
- `responses` - OpenAI Responses API (for o-series, GPT-5, Codex)
|
|
274
296
|
- `openrouter` - OpenRouter API
|
|
275
297
|
- `google` - Google Gemini API
|
|
276
298
|
- `bedrock` - AWS Bedrock (uses AWS credentials instead of api_key)
|
|
277
|
-
- `
|
|
299
|
+
- `codex_oauth` - OpenAI Codex CLI (OAuth-based, for ChatGPT Pro subscribers)
|
|
278
300
|
|
|
279
301
|
List configured providers and models:
|
|
280
302
|
|
|
@@ -374,4 +396,4 @@ The main agent can spawn specialized sub-agents for specific tasks:
|
|
|
374
396
|
| **Explore** | Fast codebase exploration - find files, search code, answer questions about the codebase |
|
|
375
397
|
| **Task** | Handle complex multi-step tasks autonomously |
|
|
376
398
|
| **WebAgent** | Search the web, fetch pages, and analyze content |
|
|
377
|
-
| **
|
|
399
|
+
| **ImageGen** | Generate images from text prompts via OpenRouter Nano Banana Pro |
|