diff --git a/src/mobile/views/ThreadsView.test.tsx b/src/mobile/views/ThreadsView.test.tsx index fb2e2fec..97e3b0a9 100644 --- a/src/mobile/views/ThreadsView.test.tsx +++ b/src/mobile/views/ThreadsView.test.tsx @@ -384,6 +384,38 @@ describe("ThreadsView grouping", () => { expect(container.querySelector(".m-thread-section")).toBeNull(); }); + it("sinks done threads into a trailing Done section ordered by last update", () => { + const { container } = renderView([ + makeThread({ id: "done-old", title: "Done old", done: true, updatedAt: oldIso() }), + makeThread({ id: "live", title: "Live", updatedAt: recentIso() }), + makeThread({ id: "done-new", title: "Done new", done: true, updatedAt: recentIso() }), + ]); + + expect(rowTitles(container)).toEqual(["Live", "Done new", "Done old"]); + expect( + [...container.querySelectorAll(".m-thread-section")].map((el) => el.textContent), + ).toEqual(["Done"]); + }); + + it("keeps a mixed worktree group live until every member is done", () => { + const worktree = { worktreePath: "/repo/wt", worktreeBranch: "feature/x" }; + const mixed = renderView([ + makeThread({ id: "done", title: "Done member", done: true, ...worktree }), + makeThread({ id: "live", title: "Live member", ...worktree }), + ]); + + expect(mixed.container.querySelector(".m-thread-section")).toBeNull(); + mixed.unmount(); + + const allDone = renderView([ + makeThread({ id: "done-a", title: "Done A", done: true, ...worktree }), + makeThread({ id: "done-b", title: "Done B", done: true, ...worktree }), + ]); + expect( + [...allDone.container.querySelectorAll(".m-thread-section")].map((el) => el.textContent), + ).toEqual(["Done"]); + }); + it("filters threads from the touch search box", () => { renderView([ makeThread({ id: "a", title: "Alpha" }), diff --git a/src/mobile/views/ThreadsView.tsx b/src/mobile/views/ThreadsView.tsx index 59550292..cd2d0b87 100644 --- a/src/mobile/views/ThreadsView.tsx +++ b/src/mobile/views/ThreadsView.tsx @@ -16,6 +16,7 @@ import { RelativeTime } from "@/renderer/components/common/RelativeTime"; import { ThreadProviderIcon, getStatusTone } from "@/renderer/components/providers"; import { useThreadHasBackgroundActivity } from "@/renderer/hooks/uiSelectors"; import { + entryIsDone, entryIsStarred, entryLatestDate, groupThreads, @@ -523,26 +524,43 @@ export function ThreadsView(props: ThreadsViewProps) { // header. Standalone threads stay as plain rows. Reuses the desktop sidebar's // grouping so both surfaces agree on what counts as a group. const groupedEntries = groupThreads([...visibleThreads]); - // Split into Pinned / Current (updated < 24h) / Older sections, floated in - // that order and labeled — mirroring the desktop sidebar. `visibleThreads` is - // already recency-sorted, so these stable partitions keep that order within - // each section. Pinning any group member floats the whole group (entryIsStarred). - const pinnedEntries = groupedEntries.filter(entryIsStarred); - const unpinnedEntries = groupedEntries.filter((entry) => !entryIsStarred(entry)); + // Done entries sink below the live list, matching the desktop sidebar. A + // mixed group stays live until every member is done. + const liveEntries: ThreadListEntry[] = []; + const datedDoneEntries: { entry: ThreadListEntry; updatedAt: string }[] = []; + for (const entry of groupedEntries) { + if (entryIsDone(entry)) { + datedDoneEntries.push({ entry, updatedAt: entryLatestDate(entry, "updatedAt") }); + } else { + liveEntries.push(entry); + } + } + const doneEntries = datedDoneEntries + .sort((a, b) => b.updatedAt.localeCompare(a.updatedAt)) + .map(({ entry }) => entry); + // Split the remaining live entries into Pinned / Current (updated < 24h) / + // Older sections. `visibleThreads` is already recency-sorted, so these stable + // partitions keep that order within each section. Pinning any group member + // floats the whole group (entryIsStarred). + const pinnedEntries = liveEntries.filter(entryIsStarred); + const unpinnedEntries = liveEntries.filter((entry) => !entryIsStarred(entry)); const currentEntries = unpinnedEntries.filter((entry) => isRecent(entryLatestDate(entry, "updatedAt")), ); const olderEntries = unpinnedEntries.filter( (entry) => !isRecent(entryLatestDate(entry, "updatedAt")), ); - const sections = [ + const liveSections = [ { key: "pinned", label: t`Pinned`, entries: pinnedEntries }, { key: "current", label: t`Current`, entries: currentEntries }, { key: "older", label: t`Older`, entries: olderEntries }, ].filter((section) => section.entries.length > 0); - // A lone section spans the whole list, so its label would be noise; only show - // the headers once there is an actual boundary between two or more sections. - const showSectionLabels = sections.length > 1; + // A lone live section spans the whole live list, so its label would be noise. + // Done always keeps its label because it is a distinct trailing state. + const showLiveSectionLabels = liveSections.length > 1; + const sections = [...liveSections, { key: "done", label: t`Done`, entries: doneEntries }].filter( + (section) => section.entries.length > 0, + ); // A worktree-group child drops its worktree + git badges (the header carries // them); any group child drops the project name (the header carries that too). @@ -633,7 +651,9 @@ export function ThreadsView(props: ThreadsViewProps) {
{sections.map((section) => ( - {showSectionLabels ?
{section.label}
: null} + {section.key === "done" || showLiveSectionLabels ? ( +
{section.label}
+ ) : null} {section.entries.map((entry) => renderEntry(entry))}
))} diff --git a/src/renderer/locales/de/messages.po b/src/renderer/locales/de/messages.po index 04c08c55..80448c3c 100644 --- a/src/renderer/locales/de/messages.po +++ b/src/renderer/locales/de/messages.po @@ -3491,6 +3491,7 @@ msgid "done" msgstr "erledigt" #. Notification status: thread is done +#: src/mobile/views/ThreadsView.tsx #: src/renderer/views/MainView/parts/Sidebar/parts/sidebarProjectRows.ts #: src/renderer/views/SettingsOverlay/parts/NotificationSettings.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts diff --git a/src/renderer/locales/en/messages.po b/src/renderer/locales/en/messages.po index da2ca53a..b5d23e64 100644 --- a/src/renderer/locales/en/messages.po +++ b/src/renderer/locales/en/messages.po @@ -3491,6 +3491,7 @@ msgid "done" msgstr "done" #. Notification status: thread is done +#: src/mobile/views/ThreadsView.tsx #: src/renderer/views/MainView/parts/Sidebar/parts/sidebarProjectRows.ts #: src/renderer/views/SettingsOverlay/parts/NotificationSettings.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts diff --git a/src/renderer/locales/es/messages.po b/src/renderer/locales/es/messages.po index 2dafa488..42ac33a4 100644 --- a/src/renderer/locales/es/messages.po +++ b/src/renderer/locales/es/messages.po @@ -3491,6 +3491,7 @@ msgid "done" msgstr "listo" #. Notification status: thread is done +#: src/mobile/views/ThreadsView.tsx #: src/renderer/views/MainView/parts/Sidebar/parts/sidebarProjectRows.ts #: src/renderer/views/SettingsOverlay/parts/NotificationSettings.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts diff --git a/src/renderer/locales/fr/messages.po b/src/renderer/locales/fr/messages.po index 220026f9..fbb36112 100644 --- a/src/renderer/locales/fr/messages.po +++ b/src/renderer/locales/fr/messages.po @@ -3491,6 +3491,7 @@ msgid "done" msgstr "fait" #. Notification status: thread is done +#: src/mobile/views/ThreadsView.tsx #: src/renderer/views/MainView/parts/Sidebar/parts/sidebarProjectRows.ts #: src/renderer/views/SettingsOverlay/parts/NotificationSettings.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts diff --git a/src/renderer/locales/ja/messages.po b/src/renderer/locales/ja/messages.po index df93eb6a..d05e5bae 100644 --- a/src/renderer/locales/ja/messages.po +++ b/src/renderer/locales/ja/messages.po @@ -3490,6 +3490,7 @@ msgid "done" msgstr "完了しました" #. Notification status: thread is done +#: src/mobile/views/ThreadsView.tsx #: src/renderer/views/MainView/parts/Sidebar/parts/sidebarProjectRows.ts #: src/renderer/views/SettingsOverlay/parts/NotificationSettings.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts diff --git a/src/renderer/locales/ko/messages.po b/src/renderer/locales/ko/messages.po index 5fcc806e..490f3eee 100644 --- a/src/renderer/locales/ko/messages.po +++ b/src/renderer/locales/ko/messages.po @@ -3491,6 +3491,7 @@ msgid "done" msgstr "완료" #. Notification status: thread is done +#: src/mobile/views/ThreadsView.tsx #: src/renderer/views/MainView/parts/Sidebar/parts/sidebarProjectRows.ts #: src/renderer/views/SettingsOverlay/parts/NotificationSettings.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts diff --git a/src/renderer/locales/pl/messages.po b/src/renderer/locales/pl/messages.po index 417739de..ceaecd93 100644 --- a/src/renderer/locales/pl/messages.po +++ b/src/renderer/locales/pl/messages.po @@ -3491,6 +3491,7 @@ msgid "done" msgstr "zrobione" #. Notification status: thread is done +#: src/mobile/views/ThreadsView.tsx #: src/renderer/views/MainView/parts/Sidebar/parts/sidebarProjectRows.ts #: src/renderer/views/SettingsOverlay/parts/NotificationSettings.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts diff --git a/src/renderer/locales/pt-BR/messages.po b/src/renderer/locales/pt-BR/messages.po index 6056d5e5..7850646d 100644 --- a/src/renderer/locales/pt-BR/messages.po +++ b/src/renderer/locales/pt-BR/messages.po @@ -3491,6 +3491,7 @@ msgid "done" msgstr "feito" #. Notification status: thread is done +#: src/mobile/views/ThreadsView.tsx #: src/renderer/views/MainView/parts/Sidebar/parts/sidebarProjectRows.ts #: src/renderer/views/SettingsOverlay/parts/NotificationSettings.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts diff --git a/src/renderer/locales/ru/messages.po b/src/renderer/locales/ru/messages.po index 56952cf6..a760ff94 100644 --- a/src/renderer/locales/ru/messages.po +++ b/src/renderer/locales/ru/messages.po @@ -3491,6 +3491,7 @@ msgid "done" msgstr "готово" #. Notification status: thread is done +#: src/mobile/views/ThreadsView.tsx #: src/renderer/views/MainView/parts/Sidebar/parts/sidebarProjectRows.ts #: src/renderer/views/SettingsOverlay/parts/NotificationSettings.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts diff --git a/src/renderer/locales/tr/messages.po b/src/renderer/locales/tr/messages.po index 6f48ec0e..dc333b51 100644 --- a/src/renderer/locales/tr/messages.po +++ b/src/renderer/locales/tr/messages.po @@ -3491,6 +3491,7 @@ msgid "done" msgstr "bitti" #. Notification status: thread is done +#: src/mobile/views/ThreadsView.tsx #: src/renderer/views/MainView/parts/Sidebar/parts/sidebarProjectRows.ts #: src/renderer/views/SettingsOverlay/parts/NotificationSettings.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts diff --git a/src/renderer/locales/uk/messages.po b/src/renderer/locales/uk/messages.po index 344336ae..4ff69072 100644 --- a/src/renderer/locales/uk/messages.po +++ b/src/renderer/locales/uk/messages.po @@ -3491,6 +3491,7 @@ msgid "done" msgstr "готово" #. Notification status: thread is done +#: src/mobile/views/ThreadsView.tsx #: src/renderer/views/MainView/parts/Sidebar/parts/sidebarProjectRows.ts #: src/renderer/views/SettingsOverlay/parts/NotificationSettings.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts diff --git a/src/renderer/locales/vi/messages.po b/src/renderer/locales/vi/messages.po index 1e46968e..86ea8fa3 100644 --- a/src/renderer/locales/vi/messages.po +++ b/src/renderer/locales/vi/messages.po @@ -3491,6 +3491,7 @@ msgid "done" msgstr "xong" #. Notification status: thread is done +#: src/mobile/views/ThreadsView.tsx #: src/renderer/views/MainView/parts/Sidebar/parts/sidebarProjectRows.ts #: src/renderer/views/SettingsOverlay/parts/NotificationSettings.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts diff --git a/src/renderer/locales/zh-CN/messages.po b/src/renderer/locales/zh-CN/messages.po index 1fa158b0..d8fd06eb 100644 --- a/src/renderer/locales/zh-CN/messages.po +++ b/src/renderer/locales/zh-CN/messages.po @@ -3491,6 +3491,7 @@ msgid "done" msgstr "完成" #. Notification status: thread is done +#: src/mobile/views/ThreadsView.tsx #: src/renderer/views/MainView/parts/Sidebar/parts/sidebarProjectRows.ts #: src/renderer/views/SettingsOverlay/parts/NotificationSettings.tsx #: src/renderer/views/SettingsOverlay/parts/settingsSearchIndex.ts