From 562687591f50656530a322cbd69a927ea1b9decc Mon Sep 17 00:00:00 2001 From: Dante Date: Sat, 12 Sep 2026 13:19:20 +0800 Subject: [PATCH 1/2] fix(desktop): add HTML artifact Finder fallback Generated-by: Codex --- .../artifact-html-preview-fallback.test.ts | 120 ++++++++++++++++++ .../workbar-services-adapter.test.ts | 4 + .../artifact-preview-registry-shell.tsx | 2 +- .../tools/artifacts/artifact-preview.tsx | 46 ++++++- 4 files changed, 164 insertions(+), 8 deletions(-) create mode 100644 apps/desktop/src/main/__tests__/artifact-html-preview-fallback.test.ts diff --git a/apps/desktop/src/main/__tests__/artifact-html-preview-fallback.test.ts b/apps/desktop/src/main/__tests__/artifact-html-preview-fallback.test.ts new file mode 100644 index 0000000000..1afc3a799f --- /dev/null +++ b/apps/desktop/src/main/__tests__/artifact-html-preview-fallback.test.ts @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import assert from 'node:assert/strict'; +import { afterEach, test } from 'node:test'; +import { parseHTML } from 'linkedom'; +import { act, createElement } from 'react'; +import { createRoot, type Root } from 'react-dom/client'; +import { AstryxLocaleProvider, LocaleProvider } from '@maka/ui'; +import type { ArtifactDescriptor } from '@maka/core/artifacts'; +import { ArtifactPreview } from '../../renderer/features/workbar/tools/artifacts/artifact-preview.js'; +import { + createFakeWorkbarServices, + WorkbarServicesProvider, +} from '../../renderer/features/workbar/testing.js'; + +const originalGlobals = { + document: globalThis.document, + window: globalThis.window, + HTMLElement: globalThis.HTMLElement, + HTMLIFrameElement: globalThis.HTMLIFrameElement, + Event: globalThis.Event, + Node: globalThis.Node, + IS_REACT_ACT_ENVIRONMENT: (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }) + .IS_REACT_ACT_ENVIRONMENT, +}; + +let mountedRoot: Root | undefined; + +afterEach(() => { + if (mountedRoot) { + act(() => mountedRoot?.unmount()); + mountedRoot = undefined; + } + Object.assign(globalThis, originalGlobals); +}); + +test('HTML read failure offers the existing safe open-path action', async () => { + const { document, window } = parseHTML('
'); + Object.assign(globalThis, { + document, + window, + HTMLElement: window.HTMLElement, + HTMLIFrameElement: window.HTMLIFrameElement ?? class HTMLIFrameElement {}, + Event: window.Event, + Node: window.Node, + IS_REACT_ACT_ENVIRONMENT: true, + }); + const container = document.querySelector('#root'); + assert.ok(container); + + const record: ArtifactDescriptor = { + id: 'artifact-html', + sessionId: 'session-html', + turnId: 'turn-html', + name: 'report.html', + kind: 'html', + mimeType: 'text/html', + sizeBytes: 42, + createdAt: 1, + source: 'tool_result', + }; + const calls: Array<[string, string]> = []; + const defaults = createFakeWorkbarServices(); + const services = { + ...defaults, + artifacts: { + ...defaults.artifacts, + readText: async () => ({ ok: false as const, reason: 'read_failed' as const }), + openPath: async (sessionId: string, artifactId: string) => { + calls.push([sessionId, artifactId]); + return { ok: true as const, opened: 'report.html' }; + }, + }, + }; + const root = createRoot(container); + mountedRoot = root; + + await act(async () => { + root.render(createElement(LocaleProvider, { + locale: 'en', + children: createElement(AstryxLocaleProvider, { + children: createElement(WorkbarServicesProvider, { + services, + children: createElement(ArtifactPreview, { + record, + onShowInFolder: () => void services.artifacts.openPath(record.sessionId, record.id), + }), + }), + }), + })); + await Promise.resolve(); + }); + + const button = Array.from(container.querySelectorAll('button')).find( + (candidate) => candidate.textContent === 'Show in Finder', + ); + assert.ok(button, 'HTML read failure should render a Finder fallback button'); + await act(async () => { + button.dispatchEvent(new window.Event('click', { bubbles: true })); + await Promise.resolve(); + }); + assert.deepEqual(calls, [['session-html', 'artifact-html']]); +}); diff --git a/apps/desktop/src/main/__tests__/workbar-services-adapter.test.ts b/apps/desktop/src/main/__tests__/workbar-services-adapter.test.ts index 6e92fabf11..167cdb0bc3 100644 --- a/apps/desktop/src/main/__tests__/workbar-services-adapter.test.ts +++ b/apps/desktop/src/main/__tests__/workbar-services-adapter.test.ts @@ -240,6 +240,10 @@ describe('createDesktopWorkbarServices', () => { 's', 'a', ]); + assert.deepEqual(calls.find((call) => call.name === 'app.openArtifactPath')?.args, [ + 's', + 'a', + ]); assert.deepEqual(calls.find((call) => call.name === 'inspector.trace')?.args, [ 's', 'cursor-1', diff --git a/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-preview-registry-shell.tsx b/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-preview-registry-shell.tsx index bec82c55ae..bad400ef13 100644 --- a/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-preview-registry-shell.tsx +++ b/apps/desktop/src/renderer/features/workbar/tools/artifacts/artifact-preview-registry-shell.tsx @@ -31,7 +31,7 @@ import { formatPreviewSize, resolvePreviewKind, } from '@maka/ui/artifact-preview-registry'; -import { getArtifactCopy, type ArtifactCopy } from '../../../../locales/artifact-copy'; +import { getArtifactCopy, type ArtifactCopy } from '../../../../locales/artifact-copy.js'; import { useWorkbarServices } from '../../services-context.js'; /** 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..e4fb301d35 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 @@ -66,8 +66,8 @@ import { import { Banner } from '@astryxdesign/core/Banner'; import { CodeBlock } from '@astryxdesign/core/CodeBlock'; import { Spinner } from '@astryxdesign/core/Spinner'; -import { RegistryArtifactPreview } from './artifact-preview-registry-shell'; -import { getArtifactCopy, type ArtifactCopy } from '../../../../locales/artifact-copy'; +import { RegistryArtifactPreview } from './artifact-preview-registry-shell.js'; +import { getArtifactCopy, type ArtifactCopy } from '../../../../locales/artifact-copy.js'; import { useWorkbarServices } from '../../services-context.js'; export function ArtifactPreview(props: { record: ArtifactDescriptor; onShowInFolder?: () => void }) { @@ -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,23 @@ 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 +348,28 @@ 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 ( + + ) : undefined} + /> + ); } function BinaryFailureCard(props: { record: ArtifactDescriptor; reason: BinaryFailureReason; copy: ArtifactCopy }) { From 2bce8eee6a4ae4dcd5c0d0d6808b865c91430152 Mon Sep 17 00:00:00 2001 From: Dante Date: Sat, 12 Sep 2026 14:15:26 +0800 Subject: [PATCH 2/2] test(desktop): use workbar testing entry Generated-by: Codex --- .../src/main/__tests__/artifact-html-preview-fallback.test.ts | 2 +- apps/desktop/src/renderer/features/workbar/testing.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/main/__tests__/artifact-html-preview-fallback.test.ts b/apps/desktop/src/main/__tests__/artifact-html-preview-fallback.test.ts index 1afc3a799f..89c678bce8 100644 --- a/apps/desktop/src/main/__tests__/artifact-html-preview-fallback.test.ts +++ b/apps/desktop/src/main/__tests__/artifact-html-preview-fallback.test.ts @@ -24,8 +24,8 @@ import { act, createElement } from 'react'; import { createRoot, type Root } from 'react-dom/client'; import { AstryxLocaleProvider, LocaleProvider } from '@maka/ui'; import type { ArtifactDescriptor } from '@maka/core/artifacts'; -import { ArtifactPreview } from '../../renderer/features/workbar/tools/artifacts/artifact-preview.js'; import { + ArtifactPreview, createFakeWorkbarServices, WorkbarServicesProvider, } from '../../renderer/features/workbar/testing.js'; diff --git a/apps/desktop/src/renderer/features/workbar/testing.ts b/apps/desktop/src/renderer/features/workbar/testing.ts index 11c1e45a56..bb82ea6c3f 100644 --- a/apps/desktop/src/renderer/features/workbar/testing.ts +++ b/apps/desktop/src/renderer/features/workbar/testing.ts @@ -30,6 +30,7 @@ export * from './model/workbar-tabs.js'; export * from './model/workbar-layout.js'; export * from './model/workbar-tool-definitions.js'; export * from './tools/artifacts/artifact-list-keyboard.js'; +export { ArtifactPreview } from './tools/artifacts/artifact-preview.js'; export * from './tools/artifacts/artifact-visibility.js'; export * from './tools/inspector/session-inspector-panel-model.js'; export {