From 9b206cbdfa8adc5fc15f41cc59a936eb4b10daf3 Mon Sep 17 00:00:00 2001 From: Mon Sola Date: Sat, 30 May 2026 13:34:58 +0800 Subject: [PATCH] feat(artifacts): show scrollable artifact list in side panel MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, clicking the Artifacts button only opened the first artifact as a tab — the other N-1 artifacts were invisible. Now the side panel shows a scrollable list of all artifacts when no tab is active. Clicking an artifact opens it in the panel. Closing the artifact returns to the list. Also adds 'text' to ARTIFACT_FILE_PREVIEWS so plain-text files (.ts, .js, .json, .py, .txt, etc.) appear in the artifact list. --- .../domains/session/artifacts/open-target.ts | 2 +- .../domains/session/chat/session-page.tsx | 28 ++-------- .../domains/session/panel/side-panel.tsx | 51 ++++++++++++++++++- 3 files changed, 55 insertions(+), 26 deletions(-) diff --git a/apps/app/src/react-app/domains/session/artifacts/open-target.ts b/apps/app/src/react-app/domains/session/artifacts/open-target.ts index b284dfe93..d0670c2e8 100644 --- a/apps/app/src/react-app/domains/session/artifacts/open-target.ts +++ b/apps/app/src/react-app/domains/session/artifacts/open-target.ts @@ -34,7 +34,7 @@ const WORKSPACE_ID_PREFIX_PATTERN = /^workspace\/(?:ws_[^/]+|\d+|[0-9a-f-]{6,})\ const FILE_PATTERN = /(?:^|[\s"'`([{])((?:\.{1,2}[/\\]|~[/\\]|[/\\])?[\w.\-]+(?:[/\\][\w.\-]+)+\.[a-z][a-z0-9]{0,9}|[\w.\-]+\.[a-z][a-z0-9]{0,9})/gi; const URL_PATTERN = /https?:\/\/[^\s)\]}>"'`]+/gi; const SOCKET_PATTERN = /(?:ws|wss):\/\/[^\s)\]}>"'`]+/gi; -const ARTIFACT_FILE_PREVIEWS = new Set(["markdown", "sheet", "image", "pdf", "html"]); +const ARTIFACT_FILE_PREVIEWS = new Set(["markdown", "sheet", "image", "pdf", "html", "text"]); const DISCOVERY_TOOL_NAMES = new Set(["glob", "grep", "search", "find"]); const ARTIFACT_METADATA_TOOL_NAMES = new Set(["openwork_extension_call"]); const WRITE_TOOL_NAMES = new Set([ diff --git a/apps/app/src/react-app/domains/session/chat/session-page.tsx b/apps/app/src/react-app/domains/session/chat/session-page.tsx index b0c5e5560..9e3edcfc5 100644 --- a/apps/app/src/react-app/domains/session/chat/session-page.tsx +++ b/apps/app/src/react-app/domains/session/chat/session-page.tsx @@ -371,33 +371,13 @@ export function SessionPage(props: SessionPageProps) { }, [toggleCurrentSidePanel]); const openArtifactRailPane = useCallback(() => { if (!hasArtifactTargets || !props.selectedSessionId) return; - const activeTab = sessionPanelState.tabs.find((tab) => tab.id === sessionPanelState.activeTabId); - const artifactTargetIds = new Set(artifactFileTargets.map((target) => target.id)); - const artifactTab = sessionPanelState.tabs.find((tab) => ( - tab.type === "artifact" && artifactTargetIds.has(tab.id) - )); - const firstArtifact = artifactFileTargets[0]; - if (panelRailActive && activeTab?.type === "artifact") { + if (panelRailActive) { toggleCurrentSidePanel("panel"); return; } - if (!panelRailActive) { - preserveSidePanelOnPanelOpenRef.current = true; - } - if (artifactTab) { - selectTab(props.selectedSessionId, artifactTab.id); - } else if (firstArtifact) { - openTab(props.selectedSessionId, { - id: firstArtifact.id, - type: "artifact", - label: firstArtifact.name, - preview: firstArtifact.preview, - }); - } - if (!panelRailActive) { - toggleCurrentSidePanel("panel"); - } - }, [artifactFileTargets, hasArtifactTargets, openTab, panelRailActive, props.selectedSessionId, selectTab, sessionPanelState, toggleCurrentSidePanel]); + preserveSidePanelOnPanelOpenRef.current = true; + toggleCurrentSidePanel("panel"); + }, [hasArtifactTargets, panelRailActive, props.selectedSessionId, toggleCurrentSidePanel]); const openExtensionsRailPane = useCallback(() => { toggleCurrentSidePanel("extensions"); }, [toggleCurrentSidePanel]); diff --git a/apps/app/src/react-app/domains/session/panel/side-panel.tsx b/apps/app/src/react-app/domains/session/panel/side-panel.tsx index edeebce03..d436efffa 100644 --- a/apps/app/src/react-app/domains/session/panel/side-panel.tsx +++ b/apps/app/src/react-app/domains/session/panel/side-panel.tsx @@ -20,13 +20,16 @@ import { InputGroupInput, } from "@/components/ui/input-group"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"; +import { formatFileSize } from "@/lib/utils"; +import { isCollectibleArtifactTarget, type OpenTarget } from "../artifacts/open-target"; import { ArtifactIcon } from "../artifacts/artifact-icon"; import { ArtifactPanel } from "../artifacts/artifact-panel"; import { type BrowserPanelTab, type PanelTab as PanelTabEntry, useActivePanelTab, + usePanelTabStore, useSessionPanelState, } from "./panel-tab-store"; import { useSidePanelTabs } from "./use-side-panel-tabs"; @@ -433,7 +436,7 @@ export function SidePanel({ {!activeTab ? ( - + ) : null} {activeTab?.type === "browser" ? ( @@ -455,6 +458,52 @@ export function SidePanel({ ); } +function ArtifactList({ sessionId }: { sessionId: string }) { + const openTab = usePanelTabStore((state) => state.openTab); + const targets = usePanelTabStore((state) => + (state.transcriptArtifactTargets[sessionId] ?? []).filter(isCollectibleArtifactTarget) + ); + + if (targets.length === 0) { + return ( +
+

No artifacts yet.

+
+ ); + } + + return ( +
+
+

Artifacts ({targets.length})

+
+
+ {targets.map((target) => ( + + ))} +
+
+ ); +} + function PanelEmpty() { return (