Add issue Output UI for artifact playback (PAP-10168)
Surface attachment-backed artifact work products as a first-class Output section on the issue detail page so cloud users can watch and download agent-generated videos without host filesystem access. - ui/src/lib/issue-output.ts: formatBytes/formatDuration/getOutputFileGlyph helpers + getIssueOutputs selector that validates the Phase-2 attachment artifact metadata contract and tolerates malformed metadata (degraded). - issue-output components: IssueOutputSection, OutputPrimaryCard (native <video>/image/generic), OutputRow, OutputVideoPlayer, OutputFileTile. - IssueDetail: fetch work products and render the Output section between Documents and Attachments; reuse formatBytes in the attachments list. - DesignGuide: showcase multiple-output, degraded, and empty states. - Focused tests for video output, empty state, multiple outputs, and failed attachment metadata (15 tests). Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
@@ -67,6 +67,8 @@ import { IssueChatThread, type IssueChatComposerHandle } from "../components/Iss
|
||||
import { IssueContinuationHandoff } from "../components/IssueContinuationHandoff";
|
||||
import { IssueDocumentsSection } from "../components/IssueDocumentsSection";
|
||||
import { IssuePlanDecompositionsSection } from "../components/IssuePlanDecompositionsSection";
|
||||
import { IssueOutputSection } from "../components/issue-output/IssueOutputSection";
|
||||
import { formatBytes } from "../lib/issue-output";
|
||||
import { IssueSiblingNavigation } from "../components/IssueSiblingNavigation";
|
||||
import { IssuesList } from "../components/IssuesList";
|
||||
import { AgentIcon } from "../components/AgentIconPicker";
|
||||
@@ -150,6 +152,7 @@ import {
|
||||
type Issue,
|
||||
type IssueAttachment,
|
||||
type IssueComment,
|
||||
type IssueWorkProduct,
|
||||
type IssueWorkMode,
|
||||
type IssueThreadInteraction,
|
||||
type RequestConfirmationInteraction,
|
||||
@@ -1339,6 +1342,13 @@ export function IssueDetail() {
|
||||
placeholderData: keepPreviousDataForSameQueryTail<IssueAttachment[]>(issueId ?? "pending"),
|
||||
});
|
||||
|
||||
const { data: workProducts } = useQuery({
|
||||
queryKey: queryKeys.issues.workProducts(issueId!),
|
||||
queryFn: () => issuesApi.listWorkProducts(issueId!),
|
||||
enabled: !!issueId,
|
||||
placeholderData: keepPreviousDataForSameQueryTail<IssueWorkProduct[]>(issueId ?? "pending"),
|
||||
});
|
||||
|
||||
const { data: liveRunCount = 0 } = useQuery<LiveRunForIssue[], Error, number>({
|
||||
queryKey: queryKeys.issues.liveRuns(issueId!),
|
||||
queryFn: () => heartbeatsApi.liveRunsForIssue(issueId!),
|
||||
@@ -3757,6 +3767,8 @@ export function IssueDetail() {
|
||||
userProfileMap={userProfileMap}
|
||||
/>
|
||||
|
||||
<IssueOutputSection workProducts={workProducts} />
|
||||
|
||||
{attachmentsInitialLoading ? (
|
||||
<IssueSectionSkeleton titleWidth="w-24" rows={2} />
|
||||
) : hasAttachments ? (
|
||||
@@ -3880,7 +3892,7 @@ export function IssueDetail() {
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[11px] text-muted-foreground">
|
||||
{attachment.contentType} · {(attachment.byteSize / 1024).toFixed(1)} KB
|
||||
{attachment.contentType} · {formatBytes(attachment.byteSize)}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user