From 5c5a5be020a813eaebdd5e323864efa8943517fe Mon Sep 17 00:00:00 2001 From: liugddx Date: Sat, 12 Sep 2026 12:32:24 +0800 Subject: [PATCH 01/13] fix(desktop): offer Finder fallback for unreadable HTML artifacts --- .../tools/artifacts/artifact-preview.tsx | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-preview.tsx b/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-preview.tsx index 4cf547a0d9..26fbf8b38e 100644 --- a/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-preview.tsx +++ b/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-preview.tsx @@ -79,7 +79,7 @@ export function ArtifactPreview(props: { record: ArtifactDescriptor; onShowInFol case 'diff': return ; case 'html': - return ; + return ; case 'image': // PR-UI-RENDER-3a: route image previews through the typed // registry shell so the resolution path (mime match / ext @@ -176,10 +176,10 @@ function DiffPreview(props: { record: ArtifactDescriptor; copy: ArtifactCopy }) ); } -function HtmlPreview(props: { record: ArtifactDescriptor; copy: ArtifactCopy }) { +function HtmlPreview(props: { record: ArtifactDescriptor; copy: ArtifactCopy; onShowInFolder?: () => void }) { const result = useTextRead(props.record.sessionId, props.record.id); if (result.state === 'loading') return ; - if (!result.value.ok) return ; + if (!result.value.ok) return ; const bounded = boundPreviewText(result.value.text); if (bounded.isDisplayTruncated) { return ( @@ -335,9 +335,18 @@ function PreviewLoading(props: { label: string }) { ); } -function TextFailureCard(props: { record: ArtifactDescriptor; reason: TextFailureReason; copy: ArtifactCopy }) { +function TextFailureCard(props: { record: ArtifactDescriptor; reason: TextFailureReason; copy: ArtifactCopy; onShowInFolder?: () => void }) { const { status, title, description } = failureCopyText(props.record, props.reason, props.copy); - return ; + return ( +
+ + {props.onShowInFolder ? ( + + ) : null} +
+ ); } function BinaryFailureCard(props: { record: ArtifactDescriptor; reason: BinaryFailureReason; copy: ArtifactCopy }) { From c2c795fd041f21ea7a0f17acdb537b5b38449e94 Mon Sep 17 00:00:00 2001 From: liugddx Date: Sat, 12 Sep 2026 12:39:13 +0800 Subject: [PATCH 02/13] fix(desktop): open HTML artifacts with the default app --- apps/desktop/src/main/runtime-host-artifacts-ipc-main.ts | 8 +++++++- apps/desktop/src/main/runtime-host-boot.ts | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/main/runtime-host-artifacts-ipc-main.ts b/apps/desktop/src/main/runtime-host-artifacts-ipc-main.ts index 6b136f81ba..4ef56c85b4 100644 --- a/apps/desktop/src/main/runtime-host-artifacts-ipc-main.ts +++ b/apps/desktop/src/main/runtime-host-artifacts-ipc-main.ts @@ -42,6 +42,7 @@ interface RuntimeHostArtifactsIpcDeps { readonly client: DesktopRuntimeHostClient; readonly mainWindowController: ReturnType; readonly showItemInFolder: (path: string) => void; + readonly openPath?: (path: string) => Promise; readonly presentationRoot?: string; } @@ -96,7 +97,12 @@ export function registerRuntimeHostArtifactsIpc( `${artifactId}-${sanitizeArtifactName(artifact.name)}`, ); await materializeArtifact(deps.client, sessionId, artifactId, path, artifact.sizeBytes); - deps.showItemInFolder(path); + if (artifact.kind === 'html' && deps.openPath) { + const error = await deps.openPath(path); + if (error) return { ok: false as const, reason: "open-failed" as const }; + } else { + deps.showItemInFolder(path); + } return { ok: true as const, opened: artifact.name }; } catch { return { ok: false as const, reason: "open-failed" as const }; diff --git a/apps/desktop/src/main/runtime-host-boot.ts b/apps/desktop/src/main/runtime-host-boot.ts index a82669b29a..74098a21fc 100644 --- a/apps/desktop/src/main/runtime-host-boot.ts +++ b/apps/desktop/src/main/runtime-host-boot.ts @@ -1640,6 +1640,7 @@ function registerHostClientIpc( client, mainWindowController, showItemInFolder: (path) => shell.showItemInFolder(path), + openPath: (path) => shell.openPath(path), }); registerRuntimeHostOAuthIpc({ ipcMain: scopedIpc, From b4264d59b5771c771b415c0194930622bf640840 Mon Sep 17 00:00:00 2001 From: liugddx Date: Sat, 12 Sep 2026 13:49:26 +0800 Subject: [PATCH 03/13] fix(desktop): localize artifact fallback action --- .../features/workbar/tools/artifacts/artifact-preview.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-preview.tsx b/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-preview.tsx index 26fbf8b38e..be1298ce6e 100644 --- a/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-preview.tsx +++ b/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-preview.tsx @@ -341,8 +341,8 @@ function TextFailureCard(props: { record: ArtifactDescriptor; reason: TextFailur
{props.onShowInFolder ? ( - ) : null}
From 508df61acab8d8187c9cfbae90e2b9f3601cf7f5 Mon Sep 17 00:00:00 2001 From: liugddx Date: Sat, 12 Sep 2026 14:03:49 +0800 Subject: [PATCH 04/13] fix(runtime-host): admit desktop workhub capabilities --- .../src/server/client-capability-coordinator.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/packages/runtime-host/src/server/client-capability-coordinator.ts b/packages/runtime-host/src/server/client-capability-coordinator.ts index b1ce6da249..af9b155473 100644 --- a/packages/runtime-host/src/server/client-capability-coordinator.ts +++ b/packages/runtime-host/src/server/client-capability-coordinator.ts @@ -65,6 +65,7 @@ import { clientCapabilityProviderId } from './client-capability-provider-id.js'; const DEFAULT_CALL_TIMEOUT_MS = 150_000; const DESKTOP_BROWSER_SERVER_ID = 'desktop_browser'; const DESKTOP_SETTINGS_SERVER_ID = 'desktop_settings'; +const DESKTOP_WORKHUB_SERVER_ID = 'desktop_workhub'; const DESKTOP_MCP_OFFER_PREFIX = 'desktop_mcp'; const DESKTOP_BROWSER_TOOLS = new Set([ 'browser_navigate', @@ -1580,6 +1581,19 @@ function managedClientCapabilityGrantTarget( scope: Object.freeze({ kind: 'browser_origin', origin: url.origin }), }); } + if (tool.offerId === DESKTOP_WORKHUB_SERVER_ID && serverId === DESKTOP_WORKHUB_SERVER_ID) { + if (evidence.kind !== 'none') { + throw new Error('Desktop WorkHub admission does not accept scope evidence'); + } + return Object.freeze({ + providerId: registration.providerId, + contractId, + serverId, + toolName, + capability: 'computer_use', + scope: Object.freeze({ kind: 'capability' }), + }); + } // Desktop MCP tools publish one offer per MCP server (chunked past the // single-offer tool limit), every offerId carrying the desktop_mcp prefix. // The Session Grant scope takes the descriptor's real MCP server identity. From e04752fad6ce1db5ec26009ae98e36b943fa1b47 Mon Sep 17 00:00:00 2001 From: liugddx Date: Sat, 12 Sep 2026 20:30:11 +0800 Subject: [PATCH 05/13] test(desktop): cover opening HTML artifacts with default app --- .../runtime-host-artifacts-ipc-main.test.ts | 57 +++++++++++++++++++ .../server/client-capability-coordinator.ts | 14 ----- 2 files changed, 57 insertions(+), 14 deletions(-) diff --git a/apps/desktop/src/main/__tests__/runtime-host-artifacts-ipc-main.test.ts b/apps/desktop/src/main/__tests__/runtime-host-artifacts-ipc-main.test.ts index 77f5186af0..d7852886ae 100644 --- a/apps/desktop/src/main/__tests__/runtime-host-artifacts-ipc-main.test.ts +++ b/apps/desktop/src/main/__tests__/runtime-host-artifacts-ipc-main.test.ts @@ -250,6 +250,63 @@ test("Runtime Host Artifact IPC preserves previews and streams complete exports" } }); +test("HTML Artifact materializes and opens with the operating system default app", async () => { + const root = await mkdtemp(join(tmpdir(), "maka-host-html-artifact-ipc-")); + const presentationRoot = join(root, "presentations"); + const content = Buffer.from(""); + const handlers = new Map(); + const openedPaths: string[] = []; + const artifact = previewArtifact({ + name: "interactive.html", + kind: "html", + mimeType: "text/html", + sizeBytes: content.byteLength, + }); + + try { + registerRuntimeHostArtifactsIpc({ + uiLocale: () => "en" as const, + ipcMain: { + handle: (channel, handler) => handlers.set(channel, handler as Handler), + }, + client: { + hostEpoch: "host-1", + async getArtifact() { + return artifact; + }, + async streamArtifact( + _sessionId: string, + _artifactId: string, + writeChunk: (chunk: Uint8Array) => Promise, + ) { + await writeChunk(content); + return content.byteLength; + }, + } as never, + mainWindowController: {} as never, + showItemInFolder: () => { + throw new Error("HTML artifacts must use openPath"); + }, + openPath: async (path) => { + openedPaths.push(path); + return ""; + }, + presentationRoot, + }); + + const open = handlers.get("app:openArtifactPath"); + assert.ok(open); + assert.deepEqual(await open({}, "session-1", "artifact-1"), { + ok: true, + opened: "interactive.html", + }); + assert.equal(openedPaths.length, 1); + assert.equal(await readFile(openedPaths[0]!, "utf8"), content.toString("utf8")); + } finally { + await rm(root, { recursive: true, force: true }); + } +}); + test("Attachment byte IPC rejects preview-ineligible metadata before streaming", async () => { for (const [overrides, reason] of [ [{ id: "artifact-large", sizeBytes: 2 * 1024 * 1024 + 1 }, "too_large"], diff --git a/packages/runtime-host/src/server/client-capability-coordinator.ts b/packages/runtime-host/src/server/client-capability-coordinator.ts index af9b155473..b1ce6da249 100644 --- a/packages/runtime-host/src/server/client-capability-coordinator.ts +++ b/packages/runtime-host/src/server/client-capability-coordinator.ts @@ -65,7 +65,6 @@ import { clientCapabilityProviderId } from './client-capability-provider-id.js'; const DEFAULT_CALL_TIMEOUT_MS = 150_000; const DESKTOP_BROWSER_SERVER_ID = 'desktop_browser'; const DESKTOP_SETTINGS_SERVER_ID = 'desktop_settings'; -const DESKTOP_WORKHUB_SERVER_ID = 'desktop_workhub'; const DESKTOP_MCP_OFFER_PREFIX = 'desktop_mcp'; const DESKTOP_BROWSER_TOOLS = new Set([ 'browser_navigate', @@ -1581,19 +1580,6 @@ function managedClientCapabilityGrantTarget( scope: Object.freeze({ kind: 'browser_origin', origin: url.origin }), }); } - if (tool.offerId === DESKTOP_WORKHUB_SERVER_ID && serverId === DESKTOP_WORKHUB_SERVER_ID) { - if (evidence.kind !== 'none') { - throw new Error('Desktop WorkHub admission does not accept scope evidence'); - } - return Object.freeze({ - providerId: registration.providerId, - contractId, - serverId, - toolName, - capability: 'computer_use', - scope: Object.freeze({ kind: 'capability' }), - }); - } // Desktop MCP tools publish one offer per MCP server (chunked past the // single-offer tool limit), every offerId carrying the desktop_mcp prefix. // The Session Grant scope takes the descriptor's real MCP server identity. From 228f12d928297d1c2344a55f7d7464bf1f4b2d50 Mon Sep 17 00:00:00 2001 From: liugddx Date: Sat, 12 Sep 2026 20:45:54 +0800 Subject: [PATCH 06/13] fix(artifacts): expose generated HTML in files panel --- packages/core/src/__tests__/artifacts.test.ts | 6 ++++++ packages/core/src/artifacts.ts | 9 ++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/packages/core/src/__tests__/artifacts.test.ts b/packages/core/src/__tests__/artifacts.test.ts index 93557757ce..66fd52cb00 100644 --- a/packages/core/src/__tests__/artifacts.test.ts +++ b/packages/core/src/__tests__/artifacts.test.ts @@ -81,4 +81,10 @@ describe('Artifact source policy', () => { assert.equal(isArtifactUserVisible(projection), false); assert.equal(isArtifactSharedSessionReadable(projection), true); }); + + test('exposes directly written HTML files while keeping other tool results internal', () => { + assert.equal(isArtifactUserVisible({ source: 'tool_result', kind: 'html' }), true); + assert.equal(isArtifactUserVisible({ source: 'tool_result', kind: 'file' }), false); + assert.equal(isArtifactUserVisible({ source: 'tool_result', kind: 'diff' }), false); + }); }); diff --git a/packages/core/src/artifacts.ts b/packages/core/src/artifacts.ts index 43921173bc..51a598d5ba 100644 --- a/packages/core/src/artifacts.ts +++ b/packages/core/src/artifacts.ts @@ -164,7 +164,14 @@ const CHILD_RESULT_OUTPUT_SOURCES = new Set([ 'deep_research', ]); -export function isArtifactUserVisible(record: Pick): boolean { +export function isArtifactUserVisible( + record: Pick & Partial>, +): boolean { + // A directly written HTML file is an intentional user-facing deliverable: + // the Artifact Pane must be able to preview and open it without requiring a + // child-workspace writeback. Other tool results remain internal to avoid + // flooding the Generated Files tab with command output and diffs. + if (record.source === 'tool_result' && record.kind === 'html') return true; return ARTIFACT_SOURCE_POLICIES[record.source].userVisible; } From da213246597f40360705099a5273f1c6033ef693 Mon Sep 17 00:00:00 2001 From: liugddx Date: Sat, 12 Sep 2026 21:04:10 +0800 Subject: [PATCH 07/13] fix(desktop): label HTML artifact action as open --- .../features/workbar/tools/artifacts/artifact-pane.tsx | 5 +++-- apps/desktop/src/renderer/locales/artifact-copy.ts | 4 ++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-pane.tsx b/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-pane.tsx index 135bc78349..e2f5fa5dfe 100644 --- a/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-pane.tsx +++ b/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-pane.tsx @@ -39,7 +39,8 @@ * 5. **Copy/export policy**: only the text-based kinds (`file`, `diff`, * `html`) expose a Copy button. `image` / `pdf` rows do NOT — those are * binary, and silently base64-stuffing a multi-MB PDF into the clipboard - * is a footgun. Both kinds still get「在 Finder 中打开」and「另存为」. + * is a footgun. HTML gets「打开」(the system default app); other kinds + * get「在 Finder 中打开」. All kinds still get「另存为」. * * Layout: fills the Generated files tab and switches between a list and one * full-panel preview while reporting its authoritative filtered count. @@ -544,7 +545,7 @@ export function ArtifactPane(props: { onOpenChange={setMoreMenuOpen} items={[ { - label: copy.pane.openInFinder, + label: previewRecord.kind === 'html' ? copy.pane.open : copy.pane.openInFinder, icon: