From f753220ae152690dae94d9bdf9bf84d44e3595de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carlos=20Flor=C3=AAncio?= Date: Sat, 22 Aug 2026 15:33:57 +0000 Subject: [PATCH] feat(sidebar): clarify task sort descriptions (#2911) (#2920) --- apps/packages/ui/src/select.tsx | 23 +++++++- .../task/sidebar-filter/sort-picker.tsx | 57 +++++++++++++++---- .../tests/task/mobile-sidebar-views.spec.ts | 37 ++++++++++++ .../web/e2e/tests/task/sidebar-filter.spec.ts | 39 +++++++++++++ apps/web/src/locales/en/task.json | 6 ++ apps/web/src/locales/pseudo/task.json | 6 ++ apps/web/src/locales/pt-pt/task.json | 6 ++ apps/web/src/locales/zh-cn/task.json | 6 ++ apps/web/src/locales/zh-hk/task.json | 6 ++ apps/web/src/locales/zh-tw/task.json | 6 ++ docs/public/tasks-and-workflows.md | 9 +++ 11 files changed, 187 insertions(+), 14 deletions(-) diff --git a/apps/packages/ui/src/select.tsx b/apps/packages/ui/src/select.tsx index c780705f7d..213d54d7e3 100644 --- a/apps/packages/ui/src/select.tsx +++ b/apps/packages/ui/src/select.tsx @@ -105,8 +105,17 @@ function SelectLabel({ className, ...props }: React.ComponentProps) { +}: React.ComponentProps & { + description?: React.ReactNode; +}) { + const descriptionId = React.useId(); + const describedBy = description + ? [ariaDescribedBy, descriptionId].filter(Boolean).join(" ") + : ariaDescribedBy; + return ( @@ -121,7 +131,16 @@ function SelectItem({ - {children} + {description ? ( +
+ {children} +
+ {description} +
+
+ ) : ( + {children} + )}
); } diff --git a/apps/web/components/task/sidebar-filter/sort-picker.tsx b/apps/web/components/task/sidebar-filter/sort-picker.tsx index 7a514fdc0e..ad44a46f7c 100644 --- a/apps/web/components/task/sidebar-filter/sort-picker.tsx +++ b/apps/web/components/task/sidebar-filter/sort-picker.tsx @@ -9,13 +9,37 @@ import { useTranslation } from "react-i18next"; // `labelKey` holds a catalog key, not copy: this table is module scope, so a // resolved `t()` here would freeze at the boot locale. `key` is a persisted // sort value and stays in English. -const SORT_OPTIONS: Array<{ key: SortKey; labelKey: string }> = [ - { key: "state", labelKey: "task:sortStatus" }, - { key: "updatedAt", labelKey: "task:sortUpdated" }, - { key: "lastActivityAt", labelKey: "task:sortLastActivity" }, - { key: "createdAt", labelKey: "task:sortCreated" }, - { key: "title", labelKey: "task:sortTitle" }, - { key: "custom", labelKey: "task:sortCustom" }, +const SORT_OPTIONS: Array<{ key: SortKey; labelKey: string; descriptionKey: string }> = [ + { + key: "state", + labelKey: "task:sortStatus", + descriptionKey: "task:sortStatusDescription", + }, + { + key: "updatedAt", + labelKey: "task:sortUpdated", + descriptionKey: "task:sortUpdatedDescription", + }, + { + key: "lastActivityAt", + labelKey: "task:sortLastActivity", + descriptionKey: "task:sortLastActivityDescription", + }, + { + key: "createdAt", + labelKey: "task:sortCreated", + descriptionKey: "task:sortCreatedDescription", + }, + { + key: "title", + labelKey: "task:sortTitle", + descriptionKey: "task:sortTitleDescription", + }, + { + key: "custom", + labelKey: "task:sortCustom", + descriptionKey: "task:sortCustomDescription", + }, ]; type Props = { @@ -35,11 +59,20 @@ export function SortPicker({ value, onChange }: Props) { - {SORT_OPTIONS.map((opt) => ( - - {t(opt.labelKey)} - - ))} + {SORT_OPTIONS.map((opt) => { + const label = t(opt.labelKey); + return ( + + {label} + + ); + })} {!isCustom && ( diff --git a/apps/web/e2e/tests/task/mobile-sidebar-views.spec.ts b/apps/web/e2e/tests/task/mobile-sidebar-views.spec.ts index f60d433f55..db7ae03a72 100644 --- a/apps/web/e2e/tests/task/mobile-sidebar-views.spec.ts +++ b/apps/web/e2e/tests/task/mobile-sidebar-views.spec.ts @@ -275,6 +275,43 @@ test.describe("Mobile sidebar — view system", () => { const popover = testPage.getByTestId("sidebar-filter-popover"); await expect(popover).toBeVisible(); await popover.getByTestId("sort-key-select").tap(); + await expect(testPage.getByRole("option", { name: "Updated", exact: true })).toContainText( + "Last task summary refresh. Background events can change it.", + ); + await expect( + testPage.getByRole("option", { name: "Last activity", exact: true }), + ).toContainText("Last user or agent action. Viewing a task does not change it."); + await expect(testPage.getByRole("option", { name: "Status", exact: true })).toContainText( + "Task state, from review to backlog.", + ); + await expect(testPage.getByRole("option", { name: "Created", exact: true })).toContainText( + "When the task was created.", + ); + await expect(testPage.getByRole("option", { name: "Title", exact: true })).toContainText( + "Task title in alphabetical order.", + ); + await expect(testPage.getByRole("option", { name: "Custom", exact: true })).toContainText( + "The manual order you set for tasks.", + ); + for (const { label, description } of [ + { + label: "Updated", + description: "Last task summary refresh. Background events can change it.", + }, + { + label: "Last activity", + description: "Last user or agent action. Viewing a task does not change it.", + }, + { label: "Status", description: "Task state, from review to backlog." }, + { label: "Created", description: "When the task was created." }, + { label: "Title", description: "Task title in alphabetical order." }, + { label: "Custom", description: "The manual order you set for tasks." }, + ]) { + const option = testPage.getByRole("option", { name: label, exact: true }); + const descriptionId = await option.getAttribute("aria-describedby"); + expect(descriptionId).toBeTruthy(); + await expect(testPage.locator(`[id="${descriptionId}"]`)).toHaveText(description); + } await testPage.getByRole("option", { name: "Last activity", exact: true }).tap(); const direction = popover.getByTestId("sort-direction-toggle"); if ((await direction.getAttribute("data-direction")) !== "desc") await direction.tap(); diff --git a/apps/web/e2e/tests/task/sidebar-filter.spec.ts b/apps/web/e2e/tests/task/sidebar-filter.spec.ts index 1225f0701a..54f51366d1 100644 --- a/apps/web/e2e/tests/task/sidebar-filter.spec.ts +++ b/apps/web/e2e/tests/task/sidebar-filter.spec.ts @@ -350,6 +350,45 @@ test.describe("Sidebar filter — group + sort", () => { await session.waitForLoad(); const filters = new SidebarFilterPopoverPage(testPage); await filters.open(); + await filters.popover.getByTestId("sort-key-select").click(); + await expect(testPage.getByRole("option", { name: "Updated", exact: true })).toContainText( + "Last task summary refresh. Background events can change it.", + ); + await expect( + testPage.getByRole("option", { name: "Last activity", exact: true }), + ).toContainText("Last user or agent action. Viewing a task does not change it."); + await expect(testPage.getByRole("option", { name: "Status", exact: true })).toContainText( + "Task state, from review to backlog.", + ); + await expect(testPage.getByRole("option", { name: "Created", exact: true })).toContainText( + "When the task was created.", + ); + await expect(testPage.getByRole("option", { name: "Title", exact: true })).toContainText( + "Task title in alphabetical order.", + ); + await expect(testPage.getByRole("option", { name: "Custom", exact: true })).toContainText( + "The manual order you set for tasks.", + ); + for (const { label, description } of [ + { + label: "Updated", + description: "Last task summary refresh. Background events can change it.", + }, + { + label: "Last activity", + description: "Last user or agent action. Viewing a task does not change it.", + }, + { label: "Status", description: "Task state, from review to backlog." }, + { label: "Created", description: "When the task was created." }, + { label: "Title", description: "Task title in alphabetical order." }, + { label: "Custom", description: "The manual order you set for tasks." }, + ]) { + const option = testPage.getByRole("option", { name: label, exact: true }); + const descriptionId = await option.getAttribute("aria-describedby"); + expect(descriptionId).toBeTruthy(); + await expect(testPage.locator(`[id="${descriptionId}"]`)).toHaveText(description); + } + await testPage.keyboard.press("Escape"); await filters.setGroup("None"); await filters.setSort("Last activity", "desc"); await filters.saveAs("Last activity view"); diff --git a/apps/web/src/locales/en/task.json b/apps/web/src/locales/en/task.json index 05861e5958..c8a9b19baa 100644 --- a/apps/web/src/locales/en/task.json +++ b/apps/web/src/locales/en/task.json @@ -1351,12 +1351,18 @@ "someone": "Someone", "sort": "Sort", "sortCreated": "Created", + "sortCreatedDescription": "When the task was created.", "sortCustom": "Custom", + "sortCustomDescription": "The manual order you set for tasks.", "sortDirection": "Sort direction {{direction}}", "sortLastActivity": "Last activity", + "sortLastActivityDescription": "Last user or agent action. Viewing a task does not change it.", "sortStatus": "Status", + "sortStatusDescription": "Task state, from review to backlog.", "sortTitle": "Title", + "sortTitleDescription": "Task title in alphabetical order.", "sortUpdated": "Updated", + "sortUpdatedDescription": "Last task summary refresh. Background events can change it.", "source": "Source", "sourceChangesTitle": "{{source}} Changes", "specifyALocalPortOrLeave": "Specify a local port or leave blank for a random one. For Docker/K8s, use a port you've pre-exposed.", diff --git a/apps/web/src/locales/pseudo/task.json b/apps/web/src/locales/pseudo/task.json index ec48097154..bb4f942964 100644 --- a/apps/web/src/locales/pseudo/task.json +++ b/apps/web/src/locales/pseudo/task.json @@ -1351,12 +1351,18 @@ "someone": "Śōḿēōńē", "sort": "Śōŕţ", "sortCreated": "Ćŕēàţēď", + "sortCreatedDescription": "Ŵĥēń ţĥē ţàśķ ŵàś ćŕēàţēď.", "sortCustom": "Ćũśţōḿ", + "sortCustomDescription": "Ŧĥē ḿàńũàĺ ōŕďēŕ ẏōũ śēţ ƒōŕ ţàśķś.", "sortDirection": "Śōŕţ ďĩŕēćţĩōń {{direction}}", "sortLastActivity": "Ĺàśţ àćţĩvĩţŷ", + "sortLastActivityDescription": "Ĺàśţ ũśēŕ ōŕ àĝēńţ àćţĩōń. Vĩēŵĩńĝ à ţàśķ ďōēś ńōţ ćĥàńĝē ĩţ.", "sortStatus": "Śţàţũś", + "sortStatusDescription": "Ŧàśķ śţàţē, ƒŕōḿ ŕēvĩēŵ ţō ƀàćķĺōĝ.", "sortTitle": "Ţĩţĺē", + "sortTitleDescription": "Ŧàśķ ţĩţĺē ĩń àĺƥĥàƀēţĩćàĺ ōŕďēŕ.", "sortUpdated": "Ũƥďàţēď", + "sortUpdatedDescription": "Ĺàśţ ţàśķ śũḿḿàŕŷ ŕēƒŕēśĥ. Ɓàćķĝŕōũńď ēvēńţś ćàń ćĥàńĝē ĩţ.", "source": "Śōũŕćē", "sourceChangesTitle": "{{source}} Ćĥàńĝēś", "specifyALocalPortOrLeave": "Śƥēćĩƒŷ à ĺōćàĺ ƥōŕţ ōŕ ĺēàvē ƀĺàńķ ƒōŕ à ŕàńďōḿ ōńē. Ƒōŕ Ďōćķēŕ/Ķ8ś, ũśē à ƥōŕţ ŷōũ'vē ƥŕē-ēxƥōśēď.", diff --git a/apps/web/src/locales/pt-pt/task.json b/apps/web/src/locales/pt-pt/task.json index 10a363ecdf..854fb09288 100644 --- a/apps/web/src/locales/pt-pt/task.json +++ b/apps/web/src/locales/pt-pt/task.json @@ -1306,12 +1306,18 @@ "someone": "Alguém", "sort": "Ordenar", "sortCreated": "Criação", + "sortCreatedDescription": "Quando a tarefa foi criada.", "sortCustom": "Personalizada", + "sortCustomDescription": "A ordem manual que definiu para as tarefas.", "sortDirection": "Sentido da ordenação {{direction}}", "sortLastActivity": "Última atividade", + "sortLastActivityDescription": "Última ação do utilizador ou turno do agente. Ver uma tarefa não conta como atividade.", "sortStatus": "Estado", + "sortStatusDescription": "Estado da tarefa, de revisão a pendente.", "sortTitle": "Título", + "sortTitleDescription": "Título da tarefa por ordem alfabética.", "sortUpdated": "Atualização", + "sortUpdatedDescription": "Última atualização do resumo da tarefa. Eventos em segundo plano podem alterá-la.", "source": "Fonte", "sourceChangesTitle": "Alterações de {{source}}", "specifyALocalPortOrLeave": "Indique uma porta local ou deixe em branco para uma aleatória. No Docker/K8s, use uma porta que já tenha exposto.", diff --git a/apps/web/src/locales/zh-cn/task.json b/apps/web/src/locales/zh-cn/task.json index 1aec0e37ab..e23f760b94 100644 --- a/apps/web/src/locales/zh-cn/task.json +++ b/apps/web/src/locales/zh-cn/task.json @@ -1305,12 +1305,18 @@ "someone": "某人", "sort": "排序", "sortCreated": "创建时间", + "sortCreatedDescription": "任务创建的时间。", "sortCustom": "自定义", + "sortCustomDescription": "你为任务设置的手动顺序。", "sortDirection": "排序方向 {{direction}}", "sortLastActivity": "最后活动", + "sortLastActivityDescription": "最近一次用户操作或智能体回合。查看任务不会改变此时间。", "sortStatus": "状态", + "sortStatusDescription": "任务状态,按审核、进行中和待处理排序。", "sortTitle": "标题", + "sortTitleDescription": "按任务标题的字母顺序。", "sortUpdated": "更新时间", + "sortUpdatedDescription": "任务摘要的最近刷新时间。后台事件可能会改变此时间。", "source": "来源", "sourceChangesTitle": "{{source}} 变更", "specifyALocalPortOrLeave": "指定本地端口,或留空以随机分配。对于 Docker/K8s,请使用已预先暴露的端口。", diff --git a/apps/web/src/locales/zh-hk/task.json b/apps/web/src/locales/zh-hk/task.json index 416889c120..bfb9ac37a3 100644 --- a/apps/web/src/locales/zh-hk/task.json +++ b/apps/web/src/locales/zh-hk/task.json @@ -1305,12 +1305,18 @@ "someone": "某人", "sort": "排序", "sortCreated": "建立時間", + "sortCreatedDescription": "任務建立的時間。", "sortCustom": "自訂", + "sortCustomDescription": "你為任務設定的手動順序。", "sortDirection": "排序方向 {{direction}}", "sortLastActivity": "最後活動", + "sortLastActivityDescription": "最近一次使用者操作或代理程式回合。查看任務不會改變此時間。", "sortStatus": "狀態", + "sortStatusDescription": "任務狀態,按審核、進行中和待處理排序。", "sortTitle": "標題", + "sortTitleDescription": "按任務標題的字母順序。", "sortUpdated": "更新時間", + "sortUpdatedDescription": "任務摘要的最近重新整理時間。背景事件可能會改變此時間。", "source": "來源", "sourceChangesTitle": "{{source}} 變更", "specifyALocalPortOrLeave": "指定本地端口,或留空以隨機分配。對於 Docker/K8s,請使用已預先暴露的端口。", diff --git a/apps/web/src/locales/zh-tw/task.json b/apps/web/src/locales/zh-tw/task.json index 06cfe1cd7c..6e2091327d 100644 --- a/apps/web/src/locales/zh-tw/task.json +++ b/apps/web/src/locales/zh-tw/task.json @@ -1305,12 +1305,18 @@ "someone": "某人", "sort": "排序", "sortCreated": "建立時間", + "sortCreatedDescription": "任務建立的時間。", "sortCustom": "自訂", + "sortCustomDescription": "你為任務設定的手動順序。", "sortDirection": "排序方向 {{direction}}", "sortLastActivity": "最後活動", + "sortLastActivityDescription": "最近一次使用者操作或代理程式回合。檢視任務不會改變此時間。", "sortStatus": "狀態", + "sortStatusDescription": "任務狀態,依審核、進行中和待處理排序。", "sortTitle": "標題", + "sortTitleDescription": "依任務標題的字母順序。", "sortUpdated": "更新時間", + "sortUpdatedDescription": "任務摘要的最近重新整理時間。背景事件可能會改變此時間。", "source": "來源", "sourceChangesTitle": "{{source}} 變更", "specifyALocalPortOrLeave": "指定本地埠,或留空以隨機分配。對於 Docker/K8s,請使用已預先暴露的埠。", diff --git a/docs/public/tasks-and-workflows.md b/docs/public/tasks-and-workflows.md index 424ff63ede..2e474580ae 100644 --- a/docs/public/tasks-and-workflows.md +++ b/docs/public/tasks-and-workflows.md @@ -367,6 +367,15 @@ On desktop and tablet, the header switches between **Kanban**, **Pipeline**, and Under **Settings → General → Appearance → Startup Page**, choose **Task overview** (the default) or **Last visited task**. The latter resumes the most recently opened task in the current workspace on that device when Kandev starts or you open bare Home. It does not change an explicit task or workflow link. Home navigation and a task's Back action always return to the task overview; when there is no matching local recent task, Kandev opens the overview instead. +The **TASKS** list in the left sidebar has two time-based sort choices. These choices are separate from the sort choices in the task **List** view. + +| Sort choice | Meaning | +| --- | --- | +| **Updated** | The last task summary refresh. Background events, such as pull-request status changes, can change this time. | +| **Last activity** | The last real user or agent action. Opening or focusing a task and background provider polling do not change this time. | + +Choose **Last activity** when you want to review tasks by the least recent user or agent interaction. + - Search matches tasks without changing their state. - The display menu filters by **Workflow** and **Repository** and can enable **Open preview on click**. In Kanban/Pipeline, each workflow lane has a **Columns** menu to hide individual steps. Unticking a step hides its column and tasks on that board, scoped to its own workflow, until you re-tick it. The optional **Auto-hide empty columns** setting collapses unoccupied steps without changing those manual choices; auto-hidden empty steps return as move destinations while a task is being moved, while manually hidden steps remain unavailable for pointer and bulk moves. On phones, open the menu drawer to change columns for the focused workflow. - In **List**, the display menu can enable **Show task details** to include available repository, description, pull-request, session, parent, review, and archive context in each row. This option is off by default and follows the user across devices.