66 Clock3 ,
77 ExternalLink ,
88 FileText ,
9+ FolderOpen ,
910 Info ,
1011 Link2 ,
1112 LoaderCircle ,
@@ -40,6 +41,7 @@ import type {
4041 TaskStatus ,
4142} from "../../shared/types" ;
4243import { COLUMN_LABELS , PRIORITIES , TASK_STATUSES } from "../../shared/types" ;
44+ import { api } from "../api" ;
4345import { Alert , AlertDescription } from "@/components/ui/alert" ;
4446import {
4547 AlertDialog ,
@@ -206,7 +208,7 @@ const INSPECTOR_TABS: Array<{
206208 { value : "details" , label : "Details" , icon : Info } ,
207209 { value : "context" , label : "Context" , icon : MessageSquareText } ,
208210 { value : "runs" , label : "Runs" , icon : Clock3 } ,
209- { value : "artifacts" , label : "Files " , icon : FileText } ,
211+ { value : "artifacts" , label : "Artifacts " , icon : FileText } ,
210212] ;
211213
212214export function TaskModal ( props : {
@@ -1424,8 +1426,8 @@ function TaskThreadMessageRow(props: {
14241426
14251427 return (
14261428 < ItemGroup >
1427- { message . execution . artifacts . map ( ( artifact ) => (
1428- < ArtifactRow artifact = { artifact } key = { artifact . id } />
1429+ { dedupeTaskExecutionArtifacts ( message . execution . artifacts ) . map ( ( artifact ) => (
1430+ < ArtifactRow artifact = { artifact } key = { artifact . id } taskId = { message . execution . taskId } />
14291431 ) ) }
14301432 </ ItemGroup >
14311433 ) ;
@@ -1582,7 +1584,7 @@ function TaskInspector(props: {
15821584 < TabsTrigger value = "details" > Details</ TabsTrigger >
15831585 < TabsTrigger value = "context" > Context</ TabsTrigger >
15841586 < TabsTrigger value = "runs" > Runs</ TabsTrigger >
1585- < TabsTrigger value = "artifacts" > Files </ TabsTrigger >
1587+ < TabsTrigger value = "artifacts" > Artifacts </ TabsTrigger >
15861588 </ TabsList >
15871589 < Button
15881590 type = "button"
@@ -1771,7 +1773,9 @@ function TaskRunsPanel(props: { task: Task }) {
17711773}
17721774
17731775function TaskArtifactsPanel ( props : { task : Task } ) {
1774- const artifacts = getTaskExecutionHistory ( props . task ) . flatMap ( ( execution ) => execution . artifacts ) ;
1776+ const artifacts = dedupeTaskExecutionArtifacts (
1777+ getTaskExecutionHistory ( props . task ) . flatMap ( ( execution ) => execution . artifacts ) ,
1778+ ) ;
17751779 if ( artifacts . length === 0 ) {
17761780 return (
17771781 < Empty className = "min-h-40 border" >
@@ -1788,7 +1792,7 @@ function TaskArtifactsPanel(props: { task: Task }) {
17881792 return (
17891793 < ItemGroup >
17901794 { artifacts . map ( ( artifact ) => (
1791- < ArtifactRow artifact = { artifact } key = { artifact . id } />
1795+ < ArtifactRow artifact = { artifact } key = { artifact . id } taskId = { props . task . id } />
17921796 ) ) }
17931797 </ ItemGroup >
17941798 ) ;
@@ -2251,7 +2255,10 @@ function ExecutionOutput(props: {
22512255 ) ;
22522256}
22532257
2254- function ArtifactRow ( props : { artifact : TaskExecutionArtifact } ) {
2258+ function ArtifactRow ( props : { artifact : TaskExecutionArtifact ; taskId : string } ) {
2259+ const [ opening , setOpening ] = useState ( false ) ;
2260+ const canRevealFile = isLocalFileArtifact ( props . artifact ) ;
2261+ const externalUrl = isHttpArtifactUrl ( props . artifact . url ) ? props . artifact . url : null ;
22552262 const icon =
22562263 props . artifact . type === "link" ? (
22572264 < Link2 />
@@ -2266,20 +2273,60 @@ function ArtifactRow(props: { artifact: TaskExecutionArtifact }) {
22662273 < ItemContent >
22672274 < ItemTitle > { props . artifact . title } </ ItemTitle >
22682275 { props . artifact . content && (
2269- < p className = "text-sm text-muted-foreground" > { props . artifact . content } </ p >
2276+ < p className = "break-words text-sm text-muted-foreground" > { props . artifact . content } </ p >
22702277 ) }
22712278 </ ItemContent >
2272- { props . artifact . url && (
2279+ { ( externalUrl || canRevealFile ) && (
22732280 < ItemActions >
2274- < ExternalLink />
2281+ { canRevealFile ? (
2282+ opening ? (
2283+ < LoaderCircle className = "animate-spin" />
2284+ ) : (
2285+ < FolderOpen />
2286+ )
2287+ ) : (
2288+ < ExternalLink />
2289+ ) }
22752290 </ ItemActions >
22762291 ) }
22772292 </ >
22782293 ) ;
22792294
2280- return props . artifact . url ? (
2295+ async function revealFile ( ) {
2296+ if ( opening ) {
2297+ return ;
2298+ }
2299+ setOpening ( true ) ;
2300+ try {
2301+ await api . openTaskArtifact ( props . taskId , props . artifact . id ) ;
2302+ } catch ( error ) {
2303+ const localPath = props . artifact . content ?? props . artifact . url ?? "" ;
2304+ if ( localPath ) {
2305+ await navigator . clipboard ?. writeText ( localPath ) . catch ( ( ) => undefined ) ;
2306+ }
2307+ window . alert ( error instanceof Error ? error . message : "Could not reveal artifact file." ) ;
2308+ } finally {
2309+ setOpening ( false ) ;
2310+ }
2311+ }
2312+
2313+ if ( canRevealFile ) {
2314+ return (
2315+ < Item asChild variant = "outline" size = "sm" >
2316+ < button
2317+ type = "button"
2318+ aria-label = { `Reveal ${ props . artifact . title } ` }
2319+ onClick = { ( ) => void revealFile ( ) }
2320+ >
2321+ { content }
2322+ </ button >
2323+ </ Item >
2324+ ) ;
2325+ }
2326+
2327+ return externalUrl ? (
22812328 < Item asChild variant = "outline" size = "sm" >
2282- < a href = { props . artifact . url } target = "_blank" rel = "noreferrer" >
2329+ < a href = { externalUrl } target = "_blank" rel = "noreferrer" >
22832330 { content }
22842331 </ a >
22852332 </ Item >
@@ -2290,6 +2337,35 @@ function ArtifactRow(props: { artifact: TaskExecutionArtifact }) {
22902337 ) ;
22912338}
22922339
2340+ function isHttpArtifactUrl ( url : string | undefined ) : url is string {
2341+ return Boolean ( url ?. startsWith ( "http://" ) || url ?. startsWith ( "https://" ) ) ;
2342+ }
2343+
2344+ function isLocalFileArtifact ( artifact : TaskExecutionArtifact ) {
2345+ return (
2346+ artifact . url ?. startsWith ( "file://" ) ||
2347+ ( artifact . type === "output" && Boolean ( artifact . content && isAbsoluteLocalPath ( artifact . content ) ) )
2348+ ) ;
2349+ }
2350+
2351+ function isAbsoluteLocalPath ( value : string ) {
2352+ return value . startsWith ( "/" ) || / ^ [ A - Z a - z ] : [ \\ / ] / . test ( value ) ;
2353+ }
2354+
2355+ function dedupeTaskExecutionArtifacts ( artifacts : TaskExecutionArtifact [ ] ) {
2356+ const seen = new Set < string > ( ) ;
2357+ return artifacts . filter ( ( artifact ) => {
2358+ const key =
2359+ artifact . url ?. trim ( ) . toLowerCase ( ) ??
2360+ `${ artifact . type } :${ artifact . title . trim ( ) } :${ artifact . content ?. trim ( ) ?? "" } ` ;
2361+ if ( seen . has ( key ) ) {
2362+ return false ;
2363+ }
2364+ seen . add ( key ) ;
2365+ return true ;
2366+ } ) ;
2367+ }
2368+
22932369function executionStatusIcon ( execution : TaskExecution ) {
22942370 if ( execution . status === "succeeded" ) {
22952371 return < CheckCircle2 data-icon = "inline-start" /> ;
0 commit comments