fix(FN-1024): fix done-task changes tab overcounting and add diff fallback
- Fix TaskChangesTab overcounting changes for done tasks by handling missing commit SHA gracefully - Add /api/tasks/:id/diff endpoint with fallback for done tasks without merge commit info - Add comprehensive tests for both TaskChangesTab component and diff route edge cases - Document done-task Changes tab fallback behavior in README
This commit is contained in:
@@ -51,7 +51,7 @@ AI-guided interactive planning for creating well-specified tasks from high-level
|
||||
- **Layered Model Dropdowns**: Shared model combobox menus render in a top-level portal attached to `document.body`, so they stay above board columns and scrollable modal content instead of being clipped behind surrounding dashboard surfaces. The dropdown constrains horizontal overflow — long model IDs and provider labels truncate with ellipsis rather than creating sideways scrolling, keeping the menu usable on smaller viewports. On mobile viewports (≤640px), the quick-entry Models menu widens to fill the viewport (minus side padding) and remains viewport-clamped for comfortable model selection without horizontal crowding.
|
||||
- **Fuzzy Model Search**: Model dropdown search (`filterModels`) supports fuzzy matching so users can find models despite minor typing imperfections. Three matching strategies are applied in order (first match wins): (1) **separator-insensitive substring** — hyphens, underscores, dots, and slashes are stripped before comparison, so `gpt4o` finds `gpt-4o`; (2) **subsequence matching** (≥ 3 chars) — characters must appear in order within a single token but need not be contiguous, so `cld` finds `claude`; (3) **typo tolerance** (≥ 4 chars) — Damerau-Levenshtein edit distance ≤ 1 supports single-character insertion, deletion, substitution, and adjacent transposition, so `sonet` finds `sonnet`. Multi-term space-separated queries use AND logic. Result ordering is stable (input-array order, no score re-sorting). Exact and substring matches from the original implementation continue to work unchanged.
|
||||
- **Bulk Model Editing**: Update AI model configuration for multiple tasks at once in the list view. Select tasks via checkboxes (archived tasks excluded), then use the "Bulk Edit Models" toolbar to apply executor and/or validator model changes to all selected tasks. Selection persists in localStorage across page reloads.
|
||||
- **Task Details**: View full task specifications, agent logs, and attachments. The task detail modal uses a top-level tab bar with the following tabs: **Definition**, **Logs**, **Changes** (for in-progress/in-review/done tasks), **Commits** (for done tasks with `mergeDetails.commitSha`), **Comments**, **Model**, and **Workflow** (when workflow steps are configured or the task has previous workflow results). **Activity** and **Agent Log** are subviews within the unified **Logs** tab — click Logs, then toggle between Activity (task lifecycle events, default) and Agent Log (live agent output). The Agent Log subview expands to fill the full modal body height above the action bar, providing maximum vertical space for watching live agent output. The Agent Log header shows the effective executor, validator, and planning/triage model names resolved from task-level overrides or project/global settings fallbacks, matching the same resolution order the engine uses at runtime. A **Markdown/Plain toggle** in the Agent Log header switches between formatted markdown rendering (default) and literal plain-text display — useful for debugging raw agent output, checking escaped markdown syntax, or inspecting exactly what the agent emitted without formatting. The toggle applies to `text` and `thinking` entries only; tool entries always render as plain text. React-markdown handles sanitization in markdown mode (no raw HTML is executed); plain-text mode uses React's built-in text escaping for safe literal output. The refinement modal positions the "Create Refinement Task" button adjacent to the feedback textarea alongside the character count, creating a tight input group that connects the submit action directly to the text being edited. The **Changes** tab for done tasks loads the diff from the recorded merge commit (`mergeDetails.commitSha`) via `fetchCommitDiff` rather than requiring a live worktree — changes remain visible even after the worktree is cleaned up. The tab shows commit metadata (short SHA, merge commit message, merged timestamp) alongside the file-level diff. The header displays "Files Changed (N)" as the primary title with additions/deletions totals on a second line below, freeing horizontal space for navigation and action controls. In-progress and in-review tasks continue to use the worktree-based diff path. Changed-file status indicators (added, modified, deleted, unknown) use semantic CSS classes and theme-aware color variables, ensuring readable contrast across all dashboard themes and light/dark modes.
|
||||
- **Task Details**: View full task specifications, agent logs, and attachments. The task detail modal uses a top-level tab bar with the following tabs: **Definition**, **Logs**, **Changes** (for in-progress/in-review/done tasks), **Commits** (for done tasks with `mergeDetails.commitSha`), **Comments**, **Model**, and **Workflow** (when workflow steps are configured or the task has previous workflow results). **Activity** and **Agent Log** are subviews within the unified **Logs** tab — click Logs, then toggle between Activity (task lifecycle events, default) and Agent Log (live agent output). The Agent Log subview expands to fill the full modal body height above the action bar, providing maximum vertical space for watching live agent output. The Agent Log header shows the effective executor, validator, and planning/triage model names resolved from task-level overrides or project/global settings fallbacks, matching the same resolution order the engine uses at runtime. A **Markdown/Plain toggle** in the Agent Log header switches between formatted markdown rendering (default) and literal plain-text display — useful for debugging raw agent output, checking escaped markdown syntax, or inspecting exactly what the agent emitted without formatting. The toggle applies to `text` and `thinking` entries only; tool entries always render as plain text. React-markdown handles sanitization in markdown mode (no raw HTML is executed); plain-text mode uses React's built-in text escaping for safe literal output. The refinement modal positions the "Create Refinement Task" button adjacent to the feedback textarea alongside the character count, creating a tight input group that connects the submit action directly to the text being edited. The **Changes** tab for done tasks loads the diff from the recorded merge commit (`mergeDetails.commitSha`) via the `/api/tasks/:id/diff` endpoint rather than requiring a live worktree — changes remain visible even after the worktree is cleaned up. Done tasks **without** a recorded `commitSha` do not attempt to fetch a detailed file diff; instead, the tab shows a safe summary fallback displaying the merge summary numbers (`filesChanged`, `insertions`, `deletions`) from `mergeDetails`. This prevents inflated file lists that would result from a repository-wide fallback diff scan. The tab shows commit metadata (short SHA, merge commit message, merged timestamp) alongside the file-level diff when a commit SHA is available. The header displays "Files Changed (N)" as the primary title with additions/deletions totals on a second line below, freeing horizontal space for navigation and action controls. In-progress and in-review tasks continue to use the worktree-based diff path. Changed-file status indicators (added, modified, deleted, unknown) use semantic CSS classes and theme-aware color variables, ensuring readable contrast across all dashboard themes and light/dark modes.
|
||||
- **Changed Files Viewer**: Click a task card's "files changed" button to open a dedicated diff viewer showing only files changed in that task worktree, with per-file statuses and sidebar navigation. The sidebar file list uses dedicated `changed-files-entry` styling with explicit button resets (no browser-default background/border/font inheritance) and theme-variable-driven colors for text, icons, hover, active, and focus states — ensuring correct rendering across both dark and light modes and all color themes. On mobile (≤768px), the viewer switches to a single-pane flow: the file list and diff are shown one at a time with a back button for navigation between them. The viewer always opens to the file list on mobile, and only switches to the diff view when the user taps a specific file. Pressing Escape on the diff view returns to the file list first; pressing Escape again closes the modal. Loading, error, and empty states use theme-aware styling (including light mode). Diff syntax highlighting (additions, deletions, hunks) adapts to the active theme for correct contrast. Status badges in the sidebar and diff toolbar use semantic CSS classes (e.g., `changed-files-badge--added`) with theme-aware colors for consistent readability across all themes. The board card file count and the changed-files viewer always agree — both use a shared diff-base resolution strategy. When the task has a `baseCommitSha` (captured at worktree creation time) that is still a valid ancestor of the current HEAD, the diff is scoped to only files introduced by that specific task. If `baseCommitSha` is stale or unavailable, the system falls back to a branch merge-base, then to `HEAD~1`. This ensures accurate file counts in shared or recycled worktree scenarios where a broader merge-base would include files from previous tasks.
|
||||
- **Task Detail Changes Tab**: The task detail modal's "Changes" tab (TaskChangesTab) uses a compact spacing treatment for file detail rows. The file list container has a `task-changes-file-list--compact` modifier class that tightens padding and gaps on file headers, stat badges, and list item spacing compared to the shared base `.changes-file-*` styles. This compact treatment is scoped to TaskChangesTab only and does not affect CommitDiffTab or other diff surfaces.
|
||||
- **GitHub Import**: Import issues directly from GitHub repositories
|
||||
|
||||
@@ -43,6 +43,12 @@ interface NormalizedFile {
|
||||
* For done tasks with a recorded merge commit (mergeDetails.commitSha) it loads
|
||||
* the diff from git history instead, so changes remain visible even after the
|
||||
* worktree is cleaned up.
|
||||
*
|
||||
* For done tasks WITHOUT a recorded commit SHA, the tab shows a safe summary
|
||||
* fallback using the merge details numbers (filesChanged/insertions/deletions)
|
||||
* rather than fetching a detailed diff that could include unrelated repository
|
||||
* changes. This prevents inflated file counts that don't match the card-level
|
||||
* display.
|
||||
*/
|
||||
export function TaskChangesTab({ taskId, worktree, projectId, column, mergeDetails }: TaskChangesTabProps) {
|
||||
const [files, setFiles] = useState<NormalizedFile[]>([]);
|
||||
@@ -54,7 +60,12 @@ export function TaskChangesTab({ taskId, worktree, projectId, column, mergeDetai
|
||||
const [wordWrap, setWordWrap] = useState(true);
|
||||
const [expandedViewOpen, setExpandedViewOpen] = useState(false);
|
||||
|
||||
const canLoad = column === "in-progress" || column === "in-review" || column === "done";
|
||||
const isDone = column === "done";
|
||||
const isDoneWithCommit = isDone && Boolean(mergeDetails?.commitSha);
|
||||
|
||||
// Done tasks without commit SHA must not fetch detailed diffs — the server
|
||||
// would fall back to a repository-wide scan that inflates the file list.
|
||||
const canLoad = (column === "in-progress" || column === "in-review") || isDoneWithCommit;
|
||||
|
||||
const loadDiff = useCallback(async () => {
|
||||
if (!canLoad) {
|
||||
@@ -140,8 +151,6 @@ export function TaskChangesTab({ taskId, worktree, projectId, column, mergeDetai
|
||||
);
|
||||
}
|
||||
|
||||
const isDone = column === "done";
|
||||
|
||||
// Non-done task without a worktree → show worktree empty state
|
||||
if (!isDone && !worktree) {
|
||||
return (
|
||||
@@ -157,6 +166,30 @@ export function TaskChangesTab({ taskId, worktree, projectId, column, mergeDetai
|
||||
);
|
||||
}
|
||||
|
||||
// Done task without commit SHA → show safe summary fallback.
|
||||
// We must NOT fetch detailed diffs here because the server would fall back
|
||||
// to a repository-wide scan, producing an inflated/unrelated file list.
|
||||
if (isDone && !isDoneWithCommit) {
|
||||
const summaryFiles = mergeDetails?.filesChanged;
|
||||
const summaryAdditions = mergeDetails?.insertions;
|
||||
const summaryDeletions = mergeDetails?.deletions;
|
||||
const hasSummary = summaryFiles != null || summaryAdditions != null || summaryDeletions != null;
|
||||
|
||||
return (
|
||||
<div className="detail-section">
|
||||
<div className="task-changes-state task-changes-state--empty">
|
||||
<FileCode size={24} />
|
||||
<p>Detailed file changes unavailable.</p>
|
||||
<span className="task-changes-state-hint">
|
||||
{hasSummary
|
||||
? `Merge summary: ${summaryFiles ?? 0} file${(summaryFiles ?? 0) === 1 ? "" : "s"} changed, +${summaryAdditions ?? 0} additions, -${summaryDeletions ?? 0} deletions.`
|
||||
: "No merge commit was recorded for this task."}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (files.length === 0) {
|
||||
return (
|
||||
<div className="detail-section">
|
||||
|
||||
@@ -387,9 +387,7 @@ describe("TaskChangesTab — regression: non-done tasks still use worktree path"
|
||||
expect(mockFetchTaskDiff).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("done task without commitSha calls fetchTaskDiff (server handles it)", async () => {
|
||||
mockFetchTaskDiff.mockResolvedValue({ files: [], stats: { filesChanged: 0, additions: 0, deletions: 0 } });
|
||||
|
||||
it("done task without commitSha does NOT call fetchTaskDiff — shows summary fallback", async () => {
|
||||
render(
|
||||
<TaskChangesTab
|
||||
taskId="FN-001"
|
||||
@@ -400,8 +398,60 @@ describe("TaskChangesTab — regression: non-done tasks still use worktree path"
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockFetchTaskDiff).toHaveBeenCalledWith("FN-001", undefined, undefined);
|
||||
expect(screen.getByText("Detailed file changes unavailable.")).toBeTruthy();
|
||||
});
|
||||
expect(screen.getByText("No merge commit was recorded for this task.")).toBeTruthy();
|
||||
// Must NOT have called fetchTaskDiff — that would trigger repo-wide fallback
|
||||
expect(mockFetchTaskDiff).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("done task without commitSha shows merge summary when available", async () => {
|
||||
render(
|
||||
<TaskChangesTab
|
||||
taskId="FN-001"
|
||||
worktree={undefined}
|
||||
column="done"
|
||||
mergeDetails={{ filesChanged: 3, insertions: 10, deletions: 2 }}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Detailed file changes unavailable.")).toBeTruthy();
|
||||
});
|
||||
expect(screen.getByText("Merge summary: 3 files changed, +10 additions, -2 deletions.")).toBeTruthy();
|
||||
expect(mockFetchTaskDiff).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("done task without commitSha shows singular 'file' when filesChanged is 1", async () => {
|
||||
render(
|
||||
<TaskChangesTab
|
||||
taskId="FN-001"
|
||||
worktree={undefined}
|
||||
column="done"
|
||||
mergeDetails={{ filesChanged: 1, insertions: 5, deletions: 0 }}
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Detailed file changes unavailable.")).toBeTruthy();
|
||||
});
|
||||
expect(screen.getByText("Merge summary: 1 file changed, +5 additions, -0 deletions.")).toBeTruthy();
|
||||
});
|
||||
|
||||
it("done task without commitSha and no mergeDetails shows fallback without summary", async () => {
|
||||
render(
|
||||
<TaskChangesTab
|
||||
taskId="FN-001"
|
||||
worktree={undefined}
|
||||
column="done"
|
||||
/>,
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Detailed file changes unavailable.")).toBeTruthy();
|
||||
});
|
||||
expect(screen.getByText("No merge commit was recorded for this task.")).toBeTruthy();
|
||||
expect(mockFetchTaskDiff).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -4042,6 +4042,204 @@ describe("POST /tasks/:id/reject-plan", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// --- Task diff route tests ---
|
||||
|
||||
describe("GET /tasks/:id/diff", () => {
|
||||
let store: TaskStore;
|
||||
|
||||
beforeEach(() => {
|
||||
store = createMockStore();
|
||||
});
|
||||
|
||||
function buildApp() {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use("/api", createApiRoutes(store));
|
||||
return app;
|
||||
}
|
||||
|
||||
it("returns 404 when task not found", async () => {
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(undefined);
|
||||
|
||||
const res = await GET(buildApp(), "/api/tasks/FN-999/diff");
|
||||
|
||||
expect(res.status).toBe(404);
|
||||
expect(res.body.error).toContain("Task not found");
|
||||
});
|
||||
|
||||
describe("done tasks without commit SHA", () => {
|
||||
it("returns safe empty file list with merge summary stats", async () => {
|
||||
const doneTask = {
|
||||
...FAKE_TASK_DETAIL,
|
||||
id: "FN-001",
|
||||
column: "done",
|
||||
mergeDetails: {
|
||||
filesChanged: 3,
|
||||
insertions: 10,
|
||||
deletions: 2,
|
||||
},
|
||||
};
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(doneTask);
|
||||
|
||||
const res = await GET(buildApp(), "/api/tasks/FN-001/diff");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.files).toEqual([]);
|
||||
expect(res.body.stats).toEqual({
|
||||
filesChanged: 3,
|
||||
additions: 10,
|
||||
deletions: 2,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns zeros when mergeDetails has no summary numbers", async () => {
|
||||
const doneTask = {
|
||||
...FAKE_TASK_DETAIL,
|
||||
id: "FN-001",
|
||||
column: "done",
|
||||
mergeDetails: {},
|
||||
};
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(doneTask);
|
||||
|
||||
const res = await GET(buildApp(), "/api/tasks/FN-001/diff");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.files).toEqual([]);
|
||||
expect(res.body.stats).toEqual({
|
||||
filesChanged: 0,
|
||||
additions: 0,
|
||||
deletions: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("returns zeros when mergeDetails is undefined", async () => {
|
||||
const doneTask = {
|
||||
...FAKE_TASK_DETAIL,
|
||||
id: "FN-001",
|
||||
column: "done",
|
||||
mergeDetails: undefined,
|
||||
};
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(doneTask);
|
||||
|
||||
const res = await GET(buildApp(), "/api/tasks/FN-001/diff");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body.files).toEqual([]);
|
||||
expect(res.body.stats).toEqual({
|
||||
filesChanged: 0,
|
||||
additions: 0,
|
||||
deletions: 0,
|
||||
});
|
||||
});
|
||||
|
||||
it("response is schema-compatible with TaskDiff type", async () => {
|
||||
const doneTask = {
|
||||
...FAKE_TASK_DETAIL,
|
||||
id: "FN-001",
|
||||
column: "done",
|
||||
mergeDetails: { filesChanged: 5, insertions: 20, deletions: 3 },
|
||||
};
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(doneTask);
|
||||
|
||||
const res = await GET(buildApp(), "/api/tasks/FN-001/diff");
|
||||
|
||||
// Must have both `files` array and `stats` object
|
||||
expect(res.status).toBe(200);
|
||||
expect(Array.isArray(res.body.files)).toBe(true);
|
||||
expect(res.body.stats).toHaveProperty("filesChanged");
|
||||
expect(res.body.stats).toHaveProperty("additions");
|
||||
expect(res.body.stats).toHaveProperty("deletions");
|
||||
});
|
||||
});
|
||||
|
||||
describe("done tasks with commit SHA", () => {
|
||||
it("attempts git diff when commitSha is present", async () => {
|
||||
// Use a real git repo to test the commit-backed path
|
||||
const testDir = mkdtempSync(join(tmpdir(), "kb-diff-test-"));
|
||||
try {
|
||||
execFileSync("git", ["init", testDir]);
|
||||
execFileSync("git", ["-C", testDir, "config", "user.email", "test@test.com"]);
|
||||
execFileSync("git", ["-C", testDir, "config", "user.name", "Test"]);
|
||||
writeFileSync(join(testDir, "a.txt"), "initial\n");
|
||||
execFileSync("git", ["-C", testDir, "add", "a.txt"]);
|
||||
execFileSync("git", ["-C", testDir, "commit", "-m", "init"]);
|
||||
|
||||
const headSha = execFileSync("git", ["-C", testDir, "rev-parse", "HEAD"], { encoding: "utf-8" }).trim();
|
||||
|
||||
const localStore = createMockStore({
|
||||
getRootDir: vi.fn().mockReturnValue(testDir),
|
||||
});
|
||||
const doneTask = {
|
||||
...FAKE_TASK_DETAIL,
|
||||
id: "FN-001",
|
||||
column: "done",
|
||||
mergeDetails: { commitSha: headSha },
|
||||
};
|
||||
(localStore.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(doneTask);
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use("/api", createApiRoutes(localStore));
|
||||
|
||||
const res = await GET(app, "/api/tasks/FN-001/diff");
|
||||
expect(res.status).toBe(200);
|
||||
// The diff should be schema-compatible even if it returns empty
|
||||
expect(Array.isArray(res.body.files)).toBe(true);
|
||||
expect(res.body.stats).toHaveProperty("filesChanged");
|
||||
} finally {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /tasks/:id/file-diffs", () => {
|
||||
let store: TaskStore;
|
||||
|
||||
beforeEach(() => {
|
||||
store = createMockStore();
|
||||
});
|
||||
|
||||
function buildApp() {
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.use("/api", createApiRoutes(store));
|
||||
return app;
|
||||
}
|
||||
|
||||
describe("done tasks without commit SHA", () => {
|
||||
it("returns empty array instead of scanning repository", async () => {
|
||||
const doneTask = {
|
||||
...FAKE_TASK_DETAIL,
|
||||
id: "FN-001",
|
||||
column: "done",
|
||||
mergeDetails: { filesChanged: 3 },
|
||||
};
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(doneTask);
|
||||
|
||||
const res = await GET(buildApp(), "/api/tasks/FN-001/file-diffs");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual([]);
|
||||
});
|
||||
|
||||
it("returns empty array when mergeDetails is undefined", async () => {
|
||||
const doneTask = {
|
||||
...FAKE_TASK_DETAIL,
|
||||
id: "FN-001",
|
||||
column: "done",
|
||||
mergeDetails: undefined,
|
||||
};
|
||||
(store.getTask as ReturnType<typeof vi.fn>).mockResolvedValue(doneTask);
|
||||
|
||||
const res = await GET(buildApp(), "/api/tasks/FN-001/file-diffs");
|
||||
|
||||
expect(res.status).toBe(200);
|
||||
expect(res.body).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// --- Git Management route tests ---
|
||||
// These are integration tests that run against the actual git repository
|
||||
|
||||
|
||||
@@ -7857,6 +7857,22 @@ Output ONLY the prompt text (no markdown, no explanations).`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Done tasks without a commit SHA: return safe, deterministic response.
|
||||
// Do NOT fall through to the worktree-based diff logic, which would use
|
||||
// the repo root as cwd and return an inflated repository-wide diff.
|
||||
if (task.column === "done") {
|
||||
const md = task.mergeDetails;
|
||||
res.json({
|
||||
files: [],
|
||||
stats: {
|
||||
filesChanged: md?.filesChanged ?? 0,
|
||||
additions: md?.insertions ?? 0,
|
||||
deletions: md?.deletions ?? 0,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const worktree = typeof req.query.worktree === "string" ? req.query.worktree : undefined;
|
||||
const cwd = worktree || task.worktree || scopedStore.getRootDir();
|
||||
|
||||
@@ -8024,6 +8040,14 @@ Output ONLY the prompt text (no markdown, no explanations).`;
|
||||
return;
|
||||
}
|
||||
|
||||
// Done tasks without a commit SHA: return safe, empty response.
|
||||
// Do NOT fall through to worktree-based logic that could scan the
|
||||
// entire repository when the worktree has been cleaned up.
|
||||
if (task.column === "done") {
|
||||
res.json([]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!task.worktree || !nodeFs.existsSync(task.worktree)) {
|
||||
res.json([]);
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user