From be94d0963e93962a8222af6f4c6097bf6249ab05 Mon Sep 17 00:00:00 2001 From: Mariano Blua Date: Tue, 6 Oct 2026 22:53:43 -0300 Subject: [PATCH 1/2] fix(sidebar): recognize empty canonical Clean descriptions (#2935) --- .../ProjectPanel.context-menu.test.tsx | 17 +++++++++++++---- src/sidebar/components/ProjectPanel.tsx | 1 + src/sidebar/stores/project.test.ts | 1 + 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/sidebar/components/ProjectPanel.context-menu.test.tsx b/src/sidebar/components/ProjectPanel.context-menu.test.tsx index f5f0b17b..0eb3063e 100644 --- a/src/sidebar/components/ProjectPanel.context-menu.test.tsx +++ b/src/sidebar/components/ProjectPanel.context-menu.test.tsx @@ -461,14 +461,23 @@ describe("ProjectPanel replica context menu — gray/red (#545)", () => { for (const active of [false, true]) { it.each([ - ["canonical LF", "Ready to start a new topic\n", true], + ["legacy LF", "Ready to start a new topic\n", true], ["CRLF", "Ready to start a new topic\r\n", true], ["no framing LF", "Ready to start a new topic", true], ["extra blank", "Ready to start a new topic\n\n", false], ["extra space", "Ready to start a new topic \n", false], ["case change", "ready to start a new topic\n", false], ["custom multiline", "human\nnotes", false], - ["empty body", "", false], + ["empty body", "", true], + ["space", " ", false], + ["tab", " ", false], + ["LF", " +", false], + ["CRLF whitespace", " +", false], + ["multiple LF", " + +", false], ])(`Clean description %s in ${active ? "active" : "inactive"} menu`, async (_label, description, disabled) => { const fake = await setupPanel(active ? [coordSession(), memberSession()] : [], projectDiscovery("Clean")); fake.resolve("task_get_snapshot_at", sidebarSnapshot(workgroupPath, { taskTitle: "Clean", description })); @@ -479,12 +488,12 @@ describe("ProjectPanel replica context menu — gray/red (#545)", () => { expect(broom.title).toBe(disabled ? "Nothing to clear" : "Clear task title"); }); - it(`allows status and unknown snapshots in ${active ? "active" : "inactive"} menu`, async () => { + it.each(["title", "status", "status record"])(`allows %s and unknown snapshots in ${active ? "active" : "inactive"} menu`, async () => { const fake = await setupPanel(active ? [coordSession(), memberSession()] : [], projectDiscovery("Clean")); fake.resolve("task_get_snapshot_at", sidebarSnapshot(workgroupPath, { taskTitle: "Clean", description: "Ready to start a new topic\n", status: "complete status", statusRecord: { schemaVersion: 1, kind: "status", topicId: "topic", sequence: 1, - requestId: null, baseRevision: null, recordedAt: "now", author: null, status: "complete status" }, + requestId: null, baseRevision: null, recordedAt: "now", author: null, status: "complete status" } : null, })); await projectStore.refreshTaskSnapshot(workgroupPath); contextMenu(findRow(rendered!.root, memberRowTestId)); diff --git a/src/sidebar/components/ProjectPanel.tsx b/src/sidebar/components/ProjectPanel.tsx index 253b1c52..e57e1679 100644 --- a/src/sidebar/components/ProjectPanel.tsx +++ b/src/sidebar/components/ProjectPanel.tsx @@ -496,6 +496,7 @@ function taskCleanDisabled(room: string): boolean { const snapshot = projectStore.taskSnapshot(room); if (!snapshot || snapshot.statusRecord?.kind === "status" || !isTaskClean(snapshot.taskTitle)) return false; if (snapshot.status !== null) return false; + if (snapshot.description === "") return true; const description = snapshot.description.replace(/\r\n/g, "\n").replace(/\n$/, ""); return description === "Ready to start a new topic"; } diff --git a/src/sidebar/stores/project.test.ts b/src/sidebar/stores/project.test.ts index 52eec46f..7a6073d2 100644 --- a/src/sidebar/stores/project.test.ts +++ b/src/sidebar/stores/project.test.ts @@ -160,6 +160,7 @@ describe("projectStore", () => { await projectStore.refreshTaskSnapshot(wg.path); old.resolve(taskSnapshot(wg.path, { status: "before Clean" })); await pending; + expect(projectStore.taskSnapshot(wg.path)?.description).toBe(""); expect(projectStore.taskSnapshot(wg.path)?.taskTitle).toBe("Clean"); expect(projectStore.taskSnapshot(wg.path)?.status).toBeNull(); }); From 8ec20eeaeb7268ac4b9129384947abbe6fef443a Mon Sep 17 00:00:00 2001 From: Mariano Blua Date: Tue, 6 Oct 2026 22:54:14 -0300 Subject: [PATCH 2/2] test(sidebar): complete empty Clean fixtures and independent guards --- .../ProjectPanel.context-menu.test.tsx | 19 ++++++++----------- src/sidebar/stores/project.test.ts | 2 +- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/sidebar/components/ProjectPanel.context-menu.test.tsx b/src/sidebar/components/ProjectPanel.context-menu.test.tsx index 0eb3063e..4e98245e 100644 --- a/src/sidebar/components/ProjectPanel.context-menu.test.tsx +++ b/src/sidebar/components/ProjectPanel.context-menu.test.tsx @@ -370,7 +370,7 @@ function registerTaskPanelTransport({ fake, discoveryResult }: { fake: FakeTrans const wg = discoveryResult.workgroups.find(wg => wg.path === workgroupRoot); return sidebarSnapshot(String(workgroupRoot), { taskTitle: wg?.taskTitle ?? null, - description: wg?.task?.includes("stale body") ? "stale body text" : "Ready to start a new topic\n", + description: wg?.task?.includes("stale body") ? "stale body text" : "", }); }); fake.resolve("task_clean", { workgroupRoot: workgroupPath, task: null }); @@ -471,13 +471,9 @@ describe("ProjectPanel replica context menu — gray/red (#545)", () => { ["empty body", "", true], ["space", " ", false], ["tab", " ", false], - ["LF", " -", false], - ["CRLF whitespace", " -", false], - ["multiple LF", " - -", false], + ["LF", "\n", false], + ["CRLF whitespace", "\r\n", false], + ["multiple LF", "\n\n", false], ])(`Clean description %s in ${active ? "active" : "inactive"} menu`, async (_label, description, disabled) => { const fake = await setupPanel(active ? [coordSession(), memberSession()] : [], projectDiscovery("Clean")); fake.resolve("task_get_snapshot_at", sidebarSnapshot(workgroupPath, { taskTitle: "Clean", description })); @@ -488,11 +484,12 @@ describe("ProjectPanel replica context menu — gray/red (#545)", () => { expect(broom.title).toBe(disabled ? "Nothing to clear" : "Clear task title"); }); - it.each(["title", "status", "status record"])(`allows %s and unknown snapshots in ${active ? "active" : "inactive"} menu`, async () => { + it.each(["title", "status", "status record"])(`allows %s and unknown snapshots in ${active ? "active" : "inactive"} menu`, async (guard) => { const fake = await setupPanel(active ? [coordSession(), memberSession()] : [], projectDiscovery("Clean")); fake.resolve("task_get_snapshot_at", sidebarSnapshot(workgroupPath, { - taskTitle: "Clean", description: "Ready to start a new topic\n", status: "complete status", - statusRecord: { schemaVersion: 1, kind: "status", topicId: "topic", sequence: 1, + taskTitle: guard === "title" ? "USER title" : "Clean", description: "", + status: guard === "status" ? "complete status" : null, + statusRecord: guard === "status record" ? { schemaVersion: 1, kind: "status", topicId: "topic", sequence: 1, requestId: null, baseRevision: null, recordedAt: "now", author: null, status: "complete status" } : null, })); await projectStore.refreshTaskSnapshot(workgroupPath); diff --git a/src/sidebar/stores/project.test.ts b/src/sidebar/stores/project.test.ts index 7a6073d2..beb4ac9f 100644 --- a/src/sidebar/stores/project.test.ts +++ b/src/sidebar/stores/project.test.ts @@ -156,7 +156,7 @@ describe("projectStore", () => { const old = deferred(); m.getSnapshot.mockReturnValueOnce(old.promise); const pending = projectStore.refreshTaskSnapshot(wg.path); - m.getSnapshot.mockResolvedValueOnce(taskSnapshot(wg.path, { taskTitle: "Clean", description: "Ready to start a new topic\n", status: null })); + m.getSnapshot.mockResolvedValueOnce(taskSnapshot(wg.path, { taskTitle: "Clean", description: "", status: null })); await projectStore.refreshTaskSnapshot(wg.path); old.resolve(taskSnapshot(wg.path, { status: "before Clean" })); await pending;