klaude-code 2.8.1__py3-none-any.whl → 2.9.1__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 +2 -1
- klaude_code/auth/antigravity/oauth.py +33 -38
- klaude_code/auth/antigravity/token_manager.py +0 -18
- klaude_code/auth/base.py +53 -0
- klaude_code/auth/claude/oauth.py +34 -49
- klaude_code/auth/codex/exceptions.py +0 -4
- klaude_code/auth/codex/oauth.py +32 -28
- klaude_code/auth/codex/token_manager.py +0 -18
- klaude_code/cli/cost_cmd.py +128 -39
- klaude_code/cli/list_model.py +27 -10
- klaude_code/cli/main.py +14 -3
- klaude_code/config/assets/builtin_config.yaml +25 -24
- klaude_code/config/config.py +47 -25
- klaude_code/config/sub_agent_model_helper.py +18 -13
- klaude_code/config/thinking.py +0 -8
- klaude_code/const.py +1 -1
- klaude_code/core/agent_profile.py +11 -56
- klaude_code/core/compaction/overflow.py +0 -4
- klaude_code/core/executor.py +33 -5
- klaude_code/core/manager/llm_clients.py +9 -1
- klaude_code/core/prompts/prompt-claude-code.md +4 -4
- klaude_code/core/reminders.py +21 -23
- klaude_code/core/task.py +1 -5
- klaude_code/core/tool/__init__.py +3 -2
- klaude_code/core/tool/file/apply_patch.py +0 -27
- klaude_code/core/tool/file/read_tool.md +3 -2
- klaude_code/core/tool/file/read_tool.py +27 -3
- klaude_code/core/tool/offload.py +0 -35
- klaude_code/core/tool/shell/bash_tool.py +1 -1
- klaude_code/core/tool/sub_agent/__init__.py +6 -0
- klaude_code/core/tool/sub_agent/image_gen.md +16 -0
- klaude_code/core/tool/sub_agent/image_gen.py +146 -0
- klaude_code/core/tool/sub_agent/task.md +20 -0
- klaude_code/core/tool/sub_agent/task.py +205 -0
- klaude_code/core/tool/tool_registry.py +0 -16
- klaude_code/core/turn.py +1 -1
- klaude_code/llm/anthropic/input.py +6 -5
- klaude_code/llm/antigravity/input.py +14 -7
- klaude_code/llm/bedrock_anthropic/__init__.py +3 -0
- klaude_code/llm/google/client.py +8 -6
- klaude_code/llm/google/input.py +20 -12
- klaude_code/llm/image.py +18 -11
- klaude_code/llm/input_common.py +32 -6
- klaude_code/llm/json_stable.py +37 -0
- klaude_code/llm/{codex → openai_codex}/__init__.py +1 -1
- klaude_code/llm/{codex → openai_codex}/client.py +24 -2
- klaude_code/llm/openai_codex/prompt_sync.py +237 -0
- klaude_code/llm/openai_compatible/client.py +3 -1
- klaude_code/llm/openai_compatible/input.py +0 -10
- klaude_code/llm/openai_compatible/stream.py +35 -10
- klaude_code/llm/{responses → openai_responses}/client.py +1 -1
- klaude_code/llm/{responses → openai_responses}/input.py +15 -5
- klaude_code/llm/registry.py +3 -8
- klaude_code/llm/stream_parts.py +3 -1
- klaude_code/llm/usage.py +1 -9
- klaude_code/protocol/events.py +2 -2
- klaude_code/protocol/message.py +3 -2
- klaude_code/protocol/model.py +34 -2
- klaude_code/protocol/op.py +13 -0
- klaude_code/protocol/op_handler.py +5 -0
- klaude_code/protocol/sub_agent/AGENTS.md +5 -5
- klaude_code/protocol/sub_agent/__init__.py +13 -34
- klaude_code/protocol/sub_agent/explore.py +7 -34
- klaude_code/protocol/sub_agent/image_gen.py +3 -74
- klaude_code/protocol/sub_agent/task.py +3 -47
- klaude_code/protocol/sub_agent/web.py +8 -52
- klaude_code/protocol/tools.py +2 -0
- klaude_code/session/session.py +80 -22
- klaude_code/session/store.py +0 -4
- klaude_code/skill/assets/deslop/SKILL.md +9 -0
- klaude_code/skill/system_skills.py +0 -20
- klaude_code/tui/command/fork_session_cmd.py +5 -2
- klaude_code/tui/command/resume_cmd.py +9 -2
- klaude_code/tui/command/sub_agent_model_cmd.py +85 -18
- klaude_code/tui/components/assistant.py +0 -26
- klaude_code/tui/components/bash_syntax.py +4 -0
- klaude_code/tui/components/command_output.py +3 -1
- klaude_code/tui/components/developer.py +3 -0
- klaude_code/tui/components/diffs.py +4 -209
- klaude_code/tui/components/errors.py +4 -0
- klaude_code/tui/components/mermaid_viewer.py +2 -2
- klaude_code/tui/components/metadata.py +0 -3
- klaude_code/tui/components/rich/markdown.py +120 -87
- klaude_code/tui/components/rich/status.py +2 -2
- klaude_code/tui/components/rich/theme.py +11 -6
- klaude_code/tui/components/sub_agent.py +2 -46
- klaude_code/tui/components/thinking.py +0 -33
- klaude_code/tui/components/tools.py +65 -21
- klaude_code/tui/components/user_input.py +2 -0
- klaude_code/tui/input/images.py +21 -18
- klaude_code/tui/input/key_bindings.py +2 -2
- klaude_code/tui/input/prompt_toolkit.py +49 -49
- klaude_code/tui/machine.py +29 -47
- klaude_code/tui/renderer.py +48 -33
- klaude_code/tui/runner.py +2 -1
- klaude_code/tui/terminal/image.py +27 -34
- klaude_code/ui/common.py +0 -70
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/METADATA +3 -6
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/RECORD +103 -99
- klaude_code/core/tool/sub_agent_tool.py +0 -126
- klaude_code/llm/bedrock/__init__.py +0 -3
- klaude_code/llm/openai_compatible/tool_call_accumulator.py +0 -108
- klaude_code/tui/components/rich/searchable_text.py +0 -68
- /klaude_code/llm/{bedrock → bedrock_anthropic}/client.py +0 -0
- /klaude_code/llm/{responses → openai_responses}/__init__.py +0 -0
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/WHEEL +0 -0
- {klaude_code-2.8.1.dist-info → klaude_code-2.9.1.dist-info}/entry_points.txt +0 -0
klaude_code/session/session.py
CHANGED
|
@@ -92,10 +92,6 @@ class Session(BaseModel):
|
|
|
92
92
|
]
|
|
93
93
|
return self._user_messages_cache
|
|
94
94
|
|
|
95
|
-
@staticmethod
|
|
96
|
-
def _project_key() -> str:
|
|
97
|
-
return project_key_from_cwd()
|
|
98
|
-
|
|
99
95
|
@classmethod
|
|
100
96
|
def paths(cls) -> ProjectPaths:
|
|
101
97
|
return get_default_store().paths
|
|
@@ -320,10 +316,15 @@ class Session(BaseModel):
|
|
|
320
316
|
prev_item: message.HistoryEvent | None = None
|
|
321
317
|
last_assistant_content: str = ""
|
|
322
318
|
report_back_result: str | None = None
|
|
319
|
+
pending_tool_calls: dict[str, events.ToolCallEvent] = {}
|
|
323
320
|
history = self.conversation_history
|
|
324
321
|
history_len = len(history)
|
|
325
322
|
yield events.TaskStartEvent(session_id=self.id, sub_agent_state=self.sub_agent_state)
|
|
326
323
|
for idx, it in enumerate(history):
|
|
324
|
+
# Flush pending tool calls if current item won't consume them
|
|
325
|
+
if pending_tool_calls and not isinstance(it, message.ToolResultMessage):
|
|
326
|
+
yield from pending_tool_calls.values()
|
|
327
|
+
pending_tool_calls.clear()
|
|
327
328
|
if self.need_turn_start(prev_item, it):
|
|
328
329
|
yield events.TurnStartEvent(session_id=self.id)
|
|
329
330
|
match it:
|
|
@@ -335,6 +336,7 @@ class Session(BaseModel):
|
|
|
335
336
|
# Reconstruct streaming boundaries from saved parts.
|
|
336
337
|
# This allows replay to reuse the same TUI state machine as live events.
|
|
337
338
|
thinking_open = False
|
|
339
|
+
thinking_had_content = False
|
|
338
340
|
assistant_open = False
|
|
339
341
|
|
|
340
342
|
for part in am.parts:
|
|
@@ -346,15 +348,23 @@ class Session(BaseModel):
|
|
|
346
348
|
thinking_open = True
|
|
347
349
|
yield events.ThinkingStartEvent(response_id=am.response_id, session_id=self.id)
|
|
348
350
|
if part.text:
|
|
351
|
+
if thinking_had_content:
|
|
352
|
+
yield events.ThinkingDeltaEvent(
|
|
353
|
+
content=" \n \n",
|
|
354
|
+
response_id=am.response_id,
|
|
355
|
+
session_id=self.id,
|
|
356
|
+
)
|
|
349
357
|
yield events.ThinkingDeltaEvent(
|
|
350
358
|
content=part.text,
|
|
351
359
|
response_id=am.response_id,
|
|
352
360
|
session_id=self.id,
|
|
353
361
|
)
|
|
362
|
+
thinking_had_content = True
|
|
354
363
|
continue
|
|
355
364
|
|
|
356
365
|
if thinking_open:
|
|
357
366
|
thinking_open = False
|
|
367
|
+
thinking_had_content = False
|
|
358
368
|
yield events.ThinkingEndEvent(response_id=am.response_id, session_id=self.id)
|
|
359
369
|
|
|
360
370
|
if isinstance(part, message.TextPart):
|
|
@@ -384,7 +394,7 @@ class Session(BaseModel):
|
|
|
384
394
|
continue
|
|
385
395
|
if part.tool_name == tools.REPORT_BACK:
|
|
386
396
|
report_back_result = part.arguments_json
|
|
387
|
-
|
|
397
|
+
pending_tool_calls[part.call_id] = events.ToolCallEvent(
|
|
388
398
|
tool_call_id=part.call_id,
|
|
389
399
|
tool_name=part.tool_name,
|
|
390
400
|
arguments=part.arguments_json,
|
|
@@ -394,6 +404,8 @@ class Session(BaseModel):
|
|
|
394
404
|
if am.stop_reason == "aborted":
|
|
395
405
|
yield events.InterruptEvent(session_id=self.id)
|
|
396
406
|
case message.ToolResultMessage() as tr:
|
|
407
|
+
if tr.call_id in pending_tool_calls:
|
|
408
|
+
yield pending_tool_calls.pop(tr.call_id)
|
|
397
409
|
status = "success" if tr.status == "success" else "error"
|
|
398
410
|
# Check if this is the last tool result in the current turn
|
|
399
411
|
next_item = history[idx + 1] if idx + 1 < history_len else None
|
|
@@ -410,7 +422,9 @@ class Session(BaseModel):
|
|
|
410
422
|
)
|
|
411
423
|
yield from self._iter_sub_agent_history(tr, seen_sub_agent_sessions)
|
|
412
424
|
case message.UserMessage() as um:
|
|
413
|
-
images = [
|
|
425
|
+
images = [
|
|
426
|
+
part for part in um.parts if isinstance(part, (message.ImageURLPart, message.ImageFilePart))
|
|
427
|
+
]
|
|
414
428
|
yield events.UserMessageEvent(
|
|
415
429
|
content=message.join_text_parts(um.parts),
|
|
416
430
|
session_id=self.id,
|
|
@@ -439,6 +453,11 @@ class Session(BaseModel):
|
|
|
439
453
|
pass
|
|
440
454
|
prev_item = it
|
|
441
455
|
|
|
456
|
+
# Flush any remaining pending tool calls (e.g., from aborted or incomplete sessions)
|
|
457
|
+
if pending_tool_calls:
|
|
458
|
+
yield from pending_tool_calls.values()
|
|
459
|
+
pending_tool_calls.clear()
|
|
460
|
+
|
|
442
461
|
has_structured_output = report_back_result is not None
|
|
443
462
|
task_result = report_back_result if has_structured_output else last_assistant_content
|
|
444
463
|
|
|
@@ -608,27 +627,66 @@ class Session(BaseModel):
|
|
|
608
627
|
return resolved[0]
|
|
609
628
|
|
|
610
629
|
@classmethod
|
|
611
|
-
def
|
|
612
|
-
|
|
613
|
-
|
|
630
|
+
def find_sessions_by_prefix(cls, prefix: str) -> list[str]:
|
|
631
|
+
"""Find main session IDs matching a prefix.
|
|
632
|
+
|
|
633
|
+
Args:
|
|
634
|
+
prefix: Session ID prefix to match.
|
|
635
|
+
|
|
636
|
+
Returns:
|
|
637
|
+
List of matching session IDs, sorted alphabetically.
|
|
638
|
+
"""
|
|
639
|
+
prefix = (prefix or "").strip().lower()
|
|
640
|
+
if not prefix:
|
|
641
|
+
return []
|
|
642
|
+
|
|
614
643
|
store = get_default_store()
|
|
615
|
-
|
|
616
|
-
|
|
644
|
+
matches: set[str] = set()
|
|
645
|
+
|
|
646
|
+
for meta_path in store.iter_meta_files():
|
|
647
|
+
data = _read_json_dict(meta_path)
|
|
648
|
+
if data is None:
|
|
617
649
|
continue
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
650
|
+
# Exclude sub-agent sessions.
|
|
651
|
+
if data.get("sub_agent_state") is not None:
|
|
652
|
+
continue
|
|
653
|
+
sid = str(data.get("id", meta_path.parent.name)).strip()
|
|
654
|
+
if sid.lower().startswith(prefix):
|
|
655
|
+
matches.add(sid)
|
|
656
|
+
|
|
657
|
+
return sorted(matches)
|
|
622
658
|
|
|
623
659
|
@classmethod
|
|
624
|
-
def
|
|
625
|
-
|
|
626
|
-
|
|
660
|
+
def shortest_unique_prefix(cls, session_id: str, min_length: int = 4) -> str:
|
|
661
|
+
"""Find the shortest unique prefix for a session ID.
|
|
662
|
+
|
|
663
|
+
Args:
|
|
664
|
+
session_id: The session ID to find prefix for.
|
|
665
|
+
min_length: Minimum prefix length (default 4).
|
|
666
|
+
|
|
667
|
+
Returns:
|
|
668
|
+
The shortest prefix that uniquely identifies this session.
|
|
669
|
+
"""
|
|
627
670
|
store = get_default_store()
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
671
|
+
other_ids: list[str] = []
|
|
672
|
+
|
|
673
|
+
for meta_path in store.iter_meta_files():
|
|
674
|
+
data = _read_json_dict(meta_path)
|
|
675
|
+
if data is None:
|
|
676
|
+
continue
|
|
677
|
+
if data.get("sub_agent_state") is not None:
|
|
678
|
+
continue
|
|
679
|
+
sid = str(data.get("id", meta_path.parent.name)).strip()
|
|
680
|
+
if sid != session_id:
|
|
681
|
+
other_ids.append(sid.lower())
|
|
682
|
+
|
|
683
|
+
session_lower = session_id.lower()
|
|
684
|
+
for length in range(min_length, len(session_id) + 1):
|
|
685
|
+
prefix = session_lower[:length]
|
|
686
|
+
if not any(other.startswith(prefix) for other in other_ids):
|
|
687
|
+
return session_id[:length]
|
|
688
|
+
|
|
689
|
+
return session_id
|
|
632
690
|
|
|
633
691
|
@classmethod
|
|
634
692
|
def exports_dir(cls) -> Path:
|
klaude_code/session/store.py
CHANGED
|
@@ -2,7 +2,6 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import json
|
|
5
|
-
import shutil
|
|
6
5
|
from collections.abc import Iterable, Sequence
|
|
7
6
|
from dataclasses import dataclass
|
|
8
7
|
from pathlib import Path
|
|
@@ -152,9 +151,6 @@ class JsonlSessionStore:
|
|
|
152
151
|
return []
|
|
153
152
|
return sessions_dir.glob("*/meta.json")
|
|
154
153
|
|
|
155
|
-
def delete_session(self, session_id: str) -> None:
|
|
156
|
-
shutil.rmtree(self._paths.session_dir(session_id), ignore_errors=True)
|
|
157
|
-
|
|
158
154
|
async def aclose(self) -> None:
|
|
159
155
|
await self._writer.aclose()
|
|
160
156
|
|
|
@@ -12,6 +12,15 @@ Remove AI-generated slop from code. Check the specified files or diff and remove
|
|
|
12
12
|
- Extra comments that a human wouldn't add or are inconsistent with the rest of the file
|
|
13
13
|
- Extra defensive checks or try/catch blocks that are abnormal for that area of the codebase (especially if called by trusted/validated codepaths)
|
|
14
14
|
- Casts to `any` or `# type: ignore` to get around type issues
|
|
15
|
+
- Unnecessary complexity and nesting that reduces readability
|
|
16
|
+
- Redundant abstractions or over-engineered solutions
|
|
15
17
|
- Any other style that is inconsistent with the file
|
|
16
18
|
|
|
19
|
+
## Principles
|
|
20
|
+
|
|
21
|
+
1. **Preserve functionality**: Never change what the code does - only how it does it
|
|
22
|
+
2. **Prefer clarity over brevity**: Explicit readable code is better than overly compact solutions
|
|
23
|
+
3. **Avoid over-simplification**: Don't create overly clever solutions that are hard to understand or debug
|
|
24
|
+
4. **Focus scope**: Only refine the specified files or recently modified code, unless instructed otherwise
|
|
25
|
+
|
|
17
26
|
Report at the end with only a 1-3 sentence summary of what you changed.
|
|
@@ -170,23 +170,3 @@ def install_system_skills() -> bool:
|
|
|
170
170
|
|
|
171
171
|
log_debug("System skills installation complete")
|
|
172
172
|
return True
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
def get_installed_system_skills() -> list[Path]:
|
|
176
|
-
"""Get list of installed system skill directories.
|
|
177
|
-
|
|
178
|
-
Returns:
|
|
179
|
-
List of paths to installed skill directories
|
|
180
|
-
"""
|
|
181
|
-
dest_dir = get_system_skills_dir()
|
|
182
|
-
if not dest_dir.exists():
|
|
183
|
-
return []
|
|
184
|
-
|
|
185
|
-
skills: list[Path] = []
|
|
186
|
-
for item in dest_dir.iterdir():
|
|
187
|
-
if item.is_dir() and not item.name.startswith("."):
|
|
188
|
-
skill_file = item / "SKILL.md"
|
|
189
|
-
if skill_file.exists():
|
|
190
|
-
skills.append(item)
|
|
191
|
-
|
|
192
|
-
return skills
|
|
@@ -6,6 +6,7 @@ from typing import Literal
|
|
|
6
6
|
from prompt_toolkit.styles import Style, merge_styles
|
|
7
7
|
|
|
8
8
|
from klaude_code.protocol import commands, events, message, model
|
|
9
|
+
from klaude_code.session import Session
|
|
9
10
|
from klaude_code.tui.input.key_bindings import copy_to_clipboard
|
|
10
11
|
from klaude_code.tui.terminal.selector import DEFAULT_PICKER_STYLE, SelectItem, select_one
|
|
11
12
|
|
|
@@ -312,7 +313,8 @@ class ForkSessionCommand(CommandABC):
|
|
|
312
313
|
new_session = agent.session.fork()
|
|
313
314
|
await new_session.wait_for_flush()
|
|
314
315
|
|
|
315
|
-
|
|
316
|
+
short_id = Session.shortest_unique_prefix(new_session.id)
|
|
317
|
+
resume_cmd = f"klaude -r {short_id}"
|
|
316
318
|
copy_to_clipboard(resume_cmd)
|
|
317
319
|
|
|
318
320
|
event = events.CommandOutputEvent(
|
|
@@ -345,7 +347,8 @@ class ForkSessionCommand(CommandABC):
|
|
|
345
347
|
else:
|
|
346
348
|
fork_description = "entire conversation" if selected == -1 else f"up to message index {selected}"
|
|
347
349
|
|
|
348
|
-
|
|
350
|
+
short_id = Session.shortest_unique_prefix(new_session.id)
|
|
351
|
+
resume_cmd = f"klaude -r {short_id}"
|
|
349
352
|
copy_to_clipboard(resume_cmd)
|
|
350
353
|
|
|
351
354
|
event = events.CommandOutputEvent(
|
|
@@ -8,9 +8,16 @@ from klaude_code.tui.terminal.selector import DEFAULT_PICKER_STYLE, SelectItem,
|
|
|
8
8
|
from .command_abc import Agent, CommandABC, CommandResult
|
|
9
9
|
|
|
10
10
|
|
|
11
|
-
def select_session_sync() -> str | None:
|
|
12
|
-
"""Interactive session selection (sync version for asyncio.to_thread).
|
|
11
|
+
def select_session_sync(session_ids: list[str] | None = None) -> str | None:
|
|
12
|
+
"""Interactive session selection (sync version for asyncio.to_thread).
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
session_ids: Optional list of session IDs to filter. If provided, only show these sessions.
|
|
16
|
+
"""
|
|
13
17
|
options = build_session_select_options()
|
|
18
|
+
if session_ids is not None:
|
|
19
|
+
session_id_set = set(session_ids)
|
|
20
|
+
options = [opt for opt in options if opt.session_id in session_id_set]
|
|
14
21
|
if not options:
|
|
15
22
|
log("No sessions found for this project.")
|
|
16
23
|
return None
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
"""Command for changing sub-agent models."""
|
|
1
|
+
"""Command for changing sub-agent models and compact model."""
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import asyncio
|
|
6
6
|
|
|
7
|
-
from klaude_code.config.config import load_config
|
|
7
|
+
from klaude_code.config.config import Config, load_config
|
|
8
8
|
from klaude_code.config.sub_agent_model_helper import SubAgentModelHelper, SubAgentModelInfo
|
|
9
9
|
from klaude_code.protocol import commands, events, message, op
|
|
10
10
|
from klaude_code.tui.terminal.selector import DEFAULT_PICKER_STYLE, SelectItem, build_model_select_items, select_one
|
|
@@ -12,16 +12,35 @@ from klaude_code.tui.terminal.selector import DEFAULT_PICKER_STYLE, SelectItem,
|
|
|
12
12
|
from .command_abc import Agent, CommandABC, CommandResult
|
|
13
13
|
|
|
14
14
|
USE_DEFAULT_BEHAVIOR = "__default__"
|
|
15
|
+
COMPACT_MODEL_OPTION = "__compact__"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _build_compact_model_item(config: Config, max_name_len: int, main_model_name: str) -> SelectItem[str]:
|
|
19
|
+
"""Build SelectItem for compact model configuration."""
|
|
20
|
+
name = "Compact"
|
|
21
|
+
model_display = config.compact_model or f"(inherit from main agent: {main_model_name})"
|
|
22
|
+
|
|
23
|
+
title = [
|
|
24
|
+
("class:msg", f"{name:<{max_name_len}}"),
|
|
25
|
+
("class:meta", f" current: {model_display}\n"),
|
|
26
|
+
]
|
|
27
|
+
return SelectItem(title=title, value=COMPACT_MODEL_OPTION, search_text="compact")
|
|
15
28
|
|
|
16
29
|
|
|
17
30
|
def _build_sub_agent_select_items(
|
|
18
31
|
sub_agents: list[SubAgentModelInfo],
|
|
19
32
|
helper: SubAgentModelHelper,
|
|
20
33
|
main_model_name: str,
|
|
34
|
+
config: Config,
|
|
21
35
|
) -> list[SelectItem[str]]:
|
|
22
|
-
"""Build SelectItem list for sub-agent selection."""
|
|
36
|
+
"""Build SelectItem list for sub-agent selection (including compact model)."""
|
|
23
37
|
items: list[SelectItem[str]] = []
|
|
38
|
+
# Include "Compact" in max_name_len calculation
|
|
24
39
|
max_name_len = max(len(sa.profile.name) for sa in sub_agents) if sub_agents else 0
|
|
40
|
+
max_name_len = max(max_name_len, len("Compact"))
|
|
41
|
+
|
|
42
|
+
# Add compact model option first
|
|
43
|
+
items.append(_build_compact_model_item(config, max_name_len, main_model_name))
|
|
25
44
|
|
|
26
45
|
for sa in sub_agents:
|
|
27
46
|
name = sa.profile.name
|
|
@@ -45,9 +64,10 @@ def _select_sub_agent_sync(
|
|
|
45
64
|
sub_agents: list[SubAgentModelInfo],
|
|
46
65
|
helper: SubAgentModelHelper,
|
|
47
66
|
main_model_name: str,
|
|
67
|
+
config: Config,
|
|
48
68
|
) -> str | None:
|
|
49
|
-
"""Synchronous sub-agent type selection."""
|
|
50
|
-
items = _build_sub_agent_select_items(sub_agents, helper, main_model_name)
|
|
69
|
+
"""Synchronous sub-agent type selection (including compact model)."""
|
|
70
|
+
items = _build_sub_agent_select_items(sub_agents, helper, main_model_name, config)
|
|
51
71
|
if not items:
|
|
52
72
|
return None
|
|
53
73
|
|
|
@@ -98,8 +118,39 @@ def _select_model_for_sub_agent_sync(
|
|
|
98
118
|
return None
|
|
99
119
|
|
|
100
120
|
|
|
121
|
+
def _select_model_for_compact_sync(
|
|
122
|
+
config: Config,
|
|
123
|
+
main_model_name: str,
|
|
124
|
+
) -> str | None:
|
|
125
|
+
"""Synchronous model selection for compact model."""
|
|
126
|
+
models = config.iter_model_entries(only_available=True, include_disabled=False)
|
|
127
|
+
|
|
128
|
+
inherit_item = SelectItem[str](
|
|
129
|
+
title=[
|
|
130
|
+
("class:msg", "(Use default behavior)"),
|
|
131
|
+
("class:meta", f" -> inherit from main agent: {main_model_name}\n"),
|
|
132
|
+
],
|
|
133
|
+
value=USE_DEFAULT_BEHAVIOR,
|
|
134
|
+
search_text="default unset",
|
|
135
|
+
)
|
|
136
|
+
model_items = build_model_select_items(models)
|
|
137
|
+
all_items = [inherit_item, *model_items]
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
result = select_one(
|
|
141
|
+
message="Select model for Compact:",
|
|
142
|
+
items=all_items,
|
|
143
|
+
pointer="→",
|
|
144
|
+
style=DEFAULT_PICKER_STYLE,
|
|
145
|
+
use_search_filter=True,
|
|
146
|
+
)
|
|
147
|
+
return result if isinstance(result, str) else None
|
|
148
|
+
except KeyboardInterrupt:
|
|
149
|
+
return None
|
|
150
|
+
|
|
151
|
+
|
|
101
152
|
class SubAgentModelCommand(CommandABC):
|
|
102
|
-
"""Configure models for sub-agents (Task, Explore,
|
|
153
|
+
"""Configure models for sub-agents (Task, Explore, Web, ImageGen) and compact model."""
|
|
103
154
|
|
|
104
155
|
@property
|
|
105
156
|
def name(self) -> commands.CommandName:
|
|
@@ -119,32 +170,48 @@ class SubAgentModelCommand(CommandABC):
|
|
|
119
170
|
main_model_name = agent.get_llm_client().model_name
|
|
120
171
|
|
|
121
172
|
sub_agents = helper.get_available_sub_agents()
|
|
122
|
-
|
|
173
|
+
|
|
174
|
+
selected_option = await asyncio.to_thread(_select_sub_agent_sync, sub_agents, helper, main_model_name, config)
|
|
175
|
+
if selected_option is None:
|
|
123
176
|
return CommandResult(
|
|
124
177
|
events=[
|
|
125
178
|
events.CommandOutputEvent(
|
|
126
179
|
session_id=agent.session.id,
|
|
127
180
|
command_name=self.name,
|
|
128
|
-
content="
|
|
129
|
-
is_error=True,
|
|
181
|
+
content="(cancelled)",
|
|
130
182
|
)
|
|
131
183
|
]
|
|
132
184
|
)
|
|
133
185
|
|
|
134
|
-
|
|
135
|
-
if
|
|
186
|
+
# Handle compact model selection
|
|
187
|
+
if selected_option == COMPACT_MODEL_OPTION:
|
|
188
|
+
selected_model = await asyncio.to_thread(_select_model_for_compact_sync, config, main_model_name)
|
|
189
|
+
if selected_model is None:
|
|
190
|
+
return CommandResult(
|
|
191
|
+
events=[
|
|
192
|
+
events.CommandOutputEvent(
|
|
193
|
+
session_id=agent.session.id,
|
|
194
|
+
command_name=self.name,
|
|
195
|
+
content="(cancelled)",
|
|
196
|
+
)
|
|
197
|
+
]
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
model_name: str | None = None if selected_model == USE_DEFAULT_BEHAVIOR else selected_model
|
|
201
|
+
|
|
136
202
|
return CommandResult(
|
|
137
|
-
|
|
138
|
-
|
|
203
|
+
operations=[
|
|
204
|
+
op.ChangeCompactModelOperation(
|
|
139
205
|
session_id=agent.session.id,
|
|
140
|
-
|
|
141
|
-
|
|
206
|
+
model_name=model_name,
|
|
207
|
+
save_as_default=True,
|
|
142
208
|
)
|
|
143
209
|
]
|
|
144
210
|
)
|
|
145
211
|
|
|
212
|
+
# Handle sub-agent model selection
|
|
146
213
|
selected_model = await asyncio.to_thread(
|
|
147
|
-
_select_model_for_sub_agent_sync, helper,
|
|
214
|
+
_select_model_for_sub_agent_sync, helper, selected_option, main_model_name
|
|
148
215
|
)
|
|
149
216
|
if selected_model is None:
|
|
150
217
|
return CommandResult(
|
|
@@ -157,13 +224,13 @@ class SubAgentModelCommand(CommandABC):
|
|
|
157
224
|
]
|
|
158
225
|
)
|
|
159
226
|
|
|
160
|
-
model_name
|
|
227
|
+
model_name = None if selected_model == USE_DEFAULT_BEHAVIOR else selected_model
|
|
161
228
|
|
|
162
229
|
return CommandResult(
|
|
163
230
|
operations=[
|
|
164
231
|
op.ChangeSubAgentModelOperation(
|
|
165
232
|
session_id=agent.session.id,
|
|
166
|
-
sub_agent_type=
|
|
233
|
+
sub_agent_type=selected_option,
|
|
167
234
|
model_name=model_name,
|
|
168
235
|
save_as_default=True,
|
|
169
236
|
)
|
|
@@ -1,28 +1,2 @@
|
|
|
1
|
-
from rich.console import RenderableType
|
|
2
|
-
from rich.padding import Padding
|
|
3
|
-
from rich.text import Text
|
|
4
|
-
|
|
5
|
-
from klaude_code.const import MARKDOWN_RIGHT_MARGIN
|
|
6
|
-
from klaude_code.tui.components.common import create_grid
|
|
7
|
-
from klaude_code.tui.components.rich.markdown import NoInsetMarkdown
|
|
8
|
-
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
9
|
-
|
|
10
1
|
# UI markers
|
|
11
2
|
ASSISTANT_MESSAGE_MARK = "•"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def render_assistant_message(content: str, *, code_theme: str) -> RenderableType | None:
|
|
15
|
-
"""Render assistant message for replay history display.
|
|
16
|
-
|
|
17
|
-
Returns None if content is empty.
|
|
18
|
-
"""
|
|
19
|
-
stripped = content.strip()
|
|
20
|
-
if len(stripped) == 0:
|
|
21
|
-
return None
|
|
22
|
-
|
|
23
|
-
grid = create_grid()
|
|
24
|
-
grid.add_row(
|
|
25
|
-
Text(ASSISTANT_MESSAGE_MARK, style=ThemeKey.ASSISTANT_MESSAGE_MARK),
|
|
26
|
-
Padding(NoInsetMarkdown(stripped, code_theme=code_theme), (0, MARKDOWN_RIGHT_MARGIN, 0, 0)),
|
|
27
|
-
)
|
|
28
|
-
return grid
|
|
@@ -187,6 +187,10 @@ def highlight_bash_command(command: str) -> Text:
|
|
|
187
187
|
expect_subcommand = False
|
|
188
188
|
elif token_type in (Token.Text.Whitespace,):
|
|
189
189
|
result.append(token_value)
|
|
190
|
+
# Newline starts a new command context (like ; or &&)
|
|
191
|
+
if "\n" in token_value:
|
|
192
|
+
expect_command = True
|
|
193
|
+
expect_subcommand = False
|
|
190
194
|
elif token_type == Token.Name.Builtin:
|
|
191
195
|
# Built-in commands are always commands
|
|
192
196
|
result.append(token_value, style=ThemeKey.BASH_COMMAND)
|
|
@@ -4,6 +4,7 @@ from rich.table import Table
|
|
|
4
4
|
from rich.text import Text
|
|
5
5
|
|
|
6
6
|
from klaude_code.protocol import events, model
|
|
7
|
+
from klaude_code.session import Session
|
|
7
8
|
from klaude_code.tui.components.common import truncate_middle
|
|
8
9
|
from klaude_code.tui.components.rich.theme import ThemeKey
|
|
9
10
|
|
|
@@ -47,10 +48,11 @@ def _render_fork_session_output(e: events.CommandOutputEvent) -> RenderableType:
|
|
|
47
48
|
|
|
48
49
|
grid = Table.grid(padding=(0, 1))
|
|
49
50
|
session_id = e.ui_extra.session_id
|
|
51
|
+
short_id = Session.shortest_unique_prefix(session_id)
|
|
50
52
|
grid.add_column(style=ThemeKey.TOOL_RESULT, overflow="fold")
|
|
51
53
|
|
|
52
54
|
grid.add_row(Text("Session forked. Resume command copied to clipboard:", style=ThemeKey.TOOL_RESULT))
|
|
53
|
-
grid.add_row(Text(f" klaude
|
|
55
|
+
grid.add_row(Text(f" klaude -r {short_id}", style=ThemeKey.TOOL_RESULT_BOLD))
|
|
54
56
|
|
|
55
57
|
return Padding.indent(grid, level=2)
|
|
56
58
|
|
|
@@ -115,5 +115,8 @@ def render_developer_message(e: events.DeveloperMessageEvent) -> RenderableType:
|
|
|
115
115
|
),
|
|
116
116
|
)
|
|
117
117
|
parts.append(grid)
|
|
118
|
+
case model.AtFileImagesUIItem():
|
|
119
|
+
# Image display is handled by renderer.display_developer_message
|
|
120
|
+
pass
|
|
118
121
|
|
|
119
122
|
return Group(*parts) if parts else Text("")
|