| author | |
| committer | |
| log | 0009aacdf146e0532327fa3d9d0109d5adca68b9 |
| tree | 9d327c813dbd317c160efd5c3e7e78705a0ea6bc |
| parent | 0e2905eb783fd2385f358a95f0b25bbf07ff7122 |
| signature | Signed by PGP key B5690EEEBB952194 |
2 files changed, 45 insertions(+), 4 deletions(-)
apps/web/src/components/chat/ComposerTasksBadge.test.tsx+30-1| ... | @@ -96,9 +96,19 @@ describe("ComposerTasksBadge", () => { | ... | @@ -96,9 +96,19 @@ describe("ComposerTasksBadge", () => { |
| 96 | ); | 96 | ); |
| 97 | 97 | ||
| 98 | expect(markup).toContain('data-chat-composer-tasks-drawer="true"'); | 98 | expect(markup).toContain('data-chat-composer-tasks-drawer="true"'); |
| 99 | expect(markup).toContain('data-chat-composer-collapsed-controls="true"'); | ||
| 99 | expect(markup).not.toContain("data-variant"); | 100 | expect(markup).not.toContain("data-variant"); |
| 100 | expect(markup).toContain('aria-expanded="true"'); | 101 | 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."'); | ||
| 101 | expect(markup).toContain('role="list"'); | 104 | 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"); | ||
| 102 | expect(markup).toContain("Inspect the composer"); | 112 | expect(markup).toContain("Inspect the composer"); |
| 103 | expect(markup).toContain('data-composer-task-duration="true"'); | 113 | expect(markup).toContain('data-composer-task-duration="true"'); |
| 104 | expect(markup).toContain("ml-auto w-10"); | 114 | expect(markup).toContain("ml-auto w-10"); |
| ... | @@ -107,13 +117,32 @@ describe("ComposerTasksBadge", () => { | ... | @@ -107,13 +117,32 @@ describe("ComposerTasksBadge", () => { |
| 107 | expect(markup).toContain("Attach task progress"); | 117 | expect(markup).toContain("Attach task progress"); |
| 108 | expect(markup).toContain("Verify the result"); | 118 | expect(markup).toContain("Verify the result"); |
| 109 | expect(markup).toContain("lucide-list-todo"); | 119 | expect(markup).toContain("lucide-list-todo"); |
| 120 | expect(markup).toContain("lucide-chevron-down"); | ||
| 110 | expect(markup).toContain('aria-label="Dismiss tasks for this turn"'); | 121 | expect(markup).toContain('aria-label="Dismiss tasks for this turn"'); |
| 111 | expect(markup).not.toContain("lucide-chevron"); | ||
| 112 | expect(markup).not.toContain("bg-success"); | 122 | expect(markup).not.toContain("bg-success"); |
| 113 | expect(markup).not.toContain("bg-primary"); | 123 | expect(markup).not.toContain("bg-primary"); |
| 114 | expect(markup).not.toContain("bg-muted-foreground/25"); | 124 | expect(markup).not.toContain("bg-muted-foreground/25"); |
| 115 | }); | 125 | }); |
| 116 | 126 | ||
| 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 | |||
| 117 | it("does not render an empty task count", () => { | 146 | it("does not render an empty task count", () => { |
| 118 | const markup = renderToStaticMarkup( | 147 | const markup = renderToStaticMarkup( |
| 119 | <ComposerTasksBadge | 148 | <ComposerTasksBadge |
apps/web/src/components/chat/ComposerTasksBadge.tsx+15-3| ... | @@ -1,4 +1,4 @@ | ... | @@ -1,4 +1,4 @@ |
| 1 | import { ListTodoIcon, XIcon } from "lucide-react"; | 1 | import { ChevronDownIcon, ListTodoIcon, XIcon } from "lucide-react"; |
| 2 | import { memo } from "react"; | 2 | import { memo } from "react"; |
| 3 | 3 | ||
| 4 | import { formatDuration } from "../../session-logic"; | 4 | import { formatDuration } from "../../session-logic"; |
| ... | @@ -174,11 +174,16 @@ export const ComposerTasksDrawer = memo(function ComposerTasksDrawer({ | ... | @@ -174,11 +174,16 @@ export const ComposerTasksDrawer = memo(function ComposerTasksDrawer({ |
| 174 | readonly steps: readonly ComposerTaskStep[]; | 174 | readonly steps: readonly ComposerTaskStep[]; |
| 175 | }) { | 175 | }) { |
| 176 | return ( | 176 | 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 | > | ||
| 178 | <div className="flex items-center gap-1 px-3 py-1.5 sm:px-4"> | 182 | <div className="flex items-center gap-1 px-3 py-1.5 sm:px-4"> |
| 179 | <button | 183 | <button |
| 180 | type="button" | 184 | type="button" |
| 181 | aria-expanded="true" | 185 | aria-expanded="true" |
| 186 | aria-label={`Collapse tasks. ${progress.completedSteps} of ${progress.totalSteps} complete.`} | ||
| 182 | 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" | 187 | 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" |
| 183 | onClick={onCollapse} | 188 | onClick={onCollapse} |
| 184 | onPointerDown={(event) => event.preventDefault()} | 189 | onPointerDown={(event) => event.preventDefault()} |
| ... | @@ -188,6 +193,7 @@ export const ComposerTasksDrawer = memo(function ComposerTasksDrawer({ | ... | @@ -188,6 +193,7 @@ export const ComposerTasksDrawer = memo(function ComposerTasksDrawer({ |
| 188 | <span className="tabular-nums"> | 193 | <span className="tabular-nums"> |
| 189 | {progress.completedSteps}/{progress.totalSteps} | 194 | {progress.completedSteps}/{progress.totalSteps} |
| 190 | </span> | 195 | </span> |
| 196 | <ChevronDownIcon aria-hidden className="ml-auto size-3.5 shrink-0" /> | ||
| 191 | </button> | 197 | </button> |
| 192 | <Button | 198 | <Button |
| 193 | size="icon-micro" | 199 | size="icon-micro" |
| ... | @@ -200,7 +206,13 @@ export const ComposerTasksDrawer = memo(function ComposerTasksDrawer({ | ... | @@ -200,7 +206,13 @@ export const ComposerTasksDrawer = memo(function ComposerTasksDrawer({ |
| 200 | <XIcon aria-hidden className="size-3" /> | 206 | <XIcon aria-hidden className="size-3" /> |
| 201 | </Button> | 207 | </Button> |
| 202 | </div> | 208 | </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 | > | ||
| 204 | {keyedTaskSteps(steps).map(({ key, step }) => ( | 216 | {keyedTaskSteps(steps).map(({ key, step }) => ( |
| 205 | <div key={key} className="flex items-baseline gap-2 text-xs leading-5" role="listitem"> | 217 | <div key={key} className="flex items-baseline gap-2 text-xs leading-5" role="listitem"> |
| 206 | <span | 218 | <span |