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
This commit is contained in:
5
.changeset/fn-6630-task-list-empty-column.md
Normal file
5
.changeset/fn-6630-task-list-empty-column.md
Normal file
@@ -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.
|
||||
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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 },
|
||||
};
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user