diff --git a/src/app/pages/session.tsx b/src/app/pages/session.tsx index c17dbb82b..6425ae82f 100644 --- a/src/app/pages/session.tsx +++ b/src/app/pages/session.tsx @@ -1,4 +1,4 @@ -import { For, Show, createEffect, createMemo } from "solid-js"; +import { For, Show, createEffect, createMemo, createSignal } from "solid-js"; import type { Part } from "@opencode-ai/sdk/v2/client"; import type { ArtifactItem, @@ -28,6 +28,7 @@ import { import Button from "../components/button"; import PartView from "../components/part-view"; import WorkspaceChip from "../components/workspace-chip"; +import { isTauriRuntime, isWindowsPlatform } from "../utils"; export type SessionViewProps = { selectedSessionId: string | null; @@ -102,6 +103,59 @@ export default function SessionView(props: SessionViewProps) { return Array.from({ length: total }, (_, idx) => idx < completed); }); + const [artifactToast, setArtifactToast] = createSignal(null); + + createEffect(() => { + if (!artifactToast()) return; + const id = window.setTimeout(() => setArtifactToast(null), 3000); + return () => window.clearTimeout(id); + }); + + const artifactActionLabel = () => (isWindowsPlatform() ? "Open" : "Reveal"); + + const artifactActionToast = () => (isWindowsPlatform() ? "Opened in default app." : "Revealed in file manager."); + + const resolveArtifactPath = (artifact: ArtifactItem) => { + const rawPath = artifact.path?.trim(); + if (!rawPath) return null; + if (/^(?:[a-zA-Z]:[\\/]|~[\\/]|\/)/.test(rawPath)) { + return rawPath; + } + + const root = props.activeWorkspaceDisplay.path?.trim(); + if (!root) return rawPath; + + const separator = root.includes("\\") ? "\\" : "/"; + const trimmedRoot = root.replace(/[\\/]+$/, ""); + const trimmedPath = rawPath.replace(/^[\\/]+/, ""); + return `${trimmedRoot}${separator}${trimmedPath}`; + }; + + const handleOpenArtifact = async (artifact: ArtifactItem) => { + const resolvedPath = resolveArtifactPath(artifact); + if (!resolvedPath) { + setArtifactToast("Artifact path missing."); + return; + } + + if (!isTauriRuntime()) { + setArtifactToast("Open is only available in the desktop app."); + return; + } + + try { + const { openPath, revealItemInDir } = await import("@tauri-apps/plugin-opener"); + if (isWindowsPlatform()) { + await openPath(resolvedPath); + } else { + await revealItemInDir(resolvedPath); + } + setArtifactToast(artifactActionToast()); + } catch (error) { + setArtifactToast(error instanceof Error ? error.message : "Could not open artifact."); + } + }; + const humanizePlugin = (name: string) => { const cleaned = name @@ -133,6 +187,23 @@ export default function SessionView(props: SessionViewProps) { props.setExpandedSidebarSections((current) => ({ ...current, [key]: !current[key] })); }; + const artifactsByMessage = createMemo(() => { + const map = new Map(); + for (const artifact of props.artifacts) { + const key = artifact.messageId?.trim(); + if (!key) continue; + const current = map.get(key); + if (current) { + current.push(artifact); + } else { + map.set(key, [artifact]); + } + } + return map; + }); + + const unlinkedArtifacts = createMemo(() => props.artifacts.filter((artifact) => !artifact.messageId)); + const modelLabelParts = createMemo(() => { const label = props.selectedSessionModelLabel || "Model"; @@ -322,6 +393,8 @@ export default function SessionView(props: SessionViewProps) { const groups = () => props.groupMessageParts(renderableParts(), String((msg.info as any).id ?? "message")); const groupSpacing = () => (isUser() ? "mb-3" : "mb-4"); + const messageId = () => String((msg.info as any).id ?? ""); + const messageArtifacts = () => artifactsByMessage().get(messageId()) ?? []; return ( 0}> @@ -403,6 +476,29 @@ export default function SessionView(props: SessionViewProps) { )} + +
+
Artifacts
+ + {(artifact) => ( +
+
+
+ +
+
+
{artifact.name}
+
Document
+
+
+ +
+ )} +
+
+
@@ -416,10 +512,40 @@ export default function SessionView(props: SessionViewProps) { + +
+
Artifacts
+ + {(artifact) => ( +
+
+
+ +
+
+
{artifact.name}
+
Document
+
+
+ +
+ )} +
+
+
+
(messagesEndEl = el)} />
+ +
+ {artifactToast()} +
+
+