mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 11:20:42 +01:00
fix(ui): task-chat phase 2e follow-ups — kill outer main scrollbar, restore pane-tab underline, conditional Plan/Artifacts tabs
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 <main> (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 <noreply@anthropic.com>
This commit is contained in:
1 parent
9a5f7e26e4
commit
111f7bac6a
3 files changed
+81
-16
No files matched your search
@@ -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. */}
|
||||
<div className="flex items-center justify-between gap-2 px-4 py-2 border-b border-border">
|
||||
<div
|
||||
id={PROPERTIES_PANE_HEADER_SLOT_ID}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { authApi } from "../../api/auth";
|
||||
import { executionWorkspacesApi } from "../../api/execution-workspaces";
|
||||
import { instanceSettingsApi } from "../../api/instanceSettings";
|
||||
import { issuesApi } from "../../api/issues";
|
||||
import { useIssuePlanDocument } from "@/hooks/useIssuePlanDocument";
|
||||
import { projectsApi } from "../../api/projects";
|
||||
import { useCompany } from "../../context/CompanyContext";
|
||||
import { queryKeys } from "../../lib/queryKeys";
|
||||
@@ -184,6 +185,25 @@ export function IssueProperties({
|
||||
}
|
||||
setPaneHeaderSlot(document.getElementById(PROPERTIES_PANE_HEADER_SLOT_ID));
|
||||
}, [taskChatRedesignEnabled, inline]);
|
||||
// Plan/Artifacts only earn a tab when they have content; with neither, the
|
||||
// header bar shows a plain "Properties" title instead of a one-tab strip.
|
||||
// Same query keys as the tab bodies, so these share their cached fetches.
|
||||
const { data: paneTabPlanDocument } = useIssuePlanDocument(
|
||||
taskChatRedesignEnabled ? issue.id : null,
|
||||
);
|
||||
const { data: paneTabAcceptedPlans } = useQuery({
|
||||
queryKey: queryKeys.issues.acceptedPlanDecompositions(issue.id),
|
||||
queryFn: () => 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(<span className="text-sm font-medium">Properties</span>, 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 = (
|
||||
<TabsList
|
||||
variant="line"
|
||||
className={paneHeaderSlot ? "justify-start gap-1" : "w-full justify-start gap-1"}
|
||||
>
|
||||
<TabsTrigger value="properties">Properties</TabsTrigger>
|
||||
<TabsTrigger value="plans">Plan</TabsTrigger>
|
||||
<TabsTrigger value="artifacts">Artifacts</TabsTrigger>
|
||||
{hasPlanTab ? <TabsTrigger value="plans">Plan</TabsTrigger> : null}
|
||||
{hasArtifactsTab ? <TabsTrigger value="artifacts">Artifacts</TabsTrigger> : null}
|
||||
</TabsList>
|
||||
);
|
||||
return (
|
||||
<Tabs defaultValue="properties" className="flex min-h-0 flex-col gap-3">
|
||||
{paneHeaderSlot ? createPortal(tabStrip, paneHeaderSlot) : tabStrip}
|
||||
<Tabs value={activePaneTab} onValueChange={setPaneTab} className="flex min-h-0 flex-col gap-3">
|
||||
{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).
|
||||
<div className="group/tabs contents" data-orientation="horizontal">
|
||||
{tabStrip}
|
||||
</div>,
|
||||
paneHeaderSlot,
|
||||
)
|
||||
: tabStrip}
|
||||
<TabsContent value="properties">{propertiesBody}</TabsContent>
|
||||
<TabsContent value="plans">
|
||||
<IssuePropertiesPlansTab issue={issue} inline={inline} />
|
||||
</TabsContent>
|
||||
<TabsContent value="artifacts">
|
||||
<IssuePropertiesArtifactsTab issue={issue} />
|
||||
</TabsContent>
|
||||
{hasPlanTab ? (
|
||||
<TabsContent value="plans">
|
||||
<IssuePropertiesPlansTab issue={issue} inline={inline} />
|
||||
</TabsContent>
|
||||
) : null}
|
||||
{hasArtifactsTab ? (
|
||||
<TabsContent value="artifacts">
|
||||
<IssuePropertiesArtifactsTab issue={issue} />
|
||||
</TabsContent>
|
||||
) : null}
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
@@ -1192,7 +1192,7 @@ const IssueDetailChatTab = memo(function IssueDetailChatTab({
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className={taskChatRedesignEnabled ? "flex min-h-0 flex-1 flex-col" : "space-y-3"}>
|
||||
{/* 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() {
|
||||
<div
|
||||
className={
|
||||
taskChatShellEnabled
|
||||
? "w-full space-y-6"
|
||||
? // Fill main exactly so the outer page never scrolls — the thread's
|
||||
// own viewport is the only scroll surface (h-full is inert on
|
||||
// mobile, where main has no fixed height and the page scrolls).
|
||||
"flex h-full min-h-0 w-full flex-col gap-6"
|
||||
: "max-w-3xl space-y-6"
|
||||
}
|
||||
>
|
||||
@@ -4809,7 +4812,11 @@ export function IssueDetail() {
|
||||
|
||||
{taskChatShellEnabled ? null : <Separator className={shellSectionClass} />}
|
||||
|
||||
<Tabs value={resolvedDetailTab} onValueChange={setDetailTab} className="space-y-3">
|
||||
<Tabs
|
||||
value={resolvedDetailTab}
|
||||
onValueChange={setDetailTab}
|
||||
className={taskChatShellEnabled ? "min-h-0 flex-1" : "space-y-3"}
|
||||
>
|
||||
{/* 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. */}
|
||||
<TabsContent value="chat" className={taskChatShellEnabled ? "-mx-4 md:-mx-6" : undefined}>
|
||||
<TabsContent
|
||||
value="chat"
|
||||
className={taskChatShellEnabled ? "-mx-4 md:-mx-6 flex min-h-0 flex-col" : undefined}
|
||||
>
|
||||
{resolvedDetailTab === "chat" ? (
|
||||
<IssueDetailChatTab
|
||||
threadHeader={taskChatThreadHeader}
|
||||
|
||||
Reference in new issue
Block a user