From 673a8a64c39a717b93397e82b6904eb50063d7cd Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Thu, 18 Jun 2026 04:53:50 -0700 Subject: [PATCH] FN-6630: return text for empty task-list columns Ensure filtered fn_task_list calls always return host-safe text for empty target columns. - Add explicit empty-state copy for column filters with no matching tasks. - Keep formatter fallback output non-empty before returning tool content. - Cover empty active-column filters and existing small listings in CLI extension tests. - Add a patch changeset for the published CLI package. Files changed: .changeset/fn-6630-task-list-empty-column.md | 5 ++++ packages/cli/src/__tests__/extension.test.ts | 36 +++++++++++++++++++++++++++- packages/cli/src/extension.ts | 15 ++++++++++-- 3 files changed, 53 insertions(+), 3 deletions(-) Fusion-Task-Id: FN-6630 Fusion-Task-Lineage: e371bf11-5841-47d0-ac72-4cdf2f1716ba --- .changeset/fn-6630-task-list-empty-column.md | 5 +++ packages/cli/src/__tests__/extension.test.ts | 36 +++++++++++++++++++- packages/cli/src/extension.ts | 15 ++++++-- 3 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 .changeset/fn-6630-task-list-empty-column.md diff --git a/.changeset/fn-6630-task-list-empty-column.md b/.changeset/fn-6630-task-list-empty-column.md new file mode 100644 index 0000000000..5702743239 --- /dev/null +++ b/.changeset/fn-6630-task-list-empty-column.md @@ -0,0 +1,5 @@ +--- +"@runfusion/fusion": patch +--- + +Fix `fn_task_list` column filters so empty target columns return explicit text instead of an empty content block. diff --git a/packages/cli/src/__tests__/extension.test.ts b/packages/cli/src/__tests__/extension.test.ts index 51f0dc5721..6979fb7956 100644 --- a/packages/cli/src/__tests__/extension.test.ts +++ b/packages/cli/src/__tests__/extension.test.ts @@ -23,7 +23,7 @@ vi.mock("../commands/task.js", () => ({ })); import kbExtension, { resolveTaskListFormatter } from "../extension.js"; -import { TaskStore, AgentStore, MANUAL_RETRY_RESET_COUNTER_KEYS, RESEARCH_RUN_STATUSES, MAX_TASK_LIST_TEXT_CHARS, formatTaskListText } from "@fusion/core"; +import { TaskStore, AgentStore, MANUAL_RETRY_RESET_COUNTER_KEYS, RESEARCH_RUN_STATUSES, MAX_TASK_LIST_TEXT_CHARS, formatTaskListText, COLUMN_LABELS } from "@fusion/core"; import type { WorkflowIr } from "@fusion/core"; import { isGhAvailable, isGhAuthenticated, runGhJsonAsync } from "@fusion/core/gh-cli"; import { hasBuiltCoreDistBarrel } from "@fusion/test-utils"; @@ -2588,6 +2588,37 @@ describe("fn pi extension (runnable structured-output regression slice)", () => } }); + it("returns explicit text for empty active-column filters on a non-empty board", async () => { + const store = new TaskStore(tmpDir); + await store.init(); + try { + await store.createTask({ description: "Finished task keeps the board non-empty", column: "done" }); + } finally { + store.close(); + } + + const listTool = api.tools.get("fn_task_list")!; + for (const column of ["triage", "todo", "in-progress", "in-review"] as const) { + const result = await listTool.execute( + `empty-${column}`, + { column }, + undefined, + undefined, + makeCtx(tmpDir), + ); + const text = result.content[0].text; + + expect(result.content).toHaveLength(1); + expect(result.content[0].type).toBe("text"); + expect(result.content.some((block: any) => block.type === "image")).toBe(false); + expect(text).toBeTruthy(); + expect(text.trim()).not.toBe(""); + expect(text).toContain(COLUMN_LABELS[column]); + expect(text).toContain(column); + expect(result.details.count).toBe(1); + } + }); + it("keeps small column-filtered listings complete without the clamp marker", async () => { const store = new TaskStore(tmpDir); await store.init(); @@ -2611,10 +2642,13 @@ describe("fn pi extension (runnable structured-output regression slice)", () => expect(result.content).toHaveLength(1); expect(result.content[0].type).toBe("text"); expect(result.content.some((block: any) => block.type === "image")).toBe(false); + expect(text).toBeTruthy(); + expect(text.trim()).not.toBe(""); expect(text).toContain("Todo (2):"); expect(text).toContain("FN-001"); expect(text).toContain("FN-002"); expect(text).toContain("[deps: FN-001]"); + expect(text).not.toContain("No tasks in Todo (todo)."); expect(text).not.toContain("truncated to fit; narrow with column/limit"); expect(result.details.count).toBe(2); }); diff --git a/packages/cli/src/extension.ts b/packages/cli/src/extension.ts index 67e273f983..71981c957c 100644 --- a/packages/cli/src/extension.ts +++ b/packages/cli/src/extension.ts @@ -854,9 +854,10 @@ export default function kbExtension(pi: ExtensionAPI) { } const perColumn = params.limit ?? 10; + const requestedColumn = params.column as ColumnId | undefined; const lines: string[] = []; for (const col of COLUMNS) { - if (params.column && params.column !== col) continue; + if (requestedColumn && requestedColumn !== col) continue; const colTasks = tasks.filter((t) => t.column === col); if (colTasks.length === 0) continue; @@ -873,6 +874,10 @@ export default function kbExtension(pi: ExtensionAPI) { lines.push(""); } + const emptyStateText = requestedColumn + ? `No tasks in ${columnLabel(requestedColumn)} (${requestedColumn}).` + : "No matching tasks."; + /* FNXC:TaskListOutput 2026-06-16-17:47: FN-6492 routes CLI fn_task_list through the shared clamp so large column-filtered board reads remain text-only instead of being converted to host attachments. @@ -882,10 +887,16 @@ export default function kbExtension(pi: ExtensionAPI) { FNXC:TaskListOutput 2026-06-17-07:25: FN-6573 requires CLI fn_task_list to resolve formatTaskListText from the runtime @fusion/core namespace with a typeof guard and a self-contained bounded fallback. A stale @fusion/core dist missing the FN-6570 formatter export crashed ambient heartbeat agents as `(0 , _core.formatTaskListText) is not a function`; the tool must now return bounded text instead. + + FNXC:TaskListOutput 2026-06-18-04:46: + FN-6630 refines FN-6492 by requiring filtered fn_task_list calls against empty target columns to return explicit empty-state text. Host runtimes can imageify empty content blocks as `(see attached image)`, so this call site must never emit empty or whitespace-only text. */ const formatter = resolveTaskListFormatter(fusionCore); + const text = lines.length === 0 + ? emptyStateText + : formatter(lines, { clamp: fusionCore.clampTaskListText }).trimEnd(); return { - content: [{ type: "text", text: formatter(lines, { clamp: fusionCore.clampTaskListText }).trimEnd() }], + content: [{ type: "text", text: text.trim().length > 0 ? text : emptyStateText }], details: { count: tasks.length }, }; },