From 08a2aee8ce2314a4de32b83fbca22eeba9a979db Mon Sep 17 00:00:00 2001 From: dawn <90008@klbr.net> Date: Fri, 11 Sep 2026 04:52:19 +0300 Subject: [PATCH] coding-agent/{kernel,skills,ui}: add display skill and ui primitive for rich user content --- packages/coding-agent/skills/display/SKILL.md | 30 ++++++ .../skills/display/pyproject.toml | 10 ++ .../display/src/display.egg-info/PKG-INFO | 5 + .../display/src/display.egg-info/SOURCES.txt | 6 ++ .../src/display.egg-info/dependency_links.txt | 1 + .../src/display.egg-info/top_level.txt | 1 + .../skills/display/src/display/__init__.py | 93 +++++++++++++++++++ .../src/core/kernel/repl-manager.ts | 8 ++ .../coding-agent/src/core/kernel/shared.ts | 29 ++++++ .../coding-agent/src/core/tools/ipython.ts | 4 + .../interactive/components/ipython-cell.ts | 74 ++++++++++++++- .../test/kernel-display-skill.test.ts | 82 ++++++++++++++++ .../test/repl-kernel-execute.test.ts | 3 + 13 files changed, 343 insertions(+), 3 deletions(-) create mode 100644 packages/coding-agent/skills/display/SKILL.md create mode 100644 packages/coding-agent/skills/display/pyproject.toml create mode 100644 packages/coding-agent/skills/display/src/display.egg-info/PKG-INFO create mode 100644 packages/coding-agent/skills/display/src/display.egg-info/SOURCES.txt create mode 100644 packages/coding-agent/skills/display/src/display.egg-info/dependency_links.txt create mode 100644 packages/coding-agent/skills/display/src/display.egg-info/top_level.txt create mode 100644 packages/coding-agent/skills/display/src/display/__init__.py create mode 100644 packages/coding-agent/test/kernel-display-skill.test.ts diff --git a/packages/coding-agent/skills/display/SKILL.md b/packages/coding-agent/skills/display/SKILL.md new file mode 100644 index 000000000..e9e86a2b3 --- /dev/null +++ b/packages/coding-agent/skills/display/SKILL.md @@ -0,0 +1,30 @@ +--- +name: display +description: Put rich content (reading diffs, markdown reports, json, tables) directly in front of the user in the UI without polluting the conversation transcript or LLM context. Use when presenting diffs from meat, summaries, or artifacts to the user. +argument-hint: "content, [title=...], [format='diff'|'markdown'|'json'|'text'|'auto']" +--- + +# display + +Put content directly in front of the user in the UI without polluting the conversation history or future token context. + +The rendered content is displayed immediately on the user's screen (in the TUI card, web view, or desktop app), but is excluded from the conversation context passed back to the model in subsequent turns. + +## Python API + +The `display` skill is pre-imported in the persistent IPython kernel and callable directly: + +```python +# 1. Display a reading diff (e.g. from meat): +diff = await meat.worktree() +await display.diff(diff.smart_diff, title=diff.summary) + +# 2. Display markdown or reports: +await display.markdown("# Architecture Review\n...", title="Audit Report") + +# 3. Display JSON data: +await display.json({"status": "ok", "items": [...]}, title="Config Payload") + +# 4. Auto-detect format: +await display(content, title="Generated Artifact") +``` diff --git a/packages/coding-agent/skills/display/pyproject.toml b/packages/coding-agent/skills/display/pyproject.toml new file mode 100644 index 000000000..a65c4f28c --- /dev/null +++ b/packages/coding-agent/skills/display/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "display" +version = "0.1.0" +description = "Display rich content (diffs, markdown, json) directly to the user UI without polluting context" +requires-python = ">=3.10" +dependencies = [] + +[build-system] +requires = ["setuptools>=61.0"] +build-backend = "setuptools.build_meta" diff --git a/packages/coding-agent/skills/display/src/display.egg-info/PKG-INFO b/packages/coding-agent/skills/display/src/display.egg-info/PKG-INFO new file mode 100644 index 000000000..1f9d6c429 --- /dev/null +++ b/packages/coding-agent/skills/display/src/display.egg-info/PKG-INFO @@ -0,0 +1,5 @@ +Metadata-Version: 2.4 +Name: display +Version: 0.1.0 +Summary: Display rich content (diffs, markdown, json) directly to the user UI without polluting context +Requires-Python: >=3.10 diff --git a/packages/coding-agent/skills/display/src/display.egg-info/SOURCES.txt b/packages/coding-agent/skills/display/src/display.egg-info/SOURCES.txt new file mode 100644 index 000000000..99d0e9e4a --- /dev/null +++ b/packages/coding-agent/skills/display/src/display.egg-info/SOURCES.txt @@ -0,0 +1,6 @@ +pyproject.toml +src/display/__init__.py +src/display.egg-info/PKG-INFO +src/display.egg-info/SOURCES.txt +src/display.egg-info/dependency_links.txt +src/display.egg-info/top_level.txt \ No newline at end of file diff --git a/packages/coding-agent/skills/display/src/display.egg-info/dependency_links.txt b/packages/coding-agent/skills/display/src/display.egg-info/dependency_links.txt new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/packages/coding-agent/skills/display/src/display.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/packages/coding-agent/skills/display/src/display.egg-info/top_level.txt b/packages/coding-agent/skills/display/src/display.egg-info/top_level.txt new file mode 100644 index 000000000..6827503b1 --- /dev/null +++ b/packages/coding-agent/skills/display/src/display.egg-info/top_level.txt @@ -0,0 +1 @@ +display diff --git a/packages/coding-agent/skills/display/src/display/__init__.py b/packages/coding-agent/skills/display/src/display/__init__.py new file mode 100644 index 000000000..866a7759d --- /dev/null +++ b/packages/coding-agent/skills/display/src/display/__init__.py @@ -0,0 +1,93 @@ +"""display: Send content directly to the user's UI without polluting LLM token context.""" + +from __future__ import annotations + +import json as _json +from typing import Any + +_CONTENT_DISPLAY_MIME = "application/vnd.prime-agent.display+json" + + +def _infer_format(content: str) -> str: + stripped = content.strip() + if ( + stripped.startswith("diff --git") + or stripped.startswith("--- ") + or stripped.startswith("+++ ") + or stripped.startswith("Index: ") + or stripped.startswith("@@ ") + or "@@ -" in stripped + ): + return "diff" + if (stripped.startswith("{") and stripped.endswith("}")) or ( + stripped.startswith("[") and stripped.endswith("]") + ): + try: + _json.loads(stripped) + return "json" + except Exception: + pass + return "markdown" + + +async def run( + content: str, + *, + title: str | None = None, + format: str = "auto", +) -> str: + """Put rich content directly in front of the user in the UI. + + Emits a first-class display event rendered in the Prime Agent UI. + The rendered content is shown to the user on-screen, but is excluded + from the LLM's future token context window. + + Args: + content: The text, diff, markdown, or JSON string to display. + title: Optional title or summary label shown in the UI header. + format: 'diff', 'markdown', 'json', 'text', or 'auto' (default). + + Returns: + A short one-line confirmation. + """ + if not isinstance(content, str): + content = str(content) + + resolved_format = _infer_format(content) if format == "auto" else format + + from rlm import emit + + emit( + { + _CONTENT_DISPLAY_MIME: { + "content": content, + "title": title, + "format": resolved_format, + }, + } + ) + + label = f": {title}" if title else "" + return f"Displayed {resolved_format} ({len(content)} chars) to user{label}" + + +async def diff(content: str, *, title: str | None = None) -> str: + """Convenience helper to display a diff.""" + return await run(content, title=title, format="diff") + + +async def markdown(content: str, *, title: str | None = None) -> str: + """Convenience helper to display markdown.""" + return await run(content, title=title, format="markdown") + + +async def json(data: Any, *, title: str | None = None) -> str: + """Convenience helper to display JSON data.""" + text = _json.dumps(data, indent=2) if not isinstance(data, str) else data + return await run(text, title=title, format="json") + + +# Direct callable alias +display = run + +__all__ = ["run", "display", "diff", "markdown", "json"] diff --git a/packages/coding-agent/src/core/kernel/repl-manager.ts b/packages/coding-agent/src/core/kernel/repl-manager.ts index 54d49d1be..e450b9b6f 100644 --- a/packages/coding-agent/src/core/kernel/repl-manager.ts +++ b/packages/coding-agent/src/core/kernel/repl-manager.ts @@ -14,6 +14,7 @@ import { AGENT_MESSAGE_DISPLAY_MIME, ATTACHMENT_DISPLAY_MIME, BASH_ACTIVITY_DISPLAY_MIME, + CONTENT_DISPLAY_MIME, createDeferred, createKernelStartupAbortError, DEFAULT_MAX_OUTPUT_CHARS, @@ -31,6 +32,7 @@ import { KERNEL_SHUTDOWN_TIMEOUT_MS, type KernelAttachment, KernelBusyAfterInterruptError, + type KernelContentDisplay, type KernelDiffDisplay, type KernelManagerOptions, type KernelSentAgentMessage, @@ -40,6 +42,7 @@ import { MAX_ATTACHMENT_DATA_CHARS, MAX_LATE_SENT_AGENT_MESSAGE_HANDLERS, parseAttachmentDisplay, + parseContentDisplay, parseDiffDisplay, parseSentAgentMessage, raceStartupWithAbort, @@ -91,6 +94,7 @@ interface ActiveExecution { stderrTruncated: boolean; result?: string; diffs: KernelDiffDisplay[]; + displays: KernelContentDisplay[]; attachments: KernelAttachment[]; sentAgentMessages: KernelSentAgentMessage[]; /** Stream text without this execution's id: user threads, other cells' leftovers, raw fd writes. */ @@ -860,6 +864,8 @@ export class ReplKernelManager { const data = isRecord(event.data) ? event.data : {}; const diff = parseDiffDisplay(data[DIFF_DISPLAY_MIME]); if (diff) execution.diffs.push(diff); + const display = parseContentDisplay(data[CONTENT_DISPLAY_MIME]); + if (display) execution.displays.push(display); const attachment = parseAttachmentDisplay(data[ATTACHMENT_DISPLAY_MIME]); if (attachment === "oversized") { execution.stderr += `${execution.stderr ? "\n" : ""}attachment dropped: exceeds ${MAX_ATTACHMENT_DATA_CHARS} base64 chars`; @@ -1006,6 +1012,7 @@ export class ReplKernelManager { stdoutTruncated: false, stderrTruncated: false, diffs: [], + displays: [], attachments: [], sentAgentMessages: [], backgroundOutput: this.pendingBackgroundOutput, @@ -1137,6 +1144,7 @@ export class ReplKernelManager { stderr, result, diffs: execution.diffs.length > 0 ? execution.diffs : undefined, + displays: execution.displays.length > 0 ? execution.displays : undefined, attachments: execution.attachments.length > 0 ? execution.attachments : undefined, sentAgentMessages: execution.sentAgentMessages.length > 0 ? execution.sentAgentMessages : undefined, backgroundOutput: backgroundOutput.length > 0 ? backgroundOutput : undefined, diff --git a/packages/coding-agent/src/core/kernel/shared.ts b/packages/coding-agent/src/core/kernel/shared.ts index ff365dc0d..f33b65d84 100644 --- a/packages/coding-agent/src/core/kernel/shared.ts +++ b/packages/coding-agent/src/core/kernel/shared.ts @@ -81,6 +81,9 @@ export interface ExecuteOptions { protocolRepair?: boolean; } +/** MIME tag the `display` skill emits content payloads under. */ +export const CONTENT_DISPLAY_MIME = "application/vnd.prime-agent.display+json"; + /** MIME tag the `edit` skill emits diff payloads under. */ export const DIFF_DISPLAY_MIME = "application/vnd.prime-agent.diff+json"; @@ -101,6 +104,15 @@ export const BASH_ACTIVITY_DISPLAY_MIME = "application/vnd.prime-agent.bash-acti */ export const MAX_ATTACHMENT_DATA_CHARS = 10_000_000; +export type DisplayFormat = "diff" | "markdown" | "text" | "json"; + +/** Content to present directly to the user in the UI without entering future LLM context. */ +export interface KernelContentDisplay { + content: string; + title?: string; + format?: DisplayFormat; +} + /** One file edit, captured from a {@link DIFF_DISPLAY_MIME} display payload. */ export interface KernelDiffDisplay { path: string; @@ -138,6 +150,8 @@ export interface ExecuteResult { result?: string; /** Diffs emitted via display events, in order. */ diffs?: KernelDiffDisplay[]; + /** Content displays emitted via display events, in order. */ + displays?: KernelContentDisplay[]; /** Media attachments emitted via display events, in order. */ attachments?: KernelAttachment[]; /** Agent messages sent from this cell, in order. */ @@ -161,6 +175,21 @@ export function parseDiffDisplay(payload: unknown): KernelDiffDisplay | undefine return { path, oldStr, newStr, startLine: typeof startLine === "number" ? startLine : undefined }; } +/** Parse a {@link CONTENT_DISPLAY_MIME} payload, tolerating malformed input. */ +export function parseContentDisplay(payload: unknown): KernelContentDisplay | undefined { + if (!isRecord(payload)) { + return undefined; + } + const { content, title, format } = payload; + if (typeof content !== "string") { + return undefined; + } + const cleanTitle = typeof title === "string" ? title : undefined; + const cleanFormat = + format === "diff" || format === "markdown" || format === "text" || format === "json" ? format : undefined; + return { content, title: cleanTitle, format: cleanFormat }; +} + /** * Parse an {@link ATTACHMENT_DISPLAY_MIME} payload. Malformed payloads are * tolerantly ignored (`undefined`); a well-formed payload exceeding diff --git a/packages/coding-agent/src/core/tools/ipython.ts b/packages/coding-agent/src/core/tools/ipython.ts index 2d651e4d7..1892feb63 100644 --- a/packages/coding-agent/src/core/tools/ipython.ts +++ b/packages/coding-agent/src/core/tools/ipython.ts @@ -14,6 +14,7 @@ import { type KernelAttachment, KernelBusyAfterInterruptError, type KernelClient, + type KernelContentDisplay, type KernelDiffDisplay, type KernelSentAgentMessage, type RemoteHostConfig, @@ -263,6 +264,8 @@ export interface IpythonToolDetails { backgroundOutput?: string; /** Diffs streamed from file edits, rendered by the cell view. */ diffs?: KernelDiffDisplay[]; + /** Rich content displays emitted to the user (e.g. by the display skill). */ + displays?: KernelContentDisplay[]; /** Media attachments loaded into context (e.g. by the attach-image skill). */ attachments?: KernelAttachment[]; /** Agent messages sent from this cell. */ @@ -694,6 +697,7 @@ export function createIpythonToolDefinition( result: r.result, backgroundOutput: r.backgroundOutput, diffs: r.diffs, + displays: r.displays, attachments: r.attachments, sentAgentMessages: r.sentAgentMessages, kernelRestarted, diff --git a/packages/coding-agent/src/modes/interactive/components/ipython-cell.ts b/packages/coding-agent/src/modes/interactive/components/ipython-cell.ts index 75fa042c3..46a001e14 100644 --- a/packages/coding-agent/src/modes/interactive/components/ipython-cell.ts +++ b/packages/coding-agent/src/modes/interactive/components/ipython-cell.ts @@ -6,7 +6,8 @@ import { wrapTextWithAnsi, } from "@earendil-works/pi-tui"; import { formatAgentMessageParticipant } from "../../../core/agent-messages.js"; -import { previewIpythonCode, pythonStatementLines } from "../../../core/tools/code-preview.js"; +import type { KernelContentDisplay } from "../../../core/kernel/index.js"; +import { previewIpythonCode, pythonStatementLines } from "../../../core/tools/code-preview.js";>>>>>>> f30729a61 (coding-agent/{kernel,skills,ui}: add display skill and ui primitive for rich user content) import { generateDiffString } from "../../../core/tools/edit-diff.js"; import { parseIpythonBashCell } from "../../../core/tools/ipython-cell-code.js"; import { formatToolDuration } from "../../../core/tools/render-utils.js"; @@ -72,6 +73,7 @@ interface IpythonDetails { result?: string; backgroundOutput?: string; diffs?: DiffDisplay[]; + displays?: KernelContentDisplay[]; sentAgentMessages?: SentAgentMessageDisplay[]; error?: IpythonErrorDetails; } @@ -153,6 +155,7 @@ function readDetails(details: unknown): IpythonDetails { result: typeof record.result === "string" ? record.result : undefined, backgroundOutput: typeof record.backgroundOutput === "string" ? record.backgroundOutput : undefined, diffs: readDiffDisplays(record.diffs), + displays: readContentDisplays(record.displays), sentAgentMessages: readSentAgentMessages(record.sentAgentMessages), error, }; @@ -224,6 +227,39 @@ function readDiffDisplays(value: unknown): DiffDisplay[] | undefined { return diffs.length > 0 ? diffs : undefined; } +function readContentDisplays(value: unknown): KernelContentDisplay[] | undefined { + if (!Array.isArray(value)) { + return undefined; + } + const displays = value.flatMap((entry): KernelContentDisplay[] => { + if (!entry || typeof entry !== "object") { + return []; + } + const record = entry as Record; + if (typeof record.content !== "string") { + return []; + } + const title = typeof record.title === "string" ? record.title : undefined; + const format = + record.format === "diff" || + record.format === "markdown" || + record.format === "text" || + record.format === "json" + ? record.format + : undefined; + return [{ content: record.content, title, format }]; + }); + return displays.length > 0 ? displays : undefined; +} + +function isDisplayConfirmation(text: string | undefined): boolean { + if (!text) { + return false; + } + const stripped = stripReprQuotes(text.trim()); + return stripped.startsWith("Displayed ") && stripped.includes(" to user"); +} + /** Strip one layer of repr quotes so an `execute_result` string compares cleanly. */ function stripReprQuotes(text: string): string { const trimmed = text.trim(); @@ -369,6 +405,10 @@ export class IPythonCellComponent implements Component { if ((details.diffs?.length ?? 0) > 0) { this.renderDiffs(lines, safeWidth, details.diffs ?? [], hasCode); } + + if ((details.displays?.length ?? 0) > 0) { + this.renderDisplays(lines, safeWidth, details.displays ?? []); + } if ((details.sentAgentMessages?.length ?? 0) > 0) { this.renderSentAgentMessages(lines, safeWidth, details.sentAgentMessages ?? []); } @@ -495,6 +535,7 @@ export class IPythonCellComponent implements Component { details.result !== undefined || details.error !== undefined || (details.diffs?.length ?? 0) > 0 || + (details.displays?.length ?? 0) > 0 || (details.sentAgentMessages?.length ?? 0) > 0 || (this.state.content?.length ?? 0) > 0 ); @@ -567,7 +608,11 @@ export class IPythonCellComponent implements Component { }; if (hasStructuredOutput) { - if (details.stdout?.trim() && !isEditConfirmation(details.stdout, diffs)) { + if ( + details.stdout?.trim() && + !isEditConfirmation(details.stdout, diffs) && + !isDisplayConfirmation(details.stdout) + ) { startOutput(); renderedTextOutput = true; this.renderOutputText(lines, width, normalizeErrorDetails(details.stdout), "out", outputPrefix); @@ -580,6 +625,7 @@ export class IPythonCellComponent implements Component { if ( details.result?.trim() && !isEditConfirmation(details.result, diffs) && + !isDisplayConfirmation(details.result) && !isAgentMessageReceipt(details.result, sentMessages) ) { startOutput(); @@ -623,6 +669,7 @@ export class IPythonCellComponent implements Component { !traceback && !details.error && diffs.length === 0 && + (details.displays?.length ?? 0) === 0 && (details.sentAgentMessages?.length ?? 0) === 0 && this.state.executionStarted && imageCount === 0 @@ -671,7 +718,28 @@ export class IPythonCellComponent implements Component { } } - // The path summary stays visible while conversation detail toggles the diff rows. + private renderDisplays(lines: string[], width: number, displays: readonly KernelContentDisplay[]): void { + for (const display of displays) { + const label = display.title ? `display · ${display.title}` : "display"; + const formatTag = display.format ? ` (${display.format})` : ""; + const header = theme.fg("accent", `◆ ${label}${formatTag}`); + this.addPlain(lines, truncateToWidth(header, Math.max(1, width - 1), "…")); + const indent = FILE_CHANGE_DIFF_INDENT.slice(0, Math.max(0, width - 1)); + const contentWidth = Math.max(1, width - indent.length); + if (display.format === "diff") { + for (const row of renderRichDiff(display.content, contentWidth)) { + lines.push(`${indent}${row}`); + } + } else { + for (const line of display.content.split("\n")) { + this.addWrapped(lines, indent, theme.fg("toolOutput", line || " "), width); + } + } + } + } + + // The `╰─ +N -M` summary line renders in both states; ctrl+j only + // attaches or removes the indented diff rows underneath it.>>>>>>> f30729a61 (coding-agent/{kernel,skills,ui}: add display skill and ui primitive for rich user content) private renderDiffs(lines: string[], width: number, diffs: readonly DiffDisplay[], hasCode: boolean): void { const diffsByPath = new Map(); for (const diff of diffs) { diff --git a/packages/coding-agent/test/kernel-display-skill.test.ts b/packages/coding-agent/test/kernel-display-skill.test.ts new file mode 100644 index 000000000..3b233681e --- /dev/null +++ b/packages/coding-agent/test/kernel-display-skill.test.ts @@ -0,0 +1,82 @@ +import { mkdirSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import { getBundledSkillsDir } from "../src/config.js"; +import type { PythonSkillRuntimeInfo } from "../src/core/skills.js"; +import { IpythonKernelProvisioner } from "../src/core/tools/ipython.js"; + +function bundledDisplaySkill(): PythonSkillRuntimeInfo { + const packagePath = join(getBundledSkillsDir(), "display"); + return { + name: "display", + importName: "display", + packagePath, + pyprojectPath: join(packagePath, "pyproject.toml"), + }; +} + +describe("display skill in persistent kernel", () => { + let tempDir: string; + let provisioner: IpythonKernelProvisioner | undefined; + + beforeEach(() => { + tempDir = join(tmpdir(), `pi-display-test-${Date.now()}-${Math.random().toString(36).slice(2)}`); + mkdirSync(tempDir, { recursive: true }); + }); + + afterEach(async () => { + await provisioner?.dispose(); + provisioner = undefined; + rmSync(tempDir, { recursive: true, force: true }); + }); + + it("emits markdown displays and excludes full content from execution result", async () => { + provisioner = new IpythonKernelProvisioner(tempDir, { + pythonSkills: [bundledDisplaySkill()], + }); + + const manager = await provisioner.ensure(); + const result = await manager.execute('await display("# Title\\n\\nSome markdown content", title="Audit Report")'); + + expect(result.status).toBe("ok"); + expect(result.result).toContain("Displayed markdown"); + expect(result.displays).toHaveLength(1); + expect(result.displays?.[0]).toEqual({ + content: "# Title\n\nSome markdown content", + title: "Audit Report", + format: "markdown", + }); + }); + + it("emits diff displays with display.diff helper", async () => { + provisioner = new IpythonKernelProvisioner(tempDir, { + pythonSkills: [bundledDisplaySkill()], + }); + + const manager = await provisioner.ensure(); + const diffText = "diff --git a/a.txt b/a.txt\n--- a/a.txt\n+++ b/a.txt\n@@ -1 +1 @@\n-a\n+b\n"; + const result = await manager.execute(`await display.diff(${JSON.stringify(diffText)}, title="Fix spelling")`); + + expect(result.status).toBe("ok"); + expect(result.displays?.[0]).toEqual({ + content: diffText, + title: "Fix spelling", + format: "diff", + }); + }); + + it("emits json displays with display.json helper", async () => { + provisioner = new IpythonKernelProvisioner(tempDir, { + pythonSkills: [bundledDisplaySkill()], + }); + + const manager = await provisioner.ensure(); + const result = await manager.execute('await display.json({"hello": "world", "num": 42}, title="Config")'); + + expect(result.status).toBe("ok"); + expect(result.displays?.[0]?.title).toBe("Config"); + expect(result.displays?.[0]?.format).toBe("json"); + expect(JSON.parse(result.displays?.[0]?.content ?? "")).toEqual({ hello: "world", num: 42 }); + }); +}); diff --git a/packages/coding-agent/test/repl-kernel-execute.test.ts b/packages/coding-agent/test/repl-kernel-execute.test.ts index 5365fcdd1..6b03d67e8 100644 --- a/packages/coding-agent/test/repl-kernel-execute.test.ts +++ b/packages/coding-agent/test/repl-kernel-execute.test.ts @@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest"; import { AGENT_MESSAGE_DISPLAY_MIME, ATTACHMENT_DISPLAY_MIME, + CONTENT_DISPLAY_MIME, DIFF_DISPLAY_MIME, type HostRequestHandlers, ReplKernelManager, @@ -75,12 +76,14 @@ describeIf("ReplKernelManager execute (real runtime)", () => { const code = [ "from rlm import emit", `emit({${JSON.stringify(DIFF_DISPLAY_MIME)}: {"path": "/tmp/f.py", "old_str": "a", "new_str": "b", "start_line": 3}})`, + `emit({${JSON.stringify(CONTENT_DISPLAY_MIME)}: {"content": "diff --git a/f.py b/f.py", "title": "my reading diff", "format": "diff"}})`, `emit({${JSON.stringify(ATTACHMENT_DISPLAY_MIME)}: {"mime_type": "image/png", "data": "aGVsbG8=", "path": "/tmp/i.png"}})`, `emit({${JSON.stringify(AGENT_MESSAGE_DISPLAY_MIME)}: {"id": "m1", "message": "hi", "deliveryStatus": "delivered", "receiverRole": "parent", "target": {"activeSessionId": "a", "sessionId": "s"}}})`, ].join("\n"); const r = await manager.execute(code); expect(r.status).toBe("ok"); expect(r.diffs).toEqual([{ path: "/tmp/f.py", oldStr: "a", newStr: "b", startLine: 3 }]); + expect(r.displays).toEqual([{ content: "diff --git a/f.py b/f.py", title: "my reading diff", format: "diff" }]); expect(r.attachments).toEqual([{ mimeType: "image/png", data: "aGVsbG8=", path: "/tmp/i.png" }]); expect(r.sentAgentMessages).toEqual([ { -- 2.51.2