klaude-code 2.6.0__py3-none-any.whl → 2.8.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.
Files changed (82) hide show
  1. klaude_code/app/runtime.py +1 -1
  2. klaude_code/auth/AGENTS.md +325 -0
  3. klaude_code/auth/__init__.py +17 -1
  4. klaude_code/auth/antigravity/__init__.py +20 -0
  5. klaude_code/auth/antigravity/exceptions.py +17 -0
  6. klaude_code/auth/antigravity/oauth.py +320 -0
  7. klaude_code/auth/antigravity/pkce.py +25 -0
  8. klaude_code/auth/antigravity/token_manager.py +45 -0
  9. klaude_code/auth/base.py +4 -0
  10. klaude_code/auth/claude/oauth.py +29 -9
  11. klaude_code/auth/codex/exceptions.py +4 -0
  12. klaude_code/auth/env.py +19 -15
  13. klaude_code/cli/auth_cmd.py +54 -4
  14. klaude_code/cli/cost_cmd.py +83 -160
  15. klaude_code/cli/list_model.py +50 -0
  16. klaude_code/cli/main.py +99 -9
  17. klaude_code/config/assets/builtin_config.yaml +108 -0
  18. klaude_code/config/builtin_config.py +5 -11
  19. klaude_code/config/config.py +24 -10
  20. klaude_code/const.py +11 -1
  21. klaude_code/core/agent.py +5 -1
  22. klaude_code/core/agent_profile.py +28 -32
  23. klaude_code/core/compaction/AGENTS.md +112 -0
  24. klaude_code/core/compaction/__init__.py +11 -0
  25. klaude_code/core/compaction/compaction.py +707 -0
  26. klaude_code/core/compaction/overflow.py +30 -0
  27. klaude_code/core/compaction/prompts.py +97 -0
  28. klaude_code/core/executor.py +103 -2
  29. klaude_code/core/manager/llm_clients.py +5 -0
  30. klaude_code/core/manager/llm_clients_builder.py +14 -2
  31. klaude_code/core/prompts/prompt-antigravity.md +80 -0
  32. klaude_code/core/prompts/prompt-codex-gpt-5-2.md +335 -0
  33. klaude_code/core/reminders.py +11 -7
  34. klaude_code/core/task.py +126 -0
  35. klaude_code/core/tool/todo/todo_write_tool.py +1 -1
  36. klaude_code/core/turn.py +3 -1
  37. klaude_code/llm/antigravity/__init__.py +3 -0
  38. klaude_code/llm/antigravity/client.py +558 -0
  39. klaude_code/llm/antigravity/input.py +261 -0
  40. klaude_code/llm/registry.py +1 -0
  41. klaude_code/protocol/commands.py +0 -1
  42. klaude_code/protocol/events.py +18 -0
  43. klaude_code/protocol/llm_param.py +1 -0
  44. klaude_code/protocol/message.py +23 -1
  45. klaude_code/protocol/op.py +15 -1
  46. klaude_code/protocol/op_handler.py +5 -0
  47. klaude_code/session/session.py +36 -0
  48. klaude_code/skill/assets/create-plan/SKILL.md +6 -6
  49. klaude_code/skill/loader.py +12 -13
  50. klaude_code/skill/manager.py +3 -3
  51. klaude_code/tui/command/__init__.py +4 -4
  52. klaude_code/tui/command/compact_cmd.py +32 -0
  53. klaude_code/tui/command/copy_cmd.py +1 -1
  54. klaude_code/tui/command/fork_session_cmd.py +114 -18
  55. klaude_code/tui/command/model_picker.py +5 -1
  56. klaude_code/tui/command/thinking_cmd.py +1 -1
  57. klaude_code/tui/commands.py +6 -0
  58. klaude_code/tui/components/command_output.py +1 -1
  59. klaude_code/tui/components/rich/markdown.py +117 -1
  60. klaude_code/tui/components/rich/theme.py +18 -2
  61. klaude_code/tui/components/tools.py +39 -25
  62. klaude_code/tui/components/user_input.py +39 -28
  63. klaude_code/tui/input/AGENTS.md +44 -0
  64. klaude_code/tui/input/__init__.py +5 -2
  65. klaude_code/tui/input/completers.py +10 -14
  66. klaude_code/tui/input/drag_drop.py +146 -0
  67. klaude_code/tui/input/images.py +227 -0
  68. klaude_code/tui/input/key_bindings.py +183 -19
  69. klaude_code/tui/input/paste.py +71 -0
  70. klaude_code/tui/input/prompt_toolkit.py +32 -9
  71. klaude_code/tui/machine.py +26 -1
  72. klaude_code/tui/renderer.py +67 -4
  73. klaude_code/tui/runner.py +19 -3
  74. klaude_code/tui/terminal/image.py +103 -10
  75. klaude_code/tui/terminal/selector.py +81 -7
  76. {klaude_code-2.6.0.dist-info → klaude_code-2.8.0.dist-info}/METADATA +10 -10
  77. {klaude_code-2.6.0.dist-info → klaude_code-2.8.0.dist-info}/RECORD +79 -61
  78. klaude_code/core/prompts/prompt-codex-gpt-5-1-codex-max.md +0 -117
  79. klaude_code/tui/command/terminal_setup_cmd.py +0 -248
  80. klaude_code/tui/input/clipboard.py +0 -152
  81. {klaude_code-2.6.0.dist-info → klaude_code-2.8.0.dist-info}/WHEEL +0 -0
  82. {klaude_code-2.6.0.dist-info → klaude_code-2.8.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,261 @@
1
+ """Message conversion utilities for Antigravity Cloud Code Assist API."""
2
+
3
+ import json
4
+ from base64 import b64decode
5
+ from binascii import Error as BinasciiError
6
+ from typing import Any, TypedDict
7
+
8
+ from klaude_code.const import EMPTY_TOOL_OUTPUT_MESSAGE
9
+ from klaude_code.llm.image import assistant_image_to_data_url, parse_data_url
10
+ from klaude_code.llm.input_common import (
11
+ DeveloperAttachment,
12
+ attach_developer_messages,
13
+ merge_reminder_text,
14
+ split_thinking_parts,
15
+ )
16
+ from klaude_code.protocol import llm_param, message
17
+
18
+
19
+ class InlineData(TypedDict, total=False):
20
+ mimeType: str
21
+ data: str
22
+
23
+
24
+ class FunctionCall(TypedDict, total=False):
25
+ id: str
26
+ name: str
27
+ args: dict[str, Any]
28
+
29
+
30
+ class FunctionResponse(TypedDict, total=False):
31
+ id: str
32
+ name: str
33
+ response: dict[str, Any]
34
+ parts: list[dict[str, Any]]
35
+
36
+
37
+ class Part(TypedDict, total=False):
38
+ text: str
39
+ thought: bool
40
+ thoughtSignature: str
41
+ inlineData: InlineData
42
+ functionCall: FunctionCall
43
+ functionResponse: FunctionResponse
44
+
45
+
46
+ class Content(TypedDict):
47
+ role: str
48
+ parts: list[Part]
49
+
50
+
51
+ class FunctionDeclaration(TypedDict, total=False):
52
+ name: str
53
+ description: str
54
+ parameters: dict[str, Any]
55
+
56
+
57
+ class Tool(TypedDict):
58
+ functionDeclarations: list[FunctionDeclaration]
59
+
60
+
61
+ def _data_url_to_inline_data(url: str) -> InlineData:
62
+ """Convert data URL to inline_data dict."""
63
+ media_type, _, decoded = parse_data_url(url)
64
+ import base64
65
+
66
+ return InlineData(mimeType=media_type, data=base64.b64encode(decoded).decode("ascii"))
67
+
68
+
69
+ def _image_part_to_part(image: message.ImageURLPart) -> Part:
70
+ """Convert ImageURLPart to Part dict."""
71
+ url = image.url
72
+ if url.startswith("data:"):
73
+ return Part(inlineData=_data_url_to_inline_data(url))
74
+ # For non-data URLs, best-effort using inline_data format
75
+ return Part(text=f"[Image: {url}]")
76
+
77
+
78
+ def _user_message_to_content(msg: message.UserMessage, attachment: DeveloperAttachment) -> Content:
79
+ """Convert UserMessage to Content dict."""
80
+ parts: list[Part] = []
81
+ for part in msg.parts:
82
+ if isinstance(part, message.TextPart):
83
+ parts.append(Part(text=part.text))
84
+ elif isinstance(part, message.ImageURLPart):
85
+ parts.append(_image_part_to_part(part))
86
+ if attachment.text:
87
+ parts.append(Part(text=attachment.text))
88
+ for image in attachment.images:
89
+ parts.append(_image_part_to_part(image))
90
+ if not parts:
91
+ parts.append(Part(text=""))
92
+ return Content(role="user", parts=parts)
93
+
94
+
95
+ def _tool_messages_to_contents(
96
+ msgs: list[tuple[message.ToolResultMessage, DeveloperAttachment]], model_name: str | None
97
+ ) -> list[Content]:
98
+ """Convert tool result messages to Content dicts."""
99
+ supports_multimodal_function_response = bool(model_name and "gemini-3" in model_name.lower())
100
+
101
+ response_parts: list[Part] = []
102
+ extra_image_contents: list[Content] = []
103
+
104
+ for msg, attachment in msgs:
105
+ merged_text = merge_reminder_text(
106
+ msg.output_text or EMPTY_TOOL_OUTPUT_MESSAGE,
107
+ attachment.text,
108
+ )
109
+ has_text = merged_text.strip() != ""
110
+
111
+ images = [part for part in msg.parts if isinstance(part, message.ImageURLPart)] + attachment.images
112
+ image_parts: list[Part] = []
113
+ function_response_parts: list[dict[str, Any]] = []
114
+
115
+ for image in images:
116
+ try:
117
+ image_parts.append(_image_part_to_part(image))
118
+ if image.url.startswith("data:"):
119
+ inline_data = _data_url_to_inline_data(image.url)
120
+ function_response_parts.append({"inlineData": inline_data})
121
+ except ValueError:
122
+ continue
123
+
124
+ has_images = len(image_parts) > 0
125
+ response_value = merged_text if has_text else "(see attached image)" if has_images else ""
126
+ response_payload = {"error": response_value} if msg.status != "success" else {"output": response_value}
127
+
128
+ function_response = FunctionResponse(
129
+ id=msg.call_id,
130
+ name=msg.tool_name,
131
+ response=response_payload,
132
+ )
133
+ if has_images and supports_multimodal_function_response:
134
+ function_response["parts"] = function_response_parts
135
+
136
+ response_parts.append(Part(functionResponse=function_response))
137
+
138
+ if has_images and not supports_multimodal_function_response:
139
+ extra_image_contents.append(Content(role="user", parts=[Part(text="Tool result image:"), *image_parts]))
140
+
141
+ contents: list[Content] = []
142
+ if response_parts:
143
+ contents.append(Content(role="user", parts=response_parts))
144
+ contents.extend(extra_image_contents)
145
+ return contents
146
+
147
+
148
+ def _decode_thought_signature(sig: str | None) -> str | None:
149
+ """Validate base64 thought signature."""
150
+ if not sig:
151
+ return None
152
+ try:
153
+ b64decode(sig)
154
+ return sig
155
+ except (BinasciiError, ValueError):
156
+ return None
157
+
158
+
159
+ def _assistant_message_to_content(msg: message.AssistantMessage, model_name: str | None) -> Content | None:
160
+ """Convert AssistantMessage to Content dict."""
161
+ parts: list[Part] = []
162
+ native_thinking_parts, degraded_thinking_texts = split_thinking_parts(msg, model_name)
163
+ native_thinking_ids = {id(part) for part in native_thinking_parts}
164
+
165
+ for part in msg.parts:
166
+ if isinstance(part, message.ThinkingTextPart):
167
+ if id(part) not in native_thinking_ids:
168
+ continue
169
+ parts.append(Part(text=part.text, thought=True))
170
+
171
+ elif isinstance(part, message.ThinkingSignaturePart):
172
+ if id(part) not in native_thinking_ids:
173
+ continue
174
+ if not part.signature or part.format != "google":
175
+ continue
176
+ # Attach signature to the previous part
177
+ if parts:
178
+ sig = _decode_thought_signature(part.signature)
179
+ if sig:
180
+ parts[-1]["thoughtSignature"] = sig
181
+
182
+ elif isinstance(part, message.TextPart):
183
+ # Skip empty text blocks
184
+ if not part.text or part.text.strip() == "":
185
+ continue
186
+ parts.append(Part(text=part.text))
187
+
188
+ elif isinstance(part, message.ToolCallPart):
189
+ args: dict[str, Any]
190
+ if part.arguments_json:
191
+ try:
192
+ args = json.loads(part.arguments_json)
193
+ except json.JSONDecodeError:
194
+ args = {"_raw": part.arguments_json}
195
+ else:
196
+ args = {}
197
+ parts.append(Part(functionCall=FunctionCall(id=part.call_id, name=part.tool_name, args=args)))
198
+
199
+ elif isinstance(part, message.ImageFilePart):
200
+ try:
201
+ data_url = assistant_image_to_data_url(part)
202
+ parts.append(_image_part_to_part(message.ImageURLPart(url=data_url)))
203
+ except (ValueError, FileNotFoundError):
204
+ pass
205
+
206
+ if degraded_thinking_texts:
207
+ parts.insert(0, Part(text="<thinking>\n" + "\n".join(degraded_thinking_texts) + "\n</thinking>"))
208
+
209
+ if not parts:
210
+ return None
211
+ return Content(role="model", parts=parts)
212
+
213
+
214
+ def convert_history_to_contents(
215
+ history: list[message.Message],
216
+ model_name: str | None,
217
+ ) -> list[Content]:
218
+ """Convert message history to Cloud Code Assist Content format."""
219
+ contents: list[Content] = []
220
+ pending_tool_messages: list[tuple[message.ToolResultMessage, DeveloperAttachment]] = []
221
+
222
+ def flush_tool_messages() -> None:
223
+ nonlocal pending_tool_messages
224
+ if pending_tool_messages:
225
+ contents.extend(_tool_messages_to_contents(pending_tool_messages, model_name=model_name))
226
+ pending_tool_messages = []
227
+
228
+ for msg, attachment in attach_developer_messages(history):
229
+ match msg:
230
+ case message.ToolResultMessage():
231
+ pending_tool_messages.append((msg, attachment))
232
+ case message.UserMessage():
233
+ flush_tool_messages()
234
+ contents.append(_user_message_to_content(msg, attachment))
235
+ case message.AssistantMessage():
236
+ flush_tool_messages()
237
+ content = _assistant_message_to_content(msg, model_name=model_name)
238
+ if content is not None:
239
+ contents.append(content)
240
+ case message.SystemMessage():
241
+ continue
242
+ case _:
243
+ continue
244
+
245
+ flush_tool_messages()
246
+ return contents
247
+
248
+
249
+ def convert_tool_schema(tools: list[llm_param.ToolSchema] | None) -> list[Tool] | None:
250
+ """Convert tool schemas to Cloud Code Assist Tool format."""
251
+ if tools is None or len(tools) == 0:
252
+ return None
253
+ declarations = [
254
+ FunctionDeclaration(
255
+ name=tool.name,
256
+ description=tool.description,
257
+ parameters=dict(tool.parameters) if tool.parameters else {},
258
+ )
259
+ for tool in tools
260
+ ]
261
+ return [Tool(functionDeclarations=declarations)]
@@ -21,6 +21,7 @@ _PROTOCOL_MODULES: dict[llm_param.LLMClientProtocol, str] = {
21
21
  llm_param.LLMClientProtocol.OPENROUTER: "klaude_code.llm.openrouter",
22
22
  llm_param.LLMClientProtocol.RESPONSES: "klaude_code.llm.responses",
23
23
  llm_param.LLMClientProtocol.GOOGLE: "klaude_code.llm.google",
24
+ llm_param.LLMClientProtocol.ANTIGRAVITY: "klaude_code.llm.antigravity",
24
25
  }
25
26
 
26
27
 
@@ -20,7 +20,6 @@ class CommandName(str, Enum):
20
20
  COMPACT = "compact"
21
21
  REFRESH_TERMINAL = "refresh-terminal"
22
22
  CLEAR = "clear"
23
- TERMINAL_SETUP = "terminal-setup"
24
23
  EXPORT = "export"
25
24
  EXPORT_ONLINE = "export-online"
26
25
  STATUS = "status"
@@ -14,6 +14,8 @@ __all__ = [
14
14
  "AssistantTextEndEvent",
15
15
  "AssistantTextStartEvent",
16
16
  "CommandOutputEvent",
17
+ "CompactionEndEvent",
18
+ "CompactionStartEvent",
17
19
  "DeveloperMessageEvent",
18
20
  "EndEvent",
19
21
  "ErrorEvent",
@@ -83,6 +85,20 @@ class TaskStartEvent(Event):
83
85
  model_id: str | None = None
84
86
 
85
87
 
88
+ class CompactionStartEvent(Event):
89
+ reason: Literal["threshold", "overflow", "manual"]
90
+
91
+
92
+ class CompactionEndEvent(Event):
93
+ reason: Literal["threshold", "overflow", "manual"]
94
+ aborted: bool = False
95
+ will_retry: bool = False
96
+ tokens_before: int | None = None
97
+ kept_from_index: int | None = None
98
+ summary: str | None = None
99
+ kept_items_brief: list[message.KeptItemBrief] = Field(default_factory=list) # pyright: ignore[reportUnknownVariableType]
100
+
101
+
86
102
  class TaskFinishEvent(Event):
87
103
  task_result: str
88
104
  has_structured_output: bool = False
@@ -185,6 +201,8 @@ type ReplayEventUnion = (
185
201
  | InterruptEvent
186
202
  | DeveloperMessageEvent
187
203
  | ErrorEvent
204
+ | CompactionStartEvent
205
+ | CompactionEndEvent
188
206
  )
189
207
 
190
208
 
@@ -16,6 +16,7 @@ class LLMClientProtocol(Enum):
16
16
  BEDROCK = "bedrock"
17
17
  CODEX_OAUTH = "codex_oauth"
18
18
  GOOGLE = "google"
19
+ ANTIGRAVITY = "antigravity"
19
20
 
20
21
 
21
22
  class ToolSchema(BaseModel):
@@ -63,6 +63,28 @@ class StreamErrorItem(BaseModel):
63
63
  created_at: datetime = Field(default_factory=datetime.now)
64
64
 
65
65
 
66
+ class CompactionDetails(BaseModel):
67
+ read_files: list[str] = Field(default_factory=list)
68
+ modified_files: list[str] = Field(default_factory=list)
69
+
70
+
71
+ class KeptItemBrief(BaseModel):
72
+ """Brief info about a kept (non-compacted) message item."""
73
+
74
+ item_type: str # "User", "Assistant", "Read", "Edit", "Bash", etc.
75
+ count: int = 1
76
+ preview: str = "" # Short preview text
77
+
78
+
79
+ class CompactionEntry(BaseModel):
80
+ summary: str
81
+ first_kept_index: int
82
+ tokens_before: int | None = None
83
+ details: CompactionDetails | None = None
84
+ kept_items_brief: list[KeptItemBrief] = Field(default_factory=list) # pyright: ignore[reportUnknownVariableType]
85
+ created_at: datetime = Field(default_factory=datetime.now)
86
+
87
+
66
88
  # Part types
67
89
 
68
90
 
@@ -173,7 +195,7 @@ class ToolResultMessage(MessageBase):
173
195
 
174
196
  Message = SystemMessage | DeveloperMessage | UserMessage | AssistantMessage | ToolResultMessage
175
197
 
176
- HistoryEvent = Message | StreamErrorItem | TaskMetadataItem
198
+ HistoryEvent = Message | StreamErrorItem | TaskMetadataItem | CompactionEntry
177
199
 
178
200
  StreamItem = AssistantTextDelta | AssistantImageDelta | ThinkingTextDelta | ToolCallStartDelta
179
201
 
@@ -8,7 +8,7 @@ that the executor uses to handle different types of requests.
8
8
  from __future__ import annotations
9
9
 
10
10
  from enum import Enum
11
- from typing import TYPE_CHECKING
11
+ from typing import TYPE_CHECKING, Literal
12
12
  from uuid import uuid4
13
13
 
14
14
  from pydantic import BaseModel, Field
@@ -24,6 +24,7 @@ class OperationType(Enum):
24
24
  """Enumeration of supported operation types."""
25
25
 
26
26
  RUN_AGENT = "run_agent"
27
+ COMPACT_SESSION = "compact_session"
27
28
  CHANGE_MODEL = "change_model"
28
29
  CHANGE_SUB_AGENT_MODEL = "change_sub_agent_model"
29
30
  CHANGE_THINKING = "change_thinking"
@@ -57,6 +58,19 @@ class RunAgentOperation(Operation):
57
58
  await handler.handle_run_agent(self)
58
59
 
59
60
 
61
+ class CompactSessionOperation(Operation):
62
+ """Operation for compacting a session's conversation history."""
63
+
64
+ type: OperationType = OperationType.COMPACT_SESSION
65
+ session_id: str
66
+ reason: Literal["threshold", "overflow", "manual"]
67
+ focus: str | None = None
68
+ will_retry: bool = False
69
+
70
+ async def execute(self, handler: OperationHandler) -> None:
71
+ await handler.handle_compact_session(self)
72
+
73
+
60
74
  class ChangeModelOperation(Operation):
61
75
  """Operation for changing the model used by the active agent session."""
62
76
 
@@ -14,6 +14,7 @@ if TYPE_CHECKING:
14
14
  ChangeSubAgentModelOperation,
15
15
  ChangeThinkingOperation,
16
16
  ClearSessionOperation,
17
+ CompactSessionOperation,
17
18
  ExportSessionOperation,
18
19
  InitAgentOperation,
19
20
  InterruptOperation,
@@ -29,6 +30,10 @@ class OperationHandler(Protocol):
29
30
  """Handle a run agent operation."""
30
31
  ...
31
32
 
33
+ async def handle_compact_session(self, operation: CompactSessionOperation) -> None:
34
+ """Handle a compact session operation."""
35
+ ...
36
+
32
37
  async def handle_change_model(self, operation: ChangeModelOperation) -> None:
33
38
  """Handle a change model operation."""
34
39
  ...
@@ -228,6 +228,30 @@ class Session(BaseModel):
228
228
  )
229
229
  self._store.append_and_flush(session_id=self.id, items=items, meta=meta)
230
230
 
231
+ def get_llm_history(self) -> list[message.HistoryEvent]:
232
+ """Return the LLM-facing history view with compaction summary injected."""
233
+ history = self.conversation_history
234
+ last_compaction: message.CompactionEntry | None = None
235
+ for item in reversed(history):
236
+ if isinstance(item, message.CompactionEntry):
237
+ last_compaction = item
238
+ break
239
+ if last_compaction is None:
240
+ return [it for it in history if not isinstance(it, message.CompactionEntry)]
241
+
242
+ summary_message = message.UserMessage(parts=[message.TextPart(text=last_compaction.summary)])
243
+ kept = [it for it in history[last_compaction.first_kept_index :] if not isinstance(it, message.CompactionEntry)]
244
+
245
+ # Guard against old/bad persisted compaction boundaries that start with tool results.
246
+ # Tool results must not appear without their corresponding assistant tool call.
247
+ if kept and isinstance(kept[0], message.ToolResultMessage):
248
+ first_non_tool = 0
249
+ while first_non_tool < len(kept) and isinstance(kept[first_non_tool], message.ToolResultMessage):
250
+ first_non_tool += 1
251
+ kept = kept[first_non_tool:]
252
+
253
+ return [summary_message, *kept]
254
+
231
255
  def fork(self, *, new_id: str | None = None, until_index: int | None = None) -> Session:
232
256
  """Create a new session as a fork of the current session.
233
257
 
@@ -399,6 +423,18 @@ class Session(BaseModel):
399
423
  yield events.DeveloperMessageEvent(session_id=self.id, item=dm)
400
424
  case message.StreamErrorItem() as se:
401
425
  yield events.ErrorEvent(error_message=se.error, can_retry=False, session_id=self.id)
426
+ case message.CompactionEntry() as ce:
427
+ yield events.CompactionStartEvent(session_id=self.id, reason="threshold")
428
+ yield events.CompactionEndEvent(
429
+ session_id=self.id,
430
+ reason="threshold",
431
+ aborted=False,
432
+ will_retry=False,
433
+ tokens_before=ce.tokens_before,
434
+ kept_from_index=ce.first_kept_index,
435
+ summary=ce.summary,
436
+ kept_items_brief=ce.kept_items_brief,
437
+ )
402
438
  case message.SystemMessage():
403
439
  pass
404
440
  prev_item = it
@@ -49,12 +49,12 @@ Throughout the entire workflow, operate in read-only mode. Do not write or updat
49
49
  - Out:
50
50
 
51
51
  ## Action items
52
- [ ] <Step 1>
53
- [ ] <Step 2>
54
- [ ] <Step 3>
55
- [ ] <Step 4>
56
- [ ] <Step 5>
57
- [ ] <Step 6>
52
+ - [ ] <Step 1>
53
+ - [ ] <Step 2>
54
+ - [ ] <Step 3>
55
+ - [ ] <Step 4>
56
+ - [ ] <Step 5>
57
+ - [ ] <Step 6>
58
58
 
59
59
  ## Open questions
60
60
  - <Question 1>
@@ -209,22 +209,21 @@ class SkillLoader:
209
209
  """Get list of all loaded skill names"""
210
210
  return list(self.loaded_skills.keys())
211
211
 
212
- def get_skills_xml(self) -> str:
213
- """Generate Level 1 metadata in XML format for tool description
212
+ def get_skills_yaml(self) -> str:
213
+ """Generate skill metadata in YAML format for system prompt.
214
214
 
215
215
  Returns:
216
- XML string with all skill metadata
216
+ YAML string with all skill metadata
217
217
  """
218
- xml_parts: list[str] = []
219
- # Prefer showing higher-priority skills first (project > user > system).
218
+ yaml_parts: list[str] = []
220
219
  location_order = {"project": 0, "user": 1, "system": 2}
221
220
  for skill in sorted(self.loaded_skills.values(), key=lambda s: location_order.get(s.location, 3)):
222
- xml_parts.append(
223
- f"""<skill>
224
- <name>{skill.name}</name>
225
- <description>{skill.description}</description>
226
- <scope>{skill.location}</scope>
227
- <location>{skill.skill_path}</location>
228
- </skill>"""
221
+ # Escape description for YAML (handle multi-line and special chars)
222
+ desc = skill.description.replace("\n", " ").strip()
223
+ yaml_parts.append(
224
+ f"- name: {skill.name}\n"
225
+ f" description: {desc}\n"
226
+ f" scope: {skill.location}\n"
227
+ f" location: {skill.skill_path}"
229
228
  )
230
- return "\n".join(xml_parts)
229
+ return "\n".join(yaml_parts)
@@ -80,8 +80,8 @@ def format_available_skills_for_system_prompt() -> str:
80
80
 
81
81
  try:
82
82
  loader = _ensure_initialized()
83
- skills_xml = loader.get_skills_xml().strip()
84
- if not skills_xml:
83
+ skills_yaml = loader.get_skills_yaml().strip()
84
+ if not skills_yaml:
85
85
  return ""
86
86
 
87
87
  return f"""
@@ -102,7 +102,7 @@ Important:
102
102
  The list below is metadata only (name/description/location). The full instructions live in the referenced file.
103
103
 
104
104
  <available_skills>
105
- {skills_xml}
105
+ {skills_yaml}
106
106
  </available_skills>"""
107
107
  except Exception:
108
108
  # Skills are an optional enhancement; do not fail prompt construction if discovery breaks.
@@ -30,6 +30,7 @@ def ensure_commands_loaded() -> None:
30
30
 
31
31
  # Import and register commands in display order
32
32
  from .clear_cmd import ClearCommand
33
+ from .compact_cmd import CompactCommand
33
34
  from .copy_cmd import CopyCommand
34
35
  from .debug_cmd import DebugCommand
35
36
  from .export_cmd import ExportCommand
@@ -40,12 +41,12 @@ def ensure_commands_loaded() -> None:
40
41
  from .resume_cmd import ResumeCommand
41
42
  from .status_cmd import StatusCommand
42
43
  from .sub_agent_model_cmd import SubAgentModelCommand
43
- from .terminal_setup_cmd import TerminalSetupCommand
44
44
  from .thinking_cmd import ThinkingCommand
45
45
 
46
46
  # Register in desired display order
47
47
  register(CopyCommand())
48
48
  register(ExportCommand())
49
+ register(CompactCommand())
49
50
  register(RefreshTerminalCommand())
50
51
  register(ModelCommand())
51
52
  register(SubAgentModelCommand())
@@ -55,7 +56,6 @@ def ensure_commands_loaded() -> None:
55
56
  register(StatusCommand())
56
57
  register(ResumeCommand())
57
58
  register(ExportOnlineCommand())
58
- register(TerminalSetupCommand())
59
59
  register(DebugCommand())
60
60
  register(ClearCommand())
61
61
 
@@ -66,6 +66,7 @@ def ensure_commands_loaded() -> None:
66
66
  def __getattr__(name: str) -> object:
67
67
  _commands_map = {
68
68
  "ClearCommand": "clear_cmd",
69
+ "CompactCommand": "compact_cmd",
69
70
  "CopyCommand": "copy_cmd",
70
71
  "DebugCommand": "debug_cmd",
71
72
  "ExportCommand": "export_cmd",
@@ -76,7 +77,6 @@ def __getattr__(name: str) -> object:
76
77
  "ResumeCommand": "resume_cmd",
77
78
  "StatusCommand": "status_cmd",
78
79
  "SubAgentModelCommand": "sub_agent_model_cmd",
79
- "TerminalSetupCommand": "terminal_setup_cmd",
80
80
  "ThinkingCommand": "thinking_cmd",
81
81
  }
82
82
  if name in _commands_map:
@@ -91,7 +91,7 @@ __all__ = [
91
91
  # Command classes are lazily loaded via __getattr__
92
92
  # "ClearCommand", "DiffCommand", "HelpCommand", "ModelCommand",
93
93
  # "ExportCommand", "RefreshTerminalCommand", "ReleaseNotesCommand",
94
- # "StatusCommand", "TerminalSetupCommand",
94
+ # "StatusCommand",
95
95
  "CommandABC",
96
96
  "CommandResult",
97
97
  "dispatch_command",
@@ -0,0 +1,32 @@
1
+ from klaude_code.protocol import commands, message, op
2
+ from klaude_code.tui.command.command_abc import Agent, CommandABC, CommandResult
3
+
4
+
5
+ class CompactCommand(CommandABC):
6
+ @property
7
+ def name(self) -> commands.CommandName:
8
+ return commands.CommandName.COMPACT
9
+
10
+ @property
11
+ def summary(self) -> str:
12
+ return "summarize older context to free up the model window"
13
+
14
+ @property
15
+ def support_addition_params(self) -> bool:
16
+ return True
17
+
18
+ @property
19
+ def placeholder(self) -> str:
20
+ return "optional focus for the summary"
21
+
22
+ async def run(self, agent: Agent, user_input: message.UserInputPayload) -> CommandResult:
23
+ focus = user_input.text.strip() if user_input.text else None
24
+ return CommandResult(
25
+ operations=[
26
+ op.CompactSessionOperation(
27
+ session_id=agent.session.id,
28
+ reason="manual",
29
+ focus=focus or None,
30
+ )
31
+ ]
32
+ )
@@ -1,5 +1,5 @@
1
1
  from klaude_code.protocol import commands, events, message
2
- from klaude_code.tui.input.clipboard import copy_to_clipboard
2
+ from klaude_code.tui.input.key_bindings import copy_to_clipboard
3
3
 
4
4
  from .command_abc import Agent, CommandABC, CommandResult
5
5