authorgravatar for 91240327+shivamhwp@users.noreply.github.comshivam <91240327+shivamhwp@users.noreply.github.com> 2026-08-29 03:34:41+05:30
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-29 03:34:41+05:30
log0009aacdf146e0532327fa3d9d0109d5adca68b9
tree9d327c813dbd317c160efd5c3e7e78705a0ea6bc
parent0e2905eb783fd2385f358a95f0b25bbf07ff7122
signature Signed by PGP key B5690EEEBB952194

fix(web): keep long task drawers usable on small screens (#8313)


2 files changed, 45 insertions(+), 4 deletions(-)

apps/web/src/components/chat/ComposerTasksBadge.test.tsx+30-1
......@@ -96,9 +96,19 @@ describe("ComposerTasksBadge", () => {
9696 );
9797
9898 expect(markup).toContain('data-chat-composer-tasks-drawer="true"');
99 expect(markup).toContain('data-chat-composer-collapsed-controls="true"');
99100 expect(markup).not.toContain("data-variant");
100101 expect(markup).toContain('aria-expanded="true"');
102 expect(markup).toContain('aria-label="Collapse tasks. 1 of 3 complete."');
103 expect(markup).toContain('aria-label="Task list. 1 of 3 complete."');
101104 expect(markup).toContain('role="list"');
105 expect(markup).toContain('data-composer-tasks-list="true"');
106 expect(markup).toContain('tabindex="0"');
107 expect(markup).toContain("max-h-[min(24rem,40dvh)]");
108 expect(markup).toContain("overflow-y-auto");
109 expect(markup).toContain("overscroll-contain");
110 expect(markup).toContain("focus-visible:ring-2");
111 expect(markup).toContain("focus-visible:ring-inset");
102112 expect(markup).toContain("Inspect the composer");
103113 expect(markup).toContain('data-composer-task-duration="true"');
104114 expect(markup).toContain("ml-auto w-10");
......@@ -107,13 +117,32 @@ describe("ComposerTasksBadge", () => {
107117 expect(markup).toContain("Attach task progress");
108118 expect(markup).toContain("Verify the result");
109119 expect(markup).toContain("lucide-list-todo");
120 expect(markup).toContain("lucide-chevron-down");
110121 expect(markup).toContain('aria-label="Dismiss tasks for this turn"');
111 expect(markup).not.toContain("lucide-chevron");
112122 expect(markup).not.toContain("bg-success");
113123 expect(markup).not.toContain("bg-primary");
114124 expect(markup).not.toContain("bg-muted-foreground/25");
115125 });
116126
127 it("keeps every long-list task inside the bounded scroll region", () => {
128 const longSteps = Array.from({ length: 20 }, (_, index) => ({
129 step: `Task ${index + 1}`,
130 status: index === 0 ? ("inProgress" as const) : ("pending" as const),
131 }));
132 const markup = renderToStaticMarkup(
133 <ComposerTasksDrawer
134 onCollapse={() => undefined}
135 onDismiss={() => undefined}
136 progress={{ step: "Task 1", completedSteps: 0, totalSteps: longSteps.length }}
137 steps={longSteps}
138 />,
139 );
140
141 const listStart = markup.indexOf('data-composer-tasks-list="true"');
142 expect(listStart).toBeGreaterThan(markup.indexOf('aria-label="Collapse tasks.'));
143 expect(markup.slice(listStart)).toContain("Task 20");
144 });
145
117146 it("does not render an empty task count", () => {
118147 const markup = renderToStaticMarkup(
119148 <ComposerTasksBadge
apps/web/src/components/chat/ComposerTasksBadge.tsx+15-3
......@@ -1,4 +1,4 @@
1import { ListTodoIcon, XIcon } from "lucide-react";
1import { ChevronDownIcon, ListTodoIcon, XIcon } from "lucide-react";
22import { memo } from "react";
33
44import { formatDuration } from "../../session-logic";
......@@ -174,11 +174,16 @@ export const ComposerTasksDrawer = memo(function ComposerTasksDrawer({
174174 readonly steps: readonly ComposerTaskStep[];
175175}) {
176176 return (
177 <div className="chat-composer-top-drawer" data-chat-composer-tasks-drawer="true">
177 <div
178 className="chat-composer-top-drawer"
179 data-chat-composer-collapsed-controls="true"
180 data-chat-composer-tasks-drawer="true"
181 >
178182 <div className="flex items-center gap-1 px-3 py-1.5 sm:px-4">
179183 <button
180184 type="button"
181185 aria-expanded="true"
186 aria-label={`Collapse tasks. ${progress.completedSteps} of ${progress.totalSteps} complete.`}
182187 className="flex min-w-0 flex-1 cursor-pointer items-center gap-2 self-stretch text-left text-xs text-muted-foreground hover:text-foreground"
183188 onClick={onCollapse}
184189 onPointerDown={(event) => event.preventDefault()}
......@@ -188,6 +193,7 @@ export const ComposerTasksDrawer = memo(function ComposerTasksDrawer({
188193 <span className="tabular-nums">
189194 {progress.completedSteps}/{progress.totalSteps}
190195 </span>
196 <ChevronDownIcon aria-hidden className="ml-auto size-3.5 shrink-0" />
191197 </button>
192198 <Button
193199 size="icon-micro"
......@@ -200,7 +206,13 @@ export const ComposerTasksDrawer = memo(function ComposerTasksDrawer({
200206 <XIcon aria-hidden className="size-3" />
201207 </Button>
202208 </div>
203 <div className="space-y-px px-3 pb-4 sm:px-4" role="list">
209 <div
210 aria-label={`Task list. ${progress.completedSteps} of ${progress.totalSteps} complete.`}
211 className="max-h-[min(24rem,40dvh)] space-y-px overflow-y-auto overscroll-contain px-3 pb-4 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70 sm:px-4"
212 data-composer-tasks-list="true"
213 role="list"
214 tabIndex={0}
215 >
204216 {keyedTaskSteps(steps).map(({ key, step }) => (
205217 <div key={key} className="flex items-baseline gap-2 text-xs leading-5" role="listitem">
206218 <span