Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions src/kimi_cli/acp/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,23 @@ def acp_blocks_to_content_parts(prompt: list[ACPContentBlock]) -> list[ContentPa
)
)
)
case acp.schema.EmbeddedResourceContentBlock():
resource = block.resource
if isinstance(resource, acp.schema.TextResourceContents):
uri = resource.uri
text = resource.text
content.append(TextPart(text=f"<resource uri={uri!r}>\n{text}\n</resource>"))
else:
logger.warning(
"Unsupported embedded resource type: {type}",
type=type(resource).__name__,
)
Comment on lines +37 to +41
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider adding a test for non-text embedded resource types (e.g., BlobResourceContents) to verify that the warning is properly logged and the block is gracefully skipped. This would ensure the unsupported resource type handling path is tested.

Copilot uses AI. Check for mistakes.
case acp.schema.ResourceContentBlock():
# ResourceContentBlock is a link reference without inline content;
# include the URI so the model is at least aware of the reference.
content.append(
TextPart(text=f"<resource_link uri={block.uri!r} name={block.name!r} />")
Comment on lines +45 to +46
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider handling the case where block.name might be None or empty. If name is None, the current code will format it as name='None' in the XML tag, which may not be the intended behavior. Consider using block.name or block.uri.split('/')[-1] to extract a filename from the URI as a fallback, or omitting the name attribute if it's None.

Suggested change
content.append(
TextPart(text=f"<resource_link uri={block.uri!r} name={block.name!r} />")
name = block.name or (block.uri.split("/")[-1] if getattr(block, "uri", None) else None)
name_attr = f" name={name!r}" if name else ""
content.append(
TextPart(text=f"<resource_link uri={block.uri!r}{name_attr} />")

Copilot uses AI. Check for mistakes.
)
case _:
logger.warning("Unsupported prompt content block: {block}", block=block)
return content
Expand Down
2 changes: 1 addition & 1 deletion src/kimi_cli/acp/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ async def initialize(
agent_capabilities=acp.schema.AgentCapabilities(
load_session=True,
prompt_capabilities=acp.schema.PromptCapabilities(
embedded_context=False, image=True, audio=False
embedded_context=True, image=True, audio=False
),
mcp_capabilities=acp.schema.McpCapabilities(http=True, sse=False),
session_capabilities=acp.schema.SessionCapabilities(
Expand Down
63 changes: 61 additions & 2 deletions tests/ui_and_conv/test_acp_convert.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import acp

from kimi_cli.acp.convert import tool_result_to_acp_content
from kimi_cli.wire.types import DiffDisplayBlock, ToolReturnValue
from kimi_cli.acp.convert import acp_blocks_to_content_parts, tool_result_to_acp_content
from kimi_cli.wire.types import DiffDisplayBlock, TextPart, ToolReturnValue


def test_tool_result_to_acp_content_handles_diff_display():
Expand All @@ -21,3 +21,62 @@ def test_tool_result_to_acp_content_handles_diff_display():
assert content.path == "foo.txt"
assert content.old_text == "before"
assert content.new_text == "after"


def test_acp_blocks_to_content_parts_handles_embedded_text_resource():
block = acp.schema.EmbeddedResourceContentBlock(
type="resource",
resource=acp.schema.TextResourceContents(
uri="file:///path/to/foo.py",
text="print('hello')",
),
)
parts = acp_blocks_to_content_parts([block])
assert len(parts) == 1
assert isinstance(parts[0], TextPart)
assert "file:///path/to/foo.py" in parts[0].text
assert "print('hello')" in parts[0].text
Comment on lines +26 to +38
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test coverage should include edge cases with special characters in URIs and text content, such as URIs containing quotes, angle brackets, or ampersands, and text containing XML special characters. This would help verify whether the XML formatting handles these cases correctly.

Copilot uses AI. Check for mistakes.


def test_acp_blocks_to_content_parts_handles_resource_link():
block = acp.schema.ResourceContentBlock(
type="resource_link",
uri="file:///path/to/bar.py",
name="bar.py",
)
parts = acp_blocks_to_content_parts([block])
assert len(parts) == 1
assert isinstance(parts[0], TextPart)
assert "file:///path/to/bar.py" in parts[0].text
assert "bar.py" in parts[0].text


def test_acp_blocks_to_content_parts_skips_blob_resource():
block = acp.schema.EmbeddedResourceContentBlock(
type="resource",
resource=acp.schema.BlobResourceContents(
uri="file:///path/to/image.png",
blob="iVBORw0KGgo=",
),
)
parts = acp_blocks_to_content_parts([block])
assert len(parts) == 0


def test_acp_blocks_to_content_parts_mixed_blocks():
blocks = [
acp.schema.TextContentBlock(type="text", text="Check this file:"),
acp.schema.EmbeddedResourceContentBlock(
type="resource",
resource=acp.schema.TextResourceContents(
uri="file:///src/main.py",
text="def main(): pass",
),
),
]
parts = acp_blocks_to_content_parts(blocks)
assert len(parts) == 2
assert isinstance(parts[0], TextPart)
assert parts[0].text == "Check this file:"
assert isinstance(parts[1], TextPart)
assert "def main(): pass" in parts[1].text
Loading