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