taskchef 8.7.0 → 9.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +7 -10
- package/.mcp.json +3 -2
- package/README.md +31 -158
- package/docs/mcp-app.md +21 -4
- package/docs/spec.md +38 -746
- package/docs/taskchef-next-storage.md +2 -2
- package/index.js +2 -99
- package/package.json +12 -49
- package/skills/delegate/SKILL.md +44 -0
- package/skills/delegate/agents/openai.yaml +4 -0
- package/src/codex-app.js +3 -152
- package/src/codex-session-scanner.js +2 -2
- package/src/file-lock.js +25 -0
- package/src/mcp-app/react/NextTaskDetail.test.tsx +1 -1
- package/src/mcp-app/react/NextTaskDetail.tsx +8 -8
- package/src/mcp-app/react/main.test.tsx +3 -3
- package/src/mcp-app/react/main.tsx +7 -7
- package/src/mcp-app.js +4 -4
- package/src/mcp.js +4 -542
- package/src/next-cache.js +2 -2
- package/src/next-github.js +3 -3
- package/src/next-notifications.js +2 -2
- package/src/{dashboard/react → ui}/components/CardStatusIcons.tsx +1 -1
- package/src/{dashboard/react → ui}/components/GitHubLinks.tsx +1 -1
- package/src/{dashboard/react → ui}/components/LinkedText.tsx +1 -1
- package/src/{dashboard/react → ui}/components/RelativeTime.test.tsx +1 -1
- package/src/{dashboard/react → ui}/components/RelativeTime.tsx +1 -1
- package/src/{dashboard/react → ui}/components/ReplyImagePreview.test.tsx +1 -1
- package/src/{dashboard/react → ui}/components/TaskBoard.tsx +1 -1
- package/src/{dashboard/react → ui}/components/TaskCard.tsx +1 -1
- package/src/{dashboard/react → ui}/components/TurnContent.tsx +1 -1
- package/src/{dashboard/react → ui}/presentation.ts +1 -1
- package/src/version.js +2 -1
- package/BACKLOG.md +0 -48
- package/assets/e2e-benchmark-example.json +0 -50
- package/assets/taskchef-dispatcher-instructions.md +0 -44
- package/bin/taskchef.js +0 -20
- package/docs/advanced-guide.md +0 -497
- package/docs/dashboard-lifecycle.md +0 -149
- package/docs/dashboard-ui.md +0 -40
- package/docs/firstmate-taskchef-comparison.md +0 -283
- package/docs/images/ccusage-token-consumption.png +0 -0
- package/docs/images/dashboard-identity.jpg +0 -0
- package/docs/images/dashboard-token-pending.jpg +0 -0
- package/docs/images/interrupted-turn-recovery.jpg +0 -0
- package/docs/images/notification-event-snapshots.jpg +0 -0
- package/docs/images/readme-board.png +0 -0
- package/docs/images/readme-completed.png +0 -0
- package/docs/images/readme-list.png +0 -0
- package/docs/images/readme-needs-input.png +0 -0
- package/docs/images/readme-roles.png +0 -0
- package/docs/images/reported-work-duration-desktop.jpg +0 -0
- package/docs/images/reported-work-duration-mobile.jpg +0 -0
- package/docs/images/result-history-dashboard.jpg +0 -0
- package/docs/images/taskchef-pr-icon-comparison.jpg +0 -0
- package/docs/images/taskchef-unified-status-popup.jpg +0 -0
- package/docs/workflows.md +0 -476
- package/mcp/dashboard-session.js +0 -51
- package/scripts/benchmark-dispatch-prepare.js +0 -90
- package/scripts/e2e-benchmark.js +0 -206
- package/scripts/roles/resolve_roles.py +0 -293
- package/skills/taskchef-bootstrap/SKILL.md +0 -96
- package/skills/taskchef-bootstrap/agents/openai.yaml +0 -4
- package/skills/taskchef-bootstrap/references/project-index.md +0 -154
- package/skills/taskchef-copilot/SKILL.md +0 -75
- package/skills/taskchef-copilot/agents/openai.yaml +0 -4
- package/skills/taskchef-dashboard/SKILL.md +0 -24
- package/skills/taskchef-dashboard/agents/openai.yaml +0 -4
- package/skills/taskchef-delegate/SKILL.md +0 -102
- package/skills/taskchef-delegate/agents/openai.yaml +0 -4
- package/skills/taskchef-delegate/references/model-roles.md +0 -29
- package/skills/taskchef-executor/SKILL.md +0 -154
- package/skills/taskchef-executor/agents/openai.yaml +0 -4
- package/skills/taskchef-executor/references/compatibility.md +0 -22
- package/skills/taskchef-executor/references/ending-actions.md +0 -26
- package/src/backups.js +0 -359
- package/src/cli.js +0 -733
- package/src/config-audit.js +0 -99
- package/src/config-mutations.js +0 -106
- package/src/dashboard/actions.js +0 -136
- package/src/dashboard/app.js +0 -1172
- package/src/dashboard/dist/app.js +0 -269
- package/src/dashboard/dist/app.js.map +0 -1
- package/src/dashboard/dist/index.html +0 -16
- package/src/dashboard/dist/styles.css +0 -1
- package/src/dashboard/index.html +0 -148
- package/src/dashboard/react/App.test.tsx +0 -346
- package/src/dashboard/react/App.tsx +0 -515
- package/src/dashboard/react/api.test.ts +0 -40
- package/src/dashboard/react/api.ts +0 -103
- package/src/dashboard/react/components/ActivityTimeline.tsx +0 -118
- package/src/dashboard/react/components/ModelSettings.test.tsx +0 -180
- package/src/dashboard/react/components/ModelSettings.tsx +0 -106
- package/src/dashboard/react/components/NotificationCenter.test.tsx +0 -76
- package/src/dashboard/react/components/NotificationCenter.tsx +0 -111
- package/src/dashboard/react/components/PullRequestBadges.tsx +0 -18
- package/src/dashboard/react/components/TaskDetail.test.tsx +0 -255
- package/src/dashboard/react/components/TaskDetail.tsx +0 -271
- package/src/dashboard/react/components/UsagePanel.tsx +0 -96
- package/src/dashboard/react/index.html +0 -15
- package/src/dashboard/react/main.tsx +0 -12
- package/src/dashboard/react/overlay-state.test.ts +0 -110
- package/src/dashboard/react/overlay-state.ts +0 -60
- package/src/dashboard/react/presentation.test.tsx +0 -571
- package/src/dashboard/styles.css +0 -261
- package/src/dashboard-control.js +0 -34
- package/src/dashboard-manager.js +0 -339
- package/src/dashboard-session-process.js +0 -88
- package/src/dashboard-session.js +0 -86
- package/src/dashboard.js +0 -1303
- package/src/delegation.js +0 -277
- package/src/github.js +0 -107
- package/src/model-roles.js +0 -70
- package/src/recorded-usage-tracker.js +0 -92
- package/src/usage-tracker.js +0 -650
- package/src/usage.js +0 -367
- package/src/workspace-path.js +0 -48
- package/src/workspace.js +0 -3629
- /package/src/{dashboard/react → ui}/components/AnimatedUsage.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/CardStatusIcons.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ImageGallery.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/OpenChatButton.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/OpenChatButton.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/PrGlyph.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/QueuedPrompts.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/QueuedPrompts.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyCover.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyCover.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyImagePreview.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyMarkdown.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ReplyMarkdown.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/SavedUsageFormat.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/SavedUsageFormat.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/ShimmerText.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/StatsGrid.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/StatusBadge.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TaskBoard.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TaskCardStats.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TaskCardStats.tsx +0 -0
- /package/src/{dashboard/react → ui}/components/TurnContent.test.tsx +0 -0
- /package/src/{dashboard/react → ui}/fixtures.ts +0 -0
- /package/src/{dashboard → ui}/github-links.js +0 -0
- /package/src/{dashboard/react → ui}/image-cache.test.ts +0 -0
- /package/src/{dashboard/react → ui}/image-cache.ts +0 -0
- /package/src/{dashboard → ui}/state.js +0 -0
- /package/src/{dashboard/react → ui}/styles.css +0 -0
- /package/src/{dashboard/react → ui}/test-setup.ts +0 -0
- /package/src/{dashboard → ui}/time.js +0 -0
- /package/src/{dashboard/react → ui}/types.ts +0 -0
package/src/config-audit.js
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
import { open, readFile, readdir, rename, stat, unlink } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { taskChefError } from "./state-store.js";
|
|
4
|
-
|
|
5
|
-
export async function appendAuditUnlocked(workspace, record) {
|
|
6
|
-
const filePath = path.join(workspace, "config-audit.jsonl");
|
|
7
|
-
const size = await stat(filePath).then((details) => details.size).catch((error) => {
|
|
8
|
-
if (error.code === "ENOENT") return 0;
|
|
9
|
-
throw error;
|
|
10
|
-
});
|
|
11
|
-
if (size >= 10 * 1024 * 1024) {
|
|
12
|
-
const timestamp = new Date().toISOString().replaceAll(":", "-").replaceAll(".", "-");
|
|
13
|
-
await rename(filePath, path.join(workspace, `config-audit.${timestamp}.jsonl`));
|
|
14
|
-
const archives = (await readdir(workspace))
|
|
15
|
-
.filter((name) => /^config-audit\..+\.jsonl$/.test(name))
|
|
16
|
-
.sort()
|
|
17
|
-
.reverse();
|
|
18
|
-
for (const name of archives.slice(10)) await unlink(path.join(workspace, name));
|
|
19
|
-
}
|
|
20
|
-
const handle = await open(filePath, "a", 0o600);
|
|
21
|
-
try {
|
|
22
|
-
await handle.writeFile(`${JSON.stringify({ schemaVersion: 1, ...record })}\n`);
|
|
23
|
-
await handle.sync();
|
|
24
|
-
} finally {
|
|
25
|
-
await handle.close();
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function readLatestAuditRecordsUnlocked(workspace) {
|
|
30
|
-
const filePath = path.join(workspace, "config-audit.jsonl");
|
|
31
|
-
const content = await readFile(filePath, "utf8").catch((error) => {
|
|
32
|
-
if (error.code === "ENOENT") return "";
|
|
33
|
-
throw error;
|
|
34
|
-
});
|
|
35
|
-
const latest = new Map();
|
|
36
|
-
for (const [index, line] of content.trimEnd().split("\n").entries()) {
|
|
37
|
-
if (line.length === 0) continue;
|
|
38
|
-
let record;
|
|
39
|
-
try {
|
|
40
|
-
record = JSON.parse(line);
|
|
41
|
-
} catch {
|
|
42
|
-
throw taskChefError(
|
|
43
|
-
"STATE_RECOVERY_REQUIRED",
|
|
44
|
-
`configuration audit is malformed at line ${index + 1}`,
|
|
45
|
-
);
|
|
46
|
-
}
|
|
47
|
-
if (record.transactionId) latest.set(record.transactionId, record);
|
|
48
|
-
}
|
|
49
|
-
return latest;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export async function validateAuditUnlocked(workspace) {
|
|
53
|
-
await readLatestAuditRecordsUnlocked(workspace);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export async function reconcileAuditUnlocked(workspace, currentHash) {
|
|
57
|
-
const latest = await readLatestAuditRecordsUnlocked(workspace);
|
|
58
|
-
for (const record of latest.values()) {
|
|
59
|
-
if (record.phase !== "prepared" && record.phase !== "recovery-required") continue;
|
|
60
|
-
if (currentHash === record.beforeHash) {
|
|
61
|
-
await appendAuditUnlocked(workspace, {
|
|
62
|
-
...record,
|
|
63
|
-
timestamp: new Date().toISOString(),
|
|
64
|
-
phase: "aborted",
|
|
65
|
-
reconciliation: "matched-before",
|
|
66
|
-
});
|
|
67
|
-
} else if (currentHash === record.afterHash) {
|
|
68
|
-
await appendAuditUnlocked(workspace, {
|
|
69
|
-
...record,
|
|
70
|
-
timestamp: new Date().toISOString(),
|
|
71
|
-
phase: "committed",
|
|
72
|
-
reconciliation: "matched-after",
|
|
73
|
-
});
|
|
74
|
-
} else {
|
|
75
|
-
throw taskChefError(
|
|
76
|
-
"STATE_RECOVERY_REQUIRED",
|
|
77
|
-
`configuration does not match either side of transaction ${record.transactionId}`,
|
|
78
|
-
{ transactionId: record.transactionId },
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
export async function supersedeAuditTransactionsUnlocked(workspace, recoveryTransactionId) {
|
|
85
|
-
const latest = await readLatestAuditRecordsUnlocked(workspace);
|
|
86
|
-
const superseded = [];
|
|
87
|
-
for (const record of latest.values()) {
|
|
88
|
-
if (record.transactionId === recoveryTransactionId
|
|
89
|
-
|| !new Set(["prepared", "recovery-required"]).has(record.phase)) continue;
|
|
90
|
-
await appendAuditUnlocked(workspace, {
|
|
91
|
-
...record,
|
|
92
|
-
timestamp: new Date().toISOString(),
|
|
93
|
-
phase: "superseded",
|
|
94
|
-
supersededBy: recoveryTransactionId,
|
|
95
|
-
});
|
|
96
|
-
superseded.push(record.transactionId);
|
|
97
|
-
}
|
|
98
|
-
return superseded;
|
|
99
|
-
}
|
package/src/config-mutations.js
DELETED
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import { semanticHash, taskChefError } from "./state-store.js";
|
|
2
|
-
|
|
3
|
-
function fieldsChanged(before, after) {
|
|
4
|
-
const fields = {};
|
|
5
|
-
for (const key of ["name", "description", "isGitRepository", "githubRepos"]) {
|
|
6
|
-
if (JSON.stringify(before[key]) !== JSON.stringify(after[key])) {
|
|
7
|
-
fields[key] = { before: before[key] ?? null, after: after[key] ?? null };
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
return fields;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function projectSetHash(projects) {
|
|
14
|
-
return semanticHash({ schemaVersion: 1, projects }, "taskchef-project-set-v1");
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
export function configHash(config) {
|
|
18
|
-
return semanticHash(config, "taskchef-config-v1");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export function projectDiff(beforeProjects, afterProjects) {
|
|
22
|
-
const before = new Map(beforeProjects.map((project) => [project.path, project]));
|
|
23
|
-
const after = new Map(afterProjects.map((project) => [project.path, project]));
|
|
24
|
-
const added = [];
|
|
25
|
-
const changed = [];
|
|
26
|
-
const removed = [];
|
|
27
|
-
for (const project of afterProjects) {
|
|
28
|
-
const prior = before.get(project.path);
|
|
29
|
-
if (!prior) added.push(project);
|
|
30
|
-
else {
|
|
31
|
-
const fields = fieldsChanged(prior, project);
|
|
32
|
-
if (Object.keys(fields).length) changed.push({ path: project.path, before: prior, after: project, fields });
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
for (const project of beforeProjects) {
|
|
36
|
-
if (!after.has(project.path)) removed.push(project);
|
|
37
|
-
}
|
|
38
|
-
return { added, changed, removed };
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function mutationPreview(workspace, operation, beforeConfig, afterConfig) {
|
|
42
|
-
const diff = projectDiff(beforeConfig.projects, afterConfig.projects);
|
|
43
|
-
const emptyIndex = { bindings: [], exclusions: [], routingHints: [] };
|
|
44
|
-
const beforeProjectIndex = beforeConfig.projectIndex ?? emptyIndex;
|
|
45
|
-
const afterProjectIndex = afterConfig.projectIndex ?? emptyIndex;
|
|
46
|
-
const projectIndexDiff = JSON.stringify(beforeProjectIndex) === JSON.stringify(afterProjectIndex)
|
|
47
|
-
? null
|
|
48
|
-
: { before: beforeProjectIndex, after: afterProjectIndex };
|
|
49
|
-
const preview = {
|
|
50
|
-
schemaVersion: 1,
|
|
51
|
-
operation,
|
|
52
|
-
beforeConfigHash: configHash(beforeConfig),
|
|
53
|
-
afterConfigHash: configHash(afterConfig),
|
|
54
|
-
beforeProjectSetHash: projectSetHash(beforeConfig.projects),
|
|
55
|
-
afterProjectSetHash: projectSetHash(afterConfig.projects),
|
|
56
|
-
beforeCount: beforeConfig.projects.length,
|
|
57
|
-
afterCount: afterConfig.projects.length,
|
|
58
|
-
diff,
|
|
59
|
-
projectIndexDiff,
|
|
60
|
-
projects: afterConfig.projects,
|
|
61
|
-
};
|
|
62
|
-
return {
|
|
63
|
-
...preview,
|
|
64
|
-
planHash: semanticHash({
|
|
65
|
-
schemaVersion: 1,
|
|
66
|
-
workspace,
|
|
67
|
-
operation,
|
|
68
|
-
beforeConfigHash: preview.beforeConfigHash,
|
|
69
|
-
afterConfigHash: preview.afterConfigHash,
|
|
70
|
-
diff,
|
|
71
|
-
projectIndexDiff,
|
|
72
|
-
}, "taskchef-mutation-plan-v1"),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function exactStringSet(actual, expected) {
|
|
77
|
-
if (!Array.isArray(actual) || new Set(actual).size !== actual.length) return false;
|
|
78
|
-
return actual.length === expected.length
|
|
79
|
-
&& [...actual].sort().every((value, index) => value === [...expected].sort()[index]);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export function assertMutationAuthorization(preview, authorization = {}, {
|
|
83
|
-
destructive = preview.diff.removed.length > 0,
|
|
84
|
-
replacement = false,
|
|
85
|
-
} = {}) {
|
|
86
|
-
if (authorization.expectConfigHash !== preview.beforeConfigHash) {
|
|
87
|
-
throw taskChefError("STALE_PREVIEW", "configuration changed or --expect-config-hash is missing", preview);
|
|
88
|
-
}
|
|
89
|
-
if (!destructive && !replacement) return;
|
|
90
|
-
const removedNames = preview.diff.removed.map((project) => project.name);
|
|
91
|
-
if (authorization.expectProjectCount !== preview.beforeCount
|
|
92
|
-
|| authorization.expectAfterCount !== preview.afterCount
|
|
93
|
-
|| authorization.confirmPlan !== preview.planHash
|
|
94
|
-
|| !exactStringSet(authorization.confirmRemoved, removedNames)) {
|
|
95
|
-
throw taskChefError(
|
|
96
|
-
"CONFIRMATION_REQUIRED",
|
|
97
|
-
"destructive project mutation requires exact preview-bound count, hash, plan, and removed-name confirmation",
|
|
98
|
-
preview,
|
|
99
|
-
);
|
|
100
|
-
}
|
|
101
|
-
const collapses = preview.beforeCount > 0
|
|
102
|
-
&& (preview.afterCount === 0 || preview.afterCount / preview.beforeCount <= 0.5);
|
|
103
|
-
if (collapses && authorization.confirmCountCollapse !== `${preview.beforeCount}:${preview.afterCount}`) {
|
|
104
|
-
throw taskChefError("CONFIRMATION_REQUIRED", "project-count collapse requires exact before:after confirmation", preview);
|
|
105
|
-
}
|
|
106
|
-
}
|
package/src/dashboard/actions.js
DELETED
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
export async function openTaskFromControl(event, taskId, {
|
|
2
|
-
fetchAction = globalThis.fetch,
|
|
3
|
-
showMessage,
|
|
4
|
-
} = {}) {
|
|
5
|
-
event.stopPropagation();
|
|
6
|
-
const control = event.currentTarget;
|
|
7
|
-
control.disabled = true;
|
|
8
|
-
try {
|
|
9
|
-
const response = await fetchAction(`/api/tasks/${encodeURIComponent(taskId)}/open-codex`, {
|
|
10
|
-
method: "POST",
|
|
11
|
-
});
|
|
12
|
-
const result = await response.json();
|
|
13
|
-
if (result.message) showMessage(result.message);
|
|
14
|
-
} finally {
|
|
15
|
-
control.disabled = false;
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export async function archiveTaskFromControl(event, task, {
|
|
20
|
-
confirmAction = globalThis.confirm,
|
|
21
|
-
fetchAction = globalThis.fetch,
|
|
22
|
-
onArchived = () => {},
|
|
23
|
-
showUpdate,
|
|
24
|
-
} = {}) {
|
|
25
|
-
event.stopPropagation();
|
|
26
|
-
const accepted = confirmAction(
|
|
27
|
-
`Archive “${task.title}” in Codex?\n\n`
|
|
28
|
-
+ "The chat will leave active Codex lists, and spawned descendant chats may also be archived. "
|
|
29
|
-
+ "TaskChef history will remain available.",
|
|
30
|
-
);
|
|
31
|
-
if (!accepted) return false;
|
|
32
|
-
const control = event.currentTarget;
|
|
33
|
-
let archived = false;
|
|
34
|
-
control.disabled = true;
|
|
35
|
-
try {
|
|
36
|
-
const response = await fetchAction(
|
|
37
|
-
`/api/tasks/${encodeURIComponent(task.id)}/archive-codex`,
|
|
38
|
-
{ method: "POST" },
|
|
39
|
-
);
|
|
40
|
-
const result = await response.json();
|
|
41
|
-
if (!response.ok) {
|
|
42
|
-
showUpdate(result.message ?? "Codex could not archive this chat.", { failed: true });
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
onArchived(task.threadId);
|
|
46
|
-
archived = true;
|
|
47
|
-
showUpdate(result.message ?? "Archived the Codex chat. TaskChef history remains available.");
|
|
48
|
-
return true;
|
|
49
|
-
} catch {
|
|
50
|
-
showUpdate("Codex chat archiving is temporarily unavailable. Try again.", { failed: true });
|
|
51
|
-
return false;
|
|
52
|
-
} finally {
|
|
53
|
-
if (!archived) control.disabled = false;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export async function manuallyTransitionTaskFromControl(event, task, targetStatus, actionId, {
|
|
58
|
-
clearTimer = globalThis.clearTimeout,
|
|
59
|
-
fetchAction = globalThis.fetch,
|
|
60
|
-
setTimer = globalThis.setTimeout,
|
|
61
|
-
timeoutMs = 10_000,
|
|
62
|
-
} = {}) {
|
|
63
|
-
event?.stopPropagation?.();
|
|
64
|
-
const controller = new AbortController();
|
|
65
|
-
const timeout = setTimer(() => controller.abort(), timeoutMs);
|
|
66
|
-
try {
|
|
67
|
-
const response = await fetchAction(
|
|
68
|
-
`/api/tasks/${encodeURIComponent(task.id)}/manual-transition`,
|
|
69
|
-
{
|
|
70
|
-
method: "POST",
|
|
71
|
-
headers: { "Content-Type": "application/json" },
|
|
72
|
-
body: JSON.stringify({
|
|
73
|
-
schemaVersion: 1,
|
|
74
|
-
actionId,
|
|
75
|
-
expected: {
|
|
76
|
-
status: task.status,
|
|
77
|
-
turnRef: task.turnRef,
|
|
78
|
-
threadId: task.threadId,
|
|
79
|
-
updatedAt: task.updatedAt,
|
|
80
|
-
},
|
|
81
|
-
targetStatus,
|
|
82
|
-
}),
|
|
83
|
-
signal: controller.signal,
|
|
84
|
-
},
|
|
85
|
-
);
|
|
86
|
-
const result = await response.json();
|
|
87
|
-
return response.ok
|
|
88
|
-
? { ok: true, ...result }
|
|
89
|
-
: {
|
|
90
|
-
ok: false,
|
|
91
|
-
code: result.code ?? "dashboard_error",
|
|
92
|
-
message: result.message ?? "Task state could not be changed.",
|
|
93
|
-
task: result.task ?? null,
|
|
94
|
-
};
|
|
95
|
-
} catch {
|
|
96
|
-
return {
|
|
97
|
-
ok: false,
|
|
98
|
-
code: controller.signal.aborted ? "request_timeout" : "network_error",
|
|
99
|
-
message: controller.signal.aborted
|
|
100
|
-
? "Task state change timed out. Try again."
|
|
101
|
-
: "Task state changes are temporarily unavailable. Try again.",
|
|
102
|
-
task: null,
|
|
103
|
-
};
|
|
104
|
-
} finally {
|
|
105
|
-
clearTimer(timeout);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export function handleManualTransitionEscape(event, {
|
|
110
|
-
active,
|
|
111
|
-
pending,
|
|
112
|
-
cancel,
|
|
113
|
-
}) {
|
|
114
|
-
if (event.key !== "Escape" || !active) return false;
|
|
115
|
-
event.preventDefault();
|
|
116
|
-
event.stopPropagation();
|
|
117
|
-
if (!pending) cancel();
|
|
118
|
-
return true;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export function focusManualTransitionStatus(panel) {
|
|
122
|
-
const status = panel.querySelector('[data-manual-focus="pending"]');
|
|
123
|
-
status?.focus();
|
|
124
|
-
return status !== null;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export function restoreTaskActionMenuFocus(panel, activeElement, fallback) {
|
|
128
|
-
if (!activeElement || !panel.contains(activeElement)) return null;
|
|
129
|
-
if (!activeElement.hidden && !activeElement.disabled) return activeElement;
|
|
130
|
-
const copyTaskId = panel.querySelector('[data-manual-focus="copy"]');
|
|
131
|
-
const destination = copyTaskId && !copyTaskId.hidden && !copyTaskId.disabled
|
|
132
|
-
? copyTaskId
|
|
133
|
-
: fallback;
|
|
134
|
-
destination?.focus();
|
|
135
|
-
return destination ?? null;
|
|
136
|
-
}
|