diff --git a/ui/src/components/PropertiesPanel.tsx b/ui/src/components/PropertiesPanel.tsx index 926927c3903..65d169d401f 100644 --- a/ui/src/components/PropertiesPanel.tsx +++ b/ui/src/components/PropertiesPanel.tsx @@ -353,8 +353,9 @@ function ResizablePropertiesPanel({ className={cn("flex-1 flex flex-col min-h-0", isFixed && "w-full")} style={isFixed ? undefined : { width, minWidth: width }} > - {/* No pane title: the tab strip (portaled into the slot by - IssueProperties) names the surface; window controls sit right. */} + {/* The slot hosts whatever IssueProperties portals in — the tab strip + when Plan/Artifacts have content, else a plain "Properties" title; + window controls sit right. */}
issuesApi.listAcceptedPlanDecompositions(issue.id), + enabled: taskChatRedesignEnabled, + }); + const { data: paneTabAttachments } = useQuery({ + queryKey: queryKeys.issues.attachments(issue.id), + queryFn: () => issuesApi.listAttachments(issue.id), + enabled: taskChatRedesignEnabled, + }); + const hasPlanTab = Boolean(paneTabPlanDocument) || (paneTabAcceptedPlans?.length ?? 0) > 0; + const hasArtifactsTab = (paneTabAttachments?.length ?? 0) > 0; + const [paneTab, setPaneTab] = useState("properties"); const [assigneeOpen, setAssigneeOpen] = useState(false); const [assigneeSearch, setAssigneeSearch] = useState(""); /** When a run is live, a selection is staged here until the operator confirms @@ -2470,30 +2490,64 @@ export function IssueProperties({ // Flag OFF (or non-redesign hosts): today's stacked pane, byte-for-byte. if (!taskChatRedesignEnabled) return propertiesBody; + // Flag ON with nothing to switch between: no tab strip — the header bar + // shows a plain title and the pane body is just the properties stack. + if (!hasPlanTab && !hasArtifactsTab) { + return ( + <> + {paneHeaderSlot + ? createPortal(Properties, paneHeaderSlot) + : null} + {propertiesBody} + + ); + } + // Flag ON: wrap the same body in a Properties | Plan | Artifacts tab shell // (v5 decision: singular "Plan", Docs merged into Artifacts). The Properties // tab is unchanged. Panel hosts portal the strip into the pane header bar; // portals keep React context, so the Tabs root still drives it. + // Fall back to Properties if the selected tab's content went away (or the + // selection was made on another issue). + const activePaneTab = + (paneTab === "plans" && !hasPlanTab) || (paneTab === "artifacts" && !hasArtifactsTab) + ? "properties" + : paneTab; const tabStrip = ( Properties - Plan - Artifacts + {hasPlanTab ? Plan : null} + {hasArtifactsTab ? Artifacts : null} ); return ( - - {paneHeaderSlot ? createPortal(tabStrip, paneHeaderSlot) : tabStrip} + + {paneHeaderSlot + ? createPortal( + // Portals keep React context but break the DOM tree the Tailwind + // group-selectors need: the active-tab underline is styled via + // `group-data-[orientation=horizontal]/tabs:*`, so restore that + // ancestor here (display: contents keeps it out of layout). +
+ {tabStrip} +
, + paneHeaderSlot, + ) + : tabStrip} {propertiesBody} - - - - - - + {hasPlanTab ? ( + + + + ) : null} + {hasArtifactsTab ? ( + + + + ) : null}
); } diff --git a/ui/src/pages/IssueDetail.tsx b/ui/src/pages/IssueDetail.tsx index f778878b0b7..ee73e650313 100644 --- a/ui/src/pages/IssueDetail.tsx +++ b/ui/src/pages/IssueDetail.tsx @@ -1192,7 +1192,7 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({ ) : null; return ( -
+
{/* Redesign: the button rides inside the thread's scroll viewport with the header so nothing sits above the thread in the page flow. */} {taskChatRedesignEnabled ? null : loadOlderButton} @@ -4549,7 +4549,10 @@ export function IssueDetail() {
@@ -4809,7 +4812,11 @@ export function IssueDetail() { {taskChatShellEnabled ? null : } - + {/* Redesign: the chat IS the page — the Chat/Activity/Related-work tab strip is hidden and the thread renders as the only surface. */} {taskChatShellEnabled ? null : ( @@ -4837,7 +4844,10 @@ export function IssueDetail() { {/* Flag ON the thread viewport extends under main's horizontal padding (symmetric, so the centered column keeps the same axis) and the scrollbar sits flush against the properties-pane border. */} - + {resolvedDetailTab === "chat" ? (