klaude-code 1.2.15__py3-none-any.whl → 1.2.16__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 +66 -42
- klaude_code/cli/runtime.py +24 -13
- klaude_code/command/export_cmd.py +2 -2
- klaude_code/command/prompt-handoff.md +33 -0
- klaude_code/command/thinking_cmd.py +5 -1
- klaude_code/config/config.py +5 -5
- klaude_code/config/list_model.py +1 -1
- klaude_code/const/__init__.py +3 -0
- klaude_code/core/executor.py +2 -2
- klaude_code/core/manager/llm_clients_builder.py +1 -1
- klaude_code/core/manager/sub_agent_manager.py +30 -6
- klaude_code/core/prompt.py +15 -13
- klaude_code/core/prompts/{prompt-subagent-explore.md → prompt-sub-agent-explore.md} +0 -1
- klaude_code/core/prompts/{prompt-subagent-oracle.md → prompt-sub-agent-oracle.md} +1 -1
- klaude_code/core/reminders.py +75 -32
- klaude_code/core/task.py +10 -22
- klaude_code/core/tool/__init__.py +2 -0
- klaude_code/core/tool/report_back_tool.py +58 -0
- klaude_code/core/tool/sub_agent_tool.py +6 -0
- klaude_code/core/tool/tool_runner.py +9 -1
- klaude_code/core/turn.py +45 -4
- klaude_code/llm/anthropic/input.py +14 -5
- klaude_code/llm/openrouter/input.py +14 -3
- klaude_code/llm/responses/input.py +19 -0
- klaude_code/protocol/events.py +1 -0
- klaude_code/protocol/model.py +24 -14
- klaude_code/protocol/sub_agent/__init__.py +117 -0
- klaude_code/protocol/sub_agent/explore.py +63 -0
- klaude_code/protocol/sub_agent/oracle.py +91 -0
- klaude_code/protocol/sub_agent/task.py +61 -0
- klaude_code/protocol/sub_agent/web_fetch.py +74 -0
- klaude_code/protocol/tools.py +1 -0
- klaude_code/session/export.py +12 -6
- klaude_code/session/session.py +12 -2
- klaude_code/session/templates/export_session.html +12 -12
- klaude_code/ui/modes/repl/completers.py +1 -1
- klaude_code/ui/modes/repl/event_handler.py +32 -2
- klaude_code/ui/modes/repl/renderer.py +8 -6
- klaude_code/ui/renderers/developer.py +18 -7
- klaude_code/ui/renderers/metadata.py +24 -12
- klaude_code/ui/renderers/sub_agent.py +59 -3
- klaude_code/ui/renderers/thinking.py +1 -1
- klaude_code/ui/renderers/tools.py +22 -29
- klaude_code/ui/rich/markdown.py +20 -48
- klaude_code/ui/rich/status.py +32 -14
- klaude_code/ui/rich/theme.py +8 -7
- {klaude_code-1.2.15.dist-info → klaude_code-1.2.16.dist-info}/METADATA +3 -2
- {klaude_code-1.2.15.dist-info → klaude_code-1.2.16.dist-info}/RECORD +52 -46
- klaude_code/protocol/sub_agent.py +0 -354
- /klaude_code/core/prompts/{prompt-subagent-webfetch.md → prompt-sub-agent-webfetch.md} +0 -0
- /klaude_code/core/prompts/{prompt-subagent.md → prompt-sub-agent.md} +0 -0
- {klaude_code-1.2.15.dist-info → klaude_code-1.2.16.dist-info}/WHEEL +0 -0
- {klaude_code-1.2.15.dist-info → klaude_code-1.2.16.dist-info}/entry_points.txt +0 -0
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import json
|
|
2
|
+
from typing import Any, cast
|
|
2
3
|
|
|
3
4
|
from rich.console import Group, RenderableType
|
|
5
|
+
from rich.json import JSON
|
|
4
6
|
from rich.panel import Panel
|
|
5
7
|
from rich.style import Style
|
|
6
8
|
from rich.text import Text
|
|
@@ -12,20 +14,67 @@ from klaude_code.ui.rich.markdown import NoInsetMarkdown
|
|
|
12
14
|
from klaude_code.ui.rich.theme import ThemeKey
|
|
13
15
|
|
|
14
16
|
|
|
17
|
+
def _compact_schema_value(value: dict[str, Any]) -> str | list[Any] | dict[str, Any]:
|
|
18
|
+
"""Convert a JSON Schema value to compact representation."""
|
|
19
|
+
value_type = value.get("type", "any")
|
|
20
|
+
desc = value.get("description", "")
|
|
21
|
+
|
|
22
|
+
if value_type == "object":
|
|
23
|
+
props = value.get("properties", {})
|
|
24
|
+
return {k: _compact_schema_value(v) for k, v in props.items()}
|
|
25
|
+
elif value_type == "array":
|
|
26
|
+
items = value.get("items", {})
|
|
27
|
+
# If items have no description, use the array's description
|
|
28
|
+
if desc and not items.get("description"):
|
|
29
|
+
items = {**items, "description": desc}
|
|
30
|
+
return [_compact_schema_value(items)]
|
|
31
|
+
else:
|
|
32
|
+
if desc:
|
|
33
|
+
return f"{value_type} // {desc}"
|
|
34
|
+
return value_type
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _compact_schema(schema: dict[str, Any]) -> dict[str, Any] | list[Any] | str:
|
|
38
|
+
"""Convert JSON Schema to compact representation for display."""
|
|
39
|
+
return _compact_schema_value(schema)
|
|
40
|
+
|
|
41
|
+
|
|
15
42
|
def render_sub_agent_call(e: model.SubAgentState, style: Style | None = None) -> RenderableType:
|
|
16
43
|
"""Render sub-agent tool call header and prompt body."""
|
|
17
44
|
desc = Text(
|
|
18
45
|
f" {e.sub_agent_desc} ",
|
|
19
46
|
style=Style(color=style.color if style else None, bold=True, reverse=True),
|
|
20
47
|
)
|
|
21
|
-
|
|
48
|
+
elements: list[RenderableType] = [
|
|
22
49
|
Text.assemble((e.sub_agent_type, ThemeKey.TOOL_NAME), " ", desc),
|
|
23
50
|
Text(e.sub_agent_prompt, style=style or ""),
|
|
24
|
-
|
|
51
|
+
]
|
|
52
|
+
if e.output_schema:
|
|
53
|
+
elements.append(Text("\nExpected Output Format JSON:", style=style or ""))
|
|
54
|
+
compact = _compact_schema(e.output_schema)
|
|
55
|
+
schema_text = json.dumps(compact, ensure_ascii=False, indent=2)
|
|
56
|
+
elements.append(JSON(schema_text))
|
|
57
|
+
return Group(*elements)
|
|
25
58
|
|
|
26
59
|
|
|
27
|
-
def render_sub_agent_result(
|
|
60
|
+
def render_sub_agent_result(
|
|
61
|
+
result: str, *, code_theme: str, style: Style | None = None, has_structured_output: bool = False
|
|
62
|
+
) -> RenderableType:
|
|
28
63
|
stripped_result = result.strip()
|
|
64
|
+
|
|
65
|
+
# Use rich JSON for structured output
|
|
66
|
+
if has_structured_output:
|
|
67
|
+
return Panel.fit(
|
|
68
|
+
Group(
|
|
69
|
+
Text(
|
|
70
|
+
"use /export to view full output",
|
|
71
|
+
style=ThemeKey.TOOL_RESULT,
|
|
72
|
+
),
|
|
73
|
+
JSON(stripped_result),
|
|
74
|
+
),
|
|
75
|
+
border_style=ThemeKey.LINES,
|
|
76
|
+
)
|
|
77
|
+
|
|
29
78
|
lines = stripped_result.splitlines()
|
|
30
79
|
if len(lines) > const.SUB_AGENT_RESULT_MAX_LINES:
|
|
31
80
|
hidden_count = len(lines) - const.SUB_AGENT_RESULT_MAX_LINES
|
|
@@ -53,6 +102,7 @@ def build_sub_agent_state_from_tool_call(e: events.ToolCallEvent) -> model.SubAg
|
|
|
53
102
|
return None
|
|
54
103
|
description = profile.name
|
|
55
104
|
prompt = ""
|
|
105
|
+
output_schema: dict[str, Any] | None = None
|
|
56
106
|
if e.arguments:
|
|
57
107
|
try:
|
|
58
108
|
payload: dict[str, object] = json.loads(e.arguments)
|
|
@@ -64,8 +114,14 @@ def build_sub_agent_state_from_tool_call(e: events.ToolCallEvent) -> model.SubAg
|
|
|
64
114
|
prompt_value = payload.get("prompt") or payload.get("task")
|
|
65
115
|
if isinstance(prompt_value, str):
|
|
66
116
|
prompt = prompt_value.strip()
|
|
117
|
+
# Extract output_schema if profile supports it
|
|
118
|
+
if profile.output_schema_arg:
|
|
119
|
+
schema_value = payload.get(profile.output_schema_arg)
|
|
120
|
+
if isinstance(schema_value, dict):
|
|
121
|
+
output_schema = cast(dict[str, Any], schema_value)
|
|
67
122
|
return model.SubAgentState(
|
|
68
123
|
sub_agent_type=profile.name,
|
|
69
124
|
sub_agent_desc=description,
|
|
70
125
|
sub_agent_prompt=prompt,
|
|
126
|
+
output_schema=output_schema,
|
|
71
127
|
)
|
|
@@ -16,7 +16,7 @@ def _normalize_thinking_content(content: str) -> str:
|
|
|
16
16
|
content.rstrip()
|
|
17
17
|
.replace("**\n\n", "** \n")
|
|
18
18
|
.replace("\\n\\n\n\n", "") # Weird case of Gemini 3
|
|
19
|
-
.replace("****", "**\n\n**") #
|
|
19
|
+
.replace("****", "**\n\n**") # Remove extra newlines after bold titles
|
|
20
20
|
)
|
|
21
21
|
|
|
22
22
|
|
|
@@ -7,7 +7,7 @@ from rich.padding import Padding
|
|
|
7
7
|
from rich.text import Text
|
|
8
8
|
|
|
9
9
|
from klaude_code import const
|
|
10
|
-
from klaude_code.protocol import events, model
|
|
10
|
+
from klaude_code.protocol import events, model, tools
|
|
11
11
|
from klaude_code.protocol.sub_agent import is_sub_agent_tool as _is_sub_agent_tool
|
|
12
12
|
from klaude_code.ui.renderers import diffs as r_diffs
|
|
13
13
|
from klaude_code.ui.renderers.common import create_grid
|
|
@@ -435,35 +435,28 @@ def get_truncation_info(tr: events.ToolResultEvent) -> model.TruncationUIExtra |
|
|
|
435
435
|
return _extract_truncation(tr.ui_extra)
|
|
436
436
|
|
|
437
437
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
"
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
"Bash": ">",
|
|
445
|
-
"apply_patch": "→",
|
|
446
|
-
"TodoWrite": "◎",
|
|
447
|
-
"update_plan": "◎",
|
|
448
|
-
"Mermaid": "⧉",
|
|
449
|
-
"Memory": "★",
|
|
450
|
-
"Skill": "◈",
|
|
451
|
-
}
|
|
438
|
+
def render_report_back_tool_call() -> RenderableType:
|
|
439
|
+
grid = create_grid()
|
|
440
|
+
tool_name_column = Text.assemble(("✔", ThemeKey.TOOL_MARK), " ", ("Report Back", ThemeKey.TOOL_NAME))
|
|
441
|
+
grid.add_row(tool_name_column, "")
|
|
442
|
+
return grid
|
|
443
|
+
|
|
452
444
|
|
|
453
445
|
# Tool name to active form mapping (for spinner status)
|
|
454
446
|
_TOOL_ACTIVE_FORM: dict[str, str] = {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
447
|
+
tools.BASH: "Bashing",
|
|
448
|
+
tools.APPLY_PATCH: "Patching",
|
|
449
|
+
tools.EDIT: "Editing",
|
|
450
|
+
tools.MULTI_EDIT: "Editing",
|
|
451
|
+
tools.READ: "Reading",
|
|
452
|
+
tools.WRITE: "Writing",
|
|
453
|
+
tools.TODO_WRITE: "Planning",
|
|
454
|
+
tools.UPDATE_PLAN: "Planning",
|
|
455
|
+
tools.SKILL: "Skilling",
|
|
456
|
+
tools.MERMAID: "Diagramming",
|
|
457
|
+
tools.MEMORY: "Memorizing",
|
|
458
|
+
tools.WEB_FETCH: "Fetching",
|
|
459
|
+
tools.REPORT_BACK: "Submitting Report",
|
|
467
460
|
}
|
|
468
461
|
|
|
469
462
|
|
|
@@ -490,7 +483,6 @@ def render_tool_call(e: events.ToolCallEvent) -> RenderableType | None:
|
|
|
490
483
|
|
|
491
484
|
Returns a Rich Renderable or None if the tool call should not be rendered.
|
|
492
485
|
"""
|
|
493
|
-
from klaude_code.protocol import tools
|
|
494
486
|
|
|
495
487
|
if is_sub_agent_tool(e.tool_name):
|
|
496
488
|
return None
|
|
@@ -518,6 +510,8 @@ def render_tool_call(e: events.ToolCallEvent) -> RenderableType | None:
|
|
|
518
510
|
return render_memory_tool_call(e.arguments)
|
|
519
511
|
case tools.SKILL:
|
|
520
512
|
return render_generic_tool_call(e.tool_name, e.arguments, "◈")
|
|
513
|
+
case tools.REPORT_BACK:
|
|
514
|
+
return render_report_back_tool_call()
|
|
521
515
|
case _:
|
|
522
516
|
return render_generic_tool_call(e.tool_name, e.arguments)
|
|
523
517
|
|
|
@@ -533,7 +527,6 @@ def render_tool_result(e: events.ToolResultEvent) -> RenderableType | None:
|
|
|
533
527
|
|
|
534
528
|
Returns a Rich Renderable or None if the tool result should not be rendered.
|
|
535
529
|
"""
|
|
536
|
-
from klaude_code.protocol import tools
|
|
537
530
|
from klaude_code.ui.renderers import errors as r_errors
|
|
538
531
|
|
|
539
532
|
if is_sub_agent_tool(e.tool_name):
|
klaude_code/ui/rich/markdown.py
CHANGED
|
@@ -32,9 +32,9 @@ class NoInsetCodeBlock(CodeBlock):
|
|
|
32
32
|
self.lexer_name,
|
|
33
33
|
theme=self.theme,
|
|
34
34
|
word_wrap=True,
|
|
35
|
-
padding=(0,
|
|
35
|
+
padding=(0, 0),
|
|
36
36
|
)
|
|
37
|
-
yield Panel.fit(syntax, padding=(0, 0),
|
|
37
|
+
yield Panel.fit(syntax, padding=(0, 0), box=box.HORIZONTALS, border_style="markdown.code.border")
|
|
38
38
|
|
|
39
39
|
|
|
40
40
|
class ThinkingCodeBlock(CodeBlock):
|
|
@@ -42,7 +42,7 @@ class ThinkingCodeBlock(CodeBlock):
|
|
|
42
42
|
|
|
43
43
|
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
44
44
|
code = str(self.text).rstrip()
|
|
45
|
-
text = Text(
|
|
45
|
+
text = Text(code, "markdown.code.block")
|
|
46
46
|
yield text
|
|
47
47
|
|
|
48
48
|
|
|
@@ -52,7 +52,10 @@ class LeftHeading(Heading):
|
|
|
52
52
|
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
53
53
|
text = self.text
|
|
54
54
|
text.justify = "left" # Override justification
|
|
55
|
-
if self.tag == "
|
|
55
|
+
if self.tag == "h1":
|
|
56
|
+
h1_text = text.assemble((" ", "markdown.h1"), text, (" ", "markdown.h1"))
|
|
57
|
+
yield h1_text
|
|
58
|
+
elif self.tag == "h2":
|
|
56
59
|
text.stylize(Style(bold=True, underline=False))
|
|
57
60
|
yield Rule(title=text, characters="-", style="markdown.h2.border", align="left")
|
|
58
61
|
else:
|
|
@@ -123,10 +126,6 @@ class MarkdownStream:
|
|
|
123
126
|
self.when: float = 0.0 # Timestamp of last update
|
|
124
127
|
self.min_delay: float = 1.0 / 20 # Minimum time between updates (20fps)
|
|
125
128
|
self.live_window: int = const.MARKDOWN_STREAM_LIVE_WINDOW
|
|
126
|
-
# Track the maximum height the live window has ever reached
|
|
127
|
-
# so we only pad when it shrinks from a previous height,
|
|
128
|
-
# instead of always padding to live_window from the start.
|
|
129
|
-
self._live_window_seen_height: int = 0
|
|
130
129
|
|
|
131
130
|
self.theme = theme
|
|
132
131
|
self.console = console
|
|
@@ -221,18 +220,16 @@ class MarkdownStream:
|
|
|
221
220
|
Markdown going to the console works better in terminal scrollback buffers.
|
|
222
221
|
The live window doesn't play nice with terminal scrollback.
|
|
223
222
|
"""
|
|
224
|
-
# On the first call, start the Live renderer
|
|
225
223
|
if not self._live_started:
|
|
226
224
|
initial_content = self._live_renderable(Text(""), final=False)
|
|
225
|
+
# transient=False keeps final frame on screen after stop()
|
|
227
226
|
self.live = Live(
|
|
228
227
|
initial_content,
|
|
229
228
|
refresh_per_second=1.0 / self.min_delay,
|
|
230
229
|
console=self.console,
|
|
231
230
|
)
|
|
232
231
|
self.live.start()
|
|
233
|
-
# Note: self._live_started is now a property derived from self.live
|
|
234
232
|
|
|
235
|
-
# If live rendering isn't available (e.g., after a final update), stop.
|
|
236
233
|
if self.live is None:
|
|
237
234
|
return
|
|
238
235
|
|
|
@@ -252,61 +249,36 @@ class MarkdownStream:
|
|
|
252
249
|
|
|
253
250
|
num_lines = len(lines)
|
|
254
251
|
|
|
255
|
-
#
|
|
256
|
-
|
|
257
|
-
if not final:
|
|
258
|
-
num_lines = max(num_lines - self.live_window, 0)
|
|
252
|
+
# Reserve last live_window lines for Live area to keep height stable
|
|
253
|
+
num_lines = max(num_lines - self.live_window, 0)
|
|
259
254
|
|
|
260
|
-
#
|
|
261
|
-
|
|
262
|
-
if final or num_lines > 0:
|
|
263
|
-
# Lines to append to stable area
|
|
255
|
+
# Print new stable lines above Live window
|
|
256
|
+
if num_lines > 0:
|
|
264
257
|
num_printed = len(self.printed)
|
|
265
258
|
to_append_count = num_lines - num_printed
|
|
266
259
|
|
|
267
260
|
if to_append_count > 0:
|
|
268
|
-
# Print new stable lines above Live window
|
|
269
261
|
append_chunk = lines[num_printed:num_lines]
|
|
270
262
|
append_chunk_text = Text.from_ansi("".join(append_chunk))
|
|
271
263
|
live = self.live
|
|
272
264
|
assert live is not None
|
|
273
|
-
live.console.print(append_chunk_text)
|
|
274
|
-
|
|
275
|
-
# Track printed stable lines
|
|
265
|
+
live.console.print(append_chunk_text)
|
|
276
266
|
self.printed = lines[:num_lines]
|
|
277
267
|
|
|
278
|
-
|
|
268
|
+
rest_lines = lines[num_lines:]
|
|
269
|
+
|
|
270
|
+
# Final: render remaining lines without spinner, then stop Live
|
|
279
271
|
if final:
|
|
280
272
|
live = self.live
|
|
281
273
|
assert live is not None
|
|
282
|
-
|
|
274
|
+
rest = "".join(rest_lines)
|
|
275
|
+
rest_text = Text.from_ansi(rest)
|
|
276
|
+
final_renderable = self._live_renderable(rest_text, final=True)
|
|
277
|
+
live.update(final_renderable)
|
|
283
278
|
live.stop()
|
|
284
279
|
self.live = None
|
|
285
280
|
return
|
|
286
281
|
|
|
287
|
-
# Update Live window to prevent timing issues
|
|
288
|
-
# with console.print above. We pad the live region
|
|
289
|
-
# so that its height stays stable when it shrinks
|
|
290
|
-
# from a previously reached height, avoiding spinner jitter.
|
|
291
|
-
rest_lines = lines[num_lines:]
|
|
292
|
-
|
|
293
|
-
if not final:
|
|
294
|
-
current_height = len(rest_lines)
|
|
295
|
-
|
|
296
|
-
# Update the maximum height we've seen so far for this live window.
|
|
297
|
-
if current_height > self._live_window_seen_height:
|
|
298
|
-
# Never exceed configured live_window, even if logic changes later.
|
|
299
|
-
self._live_window_seen_height = min(current_height, self.live_window)
|
|
300
|
-
|
|
301
|
-
target_height = min(self._live_window_seen_height, self.live_window)
|
|
302
|
-
if target_height > 0 and current_height < target_height:
|
|
303
|
-
# Pad only up to the maximum height we've seen so far.
|
|
304
|
-
# This keeps the Live region height stable without overshooting,
|
|
305
|
-
# which can cause the spinner to jump by a line.
|
|
306
|
-
pad_count = target_height - current_height
|
|
307
|
-
# Pad after the existing lines so spinner visually stays at the bottom.
|
|
308
|
-
rest_lines = rest_lines + ["\n"] * pad_count
|
|
309
|
-
|
|
310
282
|
rest = "".join(rest_lines)
|
|
311
283
|
rest = Text.from_ansi(rest)
|
|
312
284
|
live = self.live
|
klaude_code/ui/rich/status.py
CHANGED
|
@@ -2,10 +2,10 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import contextlib
|
|
4
4
|
import math
|
|
5
|
+
import random
|
|
5
6
|
import time
|
|
6
7
|
|
|
7
8
|
import rich.status as rich_status
|
|
8
|
-
from rich._spinners import SPINNERS
|
|
9
9
|
from rich.color import Color
|
|
10
10
|
from rich.console import Console, ConsoleOptions, RenderableType, RenderResult
|
|
11
11
|
from rich.spinner import Spinner as RichSpinner
|
|
@@ -17,18 +17,25 @@ from klaude_code import const
|
|
|
17
17
|
from klaude_code.ui.rich.theme import ThemeKey
|
|
18
18
|
from klaude_code.ui.terminal.color import get_last_terminal_background_rgb
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
# Use an existing Rich spinner name; BreathingSpinner overrides its rendering
|
|
21
|
+
BREATHING_SPINNER_NAME = "dots"
|
|
22
|
+
|
|
23
|
+
# Alternating glyphs for the breathing spinner - switches at each "transparent" point
|
|
24
|
+
# All glyphs are center-symmetric (point-symmetric)
|
|
25
|
+
_BREATHING_SPINNER_GLYPHS_BASE = [
|
|
26
|
+
# Stars
|
|
27
|
+
"✦",
|
|
28
|
+
"✶",
|
|
29
|
+
"✲",
|
|
30
|
+
"⏺",
|
|
31
|
+
"◆",
|
|
32
|
+
"❖",
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
# Shuffle glyphs on module load for variety across sessions
|
|
36
|
+
BREATHING_SPINNER_GLYPHS = _BREATHING_SPINNER_GLYPHS_BASE.copy()
|
|
37
|
+
random.shuffle(BREATHING_SPINNER_GLYPHS)
|
|
21
38
|
|
|
22
|
-
SPINNERS.update(
|
|
23
|
-
{
|
|
24
|
-
BREATHING_SPINNER_NAME: {
|
|
25
|
-
"interval": 100,
|
|
26
|
-
# Frames content is ignored by the custom breathing spinner implementation,
|
|
27
|
-
# but we keep a single-frame list for correct width measurement.
|
|
28
|
-
"frames": ["⏺"],
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
)
|
|
32
39
|
|
|
33
40
|
_process_start: float | None = None
|
|
34
41
|
|
|
@@ -126,6 +133,17 @@ def _breathing_intensity() -> float:
|
|
|
126
133
|
return 0.5 * (1.0 - math.cos(2.0 * math.pi * phase))
|
|
127
134
|
|
|
128
135
|
|
|
136
|
+
def _breathing_glyph() -> str:
|
|
137
|
+
"""Get the current glyph for the breathing spinner.
|
|
138
|
+
|
|
139
|
+
Alternates between glyphs at each breath cycle (when intensity reaches 0).
|
|
140
|
+
"""
|
|
141
|
+
period = max(const.SPINNER_BREATH_PERIOD_SECONDS, 0.1)
|
|
142
|
+
elapsed = _elapsed_since_start()
|
|
143
|
+
cycle = int(elapsed / period)
|
|
144
|
+
return BREATHING_SPINNER_GLYPHS[cycle % len(BREATHING_SPINNER_GLYPHS)]
|
|
145
|
+
|
|
146
|
+
|
|
129
147
|
def _breathing_style(console: Console, base_style: Style, intensity: float) -> Style:
|
|
130
148
|
"""Blend a base style's foreground color toward terminal background.
|
|
131
149
|
|
|
@@ -159,7 +177,7 @@ class ShimmerStatusText:
|
|
|
159
177
|
def __init__(self, main_text: str | Text, main_style: ThemeKey) -> None:
|
|
160
178
|
self._main_text = main_text if isinstance(main_text, Text) else Text(main_text)
|
|
161
179
|
self._main_style = main_style
|
|
162
|
-
self._hint_text = Text(
|
|
180
|
+
self._hint_text = Text(const.STATUS_HINT_TEXT)
|
|
163
181
|
self._hint_style = ThemeKey.STATUS_HINT
|
|
164
182
|
|
|
165
183
|
def __rich_console__(self, console: Console, options: ConsoleOptions) -> RenderResult:
|
|
@@ -219,7 +237,7 @@ class BreathingSpinner(RichSpinner):
|
|
|
219
237
|
intensity = _breathing_intensity()
|
|
220
238
|
style = _breathing_style(console, base_style, intensity)
|
|
221
239
|
|
|
222
|
-
glyph =
|
|
240
|
+
glyph = _breathing_glyph()
|
|
223
241
|
frame = Text(glyph, style=style)
|
|
224
242
|
|
|
225
243
|
if not self.text:
|
klaude_code/ui/rich/theme.py
CHANGED
|
@@ -29,7 +29,7 @@ class Palette:
|
|
|
29
29
|
LIGHT_PALETTE = Palette(
|
|
30
30
|
red="red",
|
|
31
31
|
yellow="yellow",
|
|
32
|
-
green="
|
|
32
|
+
green="#00875f",
|
|
33
33
|
cyan="cyan",
|
|
34
34
|
blue="#3078C5",
|
|
35
35
|
orange="#d77757",
|
|
@@ -38,8 +38,8 @@ LIGHT_PALETTE = Palette(
|
|
|
38
38
|
grey2="#93a4b1",
|
|
39
39
|
grey3="#c4ced4",
|
|
40
40
|
grey_green="#96a096",
|
|
41
|
-
purple="
|
|
42
|
-
lavender="
|
|
41
|
+
purple="#5f5fd7",
|
|
42
|
+
lavender="#5f87af",
|
|
43
43
|
diff_add="#2e5a32 on #e8f5e9",
|
|
44
44
|
diff_remove="#5a2e32 on #ffebee",
|
|
45
45
|
code_theme="ansi_light",
|
|
@@ -47,11 +47,11 @@ LIGHT_PALETTE = Palette(
|
|
|
47
47
|
)
|
|
48
48
|
|
|
49
49
|
DARK_PALETTE = Palette(
|
|
50
|
-
red="
|
|
50
|
+
red="#d75f5f",
|
|
51
51
|
yellow="yellow",
|
|
52
|
-
green="
|
|
52
|
+
green="#5fd787",
|
|
53
53
|
cyan="cyan",
|
|
54
|
-
blue="
|
|
54
|
+
blue="#00afff",
|
|
55
55
|
orange="#e6704e",
|
|
56
56
|
magenta="magenta",
|
|
57
57
|
grey1="#99aabb",
|
|
@@ -234,7 +234,7 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
234
234
|
markdown_theme=Theme(
|
|
235
235
|
styles={
|
|
236
236
|
"markdown.code": palette.purple,
|
|
237
|
-
"markdown.code.
|
|
237
|
+
"markdown.code.border": palette.grey3,
|
|
238
238
|
"markdown.h1": "bold reverse",
|
|
239
239
|
"markdown.h1.border": palette.grey3,
|
|
240
240
|
"markdown.h2.border": palette.grey3,
|
|
@@ -256,6 +256,7 @@ def get_theme(theme: str | None = None) -> Themes:
|
|
|
256
256
|
"markdown.hr": palette.grey3,
|
|
257
257
|
"markdown.item.bullet": palette.grey2,
|
|
258
258
|
"markdown.item.number": palette.grey2,
|
|
259
|
+
"markdown.code.block": palette.grey1,
|
|
259
260
|
"markdown.strong": "bold italic " + palette.grey1,
|
|
260
261
|
}
|
|
261
262
|
),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: klaude-code
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.16
|
|
4
4
|
Summary: Add your description here
|
|
5
5
|
Requires-Dist: anthropic>=0.66.0
|
|
6
6
|
Requires-Dist: openai>=1.102.0
|
|
@@ -22,6 +22,7 @@ An minimal and opinionated code agent with multi-model support.
|
|
|
22
22
|
## Key Features
|
|
23
23
|
- **Adaptive Tooling**: Model-aware toolsets (Claude Code tools for Sonnet, Codex `apply_patch` for GPT-5.1/Codex).
|
|
24
24
|
- **Multi-Provider Support**: Compatible with `anthropic-messages-api`,`openai-responses-api`, and `openai-compatible-api`(`openrouter-api`), featuring interleaved thinking, OpenRouter's provider sorting etc.
|
|
25
|
+
- **Structured Sub-Agent Output**: Main agent defines output JSON schema for sub-agents; sub-agents use `report_back` tool with constrained decoding to return schema-compliant structured data.
|
|
25
26
|
- **Skill System**: Extensible support for loading Claude Skills.
|
|
26
27
|
- **Session Management**: Robust context preservation with resumable sessions (`--continue`).
|
|
27
28
|
- **Simple TUI**: Clean interface offering full visibility into model responses, reasoning and actions.
|
|
@@ -107,7 +108,7 @@ model_list:
|
|
|
107
108
|
provider_routing:
|
|
108
109
|
sort: throughput
|
|
109
110
|
main_model: gpt-5.1-codex
|
|
110
|
-
|
|
111
|
+
sub_agent_models:
|
|
111
112
|
oracle: gpt-5.1-high
|
|
112
113
|
explore: haiku
|
|
113
114
|
task: sonnet
|