feat(FN-3350): tokenize task detail danger/error styling
Fixes task detail modal danger/error styling by switching to token-based CSS variables for consistent theming across dark and light modes. Fusion-Task-Id: FN-3350
This commit is contained in:
@@ -155,7 +155,7 @@ describe("Database", () => {
|
||||
});
|
||||
|
||||
it("seeds schema version", () => {
|
||||
expect(db.getSchemaVersion()).toBe(59);
|
||||
expect(db.getSchemaVersion()).toBe(60);
|
||||
});
|
||||
|
||||
it("seeds lastModified", () => {
|
||||
@@ -178,7 +178,7 @@ describe("Database", () => {
|
||||
|
||||
it("is idempotent - calling init() twice does not fail", () => {
|
||||
expect(() => db.init()).not.toThrow();
|
||||
expect(db.getSchemaVersion()).toBe(59);
|
||||
expect(db.getSchemaVersion()).toBe(60);
|
||||
});
|
||||
|
||||
it("does not overwrite existing config on re-init", () => {
|
||||
@@ -899,7 +899,7 @@ describe("schema migrations", () => {
|
||||
db.init();
|
||||
|
||||
// Verify version bumped to 29 (includes v1→v2 through v26→v29)
|
||||
expect(db.getSchemaVersion()).toBe(59);
|
||||
expect(db.getSchemaVersion()).toBe(60);
|
||||
|
||||
// Verify new columns exist and existing data is intact
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
@@ -924,11 +924,11 @@ describe("schema migrations", () => {
|
||||
const db = new Database(fusionDir);
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(59);
|
||||
expect(db.getSchemaVersion()).toBe(60);
|
||||
|
||||
// Re-init should not fail
|
||||
db.init();
|
||||
expect(db.getSchemaVersion()).toBe(59);
|
||||
expect(db.getSchemaVersion()).toBe(60);
|
||||
|
||||
db.close();
|
||||
});
|
||||
@@ -963,7 +963,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(59);
|
||||
expect(db.getSchemaVersion()).toBe(60);
|
||||
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
expect(cols.map((col) => col.name)).toContain("priority");
|
||||
@@ -1004,7 +1004,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(59);
|
||||
expect(db.getSchemaVersion()).toBe(60);
|
||||
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
const colNames = cols.map((col) => col.name);
|
||||
@@ -1073,7 +1073,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(59);
|
||||
expect(db.getSchemaVersion()).toBe(60);
|
||||
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
const colNames = cols.map((col) => col.name);
|
||||
@@ -1176,7 +1176,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(59);
|
||||
expect(db.getSchemaVersion()).toBe(60);
|
||||
|
||||
const cols = db.prepare("PRAGMA table_info(chat_messages)").all() as Array<{ name: string }>;
|
||||
expect(cols.map((col) => col.name)).toContain("attachments");
|
||||
@@ -1250,7 +1250,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(59);
|
||||
expect(db.getSchemaVersion()).toBe(60);
|
||||
|
||||
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'agentRatings'").all() as Array<{ name: string }>;
|
||||
expect(tables).toEqual([{ name: "agentRatings" }]);
|
||||
@@ -1274,7 +1274,7 @@ describe("schema migrations", () => {
|
||||
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(59);
|
||||
expect(db.getSchemaVersion()).toBe(60);
|
||||
|
||||
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = 'mission_events'").all() as Array<{ name: string }>;
|
||||
expect(tables).toEqual([{ name: "mission_events" }]);
|
||||
@@ -1378,7 +1378,7 @@ describe("schema migrations", () => {
|
||||
db.init();
|
||||
|
||||
// Verify version bumped to 29
|
||||
expect(db.getSchemaVersion()).toBe(59);
|
||||
expect(db.getSchemaVersion()).toBe(60);
|
||||
|
||||
// Verify new columns exist and existing data is intact
|
||||
const cols = db.prepare("PRAGMA table_info(tasks)").all() as Array<{ name: string }>;
|
||||
@@ -1847,7 +1847,7 @@ describe("createDatabase factory", () => {
|
||||
const db = createDatabase(fusionDir);
|
||||
db.init();
|
||||
|
||||
expect(db.getSchemaVersion()).toBe(59);
|
||||
expect(db.getSchemaVersion()).toBe(60);
|
||||
expect(db.getLastModified()).toBeGreaterThan(0);
|
||||
|
||||
db.close();
|
||||
|
||||
@@ -869,7 +869,7 @@ describe("Migration: pre-33 DB upgrade", () => {
|
||||
// Step 1: Create a fresh database at v33 (runs all migrations up to 33)
|
||||
const db1 = createDatabase(legacyDir);
|
||||
db1.init();
|
||||
expect(db1.getSchemaVersion()).toBe(59);
|
||||
expect(db1.getSchemaVersion()).toBe(60);
|
||||
db1.close();
|
||||
|
||||
// Step 2: Manually downgrade to version 32 and drop insight tables
|
||||
@@ -904,7 +904,7 @@ describe("Migration: pre-33 DB upgrade", () => {
|
||||
expect(tableNamesBefore).not.toContain("project_insight_runs");
|
||||
// Now run init — this triggers the v32→v33 migration
|
||||
db3.init();
|
||||
expect(db3.getSchemaVersion()).toBe(59);
|
||||
expect(db3.getSchemaVersion()).toBe(60);
|
||||
|
||||
// Step 4: Verify insight tables exist after migration
|
||||
const tablesAfter = db3.prepare(
|
||||
@@ -935,12 +935,12 @@ describe("Migration: pre-33 DB upgrade", () => {
|
||||
try {
|
||||
const db1 = createDatabase(testDir);
|
||||
db1.init();
|
||||
expect(db1.getSchemaVersion()).toBe(59);
|
||||
expect(db1.getSchemaVersion()).toBe(60);
|
||||
db1.close();
|
||||
|
||||
const db2 = createDatabase(testDir);
|
||||
expect(() => db2.init()).not.toThrow();
|
||||
expect(db2.getSchemaVersion()).toBe(59);
|
||||
expect(db2.getSchemaVersion()).toBe(60);
|
||||
db2.close();
|
||||
} finally {
|
||||
rmSync(testDir, { recursive: true, force: true });
|
||||
|
||||
@@ -2629,7 +2629,7 @@ describe("MissionStore", () => {
|
||||
|
||||
describe("Loop State & Validator Run Schema (v31)", () => {
|
||||
it("schema version is 40 after migration", () => {
|
||||
expect(db.getSchemaVersion()).toBe(59);
|
||||
expect(db.getSchemaVersion()).toBe(60);
|
||||
});
|
||||
|
||||
it("mission_features table has loop state columns", () => {
|
||||
|
||||
@@ -742,7 +742,7 @@ describe("RoadmapStore", () => {
|
||||
|
||||
describe("schema version", () => {
|
||||
it("schema version is 40 after init", () => {
|
||||
expect(db.getSchemaVersion()).toBe(59);
|
||||
expect(db.getSchemaVersion()).toBe(60);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -465,7 +465,7 @@ describe("Run Audit", () => {
|
||||
});
|
||||
|
||||
it("schema version is bumped to 40", () => {
|
||||
expect(db.getSchemaVersion()).toBe(59);
|
||||
expect(db.getSchemaVersion()).toBe(60);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -480,6 +480,33 @@ describe("TaskStore", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("pausedByAgentId persistence", () => {
|
||||
it("creates and lists a task with pausedByAgentId", async () => {
|
||||
const task = await store.createTask({ description: "Agent paused task" });
|
||||
const updated = await store.updateTask(task.id, { pausedByAgentId: "agent-1" });
|
||||
|
||||
expect(updated.pausedByAgentId).toBe("agent-1");
|
||||
|
||||
const detail = await store.getTask(task.id);
|
||||
expect(detail.pausedByAgentId).toBe("agent-1");
|
||||
|
||||
const tasks = await store.listTasks();
|
||||
const listed = tasks.find((t) => t.id === task.id);
|
||||
expect(listed?.pausedByAgentId).toBe("agent-1");
|
||||
});
|
||||
|
||||
it("clears pausedByAgentId with null via updateTask", async () => {
|
||||
const task = await store.createTask({ description: "Clear agent pause marker" });
|
||||
await store.updateTask(task.id, { pausedByAgentId: "agent-2" });
|
||||
|
||||
const cleared = await store.updateTask(task.id, { pausedByAgentId: null });
|
||||
expect(cleared.pausedByAgentId).toBeUndefined();
|
||||
|
||||
const detail = await store.getTask(task.id);
|
||||
expect(detail.pausedByAgentId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("nodeId persistence", () => {
|
||||
it("creates a task with nodeId when provided", async () => {
|
||||
const task = await store.createTask({
|
||||
@@ -592,6 +619,36 @@ describe("TaskStore", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("getTasksByAssignedAgent", () => {
|
||||
it("returns only tasks assigned to the requested agent", async () => {
|
||||
const mine = await store.createTask({ description: "mine", assignedAgentId: "agent-1" });
|
||||
await store.createTask({ description: "other", assignedAgentId: "agent-2" });
|
||||
await store.createTask({ description: "unassigned" });
|
||||
|
||||
const tasks = await store.getTasksByAssignedAgent("agent-1");
|
||||
expect(tasks.map((task) => task.id)).toEqual([mine.id]);
|
||||
});
|
||||
|
||||
it("supports pausedOnly filter", async () => {
|
||||
const paused = await store.createTask({ description: "paused", assignedAgentId: "agent-1" });
|
||||
const active = await store.createTask({ description: "active", assignedAgentId: "agent-1" });
|
||||
await store.updateTask(paused.id, { paused: true });
|
||||
|
||||
const tasks = await store.getTasksByAssignedAgent("agent-1", { pausedOnly: true });
|
||||
expect(tasks.map((task) => task.id)).toEqual([paused.id]);
|
||||
expect(tasks.some((task) => task.id === active.id)).toBe(false);
|
||||
});
|
||||
|
||||
it("supports excludeArchived filter", async () => {
|
||||
const active = await store.createTask({ description: "active", assignedAgentId: "agent-1" });
|
||||
const archived = await store.createTask({ description: "archived", assignedAgentId: "agent-1", column: "done" });
|
||||
await store.archiveTask(archived.id, false);
|
||||
|
||||
const tasks = await store.getTasksByAssignedAgent("agent-1", { excludeArchived: true });
|
||||
expect(tasks.map((task) => task.id)).toEqual([active.id]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectNextTaskForAgent", () => {
|
||||
it("returns null when no tasks exist", async () => {
|
||||
await expect(store.selectNextTaskForAgent("agent-1")).resolves.toBeNull();
|
||||
@@ -3546,6 +3603,39 @@ describe("TaskStore", () => {
|
||||
fetched = await store.getTask(task.id);
|
||||
expect(fetched.paused).toBe(true);
|
||||
});
|
||||
|
||||
it("sets pausedByAgentId and logs agent pause reason", async () => {
|
||||
const task = await createTestTask();
|
||||
const paused = await store.pauseTask(task.id, true, undefined, { pausedByAgentId: "agent-1" });
|
||||
|
||||
expect(paused.pausedByAgentId).toBe("agent-1");
|
||||
expect(paused.log.at(-1)?.action).toBe("Task paused (agent agent-1 paused)");
|
||||
});
|
||||
|
||||
it("clears pausedByAgentId and logs agent resume reason", async () => {
|
||||
const task = await createTestTask();
|
||||
await store.pauseTask(task.id, true, undefined, { pausedByAgentId: "agent-2" });
|
||||
|
||||
const unpaused = await store.pauseTask(task.id, false);
|
||||
expect(unpaused.pausedByAgentId).toBeUndefined();
|
||||
expect(unpaused.log.at(-1)?.action).toBe("Task unpaused (agent agent-2 resumed)");
|
||||
});
|
||||
|
||||
it("uses standard unpause log when task was not paused by an agent", async () => {
|
||||
const task = await createTestTask();
|
||||
await store.pauseTask(task.id, true);
|
||||
|
||||
const unpaused = await store.pauseTask(task.id, false);
|
||||
expect(unpaused.pausedByAgentId).toBeUndefined();
|
||||
expect(unpaused.log.at(-1)?.action).toBe("Task unpaused");
|
||||
});
|
||||
|
||||
it("keeps pausedByAgentId undefined when pausing without agent options", async () => {
|
||||
const task = await createTestTask();
|
||||
const paused = await store.pauseTask(task.id, true);
|
||||
|
||||
expect(paused.pausedByAgentId).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateTask — paused", () => {
|
||||
|
||||
@@ -51,7 +51,7 @@ describe("TaskStore task documents", () => {
|
||||
|
||||
expect(tableNames.has("task_documents")).toBe(true);
|
||||
expect(tableNames.has("task_document_revisions")).toBe(true);
|
||||
expect(db.getSchemaVersion()).toBe(59);
|
||||
expect(db.getSchemaVersion()).toBe(60);
|
||||
|
||||
const index = db
|
||||
.prepare(
|
||||
|
||||
@@ -88,7 +88,7 @@ export function probeFts5(db: DatabaseSync): boolean {
|
||||
|
||||
// ── Schema Definition ────────────────────────────────────────────────
|
||||
|
||||
const SCHEMA_VERSION = 59;
|
||||
const SCHEMA_VERSION = 60;
|
||||
|
||||
function normalizeTaskComments(
|
||||
steeringComments: SteeringComment[] | undefined,
|
||||
@@ -215,6 +215,7 @@ CREATE TABLE IF NOT EXISTS tasks (
|
||||
missionId TEXT,
|
||||
sliceId TEXT,
|
||||
assignedAgentId TEXT,
|
||||
pausedByAgentId TEXT,
|
||||
assigneeUserId TEXT,
|
||||
sourceType TEXT,
|
||||
sourceAgentId TEXT,
|
||||
@@ -2350,6 +2351,13 @@ export class Database {
|
||||
});
|
||||
}
|
||||
|
||||
if (version < 60) {
|
||||
this.applyMigration(60, () => {
|
||||
this.addColumnIfMissing("tasks", "pausedByAgentId", "TEXT");
|
||||
this.db.exec(`CREATE INDEX IF NOT EXISTS idxTasksPausedByAgentId ON tasks(pausedByAgentId)`);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -95,6 +95,7 @@ interface TaskRow {
|
||||
missionId: string | null;
|
||||
sliceId: string | null;
|
||||
assignedAgentId: string | null;
|
||||
pausedByAgentId: string | null;
|
||||
assigneeUserId: string | null;
|
||||
nodeId: string | null;
|
||||
effectiveNodeId: string | null;
|
||||
@@ -728,6 +729,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
missionId: row.missionId || undefined,
|
||||
sliceId: row.sliceId || undefined,
|
||||
assignedAgentId: row.assignedAgentId || undefined,
|
||||
pausedByAgentId: row.pausedByAgentId || undefined,
|
||||
assigneeUserId: row.assigneeUserId || undefined,
|
||||
nodeId: row.nodeId || undefined,
|
||||
effectiveNodeId: row.effectiveNodeId || undefined,
|
||||
@@ -965,7 +967,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
"dependencies", "steps", "comments", "workflowStepResults", "steeringComments",
|
||||
"attachments", "prInfo", "issueInfo", "sourceIssueProvider", "sourceIssueRepository", "sourceIssueExternalIssueId", "sourceIssueNumber", "sourceIssueUrl", "mergeDetails",
|
||||
"breakIntoSubtasks", "enabledWorkflowSteps", "modifiedFiles",
|
||||
"missionId", "sliceId", "assignedAgentId", "assigneeUserId", "nodeId", "effectiveNodeId", "effectiveNodeSource",
|
||||
"missionId", "sliceId", "assignedAgentId", "pausedByAgentId", "assigneeUserId", "nodeId", "effectiveNodeId", "effectiveNodeSource",
|
||||
"sourceType", "sourceAgentId", "sourceRunId", "sourceSessionId", "sourceMessageId", "sourceParentTaskId", "sourceMetadata",
|
||||
"checkedOutBy", "checkedOutAt",
|
||||
// `log` is fetched in slim mode so the server can aggregate
|
||||
@@ -1014,7 +1016,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
"dependencies", "steps", "attachments", "steeringComments",
|
||||
"comments", "workflowStepResults", "prInfo", "issueInfo", "sourceIssueProvider", "sourceIssueRepository", "sourceIssueExternalIssueId", "sourceIssueNumber", "sourceIssueUrl", "mergeDetails",
|
||||
"breakIntoSubtasks", "enabledWorkflowSteps", "modifiedFiles",
|
||||
"missionId", "sliceId", "assignedAgentId", "assigneeUserId", "nodeId", "effectiveNodeId", "effectiveNodeSource",
|
||||
"missionId", "sliceId", "assignedAgentId", "pausedByAgentId", "assigneeUserId", "nodeId", "effectiveNodeId", "effectiveNodeSource",
|
||||
"sourceType", "sourceAgentId", "sourceRunId", "sourceSessionId", "sourceMessageId", "sourceParentTaskId", "sourceMetadata",
|
||||
"checkedOutBy", "checkedOutAt",
|
||||
];
|
||||
@@ -1057,9 +1059,9 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
dependencies, steps, log, attachments, steeringComments,
|
||||
comments, workflowStepResults, prInfo, issueInfo,
|
||||
sourceIssueProvider, sourceIssueRepository, sourceIssueExternalIssueId, sourceIssueNumber, sourceIssueUrl,
|
||||
mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt
|
||||
mergeDetails, breakIntoSubtasks, enabledWorkflowSteps, modifiedFiles, missionId, sliceId, assignedAgentId, pausedByAgentId, assigneeUserId, nodeId, effectiveNodeId, effectiveNodeSource, sourceType, sourceAgentId, sourceRunId, sourceSessionId, sourceMessageId, sourceParentTaskId, sourceMetadata, checkedOutBy, checkedOutAt
|
||||
) VALUES (
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?
|
||||
)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
title = excluded.title,
|
||||
@@ -1128,6 +1130,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
missionId = excluded.missionId,
|
||||
sliceId = excluded.sliceId,
|
||||
assignedAgentId = excluded.assignedAgentId,
|
||||
pausedByAgentId = excluded.pausedByAgentId,
|
||||
assigneeUserId = excluded.assigneeUserId,
|
||||
nodeId = excluded.nodeId,
|
||||
effectiveNodeId = excluded.effectiveNodeId,
|
||||
@@ -1209,6 +1212,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
task.missionId ?? null,
|
||||
task.sliceId ?? null,
|
||||
task.assignedAgentId ?? null,
|
||||
task.pausedByAgentId ?? null,
|
||||
task.assigneeUserId ?? null,
|
||||
task.nodeId ?? null,
|
||||
task.effectiveNodeId ?? null,
|
||||
@@ -2652,6 +2656,31 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
return limit >= 0 ? matches.slice(0, limit) : matches;
|
||||
}
|
||||
|
||||
async getTasksByAssignedAgent(
|
||||
agentId: string,
|
||||
options?: { pausedOnly?: boolean; excludeArchived?: boolean },
|
||||
): Promise<Task[]> {
|
||||
const whereClauses = ["assignedAgentId = ?"];
|
||||
const params: Array<string | number> = [agentId];
|
||||
|
||||
if (options?.pausedOnly) {
|
||||
whereClauses.push("paused = 1");
|
||||
}
|
||||
|
||||
if (options?.excludeArchived) {
|
||||
whereClauses.push('"column" != \'archived\'');
|
||||
}
|
||||
|
||||
const selectClause = this.getTaskSelectClause(false);
|
||||
const rows = this.db.prepare(`
|
||||
SELECT ${selectClause} FROM tasks
|
||||
WHERE ${whereClauses.join(" AND ")}
|
||||
ORDER BY createdAt ASC
|
||||
`).all(...params) as TaskRow[];
|
||||
|
||||
return rows.map((row) => this.rowToTask(row));
|
||||
}
|
||||
|
||||
async selectNextTaskForAgent(agentId: string): Promise<InboxTask | null> {
|
||||
const tasks = await this.listTasks({ slim: true });
|
||||
if (tasks.length === 0) {
|
||||
@@ -2917,7 +2946,7 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
|
||||
async updateTask(
|
||||
id: string,
|
||||
updates: { title?: string; description?: string; priority?: TaskPriority | null; prompt?: string; worktree?: string | null; status?: string | null; dependencies?: string[]; steps?: import("./types.js").TaskStep[]; currentStep?: number; blockedBy?: string | null; assignedAgentId?: string | null; assigneeUserId?: string | null; nodeId?: string | null; effectiveNodeId?: string | null; effectiveNodeSource?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; executionMode?: import("./types.js").ExecutionMode | null; mergeRetries?: number; workflowStepRetries?: number; stuckKillCount?: number | null; postReviewFixCount?: number | null; recoveryRetryCount?: number | null; taskDoneRetryCount?: number | null; verificationFailureCount?: number | null; mergeConflictBounceCount?: number | null; nextRecoveryAt?: string | null; enabledWorkflowSteps?: string[]; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; planningModelProvider?: string | null; planningModelId?: string | null; thinkingLevel?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; executionStartedAt?: string | null; executionCompletedAt?: string | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; mergeDetails?: import("./types.js").MergeDetails | null; sourceIssue?: import("./types.js").TaskSourceIssue | null; tokenUsage?: import("./types.js").TaskTokenUsage | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null },
|
||||
updates: { title?: string; description?: string; priority?: TaskPriority | null; prompt?: string; worktree?: string | null; status?: string | null; dependencies?: string[]; steps?: import("./types.js").TaskStep[]; currentStep?: number; blockedBy?: string | null; assignedAgentId?: string | null; pausedByAgentId?: string | null; assigneeUserId?: string | null; nodeId?: string | null; effectiveNodeId?: string | null; effectiveNodeSource?: string | null; checkedOutBy?: string | null; checkedOutAt?: string | null; paused?: boolean; baseBranch?: string | null; branch?: string | null; baseCommitSha?: string | null; size?: "S" | "M" | "L"; reviewLevel?: number; executionMode?: import("./types.js").ExecutionMode | null; mergeRetries?: number; workflowStepRetries?: number; stuckKillCount?: number | null; postReviewFixCount?: number | null; recoveryRetryCount?: number | null; taskDoneRetryCount?: number | null; verificationFailureCount?: number | null; mergeConflictBounceCount?: number | null; nextRecoveryAt?: string | null; enabledWorkflowSteps?: string[]; modelProvider?: string | null; modelId?: string | null; validatorModelProvider?: string | null; validatorModelId?: string | null; planningModelProvider?: string | null; planningModelId?: string | null; thinkingLevel?: string | null; error?: string | null; summary?: string | null; sessionFile?: string | null; executionStartedAt?: string | null; executionCompletedAt?: string | null; workflowStepResults?: import("./types.js").WorkflowStepResult[] | null; mergeDetails?: import("./types.js").MergeDetails | null; sourceIssue?: import("./types.js").TaskSourceIssue | null; tokenUsage?: import("./types.js").TaskTokenUsage | null; modifiedFiles?: string[] | null; missionId?: string | null; sliceId?: string | null },
|
||||
runContext?: RunMutationContext,
|
||||
): Promise<Task> {
|
||||
return this.withTaskLock(id, async () => {
|
||||
@@ -3001,6 +3030,11 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
} else if (updates.assignedAgentId !== undefined) {
|
||||
task.assignedAgentId = updates.assignedAgentId;
|
||||
}
|
||||
if (updates.pausedByAgentId === null) {
|
||||
task.pausedByAgentId = undefined;
|
||||
} else if (updates.pausedByAgentId !== undefined) {
|
||||
task.pausedByAgentId = updates.pausedByAgentId;
|
||||
}
|
||||
if (updates.assigneeUserId === null) {
|
||||
task.assigneeUserId = undefined;
|
||||
} else if (updates.assigneeUserId !== undefined) {
|
||||
@@ -3278,7 +3312,12 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
* Pause or unpause a task. Paused tasks are excluded from all automated
|
||||
* agent and scheduler interaction. Logs the action and emits `task:updated`.
|
||||
*/
|
||||
async pauseTask(id: string, paused: boolean, runContext?: RunMutationContext): Promise<Task> {
|
||||
async pauseTask(
|
||||
id: string,
|
||||
paused: boolean,
|
||||
runContext?: RunMutationContext,
|
||||
agentOptions?: { pausedByAgentId?: string },
|
||||
): Promise<Task> {
|
||||
return this.withTaskLock(id, async () => {
|
||||
const dir = this.taskDir(id);
|
||||
const task = await this.readTaskJson(dir);
|
||||
@@ -3288,7 +3327,14 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
task.log = [];
|
||||
}
|
||||
|
||||
const previousPausedByAgentId = task.pausedByAgentId;
|
||||
task.paused = paused || undefined;
|
||||
if (paused && agentOptions?.pausedByAgentId) {
|
||||
task.pausedByAgentId = agentOptions.pausedByAgentId;
|
||||
}
|
||||
if (!paused) {
|
||||
task.pausedByAgentId = undefined;
|
||||
}
|
||||
// When pausing an in-progress/in-review task, set status so the UI can show the state.
|
||||
// When unpausing, clear the "paused" status.
|
||||
if (task.column === "in-progress" || task.column === "in-review") {
|
||||
@@ -3298,7 +3344,13 @@ export class TaskStore extends EventEmitter<TaskStoreEvents> {
|
||||
task.updatedAt = now;
|
||||
const logEntry: TaskLogEntry = {
|
||||
timestamp: now,
|
||||
action: paused ? "Task paused" : "Task unpaused",
|
||||
action: paused
|
||||
? (agentOptions?.pausedByAgentId
|
||||
? `Task paused (agent ${agentOptions.pausedByAgentId} paused)`
|
||||
: "Task paused")
|
||||
: (previousPausedByAgentId
|
||||
? `Task unpaused (agent ${previousPausedByAgentId} resumed)`
|
||||
: "Task unpaused"),
|
||||
};
|
||||
if (runContext) {
|
||||
logEntry.runContext = runContext;
|
||||
|
||||
@@ -879,6 +879,8 @@ export interface Task {
|
||||
blockedBy?: string;
|
||||
/** When true, all automated agent and scheduler interaction is suspended. */
|
||||
paused?: boolean;
|
||||
/** When set, this task was paused because the agent with this ID was paused. Cleared when the agent resumes. Distinct from user-initiated pause. */
|
||||
pausedByAgentId?: string;
|
||||
/** Git branch name (or task ID) to use as the starting point when
|
||||
* creating this task's worktree. Set by the scheduler when a task's
|
||||
* explicit dependency or `blockedBy` task is in-review with an
|
||||
|
||||
Reference in New Issue
Block a user