From 111f7bac6a905e5973f6571c9beaa7047c34baa7 Mon Sep 17 00:00:00 2001 From: scotttong Date: Thu, 30 Jul 2026 15:20:41 -0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20task-chat=20phase=202e=20follow-ups?= =?UTF-8?q?=20=E2=80=94=20kill=20outer=20main=20scrollbar,=20restore=20pan?= =?UTF-8?q?e-tab=20underline,=20conditional=20Plan/Artifacts=20tabs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flag-gated (enableTaskChatRedesign), flag OFF unchanged: - Extra scrollbar: with the flag ON the page content used a fixed calc(100dvh - 8rem) thread height inside the padded outer
(overflow-auto), so main itself also scrolled. The IssueDetail root now fills main exactly (h-full flex column) and the chain down to the thread is flexed with min-h-0, so the thread's internal viewport is the only scroll surface. TaskChatThread's dvh height token still applies on mobile, where main has no fixed height (flex-basis falls back to it). - Pane-tab underline: the portal into the pane header bar kept React context but broke the DOM ancestry that Tailwind's group-data-[orientation]/tabs selectors need, so the active-tab underline never applied. A display:contents group/tabs wrapper inside the portal restores it. - Conditional tabs: Plan/Artifacts triggers render only when the issue has a plan document/accepted decompositions or attachments (same query keys as the tab bodies — shared cache). With neither, the header bar shows a plain "Properties" title and the pane body is the plain properties stack. Tabs are controlled with a fallback to Properties if the selected tab's content disappears. Verified on :3093 light+dark via Playwright: main scrollHeight == clientHeight (923/923) on //; underline opacity 1 on active portaled tab; Properties|Plan on , all three on , title-only on ; portaled tab clicks switch content; flag-off run scrolls the legacy page (4454px) with no redesign testids. ui tsc clean; issue-properties/task-chat/IssueDetail/IssueChatThread tests pass (1 pre-existing TZ flake in IssueProperties monitor row, fails on base). Co-Authored-By: Claude Fable 5 --- ui/src/components/PropertiesPanel.tsx | 5 +- .../issue-properties/IssueProperties.tsx | 74 ++++++++++++++++--- ui/src/pages/IssueDetail.tsx | 18 ++++- 3 files changed, 81 insertions(+), 16 deletions(-) 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" ? (