klaude-code 2.9.1__py3-none-any.whl → 2.10.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/app/runtime.py +1 -1
- klaude_code/cli/cost_cmd.py +4 -4
- klaude_code/cli/list_model.py +1 -2
- klaude_code/const.py +4 -3
- klaude_code/core/bash_mode.py +276 -0
- klaude_code/core/executor.py +40 -7
- klaude_code/core/manager/llm_clients.py +1 -0
- klaude_code/core/manager/llm_clients_builder.py +2 -2
- klaude_code/core/memory.py +140 -0
- klaude_code/core/reminders.py +17 -89
- klaude_code/core/turn.py +10 -4
- klaude_code/protocol/events.py +17 -0
- klaude_code/protocol/op.py +12 -0
- klaude_code/protocol/op_handler.py +5 -0
- klaude_code/tui/command/resume_cmd.py +1 -1
- klaude_code/tui/commands.py +15 -0
- klaude_code/tui/components/command_output.py +4 -5
- klaude_code/tui/components/developer.py +1 -3
- klaude_code/tui/components/metadata.py +23 -23
- klaude_code/tui/components/rich/code_panel.py +31 -16
- klaude_code/tui/components/rich/markdown.py +53 -124
- klaude_code/tui/components/rich/theme.py +19 -10
- klaude_code/tui/components/tools.py +1 -0
- klaude_code/tui/components/user_input.py +48 -59
- klaude_code/tui/components/welcome.py +47 -2
- klaude_code/tui/display.py +15 -7
- klaude_code/tui/input/completers.py +8 -0
- klaude_code/tui/input/key_bindings.py +37 -1
- klaude_code/tui/input/prompt_toolkit.py +58 -31
- klaude_code/tui/machine.py +63 -3
- klaude_code/tui/renderer.py +113 -19
- klaude_code/tui/runner.py +22 -0
- klaude_code/tui/terminal/notifier.py +11 -12
- klaude_code/tui/terminal/selector.py +1 -1
- klaude_code/ui/terminal/title.py +4 -2
- {klaude_code-2.9.1.dist-info → klaude_code-2.10.0.dist-info}/METADATA +1 -1
- {klaude_code-2.9.1.dist-info → klaude_code-2.10.0.dist-info}/RECORD +39 -38
- klaude_code/tui/components/assistant.py +0 -2
- {klaude_code-2.9.1.dist-info → klaude_code-2.10.0.dist-info}/WHEEL +0 -0
- {klaude_code-2.9.1.dist-info → klaude_code-2.10.0.dist-info}/entry_points.txt +0 -0
klaude_code/core/reminders.py
CHANGED
|
@@ -4,9 +4,13 @@ import shlex
|
|
|
4
4
|
from dataclasses import dataclass
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
from
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
from klaude_code.const import REMINDER_COOLDOWN_TURNS, TODO_REMINDER_TOOL_CALL_THRESHOLD
|
|
8
|
+
from klaude_code.core.memory import (
|
|
9
|
+
Memory,
|
|
10
|
+
discover_memory_files_near_paths,
|
|
11
|
+
format_memories_reminder,
|
|
12
|
+
get_memory_paths,
|
|
13
|
+
)
|
|
10
14
|
from klaude_code.core.tool import BashTool, ReadTool, build_todo_context
|
|
11
15
|
from klaude_code.core.tool.context import ToolContext
|
|
12
16
|
from klaude_code.core.tool.file._utils import hash_text_sha256
|
|
@@ -382,28 +386,6 @@ def _compute_file_content_sha256(path: str) -> str | None:
|
|
|
382
386
|
return None
|
|
383
387
|
|
|
384
388
|
|
|
385
|
-
def get_memory_paths() -> list[tuple[Path, str]]:
|
|
386
|
-
return [
|
|
387
|
-
(
|
|
388
|
-
Path.home() / ".claude" / "CLAUDE.md",
|
|
389
|
-
"user's private global instructions for all projects",
|
|
390
|
-
),
|
|
391
|
-
(
|
|
392
|
-
Path.home() / ".codex" / "AGENTS.md",
|
|
393
|
-
"user's private global instructions for all projects",
|
|
394
|
-
),
|
|
395
|
-
(Path.cwd() / "AGENTS.md", "project instructions, checked into the codebase"),
|
|
396
|
-
(Path.cwd() / "CLAUDE.md", "project instructions, checked into the codebase"),
|
|
397
|
-
(Path.cwd() / ".claude" / "CLAUDE.md", "project instructions, checked into the codebase"),
|
|
398
|
-
]
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
class Memory(BaseModel):
|
|
402
|
-
path: str
|
|
403
|
-
instruction: str
|
|
404
|
-
content: str
|
|
405
|
-
|
|
406
|
-
|
|
407
389
|
def get_last_user_message_image_paths(session: Session) -> list[str]:
|
|
408
390
|
"""Get image file paths from the last user message in conversation history."""
|
|
409
391
|
for item in reversed(session.conversation_history):
|
|
@@ -502,7 +484,7 @@ def _mark_memory_loaded(session: Session, path: str) -> None:
|
|
|
502
484
|
|
|
503
485
|
async def memory_reminder(session: Session) -> message.DeveloperMessage | None:
|
|
504
486
|
"""CLAUDE.md AGENTS.md"""
|
|
505
|
-
memory_paths = get_memory_paths()
|
|
487
|
+
memory_paths = get_memory_paths(work_dir=session.work_dir)
|
|
506
488
|
memories: list[Memory] = []
|
|
507
489
|
for memory_path, instruction in memory_paths:
|
|
508
490
|
path_str = str(memory_path)
|
|
@@ -514,21 +496,12 @@ async def memory_reminder(session: Session) -> message.DeveloperMessage | None:
|
|
|
514
496
|
except (PermissionError, UnicodeDecodeError, OSError):
|
|
515
497
|
continue
|
|
516
498
|
if len(memories) > 0:
|
|
517
|
-
memories_str = "\n\n".join(
|
|
518
|
-
[f"Contents of {memory.path} ({memory.instruction}):\n\n{memory.content}" for memory in memories]
|
|
519
|
-
)
|
|
520
499
|
loaded_files = [
|
|
521
500
|
model.MemoryFileLoaded(path=memory.path, mentioned_patterns=_extract_at_patterns(memory.content))
|
|
522
501
|
for memory in memories
|
|
523
502
|
]
|
|
524
503
|
return message.DeveloperMessage(
|
|
525
|
-
parts=message.text_parts_from_str(
|
|
526
|
-
f"""<system-reminder>
|
|
527
|
-
Loaded memory files. Follow these instructions. Do not mention them to the user unless explicitly asked.
|
|
528
|
-
|
|
529
|
-
{memories_str}
|
|
530
|
-
</system-reminder>"""
|
|
531
|
-
),
|
|
504
|
+
parts=message.text_parts_from_str(format_memories_reminder(memories, include_header=True)),
|
|
532
505
|
ui_extra=model.DeveloperUIExtra(items=[model.MemoryLoadedUIItem(files=loaded_files)]),
|
|
533
506
|
)
|
|
534
507
|
return None
|
|
@@ -546,68 +519,23 @@ async def last_path_memory_reminder(
|
|
|
546
519
|
return None
|
|
547
520
|
|
|
548
521
|
paths = list(session.file_tracker.keys())
|
|
549
|
-
memories
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
p = Path(p_str)
|
|
556
|
-
full = (cwd / p).resolve() if not p.is_absolute() else p.resolve()
|
|
557
|
-
try:
|
|
558
|
-
_ = full.relative_to(cwd)
|
|
559
|
-
except ValueError:
|
|
560
|
-
# Not under cwd; skip
|
|
561
|
-
continue
|
|
562
|
-
|
|
563
|
-
# Determine the deepest directory to scan (file parent or directory itself)
|
|
564
|
-
deepest_dir = full if full.is_dir() else full.parent
|
|
565
|
-
|
|
566
|
-
# Iterate each directory level from cwd to deepest_dir
|
|
567
|
-
try:
|
|
568
|
-
rel_parts = deepest_dir.relative_to(cwd).parts
|
|
569
|
-
except ValueError:
|
|
570
|
-
# Shouldn't happen due to check above, but guard anyway
|
|
571
|
-
continue
|
|
572
|
-
|
|
573
|
-
current_dir = cwd
|
|
574
|
-
for part in rel_parts:
|
|
575
|
-
current_dir = current_dir / part
|
|
576
|
-
for fname in MEMORY_FILE_NAMES:
|
|
577
|
-
mem_path = current_dir / fname
|
|
578
|
-
mem_path_str = str(mem_path)
|
|
579
|
-
if mem_path_str in seen_memory_files or _is_memory_loaded(session, mem_path_str):
|
|
580
|
-
continue
|
|
581
|
-
if mem_path.exists() and mem_path.is_file():
|
|
582
|
-
try:
|
|
583
|
-
text = mem_path.read_text(encoding="utf-8", errors="replace")
|
|
584
|
-
except (PermissionError, UnicodeDecodeError, OSError):
|
|
585
|
-
continue
|
|
586
|
-
_mark_memory_loaded(session, mem_path_str)
|
|
587
|
-
seen_memory_files.add(mem_path_str)
|
|
588
|
-
memories.append(
|
|
589
|
-
Memory(
|
|
590
|
-
path=mem_path_str,
|
|
591
|
-
instruction="project instructions, discovered near last accessed path",
|
|
592
|
-
content=text,
|
|
593
|
-
)
|
|
594
|
-
)
|
|
522
|
+
memories = discover_memory_files_near_paths(
|
|
523
|
+
paths,
|
|
524
|
+
work_dir=session.work_dir,
|
|
525
|
+
is_memory_loaded=lambda p: _is_memory_loaded(session, p),
|
|
526
|
+
mark_memory_loaded=lambda p: _mark_memory_loaded(session, p),
|
|
527
|
+
)
|
|
595
528
|
|
|
596
529
|
if len(memories) > 0:
|
|
597
|
-
memories_str = "\n\n".join(
|
|
598
|
-
[f"Contents of {memory.path} ({memory.instruction}):\n\n{memory.content}" for memory in memories]
|
|
599
|
-
)
|
|
600
530
|
loaded_files = [
|
|
601
531
|
model.MemoryFileLoaded(path=memory.path, mentioned_patterns=_extract_at_patterns(memory.content))
|
|
602
532
|
for memory in memories
|
|
603
533
|
]
|
|
604
534
|
return message.DeveloperMessage(
|
|
605
|
-
parts=message.text_parts_from_str(
|
|
606
|
-
f"""<system-reminder>{memories_str}
|
|
607
|
-
</system-reminder>"""
|
|
608
|
-
),
|
|
535
|
+
parts=message.text_parts_from_str(format_memories_reminder(memories, include_header=False)),
|
|
609
536
|
ui_extra=model.DeveloperUIExtra(items=[model.MemoryLoadedUIItem(files=loaded_files)]),
|
|
610
537
|
)
|
|
538
|
+
return None
|
|
611
539
|
|
|
612
540
|
|
|
613
541
|
ALL_REMINDERS = [
|
klaude_code/core/turn.py
CHANGED
|
@@ -194,11 +194,17 @@ class TurnExecutor:
|
|
|
194
194
|
and self._turn_result.assistant_message is not None
|
|
195
195
|
and self._turn_result.assistant_message.parts
|
|
196
196
|
):
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
197
|
+
# Discard partial message if it only contains thinking parts
|
|
198
|
+
has_non_thinking = any(
|
|
199
|
+
not isinstance(part, message.ThinkingTextPart)
|
|
200
|
+
for part in self._turn_result.assistant_message.parts
|
|
201
201
|
)
|
|
202
|
+
if has_non_thinking:
|
|
203
|
+
session_ctx.append_history([self._turn_result.assistant_message])
|
|
204
|
+
# Add continuation prompt to avoid Anthropic thinking block requirement
|
|
205
|
+
session_ctx.append_history(
|
|
206
|
+
[message.UserMessage(parts=[message.TextPart(text="<system>continue</system>")])]
|
|
207
|
+
)
|
|
202
208
|
yield events.TurnEndEvent(session_id=session_ctx.session_id)
|
|
203
209
|
raise TurnError(self._turn_result.stream_error.error)
|
|
204
210
|
|
klaude_code/protocol/events.py
CHANGED
|
@@ -14,6 +14,9 @@ __all__ = [
|
|
|
14
14
|
"AssistantTextDeltaEvent",
|
|
15
15
|
"AssistantTextEndEvent",
|
|
16
16
|
"AssistantTextStartEvent",
|
|
17
|
+
"BashCommandEndEvent",
|
|
18
|
+
"BashCommandOutputDeltaEvent",
|
|
19
|
+
"BashCommandStartEvent",
|
|
17
20
|
"CommandOutputEvent",
|
|
18
21
|
"CompactionEndEvent",
|
|
19
22
|
"CompactionStartEvent",
|
|
@@ -81,6 +84,19 @@ class CommandOutputEvent(Event):
|
|
|
81
84
|
is_error: bool = False
|
|
82
85
|
|
|
83
86
|
|
|
87
|
+
class BashCommandStartEvent(Event):
|
|
88
|
+
command: str
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class BashCommandOutputDeltaEvent(Event):
|
|
92
|
+
content: str
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class BashCommandEndEvent(Event):
|
|
96
|
+
exit_code: int | None = None
|
|
97
|
+
cancelled: bool = False
|
|
98
|
+
|
|
99
|
+
|
|
84
100
|
class TaskStartEvent(Event):
|
|
85
101
|
sub_agent_state: model.SubAgentState | None = None
|
|
86
102
|
model_id: str | None = None
|
|
@@ -166,6 +182,7 @@ class WelcomeEvent(Event):
|
|
|
166
182
|
llm_config: llm_param.LLMConfigParameter
|
|
167
183
|
show_klaude_code_info: bool = True
|
|
168
184
|
loaded_skills: dict[str, list[str]] = Field(default_factory=dict)
|
|
185
|
+
loaded_memories: dict[str, list[str]] = Field(default_factory=dict)
|
|
169
186
|
|
|
170
187
|
|
|
171
188
|
class ErrorEvent(Event):
|
klaude_code/protocol/op.py
CHANGED
|
@@ -24,6 +24,7 @@ class OperationType(Enum):
|
|
|
24
24
|
"""Enumeration of supported operation types."""
|
|
25
25
|
|
|
26
26
|
RUN_AGENT = "run_agent"
|
|
27
|
+
RUN_BASH = "run_bash"
|
|
27
28
|
CONTINUE_AGENT = "continue_agent"
|
|
28
29
|
COMPACT_SESSION = "compact_session"
|
|
29
30
|
CHANGE_MODEL = "change_model"
|
|
@@ -60,6 +61,17 @@ class RunAgentOperation(Operation):
|
|
|
60
61
|
await handler.handle_run_agent(self)
|
|
61
62
|
|
|
62
63
|
|
|
64
|
+
class RunBashOperation(Operation):
|
|
65
|
+
"""Operation for running a user-entered bash-mode command."""
|
|
66
|
+
|
|
67
|
+
type: OperationType = OperationType.RUN_BASH
|
|
68
|
+
session_id: str
|
|
69
|
+
command: str
|
|
70
|
+
|
|
71
|
+
async def execute(self, handler: OperationHandler) -> None:
|
|
72
|
+
await handler.handle_run_bash(self)
|
|
73
|
+
|
|
74
|
+
|
|
63
75
|
class ContinueAgentOperation(Operation):
|
|
64
76
|
"""Operation for continuing an agent task without adding a new user message.
|
|
65
77
|
|
|
@@ -22,6 +22,7 @@ if TYPE_CHECKING:
|
|
|
22
22
|
InterruptOperation,
|
|
23
23
|
ResumeSessionOperation,
|
|
24
24
|
RunAgentOperation,
|
|
25
|
+
RunBashOperation,
|
|
25
26
|
)
|
|
26
27
|
|
|
27
28
|
|
|
@@ -32,6 +33,10 @@ class OperationHandler(Protocol):
|
|
|
32
33
|
"""Handle a run agent operation."""
|
|
33
34
|
...
|
|
34
35
|
|
|
36
|
+
async def handle_run_bash(self, operation: RunBashOperation) -> None:
|
|
37
|
+
"""Handle a bash-mode command execution operation."""
|
|
38
|
+
...
|
|
39
|
+
|
|
35
40
|
async def handle_continue_agent(self, operation: ContinueAgentOperation) -> None:
|
|
36
41
|
"""Handle a continue agent operation (resume without adding user message)."""
|
|
37
42
|
...
|
|
@@ -34,7 +34,7 @@ def select_session_sync(session_ids: list[str] | None = None) -> str | None:
|
|
|
34
34
|
if msg == "⋮":
|
|
35
35
|
title.append(("class:msg", f" {msg}\n"))
|
|
36
36
|
else:
|
|
37
|
-
prefix = "
|
|
37
|
+
prefix = "╰─" if is_last else "├─"
|
|
38
38
|
title.append(("fg:ansibrightblack dim", f" {prefix} "))
|
|
39
39
|
title.append(("class:msg", f"{msg}\n"))
|
|
40
40
|
title.append(("", "\n"))
|
klaude_code/tui/commands.py
CHANGED
|
@@ -38,6 +38,21 @@ class RenderCommandOutput(RenderCommand):
|
|
|
38
38
|
event: events.CommandOutputEvent
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
@dataclass(frozen=True, slots=True)
|
|
42
|
+
class RenderBashCommandStart(RenderCommand):
|
|
43
|
+
event: events.BashCommandStartEvent
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True, slots=True)
|
|
47
|
+
class AppendBashCommandOutput(RenderCommand):
|
|
48
|
+
event: events.BashCommandOutputDeltaEvent
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class RenderBashCommandEnd(RenderCommand):
|
|
53
|
+
event: events.BashCommandEndEvent
|
|
54
|
+
|
|
55
|
+
|
|
41
56
|
@dataclass(frozen=True, slots=True)
|
|
42
57
|
class RenderTurnStart(RenderCommand):
|
|
43
58
|
event: events.TurnStartEvent
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from rich.console import RenderableType
|
|
2
|
-
from rich.padding import Padding
|
|
3
2
|
from rich.table import Table
|
|
4
3
|
from rich.text import Text
|
|
5
4
|
|
|
@@ -19,7 +18,7 @@ def render_command_output(e: events.CommandOutputEvent) -> RenderableType:
|
|
|
19
18
|
case _:
|
|
20
19
|
content = e.content or "(no content)"
|
|
21
20
|
style = ThemeKey.TOOL_RESULT if not e.is_error else ThemeKey.ERROR
|
|
22
|
-
return
|
|
21
|
+
return truncate_middle(content, base_style=style)
|
|
23
22
|
|
|
24
23
|
|
|
25
24
|
def _format_tokens(tokens: int) -> str:
|
|
@@ -44,7 +43,7 @@ def _format_cost(cost: float | None, currency: str = "USD") -> str:
|
|
|
44
43
|
def _render_fork_session_output(e: events.CommandOutputEvent) -> RenderableType:
|
|
45
44
|
"""Render fork session output with usage instructions."""
|
|
46
45
|
if not isinstance(e.ui_extra, model.SessionIdUIExtra):
|
|
47
|
-
return
|
|
46
|
+
return Text(e.content, style=ThemeKey.TOOL_RESULT)
|
|
48
47
|
|
|
49
48
|
grid = Table.grid(padding=(0, 1))
|
|
50
49
|
session_id = e.ui_extra.session_id
|
|
@@ -54,7 +53,7 @@ def _render_fork_session_output(e: events.CommandOutputEvent) -> RenderableType:
|
|
|
54
53
|
grid.add_row(Text("Session forked. Resume command copied to clipboard:", style=ThemeKey.TOOL_RESULT))
|
|
55
54
|
grid.add_row(Text(f" klaude -r {short_id}", style=ThemeKey.TOOL_RESULT_BOLD))
|
|
56
55
|
|
|
57
|
-
return
|
|
56
|
+
return grid
|
|
58
57
|
|
|
59
58
|
|
|
60
59
|
def _render_status_output(e: events.CommandOutputEvent) -> RenderableType:
|
|
@@ -95,4 +94,4 @@ def _render_status_output(e: events.CommandOutputEvent) -> RenderableType:
|
|
|
95
94
|
usage_detail = "(no usage data)"
|
|
96
95
|
table.add_row(f"{model_label}:", usage_detail)
|
|
97
96
|
|
|
98
|
-
return
|
|
97
|
+
return table
|
|
@@ -6,7 +6,7 @@ from klaude_code.tui.components.common import create_grid
|
|
|
6
6
|
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
7
7
|
from klaude_code.tui.components.tools import render_path
|
|
8
8
|
|
|
9
|
-
REMINDER_BULLET = "
|
|
9
|
+
REMINDER_BULLET = "⧉"
|
|
10
10
|
|
|
11
11
|
|
|
12
12
|
def need_render_developer_message(e: events.DeveloperMessageEvent) -> bool:
|
|
@@ -56,8 +56,6 @@ def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
|
|
|
56
56
|
text = "Todo hasn't been updated recently"
|
|
57
57
|
case "empty":
|
|
58
58
|
text = "Todo list is empty"
|
|
59
|
-
case _:
|
|
60
|
-
text = "Todo reminder"
|
|
61
59
|
grid = create_grid()
|
|
62
60
|
grid.add_row(
|
|
63
61
|
Text(REMINDER_BULLET, style=ThemeKey.REMINDER),
|
|
@@ -34,8 +34,8 @@ def _render_task_metadata_block(
|
|
|
34
34
|
content = Text()
|
|
35
35
|
if metadata.provider is not None:
|
|
36
36
|
content.append_text(Text(metadata.provider.lower().replace(" ", "-"), style=ThemeKey.METADATA))
|
|
37
|
-
content.append_text(Text("/", style=ThemeKey.
|
|
38
|
-
content.append_text(Text(metadata.model_name, style=ThemeKey.
|
|
37
|
+
content.append_text(Text("/", style=ThemeKey.METADATA))
|
|
38
|
+
content.append_text(Text(metadata.model_name, style=ThemeKey.METADATA))
|
|
39
39
|
if metadata.description:
|
|
40
40
|
content.append_text(Text(" ", style=ThemeKey.METADATA)).append_text(
|
|
41
41
|
Text(metadata.description, style=ThemeKey.METADATA_ITALIC)
|
|
@@ -47,18 +47,18 @@ def _render_task_metadata_block(
|
|
|
47
47
|
if metadata.usage is not None:
|
|
48
48
|
# Tokens: ↑37k ◎5k ↓907 ∿45k ⌗ 100
|
|
49
49
|
token_text = Text()
|
|
50
|
-
token_text.append("↑", style=ThemeKey.
|
|
50
|
+
token_text.append("↑", style=ThemeKey.METADATA)
|
|
51
51
|
token_text.append(format_number(metadata.usage.input_tokens), style=ThemeKey.METADATA)
|
|
52
52
|
if metadata.usage.cached_tokens > 0:
|
|
53
|
-
token_text.append(" ◎", style=ThemeKey.
|
|
53
|
+
token_text.append(" ◎", style=ThemeKey.METADATA)
|
|
54
54
|
token_text.append(format_number(metadata.usage.cached_tokens), style=ThemeKey.METADATA)
|
|
55
|
-
token_text.append(" ↓", style=ThemeKey.
|
|
55
|
+
token_text.append(" ↓", style=ThemeKey.METADATA)
|
|
56
56
|
token_text.append(format_number(metadata.usage.output_tokens), style=ThemeKey.METADATA)
|
|
57
57
|
if metadata.usage.reasoning_tokens > 0:
|
|
58
|
-
token_text.append(" ∿", style=ThemeKey.
|
|
58
|
+
token_text.append(" ∿", style=ThemeKey.METADATA)
|
|
59
59
|
token_text.append(format_number(metadata.usage.reasoning_tokens), style=ThemeKey.METADATA)
|
|
60
60
|
if metadata.usage.image_tokens > 0:
|
|
61
|
-
token_text.append(" ⊡", style=ThemeKey.
|
|
61
|
+
token_text.append(" ⊡", style=ThemeKey.METADATA)
|
|
62
62
|
token_text.append(format_number(metadata.usage.image_tokens), style=ThemeKey.METADATA)
|
|
63
63
|
parts.append(token_text)
|
|
64
64
|
|
|
@@ -66,7 +66,7 @@ def _render_task_metadata_block(
|
|
|
66
66
|
if metadata.usage is not None and metadata.usage.total_cost is not None:
|
|
67
67
|
parts.append(
|
|
68
68
|
Text.assemble(
|
|
69
|
-
(currency_symbol, ThemeKey.
|
|
69
|
+
(currency_symbol, ThemeKey.METADATA),
|
|
70
70
|
(f"{metadata.usage.total_cost:.4f}", ThemeKey.METADATA),
|
|
71
71
|
)
|
|
72
72
|
)
|
|
@@ -79,9 +79,9 @@ def _render_task_metadata_block(
|
|
|
79
79
|
parts.append(
|
|
80
80
|
Text.assemble(
|
|
81
81
|
(context_size, ThemeKey.METADATA),
|
|
82
|
-
("/", ThemeKey.
|
|
82
|
+
("/", ThemeKey.METADATA),
|
|
83
83
|
(effective_limit_str, ThemeKey.METADATA),
|
|
84
|
-
(f"({metadata.usage.context_usage_percent:.1f}%)", ThemeKey.
|
|
84
|
+
(f"({metadata.usage.context_usage_percent:.1f}%)", ThemeKey.METADATA),
|
|
85
85
|
)
|
|
86
86
|
)
|
|
87
87
|
|
|
@@ -90,7 +90,7 @@ def _render_task_metadata_block(
|
|
|
90
90
|
parts.append(
|
|
91
91
|
Text.assemble(
|
|
92
92
|
(f"{metadata.usage.throughput_tps:.1f}", ThemeKey.METADATA),
|
|
93
|
-
("tps", ThemeKey.
|
|
93
|
+
("tps", ThemeKey.METADATA),
|
|
94
94
|
)
|
|
95
95
|
)
|
|
96
96
|
|
|
@@ -101,7 +101,7 @@ def _render_task_metadata_block(
|
|
|
101
101
|
parts.append(
|
|
102
102
|
Text.assemble(
|
|
103
103
|
(ftl_str, ThemeKey.METADATA),
|
|
104
|
-
("-ftl", ThemeKey.
|
|
104
|
+
("-ftl", ThemeKey.METADATA),
|
|
105
105
|
)
|
|
106
106
|
)
|
|
107
107
|
|
|
@@ -110,7 +110,7 @@ def _render_task_metadata_block(
|
|
|
110
110
|
parts.append(
|
|
111
111
|
Text.assemble(
|
|
112
112
|
(f"{metadata.task_duration_s:.1f}", ThemeKey.METADATA),
|
|
113
|
-
("s", ThemeKey.
|
|
113
|
+
("s", ThemeKey.METADATA),
|
|
114
114
|
)
|
|
115
115
|
)
|
|
116
116
|
|
|
@@ -120,13 +120,13 @@ def _render_task_metadata_block(
|
|
|
120
120
|
parts.append(
|
|
121
121
|
Text.assemble(
|
|
122
122
|
(str(metadata.turn_count), ThemeKey.METADATA),
|
|
123
|
-
(suffix, ThemeKey.
|
|
123
|
+
(suffix, ThemeKey.METADATA),
|
|
124
124
|
)
|
|
125
125
|
)
|
|
126
126
|
|
|
127
127
|
if parts:
|
|
128
|
-
content.append_text(Text(" ", style=ThemeKey.
|
|
129
|
-
content.append_text(Text(" ", style=ThemeKey.
|
|
128
|
+
content.append_text(Text(" ", style=ThemeKey.METADATA))
|
|
129
|
+
content.append_text(Text(" ", style=ThemeKey.METADATA).join(parts))
|
|
130
130
|
|
|
131
131
|
grid.add_row(mark, content)
|
|
132
132
|
return grid
|
|
@@ -138,14 +138,14 @@ def render_task_metadata(e: events.TaskMetadataEvent) -> RenderableType:
|
|
|
138
138
|
|
|
139
139
|
has_sub_agents = len(e.metadata.sub_agent_task_metadata) > 0
|
|
140
140
|
# Use an extra space for the main agent mark to align with two-character marks (├─, └─)
|
|
141
|
-
main_mark_text = "
|
|
141
|
+
main_mark_text = "●"
|
|
142
142
|
main_mark = Text(main_mark_text, style=ThemeKey.METADATA)
|
|
143
143
|
|
|
144
144
|
renderables.append(_render_task_metadata_block(e.metadata.main_agent, mark=main_mark, show_context_and_time=True))
|
|
145
145
|
|
|
146
146
|
# Render each sub-agent metadata block
|
|
147
147
|
for meta in e.metadata.sub_agent_task_metadata:
|
|
148
|
-
sub_mark = Text(" └", style=ThemeKey.
|
|
148
|
+
sub_mark = Text(" └", style=ThemeKey.METADATA)
|
|
149
149
|
renderables.append(_render_task_metadata_block(meta, mark=sub_mark, show_context_and_time=True))
|
|
150
150
|
|
|
151
151
|
# Add total cost line when there are sub-agents
|
|
@@ -162,11 +162,11 @@ def render_task_metadata(e: events.TaskMetadataEvent) -> RenderableType:
|
|
|
162
162
|
|
|
163
163
|
currency_symbol = "¥" if currency == "CNY" else "$"
|
|
164
164
|
total_line = Text.assemble(
|
|
165
|
-
(" └", ThemeKey.
|
|
166
|
-
(" Σ ", ThemeKey.
|
|
167
|
-
("total ", ThemeKey.
|
|
168
|
-
(currency_symbol, ThemeKey.
|
|
169
|
-
(f"{total_cost:.4f}", ThemeKey.
|
|
165
|
+
(" └", ThemeKey.METADATA),
|
|
166
|
+
(" Σ ", ThemeKey.METADATA),
|
|
167
|
+
("total ", ThemeKey.METADATA),
|
|
168
|
+
(currency_symbol, ThemeKey.METADATA),
|
|
169
|
+
(f"{total_cost:.4f}", ThemeKey.METADATA),
|
|
170
170
|
)
|
|
171
171
|
|
|
172
172
|
renderables.append(total_line)
|
|
@@ -14,12 +14,12 @@ from rich.style import StyleType
|
|
|
14
14
|
if TYPE_CHECKING:
|
|
15
15
|
from rich.console import Console, ConsoleOptions, RenderResult
|
|
16
16
|
|
|
17
|
-
# Box drawing characters
|
|
18
|
-
TOP_LEFT = "
|
|
19
|
-
TOP_RIGHT = "
|
|
20
|
-
BOTTOM_LEFT = "
|
|
21
|
-
BOTTOM_RIGHT = "
|
|
22
|
-
HORIZONTAL = "─"
|
|
17
|
+
# Box drawing characters (rounded corners)
|
|
18
|
+
TOP_LEFT = "╭"
|
|
19
|
+
TOP_RIGHT = "╮"
|
|
20
|
+
BOTTOM_LEFT = "╰"
|
|
21
|
+
BOTTOM_RIGHT = "╯"
|
|
22
|
+
HORIZONTAL = "─"
|
|
23
23
|
|
|
24
24
|
|
|
25
25
|
class CodePanel(JupyterMixin):
|
|
@@ -32,10 +32,10 @@ class CodePanel(JupyterMixin):
|
|
|
32
32
|
>>> console.print(CodePanel(Syntax(code, "python")))
|
|
33
33
|
|
|
34
34
|
Renders as:
|
|
35
|
-
|
|
35
|
+
╭──────────────────────────╮
|
|
36
36
|
code line 1
|
|
37
37
|
code line 2
|
|
38
|
-
|
|
38
|
+
╰──────────────────────────╯
|
|
39
39
|
"""
|
|
40
40
|
|
|
41
41
|
def __init__(
|
|
@@ -44,7 +44,9 @@ class CodePanel(JupyterMixin):
|
|
|
44
44
|
*,
|
|
45
45
|
border_style: StyleType = "none",
|
|
46
46
|
expand: bool = False,
|
|
47
|
-
padding: int =
|
|
47
|
+
padding: int = 0,
|
|
48
|
+
title: str | None = None,
|
|
49
|
+
title_style: StyleType = "none",
|
|
48
50
|
) -> None:
|
|
49
51
|
"""Initialize the CodePanel.
|
|
50
52
|
|
|
@@ -52,12 +54,16 @@ class CodePanel(JupyterMixin):
|
|
|
52
54
|
renderable: A console renderable object.
|
|
53
55
|
border_style: The style of the border. Defaults to "none".
|
|
54
56
|
expand: If True, expand to fill available width. Defaults to False.
|
|
55
|
-
padding: Left/right padding for content. Defaults to
|
|
57
|
+
padding: Left/right padding for content. Defaults to 0.
|
|
58
|
+
title: Optional title to display in the top border. Defaults to None.
|
|
59
|
+
title_style: The style of the title. Defaults to "none".
|
|
56
60
|
"""
|
|
57
61
|
self.renderable = renderable
|
|
58
62
|
self.border_style = border_style
|
|
59
63
|
self.expand = expand
|
|
60
64
|
self.padding = padding
|
|
65
|
+
self.title = title
|
|
66
|
+
self.title_style = title_style
|
|
61
67
|
|
|
62
68
|
@staticmethod
|
|
63
69
|
def _measure_max_line_cells(lines: list[list[Segment]]) -> int:
|
|
@@ -93,11 +99,20 @@ class CodePanel(JupyterMixin):
|
|
|
93
99
|
new_line = Segment.line()
|
|
94
100
|
pad_segment = Segment(" " * pad) if pad > 0 else None
|
|
95
101
|
|
|
96
|
-
# Top border:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
102
|
+
# Top border: ╭───...───╮ or ╭ title ───...───╮
|
|
103
|
+
if self.title and border_width >= len(self.title) + 4:
|
|
104
|
+
title_part = f" {self.title} "
|
|
105
|
+
title_style = console.get_style(self.title_style)
|
|
106
|
+
remaining = border_width - 2 - len(title_part)
|
|
107
|
+
yield Segment(TOP_LEFT, border_style)
|
|
108
|
+
yield Segment(title_part, title_style)
|
|
109
|
+
yield Segment((HORIZONTAL * remaining) + TOP_RIGHT, border_style)
|
|
110
|
+
elif border_width >= 2:
|
|
111
|
+
top_border = TOP_LEFT + (HORIZONTAL * (border_width - 2)) + TOP_RIGHT
|
|
112
|
+
yield Segment(top_border, border_style)
|
|
113
|
+
else:
|
|
114
|
+
top_border = HORIZONTAL * border_width
|
|
115
|
+
yield Segment(top_border, border_style)
|
|
101
116
|
yield new_line
|
|
102
117
|
|
|
103
118
|
# Content lines with padding
|
|
@@ -109,7 +124,7 @@ class CodePanel(JupyterMixin):
|
|
|
109
124
|
yield pad_segment
|
|
110
125
|
yield new_line
|
|
111
126
|
|
|
112
|
-
# Bottom border:
|
|
127
|
+
# Bottom border: ╰───...───╯
|
|
113
128
|
bottom_border = (
|
|
114
129
|
BOTTOM_LEFT + (HORIZONTAL * (border_width - 2)) + BOTTOM_RIGHT
|
|
115
130
|
if border_width >= 2
|