From 91ae30b1d18b0bba3cb249943f7f81f25f24c22c Mon Sep 17 00:00:00 2001 From: Bernardo Date: Tue, 10 Feb 2026 04:27:55 +0000 Subject: [PATCH 01/12] Add chat output renderer configuration and localization --- package-lock.json | 2 ++ package.json | 17 +++++++++++++++++ package.nls.json | 19 ++++++++++++++++++- package.nls.pt-br.json | 19 ++++++++++++++++++- package.nls.pt.json | 19 ++++++++++++++++++- 5 files changed, 73 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5e3c21e..8094a54 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1296,6 +1296,7 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", + "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -3507,6 +3508,7 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", "license": "MIT", + "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 1de548a..4d92297 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,9 @@ ], "license": "MIT", "main": "./dist/extension.js", + "enabledApiProposals": [ + "chatOutputRenderer" + ], "engines": { "vscode": "^1.104.0" }, @@ -271,6 +274,20 @@ } } ], + "chatOutputRenderers": [ + { + "viewType": "seamless-agent.askUserResult", + "mimeTypes": [ + "application/vnd.seamless-agent.ask-user-result" + ] + }, + { + "viewType": "seamless-agent.planReviewResult", + "mimeTypes": [ + "application/vnd.seamless-agent.plan-review-result" + ] + } + ], "commands": [ { "command": "seamless-agent.cancelPendingPlans", diff --git a/package.nls.json b/package.nls.json index f311a87..a52ce3e 100644 --- a/package.nls.json +++ b/package.nls.json @@ -111,5 +111,22 @@ "batch.deselectAll": "Deselect All", "batch.deleteSelected": "Delete Selected", "batch.selectedCount": "{0} selected", - "confirm.deleteSelected": "Are you sure you want to delete {0} selected items?" + "confirm.deleteSelected": "Are you sure you want to delete {0} selected items?", + + "renderer.userResponse": "User Response", + "renderer.defaultAgentName": "Agent", + "renderer.userDismissed": "User dismissed this prompt", + "renderer.clickToExpand": "(click to expand)", + "renderer.userResponded": "User responded:", + "renderer.attachments": "Attachments:", + "renderer.renderErrorAskUser": "Unable to render ask_user result", + "renderer.renderErrorPlanReview": "Unable to render plan review result", + "renderer.error": "Error", + "renderer.planReview": "Plan Review", + "renderer.walkthrough": "Walkthrough", + "renderer.review": "Review", + "renderer.status": "Status:", + "renderer.changesRequested": "Changes Requested", + "renderer.viewPlan": "View Plan", + "renderer.revisionsRequired": "Revisions Required:" } diff --git a/package.nls.pt-br.json b/package.nls.pt-br.json index 12db02d..e74af7d 100644 --- a/package.nls.pt-br.json +++ b/package.nls.pt-br.json @@ -111,5 +111,22 @@ "batch.deselectAll": "Desmarcar Tudo", "batch.deleteSelected": "Excluir Selecionados", "batch.selectedCount": "{0} selecionados", - "confirm.deleteSelected": "Tem certeza que deseja excluir {0} itens selecionados?" + "confirm.deleteSelected": "Tem certeza que deseja excluir {0} itens selecionados?", + + "renderer.userResponse": "Resposta do Usuário", + "renderer.defaultAgentName": "Agente", + "renderer.userDismissed": "Usuário dispensou esta solicitação", + "renderer.clickToExpand": "(clique para expandir)", + "renderer.userResponded": "Usuário respondeu:", + "renderer.attachments": "Anexos:", + "renderer.renderErrorAskUser": "Não foi possível renderizar o resultado de ask_user", + "renderer.renderErrorPlanReview": "Não foi possível renderizar o resultado da revisão de plano", + "renderer.error": "Erro", + "renderer.planReview": "Revisão de Plano", + "renderer.walkthrough": "Passo a Passo", + "renderer.review": "Revisão", + "renderer.status": "Status:", + "renderer.changesRequested": "Alterações Solicitadas", + "renderer.viewPlan": "Ver Plano", + "renderer.revisionsRequired": "Revisões Necessárias:" } diff --git a/package.nls.pt.json b/package.nls.pt.json index 6249c3e..f74a067 100644 --- a/package.nls.pt.json +++ b/package.nls.pt.json @@ -111,5 +111,22 @@ "batch.deselectAll": "Desmarcar Tudo", "batch.deleteSelected": "Eliminar Selecionados", "batch.selectedCount": "{0} selecionados", - "confirm.deleteSelected": "Tem a certeza que deseja eliminar {0} itens selecionados?" + "confirm.deleteSelected": "Tem a certeza que deseja eliminar {0} itens selecionados?", + + "renderer.userResponse": "Resposta do Utilizador", + "renderer.defaultAgentName": "Agente", + "renderer.userDismissed": "Utilizador dispensou esta solicitação", + "renderer.clickToExpand": "(clique para expandir)", + "renderer.userResponded": "Utilizador respondeu:", + "renderer.attachments": "Anexos:", + "renderer.renderErrorAskUser": "Não foi possível renderizar o resultado de ask_user", + "renderer.renderErrorPlanReview": "Não foi possível renderizar o resultado da revisão de plano", + "renderer.error": "Erro", + "renderer.planReview": "Revisão de Plano", + "renderer.walkthrough": "Passo a Passo", + "renderer.review": "Revisão", + "renderer.status": "Estado:", + "renderer.changesRequested": "Alterações Solicitadas", + "renderer.viewPlan": "Ver Plano", + "renderer.revisionsRequired": "Revisões Necessárias:" } From b45a7e5cda5a9a6f5550e7d1fc162a1994ea460f Mon Sep 17 00:00:00 2001 From: Bernardo Date: Tue, 10 Feb 2026 04:27:55 +0000 Subject: [PATCH 02/12] Define proposed API types for chat output renderers --- src/proposed-api.d.ts | 59 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 src/proposed-api.d.ts diff --git a/src/proposed-api.d.ts b/src/proposed-api.d.ts new file mode 100644 index 0000000..8624800 --- /dev/null +++ b/src/proposed-api.d.ts @@ -0,0 +1,59 @@ +/** + * Type declarations for the proposed `chatOutputRenderer` API. + * + * These types extend the stable vscode API with proposed features that are + * gated behind `enabledApiProposals: ["chatOutputRenderer"]` in package.json. + * + * Reference: https://github.com/microsoft/vscode-extension-samples/tree/main/chat-output-renderer-sample + * + * TODO: Remove this file once the API is stabilized and types are included in @types/vscode. + */ + +import * as vscode from 'vscode'; + +declare module 'vscode' { + + /** + * Extended tool result that supports attaching binary data for chat output renderers. + * The `toolResultDetails2` property provides MIME-typed binary data that VS Code + * matches against registered `chatOutputRenderers` to render inline webview cards. + */ + interface ExtendedLanguageModelToolResult2 extends LanguageModelToolResult { + toolResultDetails2?: { + /** MIME type used to match a registered chatOutputRenderer */ + mime: string; + /** Binary data (typically UTF-8 encoded JSON) passed to the renderer */ + value: Uint8Array; + }; + } + + /** + * A webview container provided to chat output renderers for rendering inline content. + */ + interface ChatOutputWebview { + readonly webview: Webview; + } + + /** + * Renderer that creates inline webview widgets in the chat stream. + */ + interface ChatOutputRenderer { + renderChatOutput( + data: { value: Uint8Array }, + chatOutputWebview: ChatOutputWebview, + ctx: unknown, + token: CancellationToken + ): Promise | void; + } + + namespace chat { + /** + * Register a chat output renderer for a specific view type. + * The view type must match a `chatOutputRenderers` contribution in package.json. + */ + function registerChatOutputRenderer( + viewType: string, + renderer: ChatOutputRenderer + ): Disposable; + } +} From 9c33dbb8921ea3af4f9864d5be00c2b2e89611c3 Mon Sep 17 00:00:00 2001 From: Bernardo Date: Tue, 10 Feb 2026 04:27:55 +0000 Subject: [PATCH 03/12] Add HTML utilities and CSS for renderer cards --- src/renderers/htmlUtils.ts | 297 +++++++++++++++++++++++++++++++++++++ 1 file changed, 297 insertions(+) create mode 100644 src/renderers/htmlUtils.ts diff --git a/src/renderers/htmlUtils.ts b/src/renderers/htmlUtils.ts new file mode 100644 index 0000000..d6bb557 --- /dev/null +++ b/src/renderers/htmlUtils.ts @@ -0,0 +1,297 @@ +/** + * Shared HTML utilities for Chat Output Renderer cards. + * + * All functions run in the extension host (not in the webview). + * They produce HTML strings that are set as `chatOutputWebview.webview.html`. + */ + +import { localize } from '../localization'; + +/** + * Generates a random nonce string for CSP script whitelisting. + */ +export function getNonce(): string { + let text = ''; + const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + for (let i = 0; i < 64; i++) { + text += possible.charAt(Math.floor(Math.random() * possible.length)); + } + return text; +} + +/** + * Escapes a string for safe insertion into HTML content. + */ +export function escapeHtml(str: string): string { + return str + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} + +/** + * Builds a CSP meta tag for renderer webviews. + * No scripts needed — cards are static HTML with inline styles. + */ +export function buildCspMetaTag(): string { + return ``; +} + +/** + * Returns shared CSS for all renderer cards using VS Code theme variables. + */ +export function getCardCss(): string { + return ` + * { + box-sizing: border-box; + margin: 0; + padding: 0; + } + body { + font-family: var(--vscode-font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif); + font-size: var(--vscode-font-size, 13px); + color: var(--vscode-foreground, #cccccc); + background: transparent; + padding: 8px 0; + margin: 0; + line-height: 1.4; + } + .card { + border: 1px solid var(--vscode-panel-border, #2d2d2d); + border-radius: 6px; + overflow: hidden; + background: var(--vscode-editor-background, #1e1e1e); + } + .card-header { + padding: 10px 14px; + border-bottom: 1px solid var(--vscode-panel-border, #2d2d2d); + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + font-size: 0.95em; + } + .card-body { + padding: 12px 14px; + } + .card-footer { + padding: 6px 14px 10px; + text-align: right; + font-size: 0.85em; + color: var(--vscode-descriptionForeground, #888888); + } + .section { + margin-bottom: 10px; + } + .section:last-child { + margin-bottom: 0; + } + .section-label { + font-size: 0.85em; + color: var(--vscode-descriptionForeground, #888888); + margin-bottom: 4px; + font-weight: 600; + } + .section-content { + padding-left: 2px; + } + .status-approved { + color: var(--vscode-testing-iconPassed, #28a745); + } + .status-changes { + color: var(--vscode-notificationsWarningIcon-foreground, #cca700); + } + .status-cancelled { + color: var(--vscode-testing-iconFailed, #f85149); + } + .status-acknowledged { + color: var(--vscode-notificationsInfoIcon-foreground, #3794ff); + } + .status-dismissed { + color: var(--vscode-testing-iconFailed, #f85149); + } + .badge { + display: inline-block; + padding: 2px 8px; + border-radius: 10px; + font-size: 0.85em; + font-weight: 600; + } + .badge-approved { + background: rgba(40, 167, 69, 0.15); + color: var(--vscode-testing-iconPassed, #28a745); + } + .badge-changes { + background: rgba(204, 167, 0, 0.15); + color: var(--vscode-notificationsWarningIcon-foreground, #cca700); + } + .badge-cancelled { + background: rgba(248, 81, 73, 0.15); + color: var(--vscode-testing-iconFailed, #f85149); + } + .badge-acknowledged { + background: rgba(55, 148, 255, 0.15); + color: var(--vscode-notificationsInfoIcon-foreground, #3794ff); + } + .attachment-list { + list-style: none; + padding: 0; + margin: 0; + } + .attachment-item { + display: flex; + align-items: center; + gap: 6px; + padding: 3px 0; + font-size: 0.9em; + } + .attachment-icon { + opacity: 0.7; + } + blockquote { + border-left: 3px solid var(--vscode-panel-border, #2d2d2d); + padding-left: 12px; + margin: 6px 0; + color: var(--vscode-descriptionForeground, #888888); + font-style: italic; + } + details { + margin-top: 6px; + } + details summary { + cursor: pointer; + color: var(--vscode-textLink-foreground, #3794ff); + font-size: 0.9em; + padding: 4px 0; + user-select: none; + } + details summary:hover { + color: var(--vscode-textLink-activeForeground, #3794ff); + } + .plan-content { + margin-top: 8px; + padding: 10px; + border: 1px solid var(--vscode-panel-border, #2d2d2d); + border-radius: 4px; + background: var(--vscode-textBlockQuote-background, #1a1a1a); + overflow-x: auto; + font-size: 0.9em; + line-height: 1.5; + } + .plan-content h1, .plan-content h2, .plan-content h3, + .plan-content h4, .plan-content h5, .plan-content h6 { + margin-top: 12px; + margin-bottom: 6px; + font-weight: 600; + } + .plan-content h1 { font-size: 1.3em; } + .plan-content h2 { font-size: 1.15em; } + .plan-content h3 { font-size: 1.05em; } + .plan-content p { margin: 6px 0; } + .plan-content ul, .plan-content ol { + padding-left: 20px; + margin: 6px 0; + } + .plan-content li { margin: 3px 0; } + .plan-content code { + background: var(--vscode-textCodeBlock-background, #2a2a2a); + padding: 1px 4px; + border-radius: 3px; + font-family: var(--vscode-editor-font-family, monospace); + font-size: 0.9em; + } + .plan-content pre { + background: var(--vscode-textCodeBlock-background, #2a2a2a); + padding: 10px; + border-radius: 4px; + overflow-x: auto; + margin: 8px 0; + } + .plan-content pre code { + background: transparent; + padding: 0; + } + .revision-list { + list-style: none; + padding: 0; + margin: 0; + } + .revision-item { + padding: 8px 0; + border-bottom: 1px solid var(--vscode-panel-border, #2d2d2d); + } + .revision-item:last-child { + border-bottom: none; + } + .revision-part { + font-weight: 600; + font-size: 0.9em; + margin-bottom: 3px; + } + .revision-instruction { + font-size: 0.9em; + color: var(--vscode-descriptionForeground, #888888); + padding-left: 8px; + } + .truncated-notice { + font-size: 0.85em; + color: var(--vscode-notificationsWarningIcon-foreground, #cca700); + font-style: italic; + margin-top: 6px; + } + .truncated-preview { + margin-top: 6px; + color: var(--vscode-foreground, #cccccc); + cursor: default; + } + details[open] .truncated-preview { + display: none; + } + `; +} + +/** + * Formats a timestamp as a relative time string. + */ +export function formatTimestamp(timestamp: number): string { + const now = Date.now(); + const diff = now - timestamp; + + if (diff < 0) { return localize('time.justNow'); } + + const seconds = Math.floor(diff / 1000); + if (seconds < 60) { return localize('time.justNow'); } + + const minutes = Math.floor(seconds / 60); + if (minutes < 60) { return localize('time.minutesAgo', minutes); } + + const hours = Math.floor(minutes / 60); + if (hours < 24) { return localize('time.hoursAgo', hours); } + + const days = Math.floor(hours / 24); + if (days < 30) { return localize('time.daysAgo', days); } + + // Fall back to absolute date + return new Date(timestamp).toLocaleDateString(); +} + +/** + * Wraps card HTML in a full document with CSP and theme CSS. + */ +export function wrapInDocument(title: string, bodyHtml: string): string { + return ` + + + + + ${buildCspMetaTag()} + ${escapeHtml(title)} + + + +${bodyHtml} + +`; +} From 4c5bbb92b6a034412952def2e0d16e1922c18905 Mon Sep 17 00:00:00 2001 From: Bernardo Date: Tue, 10 Feb 2026 04:27:55 +0000 Subject: [PATCH 04/12] Define renderer data types and encoding utilities --- src/renderers/types.ts | 121 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 src/renderers/types.ts diff --git a/src/renderers/types.ts b/src/renderers/types.ts new file mode 100644 index 0000000..0689992 --- /dev/null +++ b/src/renderers/types.ts @@ -0,0 +1,121 @@ +/** + * Data interfaces and utilities for Chat Output Renderers. + * + * These types define the binary data schemas passed via `toolResultDetails2.value` + * to registered chat output renderers. Data is encoded as UTF-8 JSON in a Uint8Array. + */ + +import type { PlanReviewMode } from '../webview/types'; + +// ================================ +// Renderer Data Interfaces +// ================================ + +/** + * Data passed to the `ask_user` chat output renderer. + * + * Combines data from the tool input (question, agentName, title) and the tool + * result (responded, response, attachments) to provide full context for the card. + */ +export interface AskUserRendererData { + /** The question that was asked */ + question: string; + /** The user's response text */ + response: string; + /** Whether the user responded (false = dismissed/cancelled) */ + responded: boolean; + /** The agent name that invoked the tool */ + agentName?: string; + /** Custom title for the interaction */ + title?: string; + /** Unix timestamp (ms) of when the interaction completed */ + timestamp: number; + /** Attachments provided by the user */ + attachments: AskUserAttachment[]; +} + +/** + * Attachment metadata for the ask_user renderer card. + */ +export interface AskUserAttachment { + /** Display name of the file */ + name: string; + /** File URI string */ + uri: string; + /** Whether the file is an image (for inline preview) */ + isImage: boolean; +} + +/** + * Data passed to the plan review chat output renderer. + */ +export interface PlanReviewRendererData { + /** Title of the plan review */ + title: string; + /** Review outcome status */ + status: 'approved' | 'recreateWithChanges' | 'cancelled' | 'acknowledged'; + /** Review mode: 'review' for plan approval, 'walkthrough' for step-by-step guides */ + mode: PlanReviewMode; + /** Full markdown plan content (truncated to MAX_PLAN_SIZE if needed) */ + plan: string; + /** Unix timestamp (ms) of when the review completed */ + timestamp: number; + /** Revision comments from the user (if status is 'recreateWithChanges') */ + requiredRevisions: PlanRevisionComment[]; + /** Unique reviewer identifier */ + reviewId: string; +} + +/** + * A single revision comment on a plan review. + */ +export interface PlanRevisionComment { + /** The part of the plan being commented on */ + revisedPart: string; + /** The user's revision instructions */ + revisorInstructions: string; +} + +// ================================ +// Encoding / Decoding Utilities +// ================================ + +const textEncoder = new TextEncoder(); +const textDecoder = new TextDecoder(); + +/** + * Maximum plan content size in bytes for renderer data. + * Plans exceeding this are truncated; the full plan remains in the Agent Console. + */ +export const MAX_PLAN_RENDERER_SIZE = 50 * 1024; // 50KB + +/** + * Encodes renderer data to a Uint8Array for `toolResultDetails2.value`. + */ +export function encodeRendererData(data: T): Uint8Array { + return textEncoder.encode(JSON.stringify(data)); +} + +/** + * Decodes renderer data from a Uint8Array received in `renderChatOutput`. + * Throws if the data is malformed. + */ +export function decodeRendererData(value: Uint8Array): T { + const json = textDecoder.decode(value); + return JSON.parse(json) as T; +} + +/** + * Truncates plan content to MAX_PLAN_RENDERER_SIZE bytes. + * Returns the truncated string and whether truncation occurred. + */ +export function truncatePlan(plan: string): { content: string; truncated: boolean } { + const encoded = textEncoder.encode(plan); + if (encoded.length <= MAX_PLAN_RENDERER_SIZE) { + return { content: plan, truncated: false }; + } + // Truncate at byte level, then decode safely (may trim a partial UTF-8 char) + const truncatedBytes = encoded.slice(0, MAX_PLAN_RENDERER_SIZE); + const content = textDecoder.decode(truncatedBytes); + return { content, truncated: true }; +} From 125241fa95aeb8dd9a1d037ff5747ff4708c0308 Mon Sep 17 00:00:00 2001 From: Bernardo Date: Tue, 10 Feb 2026 04:27:55 +0000 Subject: [PATCH 05/12] Implement ask_user chat output renderer --- src/renderers/askUserRenderer.ts | 171 +++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 src/renderers/askUserRenderer.ts diff --git a/src/renderers/askUserRenderer.ts b/src/renderers/askUserRenderer.ts new file mode 100644 index 0000000..06583f0 --- /dev/null +++ b/src/renderers/askUserRenderer.ts @@ -0,0 +1,171 @@ +/** + * Chat Output Renderer for `ask_user` tool results. + * + * Renders a rich inline card in the chat stream showing: + * - Agent name and title + * - The question asked + * - The user's response (or dismissed state) + * - Attachment list + * - Relative timestamp + */ + +import * as vscode from 'vscode'; +import MarkdownIt from 'markdown-it'; +import { AskUserRendererData, decodeRendererData } from './types'; +import { escapeHtml, formatTimestamp, wrapInDocument } from './htmlUtils'; +import { strings } from '../localization'; + +/** View type matching the chatOutputRenderers contribution in package.json */ +export const ASK_USER_VIEW_TYPE = 'seamless-agent.askUserResult'; + +/** Shared markdown-it instance for rendering user response content */ +const md = new MarkdownIt({ + html: false, // Disable HTML passthrough for security + linkify: true, + typographer: false, +}); + +/** + * Registers the ask_user chat output renderer. + */ +export function registerAskUserRenderer(): vscode.Disposable { + return vscode.chat.registerChatOutputRenderer(ASK_USER_VIEW_TYPE, { + async renderChatOutput({ value }, chatOutputWebview, _ctx, _token) { + let html: string; + try { + const data = decodeRendererData(value); + html = buildAskUserCardHtml(data); + } catch { + html = buildFallbackHtml(strings.rendererRenderErrorAskUser); + } + + chatOutputWebview.webview.options = { enableScripts: false }; + chatOutputWebview.webview.html = html; + }, + }); +} + +/** + * Builds the HTML for an ask_user result card. + */ +function buildAskUserCardHtml(data: AskUserRendererData): string { + const agentName = escapeHtml(data.agentName || strings.rendererDefaultAgentName); + const title = data.title ? escapeHtml(data.title) : strings.rendererUserResponse; + const timestamp = formatTimestamp(data.timestamp); + + let bodyHtml: string; + + if (!data.responded) { + // Dismissed / cancelled state + bodyHtml = ` +
+
+ 💬 + ${agentName}: ${title} +
+
+
+ ❌ ${escapeHtml(strings.rendererUserDismissed)} +
+
+ +
`; + } else { + // Responded state + const questionHtml = buildQuestionSection(data.question); + const responseHtml = buildResponseSection(data.response); + const attachmentsHtml = buildAttachmentsSection(data.attachments); + + bodyHtml = ` +
+
+ 💬 + ${agentName}: ${title} +
+
+ ${questionHtml} + ${responseHtml} + ${attachmentsHtml} +
+ +
`; + } + + return wrapInDocument(`${strings.rendererUserResponse}: ${title}`, bodyHtml); +} + +/** + * Builds the question section. Long questions are truncated with a
expand. + */ +function buildQuestionSection(question: string): string { + const MAX_QUESTION_LENGTH = 300; + const renderedHtml = md.render(question); + + if (question.length <= MAX_QUESTION_LENGTH) { + return ` +
+ +
${renderedHtml}
+
`; + } + + const truncatedRendered = md.render(question.substring(0, MAX_QUESTION_LENGTH) + '…'); + return ` +
+ +
+ + ${escapeHtml(strings.rendererClickToExpand)} +
${truncatedRendered}
+
+
${renderedHtml}
+
+
`; +} + +/** + * Builds the response section. + */ +function buildResponseSection(response: string): string { + const renderedHtml = md.render(response); + return ` +
+ +
${renderedHtml}
+
`; +} + +/** + * Builds the attachments section if there are any attachments. + */ +function buildAttachmentsSection(attachments: AskUserRendererData['attachments']): string { + if (!attachments || attachments.length === 0) { + return ''; + } + + const items = attachments.map(att => { + const name = escapeHtml(att.name); + const icon = att.isImage ? '🖼️' : '📎'; + return `
  • ${icon} ${name}
  • `; + }).join('\n'); + + return ` +
    + +
      + ${items} +
    +
    `; +} + +/** + * Builds a fallback HTML page when rendering fails. + */ +function buildFallbackHtml(message: string): string { + return wrapInDocument(strings.rendererError, ` +
    +
    + ${escapeHtml(message)} +
    +
    `); +} From e0ddfaf8d4afa9d4e1007f5a0eb2a15f105c7ed2 Mon Sep 17 00:00:00 2001 From: Bernardo Date: Tue, 10 Feb 2026 04:27:56 +0000 Subject: [PATCH 06/12] Implement plan review chat output renderer --- src/renderers/planReviewRenderer.ts | 160 ++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) create mode 100644 src/renderers/planReviewRenderer.ts diff --git a/src/renderers/planReviewRenderer.ts b/src/renderers/planReviewRenderer.ts new file mode 100644 index 0000000..d2a569d --- /dev/null +++ b/src/renderers/planReviewRenderer.ts @@ -0,0 +1,160 @@ +/** + * Chat Output Renderer for plan review / walkthrough review tool results. + * + * Renders a rich inline card in the chat stream showing: + * - Title and status badge (Approved / Changes Requested / Cancelled / Acknowledged) + * - Mode indicator (Review vs. Walkthrough) + * - Collapsible plan content (markdown rendered to HTML) + * - Revision comments (if any) + * - Relative timestamp + * + * Markdown is rendered server-side via `markdown-it` in the extension host, + * not in the webview. This avoids the need for `enableScripts`. + */ + +import * as vscode from 'vscode'; +import MarkdownIt from 'markdown-it'; +import { PlanReviewRendererData, decodeRendererData } from './types'; +import { escapeHtml, formatTimestamp, wrapInDocument } from './htmlUtils'; +import { strings } from '../localization'; + +/** View type matching the chatOutputRenderers contribution in package.json */ +export const PLAN_REVIEW_VIEW_TYPE = 'seamless-agent.planReviewResult'; + +/** Shared markdown-it instance */ +const md = new MarkdownIt({ + html: false, // Disable HTML passthrough for security + linkify: true, + typographer: false, +}); + +/** + * Registers the plan review chat output renderer. + */ +export function registerPlanReviewRenderer(): vscode.Disposable { + return vscode.chat.registerChatOutputRenderer(PLAN_REVIEW_VIEW_TYPE, { + async renderChatOutput({ value }, chatOutputWebview, _ctx, _token) { + let html: string; + try { + const data = decodeRendererData(value); + html = buildPlanReviewCardHtml(data); + } catch { + html = buildFallbackHtml(strings.rendererRenderErrorPlanReview); + } + + chatOutputWebview.webview.options = { enableScripts: false }; + chatOutputWebview.webview.html = html; + }, + }); +} + +/** + * Builds the HTML for a plan review result card. + */ +function buildPlanReviewCardHtml(data: PlanReviewRendererData): string { + const title = escapeHtml(data.title || strings.rendererPlanReview); + const timestamp = formatTimestamp(data.timestamp); + const statusHtml = buildStatusBadge(data.status); + const modeLabel = data.mode === 'walkthrough' ? strings.rendererWalkthrough : strings.rendererReview; + const headerIcon = data.mode === 'walkthrough' ? '📖' : '📋'; + + const planHtml = buildPlanSection(data.plan); + const revisionsHtml = buildRevisionsSection(data.requiredRevisions); + + const bodyHtml = ` +
    +
    + ${headerIcon} + ${escapeHtml(modeLabel)}: ${title} +
    +
    +
    + +
    ${statusHtml}
    +
    + ${planHtml} + ${revisionsHtml} +
    + +
    `; + + return wrapInDocument(`${escapeHtml(strings.rendererPlanReview)}: ${title}`, bodyHtml); +} + +/** + * Builds a status badge with color-coded styling. + */ +function buildStatusBadge(status: PlanReviewRendererData['status']): string { + switch (status) { + case 'approved': + return `✅ ${escapeHtml(strings.approved)}`; + case 'recreateWithChanges': + return `🔄 ${escapeHtml(strings.rendererChangesRequested)}`; + case 'cancelled': + return `❌ ${escapeHtml(strings.cancelled)}`; + case 'acknowledged': + return `📖 ${escapeHtml(strings.acknowledged)}`; + default: + return `${escapeHtml(String(status))}`; + } +} + +/** + * Builds the plan content section, rendered from markdown to HTML. + * Content is collapsed by default using
    . + */ +function buildPlanSection(plan: string): string { + if (!plan || plan.trim().length === 0) { + return ''; + } + + // Render markdown to HTML server-side + const renderedHtml = md.render(plan); + + return ` +
    +
    + ${escapeHtml(strings.rendererViewPlan)} +
    ${renderedHtml}
    +
    +
    `; +} + +/** + * Builds the revision comments section. + */ +function buildRevisionsSection(revisions: PlanReviewRendererData['requiredRevisions']): string { + if (!revisions || revisions.length === 0) { + return ''; + } + + const items = revisions.map((rev, i) => { + const part = escapeHtml(rev.revisedPart); + const instruction = escapeHtml(rev.revisorInstructions); + return ` +
  • +
    ${i + 1}. "${part}"
    +
    → ${instruction}
    +
  • `; + }).join('\n'); + + return ` +
    + +
      + ${items} +
    +
    `; +} + +/** + * Builds a fallback HTML page when rendering fails. + */ +function buildFallbackHtml(message: string): string { + return wrapInDocument(strings.rendererError, ` +
    +
    + ${escapeHtml(message)} +
    +
    `); +} From 608b2ff47ea71992488f251d077feb503cdf9d37 Mon Sep 17 00:00:00 2001 From: Bernardo Date: Tue, 10 Feb 2026 04:27:56 +0000 Subject: [PATCH 07/12] Create central renderer registration module --- src/renderers/index.ts | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 src/renderers/index.ts diff --git a/src/renderers/index.ts b/src/renderers/index.ts new file mode 100644 index 0000000..9ab29bc --- /dev/null +++ b/src/renderers/index.ts @@ -0,0 +1,36 @@ +/** + * Central registration for all Chat Output Renderers. + * + * Called from `activate()` in extension.ts. Feature-gated behind a + * runtime check for the proposed `chatOutputRenderer` API. + */ + +import * as vscode from 'vscode'; +import { registerAskUserRenderer } from './askUserRenderer'; +import { registerPlanReviewRenderer } from './planReviewRenderer'; + +/** + * Registers all chat output renderers if the proposed API is available. + * + * On VS Code versions without the `chatOutputRenderer` proposal, + * this function is a no-op — the existing text-based output continues to work. + */ +export function registerChatOutputRenderers(context: vscode.ExtensionContext): void { + // Feature gate: check if the proposed API exists + if (typeof vscode.chat?.registerChatOutputRenderer !== 'function') { + console.log('Chat Output Renderer API not available — using text-only output'); + return; + } + + console.log('Registering chat output renderers'); + + const askUserDisposable = registerAskUserRenderer(); + const planReviewDisposable = registerPlanReviewRenderer(); + + (context.subscriptions as unknown as Array).push( + askUserDisposable, + planReviewDisposable + ); + + console.log('Chat output renderers registered successfully'); +} From 947fcc80f1245edfb260471941846beb2c5f1e37 Mon Sep 17 00:00:00 2001 From: Bernardo Date: Tue, 10 Feb 2026 04:27:56 +0000 Subject: [PATCH 08/12] Add MIME type constants for renderer data --- src/tools/schemas.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/tools/schemas.ts b/src/tools/schemas.ts index fc4812c..d24284a 100644 --- a/src/tools/schemas.ts +++ b/src/tools/schemas.ts @@ -76,6 +76,16 @@ export type ApprovePlanInput = z.infer; export type PlanReviewInput = z.infer; export type WalkthroughReviewInput = z.infer; +// ================================ +// Chat Output Renderer MIME Types +// ================================ + +/** MIME type for ask_user chat output renderer */ +export const ASK_USER_RESULT_MIME = 'application/vnd.seamless-agent.ask-user-result'; + +/** MIME type for plan review chat output renderer */ +export const PLAN_REVIEW_RESULT_MIME = 'application/vnd.seamless-agent.plan-review-result'; + // ================================ // Result Interfaces // ================================ From c6b0007c8f42a87cb7015bc9d9aa05c32af40fa9 Mon Sep 17 00:00:00 2001 From: Bernardo Date: Tue, 10 Feb 2026 04:27:56 +0000 Subject: [PATCH 09/12] Attach renderer data to ask_user tool results --- src/tools/index.ts | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/tools/index.ts b/src/tools/index.ts index 9b9f034..eac293b 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -26,7 +26,11 @@ import { parseApprovePlanInput, parsePlanReviewInput, parseWalkthroughReviewInput, + ASK_USER_RESULT_MIME, + PLAN_REVIEW_RESULT_MIME, } from './schemas'; +import { AskUserRendererData, PlanReviewRendererData, encodeRendererData, truncatePlan } from '../renderers/types'; +import { getImageMimeType as getMimeType } from './utils'; /** * Registers the native VS Code LM Tools @@ -99,7 +103,36 @@ export function registerNativeTools(context: vscode.ExtensionContext, provider: } // Return result to the AI with both text and image parts - return new vscode.LanguageModelToolResult(resultParts); + const toolResult = new vscode.LanguageModelToolResult(resultParts); + + // Attach renderer data for Chat Output Renderer (rich inline card) + try { + const rendererData: AskUserRendererData = { + question: params.question, + response: result.response, + responded: result.responded, + agentName: params.agentName, + title: params.title, + timestamp: Date.now(), + attachments: (result.attachments || []).map(uri => { + const name = uri.split('/').pop() || uri; + const mime = getMimeType(vscode.Uri.parse(uri).fsPath); + return { + name, + uri, + isImage: mime !== 'application/octet-stream' + }; + }) + }; + (toolResult as vscode.ExtendedLanguageModelToolResult2).toolResultDetails2 = { + mime: ASK_USER_RESULT_MIME, + value: encodeRendererData(rendererData) + }; + } catch (e) { + console.warn('Failed to attach renderer data to ask_user result:', e); + } + + return toolResult; }, prepareInvocation(options) { return { From b51f9ac6a28869f17b08ef331fdec0f8d6c2380d Mon Sep 17 00:00:00 2001 From: Bernardo Date: Tue, 10 Feb 2026 04:27:56 +0000 Subject: [PATCH 10/12] Attach renderer data to plan review tool results --- src/tools/index.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/tools/index.ts b/src/tools/index.ts index eac293b..ce31c08 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -212,9 +212,31 @@ export function registerNativeTools(context: vscode.ExtensionContext, provider: ); // Return result to the AI - return new vscode.LanguageModelToolResult([ + const toolResult = new vscode.LanguageModelToolResult([ new vscode.LanguageModelTextPart(JSON.stringify(result)) ]); + + // Attach renderer data for Chat Output Renderer (rich inline card) + try { + const { content: planContent, truncated } = truncatePlan(params.plan); + const rendererData: PlanReviewRendererData = { + title: params.title || 'Plan Review', + status: result.status, + mode: 'review', + plan: planContent, + timestamp: Date.now(), + requiredRevisions: result.requiredRevisions || [], + reviewId: result.reviewId + }; + (toolResult as vscode.ExtendedLanguageModelToolResult2).toolResultDetails2 = { + mime: PLAN_REVIEW_RESULT_MIME, + value: encodeRendererData(rendererData) + }; + } catch (e) { + console.warn('Failed to attach renderer data to plan_review result:', e); + } + + return toolResult; } }); From a37429a49d95939e0e830b2b859c46cc39b7bf58 Mon Sep 17 00:00:00 2001 From: Bernardo Date: Tue, 10 Feb 2026 04:27:57 +0000 Subject: [PATCH 11/12] Attach renderer data to walkthrough review tool results --- src/tools/index.ts | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/tools/index.ts b/src/tools/index.ts index ce31c08..559589d 100644 --- a/src/tools/index.ts +++ b/src/tools/index.ts @@ -269,9 +269,31 @@ export function registerNativeTools(context: vscode.ExtensionContext, provider: token ); - return new vscode.LanguageModelToolResult([ + const toolResult = new vscode.LanguageModelToolResult([ new vscode.LanguageModelTextPart(JSON.stringify(result)) ]); + + // Attach renderer data for Chat Output Renderer (rich inline card) + try { + const { content: planContent, truncated } = truncatePlan(params.plan); + const rendererData: PlanReviewRendererData = { + title: params.title || 'Walkthrough Review', + status: result.status, + mode: 'walkthrough', + plan: planContent, + timestamp: Date.now(), + requiredRevisions: result.requiredRevisions || [], + reviewId: result.reviewId + }; + (toolResult as vscode.ExtendedLanguageModelToolResult2).toolResultDetails2 = { + mime: PLAN_REVIEW_RESULT_MIME, + value: encodeRendererData(rendererData) + }; + } catch (e) { + console.warn('Failed to attach renderer data to walkthrough_review result:', e); + } + + return toolResult; } }); From c34f5d290e9faeae3ca9604d5a07d6c5beacc212 Mon Sep 17 00:00:00 2001 From: Bernardo Date: Tue, 10 Feb 2026 04:27:57 +0000 Subject: [PATCH 12/12] Register renderers and integrate with extension --- src/extension.ts | 15 +++++++++++---- src/localization.ts | 18 ++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 07c0cea..853f49a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,6 +3,7 @@ import { registerNativeTools } from './tools'; import { AgentInteractionProvider } from './webview/webviewProvider'; import { initializeChatHistoryStorage, getChatHistoryStorage } from './storage/chatHistoryStorage'; import { strings } from './localization'; +import { registerChatOutputRenderers } from './renderers'; const PARTICIPANT_ID = 'seamless-agent.agent'; @@ -28,6 +29,9 @@ export function activate(context: vscode.ExtensionContext) { // Register the ask_user tool with the webview provider registerNativeTools(context, provider); + // Register chat output renderers for rich inline cards (proposed API, feature-gated) + registerChatOutputRenderers(context); + // Register command to cancel pending plans const cancelPendingPlansCommand = vscode.commands.registerCommand('seamless-agent.cancelPendingPlans', async () => { const storage = getChatHistoryStorage(); @@ -146,10 +150,13 @@ Never finish a response without first calling the ask_user tool to verify with t toolInvocationToken: request.toolInvocationToken }, token); - // Show tool result - for (const resultPart of toolResult.content) { - if (resultPart instanceof vscode.LanguageModelTextPart) { - stream.markdown(`\n\n**User Response:** ${resultPart.value}\n\n`); + // Show tool result — skip markdown when a renderer card handles the display + const hasRendererCard = !!(toolResult as any).toolResultDetails2; + if (!hasRendererCard) { + for (const resultPart of toolResult.content) { + if (resultPart instanceof vscode.LanguageModelTextPart) { + stream.markdown(`\n\n**User Response:** ${resultPart.value}\n\n`); + } } } } diff --git a/src/localization.ts b/src/localization.ts index 54c72f8..82dfa95 100644 --- a/src/localization.ts +++ b/src/localization.ts @@ -159,4 +159,22 @@ export const strings = { // Errors get noSuchInteraction() { return localize('error.noSuchInteraction'); }, + + // Renderer cards + get rendererUserResponse() { return localize('renderer.userResponse'); }, + get rendererDefaultAgentName() { return localize('renderer.defaultAgentName'); }, + get rendererUserDismissed() { return localize('renderer.userDismissed'); }, + get rendererClickToExpand() { return localize('renderer.clickToExpand'); }, + get rendererUserResponded() { return localize('renderer.userResponded'); }, + get rendererAttachments() { return localize('renderer.attachments'); }, + get rendererRenderErrorAskUser() { return localize('renderer.renderErrorAskUser'); }, + get rendererRenderErrorPlanReview() { return localize('renderer.renderErrorPlanReview'); }, + get rendererError() { return localize('renderer.error'); }, + get rendererPlanReview() { return localize('renderer.planReview'); }, + get rendererWalkthrough() { return localize('renderer.walkthrough'); }, + get rendererReview() { return localize('renderer.review'); }, + get rendererStatus() { return localize('renderer.status'); }, + get rendererChangesRequested() { return localize('renderer.changesRequested'); }, + get rendererViewPlan() { return localize('renderer.viewPlan'); }, + get rendererRevisionsRequired() { return localize('renderer.revisionsRequired'); }, };