taskchef 8.7.1 → 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 +3 -3
- 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 +2 -2
- 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 +0 -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/dashboard/app.js
DELETED
|
@@ -1,1172 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
MAX_NOTIFICATIONS,
|
|
3
|
-
canArchiveTask,
|
|
4
|
-
canManuallyTransitionTask,
|
|
5
|
-
clearNotifications,
|
|
6
|
-
dismissNotification,
|
|
7
|
-
filterTasks,
|
|
8
|
-
findCurrentTask,
|
|
9
|
-
latestTurnPresentation,
|
|
10
|
-
manualTransitionExpectedState,
|
|
11
|
-
mergeProjectedTurns,
|
|
12
|
-
nextDateFilterRefreshDelay,
|
|
13
|
-
notificationDismissLabel,
|
|
14
|
-
notificationOpenLabel,
|
|
15
|
-
notificationTitle,
|
|
16
|
-
reconcileNotifications,
|
|
17
|
-
reconcileManualTransition,
|
|
18
|
-
reconcileManualTransitionResponse,
|
|
19
|
-
statusFilterCounts,
|
|
20
|
-
statusFilterText,
|
|
21
|
-
taskStatusLabel,
|
|
22
|
-
taskMatchesManualTransitionExpected,
|
|
23
|
-
turnPresentation,
|
|
24
|
-
} from "./state.js";
|
|
25
|
-
import {
|
|
26
|
-
archiveTaskFromControl,
|
|
27
|
-
focusManualTransitionStatus,
|
|
28
|
-
handleManualTransitionEscape,
|
|
29
|
-
manuallyTransitionTaskFromControl,
|
|
30
|
-
openTaskFromControl,
|
|
31
|
-
restoreTaskActionMenuFocus,
|
|
32
|
-
} from "./actions.js";
|
|
33
|
-
import {
|
|
34
|
-
githubReferenceAccessibleLabel,
|
|
35
|
-
githubReferenceDisplayLabels,
|
|
36
|
-
groupRelatedGitHubLinks,
|
|
37
|
-
referenceSegments,
|
|
38
|
-
} from "./github-links.js";
|
|
39
|
-
import { formatRelativeTime, RelativeTimeController, parsedTimestamp } from "./time.js";
|
|
40
|
-
|
|
41
|
-
const USAGE_POLL_INTERVAL_MS = 1_500;
|
|
42
|
-
const MAX_USAGE_POLL_ATTEMPTS = 40;
|
|
43
|
-
|
|
44
|
-
const state = {
|
|
45
|
-
archivePendingThreadIds: new Set(),
|
|
46
|
-
archivedThreadIds: new Set(),
|
|
47
|
-
tasks: [],
|
|
48
|
-
signatures: new Map(),
|
|
49
|
-
notifications: [],
|
|
50
|
-
seenNotificationIds: new Set(),
|
|
51
|
-
initialized: false,
|
|
52
|
-
detailNavigation: null,
|
|
53
|
-
manualTransition: null,
|
|
54
|
-
selectedTask: null,
|
|
55
|
-
};
|
|
56
|
-
let dateRefreshTimer = null;
|
|
57
|
-
let detailRequestGeneration = 0;
|
|
58
|
-
let copyTaskIdGeneration = 0;
|
|
59
|
-
let copyTaskIdTimer = null;
|
|
60
|
-
let archiveUpdateSerial = 0;
|
|
61
|
-
const relativeTimes = new RelativeTimeController();
|
|
62
|
-
|
|
63
|
-
const elements = {
|
|
64
|
-
archiveTask: document.querySelector("#archive-codex"),
|
|
65
|
-
clearNotifications: document.querySelector("#clear-notifications"),
|
|
66
|
-
closeDialog: document.querySelector("#close-dialog"),
|
|
67
|
-
connectionDot: document.querySelector("#connection-dot"),
|
|
68
|
-
connectionLabel: document.querySelector("#connection-label"),
|
|
69
|
-
copyTaskId: document.querySelector("#copy-task-id"),
|
|
70
|
-
dashboardMessage: document.querySelector("#dashboard-message"),
|
|
71
|
-
dashboardMessageText: document.querySelector("#dashboard-message-text"),
|
|
72
|
-
dateFilter: document.querySelector("#date-filter"),
|
|
73
|
-
dialog: document.querySelector("#task-dialog"),
|
|
74
|
-
dialogInstruction: document.querySelector("#dialog-instruction"),
|
|
75
|
-
dialogMetadata: document.querySelector("#dialog-metadata"),
|
|
76
|
-
dialogProject: document.querySelector("#dialog-project"),
|
|
77
|
-
dialogRelatedLinks: document.querySelector("#dialog-related-links"),
|
|
78
|
-
dialogResults: document.querySelector("#dialog-results"),
|
|
79
|
-
dialogTitle: document.querySelector("#dialog-title"),
|
|
80
|
-
dialogUsage: document.querySelector("#dialog-usage"),
|
|
81
|
-
dismissDashboardMessage: document.querySelector("#dismiss-dashboard-message"),
|
|
82
|
-
emptyState: document.querySelector("#empty-state"),
|
|
83
|
-
notifications: document.querySelector("#notifications"),
|
|
84
|
-
notificationHost: document.querySelector("#notification-host"),
|
|
85
|
-
notificationAnnouncer: document.querySelector("#notification-announcer"),
|
|
86
|
-
manualTransitionPanel: document.querySelector("#manual-transition-panel"),
|
|
87
|
-
manualTransitionError: document.querySelector("#manual-transition-error"),
|
|
88
|
-
manualTransitionStatus: document.querySelector("#manual-transition-status"),
|
|
89
|
-
markTaskCompleted: document.querySelector("#mark-task-completed"),
|
|
90
|
-
markTaskFailed: document.querySelector("#mark-task-failed"),
|
|
91
|
-
moreTaskActions: document.querySelector("#more-task-actions"),
|
|
92
|
-
openProject: document.querySelector("#open-codex"),
|
|
93
|
-
projectFilter: document.querySelector("#project-filter"),
|
|
94
|
-
statusFilter: document.querySelector("#status-filter"),
|
|
95
|
-
taskCount: document.querySelector("#task-count"),
|
|
96
|
-
taskchefVersion: document.querySelector("#taskchef-version"),
|
|
97
|
-
taskList: document.querySelector("#task-list"),
|
|
98
|
-
toastList: document.querySelector("#toast-list"),
|
|
99
|
-
};
|
|
100
|
-
let notificationDescriptionSerial = 0;
|
|
101
|
-
|
|
102
|
-
function referenceLink(link, { compact = false, displayLabel } = {}) {
|
|
103
|
-
const anchor = document.createElement("a");
|
|
104
|
-
anchor.className = compact ? "github-link github-link-compact" : "github-link";
|
|
105
|
-
anchor.href = link.url;
|
|
106
|
-
anchor.target = "_blank";
|
|
107
|
-
anchor.rel = "noopener noreferrer";
|
|
108
|
-
anchor.dataset.focusKey = `link:${link.url}`;
|
|
109
|
-
const label = link.label ?? link.text;
|
|
110
|
-
anchor.textContent = displayLabel ?? label;
|
|
111
|
-
const accessibleLabel = githubReferenceAccessibleLabel(link);
|
|
112
|
-
const githubKind = link.type === "issue"
|
|
113
|
-
? ", GitHub issue"
|
|
114
|
-
: link.type === "pull"
|
|
115
|
-
? ", GitHub pull request"
|
|
116
|
-
: link.provider === "github" || link.owner ? " on GitHub" : "";
|
|
117
|
-
anchor.setAttribute(
|
|
118
|
-
"aria-label",
|
|
119
|
-
`${accessibleLabel}${githubKind} (opens in a new tab)`,
|
|
120
|
-
);
|
|
121
|
-
anchor.addEventListener("click", (event) => event.stopPropagation());
|
|
122
|
-
return anchor;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
function appendLinkedText(container, text, task) {
|
|
126
|
-
const segments = referenceSegments(text, {
|
|
127
|
-
projectRepositories: task.project?.githubRepos,
|
|
128
|
-
taskRepository: task.relatedGitHubRepository,
|
|
129
|
-
});
|
|
130
|
-
const links = segments.filter((segment) => segment.kind === "link");
|
|
131
|
-
const displayLabels = githubReferenceDisplayLabels(links);
|
|
132
|
-
let linkIndex = 0;
|
|
133
|
-
const children = segments.map((segment) => {
|
|
134
|
-
if (segment.kind === "text") return document.createTextNode(segment.text);
|
|
135
|
-
if (segment.kind === "link") {
|
|
136
|
-
const displayLabel = displayLabels[linkIndex];
|
|
137
|
-
linkIndex += 1;
|
|
138
|
-
return referenceLink(segment, { displayLabel });
|
|
139
|
-
}
|
|
140
|
-
const ambiguous = document.createElement("span");
|
|
141
|
-
ambiguous.className = "github-reference-ambiguous";
|
|
142
|
-
ambiguous.textContent = segment.text;
|
|
143
|
-
ambiguous.tabIndex = 0;
|
|
144
|
-
ambiguous.title = segment.reason;
|
|
145
|
-
ambiguous.setAttribute("aria-label", segment.reason);
|
|
146
|
-
return ambiguous;
|
|
147
|
-
});
|
|
148
|
-
container.replaceChildren(...children);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function relatedGitHubLinks(task, { compact = false } = {}) {
|
|
152
|
-
const groups = groupRelatedGitHubLinks(task.relatedGitHubLinks);
|
|
153
|
-
const links = groups.flat();
|
|
154
|
-
const container = document.createElement("nav");
|
|
155
|
-
container.className = `github-links${compact ? " github-links-compact" : ""}`;
|
|
156
|
-
container.setAttribute("aria-label", `Related GitHub links for ${task.title}`);
|
|
157
|
-
const displayLabels = githubReferenceDisplayLabels(links, { related: true });
|
|
158
|
-
let linkIndex = 0;
|
|
159
|
-
const children = groups.map((group) => {
|
|
160
|
-
const line = document.createElement("div");
|
|
161
|
-
line.className = "github-links-group";
|
|
162
|
-
line.replaceChildren(...group.map((link) => {
|
|
163
|
-
const anchor = referenceLink(link, {
|
|
164
|
-
compact,
|
|
165
|
-
displayLabel: displayLabels[linkIndex],
|
|
166
|
-
});
|
|
167
|
-
linkIndex += 1;
|
|
168
|
-
return anchor;
|
|
169
|
-
}));
|
|
170
|
-
return line;
|
|
171
|
-
});
|
|
172
|
-
if (task.relatedGitHubLinksTruncated) {
|
|
173
|
-
const more = document.createElement("span");
|
|
174
|
-
more.className = "github-links-more";
|
|
175
|
-
more.textContent = "+ more";
|
|
176
|
-
more.title = "Additional related GitHub references are omitted from this bounded dashboard view.";
|
|
177
|
-
more.setAttribute("aria-label", more.title);
|
|
178
|
-
const finalGroup = children.at(-1);
|
|
179
|
-
if (finalGroup) finalGroup.append(more);
|
|
180
|
-
else children.push(more);
|
|
181
|
-
}
|
|
182
|
-
container.hidden = children.length === 0;
|
|
183
|
-
container.replaceChildren(...children);
|
|
184
|
-
return container;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function renderProject(container, task) {
|
|
188
|
-
const children = [document.createTextNode(task.project.name)];
|
|
189
|
-
const repository = task.relatedGitHubRepository;
|
|
190
|
-
if (repository) {
|
|
191
|
-
const [owner, repositoryName] = repository.split("/");
|
|
192
|
-
children.push(
|
|
193
|
-
document.createTextNode(" "),
|
|
194
|
-
referenceLink({
|
|
195
|
-
label: repository,
|
|
196
|
-
owner,
|
|
197
|
-
provider: "github",
|
|
198
|
-
repository: repositoryName,
|
|
199
|
-
type: "repository",
|
|
200
|
-
url: `https://github.com/${repository}`,
|
|
201
|
-
}, { displayLabel: repositoryName }),
|
|
202
|
-
);
|
|
203
|
-
}
|
|
204
|
-
container.replaceChildren(...children);
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
function timestampControl(value, { accessibleName, key, prefix = "" }) {
|
|
208
|
-
if (!parsedTimestamp(value)) {
|
|
209
|
-
const missing = document.createElement("span");
|
|
210
|
-
missing.className = "timestamp-missing";
|
|
211
|
-
missing.textContent = `${prefix}—`;
|
|
212
|
-
return missing;
|
|
213
|
-
}
|
|
214
|
-
const control = document.createElement("button");
|
|
215
|
-
control.type = "button";
|
|
216
|
-
control.className = "timestamp-toggle";
|
|
217
|
-
control.dataset.focusKey = `timestamp:${key}`;
|
|
218
|
-
const time = document.createElement("time");
|
|
219
|
-
control.append(time);
|
|
220
|
-
relativeTimes.register(key, value, ({ exact, iso, label }) => {
|
|
221
|
-
time.dateTime = iso;
|
|
222
|
-
time.textContent = `${prefix}${label}`;
|
|
223
|
-
const action = exact ? "Show relative time" : "Show exact date and time";
|
|
224
|
-
control.title = action;
|
|
225
|
-
control.setAttribute("aria-label", `${accessibleName}: ${label}. ${action}.`);
|
|
226
|
-
}, { isActive: () => control.isConnected });
|
|
227
|
-
control.addEventListener("click", () => relativeTimes.toggle(key));
|
|
228
|
-
return control;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
function codexIcon() {
|
|
232
|
-
const picture = document.createElement("picture");
|
|
233
|
-
picture.className = "codex-icon";
|
|
234
|
-
picture.setAttribute("aria-hidden", "true");
|
|
235
|
-
const dark = document.createElement("source");
|
|
236
|
-
dark.srcset = "/assets/codex-app-dark.png";
|
|
237
|
-
dark.media = "(prefers-color-scheme: dark)";
|
|
238
|
-
const image = document.createElement("img");
|
|
239
|
-
image.src = "/assets/codex-app-light.png";
|
|
240
|
-
image.alt = "";
|
|
241
|
-
image.width = 18;
|
|
242
|
-
image.height = 18;
|
|
243
|
-
picture.append(dark, image);
|
|
244
|
-
return picture;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function configureOpenTaskControl(control, ariaLabel) {
|
|
248
|
-
control.classList.add("task-action");
|
|
249
|
-
control.setAttribute("aria-label", ariaLabel);
|
|
250
|
-
const label = document.createElement("span");
|
|
251
|
-
label.textContent = "Open task";
|
|
252
|
-
control.replaceChildren(codexIcon(), label);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
function setCopyTaskIdLabel(label) {
|
|
256
|
-
elements.copyTaskId.textContent = label;
|
|
257
|
-
elements.copyTaskId.setAttribute("aria-label", label);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
function setConnection(connected) {
|
|
261
|
-
elements.connectionDot.classList.toggle("connected", connected);
|
|
262
|
-
elements.connectionLabel.textContent = connected ? "Live" : "Reconnecting…";
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
async function loadDashboardVersion() {
|
|
266
|
-
try {
|
|
267
|
-
const response = await fetch("/api/health");
|
|
268
|
-
if (!response.ok) return;
|
|
269
|
-
const identity = await response.json();
|
|
270
|
-
if (typeof identity.taskchefVersion !== "string" || !identity.taskchefVersion) return;
|
|
271
|
-
elements.taskchefVersion.textContent = `v${identity.taskchefVersion}`;
|
|
272
|
-
elements.taskchefVersion.setAttribute(
|
|
273
|
-
"aria-label",
|
|
274
|
-
`TaskChef version ${identity.taskchefVersion}`,
|
|
275
|
-
);
|
|
276
|
-
} catch {
|
|
277
|
-
// The live connection state already communicates dashboard availability.
|
|
278
|
-
}
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
function replaceOptions(select, values, allLabel) {
|
|
282
|
-
const selected = select.value;
|
|
283
|
-
select.replaceChildren();
|
|
284
|
-
const all = document.createElement("option");
|
|
285
|
-
all.value = "";
|
|
286
|
-
all.textContent = allLabel;
|
|
287
|
-
select.append(all);
|
|
288
|
-
for (const value of values) {
|
|
289
|
-
const option = document.createElement("option");
|
|
290
|
-
option.value = value;
|
|
291
|
-
option.textContent = value;
|
|
292
|
-
select.append(option);
|
|
293
|
-
}
|
|
294
|
-
if (values.includes(selected)) select.value = selected;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
function notificationToast(notification) {
|
|
298
|
-
const task = findCurrentTask(state.tasks, notification.taskId);
|
|
299
|
-
const toast = document.createElement("div");
|
|
300
|
-
toast.className = `toast${task ? "" : " toast-missing"}`;
|
|
301
|
-
const text = document.createElement("button");
|
|
302
|
-
text.type = "button";
|
|
303
|
-
text.className = "toast-content";
|
|
304
|
-
text.setAttribute("aria-label", notificationOpenLabel(notification, Boolean(task)));
|
|
305
|
-
const describedBy = [];
|
|
306
|
-
const title = document.createElement("strong");
|
|
307
|
-
title.textContent = notificationTitle(notification);
|
|
308
|
-
const description = document.createElement("span");
|
|
309
|
-
description.textContent = notification.title;
|
|
310
|
-
text.append(title, description);
|
|
311
|
-
if (notification.summary) {
|
|
312
|
-
const summary = document.createElement("span");
|
|
313
|
-
summary.className = "toast-summary preserve-lines";
|
|
314
|
-
summary.id = `notification-description-${notificationDescriptionSerial += 1}`;
|
|
315
|
-
summary.textContent = notification.summary;
|
|
316
|
-
text.append(summary);
|
|
317
|
-
describedBy.push(summary.id);
|
|
318
|
-
}
|
|
319
|
-
const metadata = document.createElement("span");
|
|
320
|
-
metadata.className = "toast-metadata";
|
|
321
|
-
metadata.id = `notification-description-${notificationDescriptionSerial += 1}`;
|
|
322
|
-
const timestamp = document.createElement("time");
|
|
323
|
-
timestamp.dateTime = notification.timestamp ?? "";
|
|
324
|
-
timestamp.textContent = formatRelativeTime(notification.timestamp);
|
|
325
|
-
metadata.append(timestamp);
|
|
326
|
-
if (!task) metadata.append(" · Task no longer available");
|
|
327
|
-
text.append(metadata);
|
|
328
|
-
describedBy.push(metadata.id);
|
|
329
|
-
text.setAttribute("aria-describedby", describedBy.join(" "));
|
|
330
|
-
text.addEventListener("click", async () => {
|
|
331
|
-
const current = findCurrentTask(state.tasks, notification.taskId);
|
|
332
|
-
state.notifications = dismissNotification(state.notifications, notification.id);
|
|
333
|
-
renderNotifications();
|
|
334
|
-
if (current) {
|
|
335
|
-
await openDialog(current, {
|
|
336
|
-
focus: true,
|
|
337
|
-
highlightTurnRef: notification.turnRef ?? notification.turnId,
|
|
338
|
-
});
|
|
339
|
-
} else showMessage("This task is no longer present in the current snapshot.");
|
|
340
|
-
});
|
|
341
|
-
const dismiss = document.createElement("button");
|
|
342
|
-
dismiss.type = "button";
|
|
343
|
-
dismiss.className = "icon-button";
|
|
344
|
-
dismiss.setAttribute("aria-label", notificationDismissLabel(notification));
|
|
345
|
-
dismiss.setAttribute("aria-describedby", describedBy.join(" "));
|
|
346
|
-
dismiss.textContent = "×";
|
|
347
|
-
dismiss.addEventListener("click", () => {
|
|
348
|
-
state.notifications = dismissNotification(state.notifications, notification.id);
|
|
349
|
-
renderNotifications();
|
|
350
|
-
});
|
|
351
|
-
toast.append(text, dismiss);
|
|
352
|
-
return toast;
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
function notificationAnnouncement(notification) {
|
|
356
|
-
return [
|
|
357
|
-
notificationTitle(notification),
|
|
358
|
-
notification.title,
|
|
359
|
-
notification.summary,
|
|
360
|
-
notification.turnRef ? `Turn ref ${notification.turnRef}` : null,
|
|
361
|
-
formatRelativeTime(notification.timestamp),
|
|
362
|
-
].filter(Boolean).join(". ");
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function renderNotifications(additions = []) {
|
|
366
|
-
elements.toastList.replaceChildren(
|
|
367
|
-
...state.notifications.map(notificationToast),
|
|
368
|
-
);
|
|
369
|
-
const notificationHost = elements.dialog.open ? elements.dialog : elements.notificationHost;
|
|
370
|
-
const movingLayer = elements.notifications.parentNode !== notificationHost;
|
|
371
|
-
if ((movingLayer || additions.length > 0) && state.notifications.length > 0) {
|
|
372
|
-
try {
|
|
373
|
-
elements.notifications.hidePopover?.();
|
|
374
|
-
} catch {
|
|
375
|
-
// The notification layer is not open yet.
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
if (movingLayer) notificationHost.append(elements.notifications);
|
|
379
|
-
if (state.notifications.length === 0) {
|
|
380
|
-
try {
|
|
381
|
-
elements.notifications.hidePopover?.();
|
|
382
|
-
} catch {
|
|
383
|
-
// The notification layer is already closed.
|
|
384
|
-
}
|
|
385
|
-
elements.notifications.hidden = true;
|
|
386
|
-
} else {
|
|
387
|
-
elements.notifications.hidden = false;
|
|
388
|
-
try {
|
|
389
|
-
elements.notifications.showPopover?.();
|
|
390
|
-
} catch {
|
|
391
|
-
// Re-rendering an already-open manual popover needs no further action.
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
if (additions.length > 0) {
|
|
395
|
-
elements.notificationAnnouncer.textContent = additions
|
|
396
|
-
.map(notificationAnnouncement)
|
|
397
|
-
.join(". ");
|
|
398
|
-
}
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
export function showArchiveUpdate(task, message, { failed = false } = {}) {
|
|
402
|
-
const notification = Object.freeze({
|
|
403
|
-
id: `archive:${task.id}:${Date.now()}:${archiveUpdateSerial += 1}`,
|
|
404
|
-
taskId: task.id,
|
|
405
|
-
title: task.title,
|
|
406
|
-
status: task.status,
|
|
407
|
-
event: failed ? "archive_failed" : "archive_succeeded",
|
|
408
|
-
turnRef: null,
|
|
409
|
-
turnId: null,
|
|
410
|
-
timestamp: new Date().toISOString(),
|
|
411
|
-
summary: message,
|
|
412
|
-
});
|
|
413
|
-
state.notifications = [notification, ...state.notifications].slice(0, MAX_NOTIFICATIONS);
|
|
414
|
-
renderNotifications([notification]);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
function detailRow(term, value) {
|
|
418
|
-
const dt = document.createElement("dt");
|
|
419
|
-
dt.textContent = term;
|
|
420
|
-
const dd = document.createElement("dd");
|
|
421
|
-
if (value instanceof Node) dd.append(value);
|
|
422
|
-
else dd.textContent = value ?? "—";
|
|
423
|
-
return [dt, dd];
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
const tokenFormatter = new Intl.NumberFormat();
|
|
427
|
-
|
|
428
|
-
function formatEstimatedCost(value) {
|
|
429
|
-
if (typeof value !== "number") return "cost unavailable";
|
|
430
|
-
if (value === 0) return "estimated $0.00";
|
|
431
|
-
return `estimated $${value < 0.01 ? value.toFixed(4) : value.toFixed(2)}`;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
function usageBreakdownText(usage) {
|
|
435
|
-
return [
|
|
436
|
-
`${tokenFormatter.format(usage.inputTokens)} input`,
|
|
437
|
-
`${tokenFormatter.format(usage.cachedInputTokens)} cached input`,
|
|
438
|
-
`${tokenFormatter.format(usage.outputTokens)} output`,
|
|
439
|
-
`${tokenFormatter.format(usage.reasoningOutputTokens)} reasoning`,
|
|
440
|
-
].join(" · ");
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
export function renderTextShimmer(element, value) {
|
|
444
|
-
element.textContent = String(value ?? "");
|
|
445
|
-
return element;
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
export function usagePresentation(usage, { wholeTask = false } = {}) {
|
|
449
|
-
const container = document.createElement("div");
|
|
450
|
-
container.className = `usage-summary usage-${usage?.status ?? "unavailable"}`;
|
|
451
|
-
if (usage?.status === "pending" || usage?.status === "calculating") {
|
|
452
|
-
const text = document.createElement("span");
|
|
453
|
-
text.className = "text-shimmer";
|
|
454
|
-
renderTextShimmer(text, usage.status === "pending"
|
|
455
|
-
? "Tokens pending · available when turn finishes"
|
|
456
|
-
: "Calculating token usage…");
|
|
457
|
-
container.replaceChildren(text);
|
|
458
|
-
return container;
|
|
459
|
-
}
|
|
460
|
-
if (usage?.status !== "available") {
|
|
461
|
-
const headline = document.createElement("strong");
|
|
462
|
-
headline.textContent = "Token usage unavailable";
|
|
463
|
-
container.append(headline);
|
|
464
|
-
if (usage?.reason) {
|
|
465
|
-
const reason = document.createElement("span");
|
|
466
|
-
reason.textContent = usage.reason;
|
|
467
|
-
container.append(reason);
|
|
468
|
-
}
|
|
469
|
-
return container;
|
|
470
|
-
}
|
|
471
|
-
const headline = document.createElement("strong");
|
|
472
|
-
headline.textContent = `${tokenFormatter.format(usage.totalTokens)} tokens · ${formatEstimatedCost(usage.estimatedCostUsd)}${usage.knownSoFar ? " · known so far" : ""}`;
|
|
473
|
-
const breakdown = document.createElement("span");
|
|
474
|
-
breakdown.textContent = usageBreakdownText(usage);
|
|
475
|
-
container.append(headline, breakdown);
|
|
476
|
-
const provenance = document.createElement("span");
|
|
477
|
-
const version = usage.provenance?.version ? ` ${usage.provenance.version}` : "";
|
|
478
|
-
const pricingMode = usage.provenance?.pricingMode
|
|
479
|
-
? ` · ${usage.provenance.pricingMode === "online"
|
|
480
|
-
? "online pricing requested"
|
|
481
|
-
: usage.provenance.pricingMode === "standard" ? "standard API pricing" : "offline pricing fallback"}`
|
|
482
|
-
: "";
|
|
483
|
-
const freshness = usage.sourceUpdatedAt ?? usage.sampledAt;
|
|
484
|
-
provenance.textContent = `Source: ${usage.provenance?.provider === "taskchef" ? "TaskChef" : "ccusage (historical)"}${version}${pricingMode}${freshness ? ` · updated ${formatRelativeTime(freshness)}` : ""}. Dollar cost is an API-equivalent estimate${wholeTask ? " for the task" : " for this turn"}.`;
|
|
485
|
-
container.append(provenance);
|
|
486
|
-
return container;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
function usageStillCalculating(task) {
|
|
490
|
-
return task.usage?.status === "calculating"
|
|
491
|
-
|| (task.status !== "working" && Object.values(task.usage?.turns ?? {})
|
|
492
|
-
.some((turn) => turn.status === "calculating"));
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
function turnTimeline(task, { highlightTurnRef = null } = {}) {
|
|
496
|
-
if (task.turns.length === 0) {
|
|
497
|
-
const empty = document.createElement("p");
|
|
498
|
-
empty.className = "result-history-empty";
|
|
499
|
-
empty.textContent = "No executor turn has been reported yet.";
|
|
500
|
-
return [empty];
|
|
501
|
-
}
|
|
502
|
-
return [...task.turns].reverse().map((turn, index) => {
|
|
503
|
-
const item = document.createElement("article");
|
|
504
|
-
item.className = `result-history-item${index === 0 ? " result-history-latest" : ""}`;
|
|
505
|
-
const header = document.createElement("div");
|
|
506
|
-
header.className = "result-history-header";
|
|
507
|
-
const status = document.createElement("span");
|
|
508
|
-
const presentation = turnPresentation(turn);
|
|
509
|
-
const turnStatus = presentation.status;
|
|
510
|
-
status.className = `status status-${turnStatus}`;
|
|
511
|
-
status.textContent = turnStatus.replaceAll("_", " ");
|
|
512
|
-
const turnKey = turn.turnRef ?? turn.turnId ?? `no-turn:${index}`;
|
|
513
|
-
item.dataset.turnRef = turnKey;
|
|
514
|
-
if (highlightTurnRef !== null && turnKey === highlightTurnRef) {
|
|
515
|
-
item.className += " result-history-notified";
|
|
516
|
-
item.tabIndex = -1;
|
|
517
|
-
}
|
|
518
|
-
const timestamp = timestampControl(presentation.updatedAt, {
|
|
519
|
-
accessibleName: `Turn updated time for ${turnStatus.replaceAll("_", " ")}`,
|
|
520
|
-
key: `detail:${task.id}:turn:${turnKey}`,
|
|
521
|
-
});
|
|
522
|
-
header.append(status, timestamp);
|
|
523
|
-
if (presentation.sourceLabel) {
|
|
524
|
-
const source = document.createElement("span");
|
|
525
|
-
source.className = "result-history-source";
|
|
526
|
-
source.textContent = presentation.sourceLabel;
|
|
527
|
-
header.insertBefore(source, timestamp);
|
|
528
|
-
}
|
|
529
|
-
const requestLabel = document.createElement("h4");
|
|
530
|
-
requestLabel.textContent = "Request";
|
|
531
|
-
const request = document.createElement("p");
|
|
532
|
-
request.className = "preserve-lines";
|
|
533
|
-
appendLinkedText(
|
|
534
|
-
request,
|
|
535
|
-
turn.requestSummary ?? "Request not recorded by this TaskChef version.",
|
|
536
|
-
task,
|
|
537
|
-
);
|
|
538
|
-
const resultLabel = document.createElement("h4");
|
|
539
|
-
resultLabel.textContent = "Result";
|
|
540
|
-
const result = document.createElement("p");
|
|
541
|
-
result.className = `preserve-lines${turnStatus === "working" ? " text-shimmer" : ""}`;
|
|
542
|
-
if (turnStatus === "working") renderTextShimmer(result, presentation.summary);
|
|
543
|
-
else appendLinkedText(result, presentation.summary, task);
|
|
544
|
-
const turnMetadata = document.createElement("p");
|
|
545
|
-
turnMetadata.className = "result-history-turn";
|
|
546
|
-
turnMetadata.textContent = `Turn ref ${turn.turnRef ?? "not recorded"}; Codex turn ${turn.turnId ?? "unavailable"}`;
|
|
547
|
-
const recordedUsage = task.usage?.turns?.[turn.turnRef ?? turn.turnId];
|
|
548
|
-
const turnUsage = turn.result === null
|
|
549
|
-
? { status: "pending" }
|
|
550
|
-
: recordedUsage ?? {
|
|
551
|
-
status: "unavailable",
|
|
552
|
-
reason: "No reliable turn boundary is available.",
|
|
553
|
-
};
|
|
554
|
-
item.append(
|
|
555
|
-
header,
|
|
556
|
-
requestLabel,
|
|
557
|
-
request,
|
|
558
|
-
resultLabel,
|
|
559
|
-
result,
|
|
560
|
-
usagePresentation(turnUsage),
|
|
561
|
-
turnMetadata,
|
|
562
|
-
);
|
|
563
|
-
return item;
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
function descendantPath(root, descendant) {
|
|
568
|
-
const path = [];
|
|
569
|
-
let current = descendant;
|
|
570
|
-
while (current && current !== root) {
|
|
571
|
-
const parent = current.parentNode;
|
|
572
|
-
if (!parent) return null;
|
|
573
|
-
const index = Array.prototype.indexOf.call(parent.children, current);
|
|
574
|
-
if (index < 0) return null;
|
|
575
|
-
path.unshift(index);
|
|
576
|
-
current = parent;
|
|
577
|
-
}
|
|
578
|
-
return current === root ? path : null;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
function descendantAtPath(root, path) {
|
|
582
|
-
let current = root;
|
|
583
|
-
for (const index of path ?? []) {
|
|
584
|
-
current = current?.children?.[index];
|
|
585
|
-
if (!current) return null;
|
|
586
|
-
}
|
|
587
|
-
return current;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
function descendantWithFocusKey(root, focusKey) {
|
|
591
|
-
if (!root || !focusKey) return null;
|
|
592
|
-
if (root.dataset?.focusKey === focusKey) return root;
|
|
593
|
-
for (const child of root.children ?? []) {
|
|
594
|
-
const match = descendantWithFocusKey(child, focusKey);
|
|
595
|
-
if (match) return match;
|
|
596
|
-
}
|
|
597
|
-
return null;
|
|
598
|
-
}
|
|
599
|
-
|
|
600
|
-
function manualTransitionPending() {
|
|
601
|
-
return state.manualTransition?.stage === "pending";
|
|
602
|
-
}
|
|
603
|
-
|
|
604
|
-
function resetManualTransition({ focus = false } = {}) {
|
|
605
|
-
state.manualTransition = null;
|
|
606
|
-
elements.closeDialog.disabled = false;
|
|
607
|
-
if (state.selectedTask) renderManualTransition(state.selectedTask);
|
|
608
|
-
if (focus) elements.moreTaskActions.focus();
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
function replaceCurrentTask(task) {
|
|
612
|
-
state.tasks = state.tasks.map((candidate) => candidate.id === task.id ? task : candidate);
|
|
613
|
-
state.selectedTask = task;
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
function renderManualTransition(task) {
|
|
617
|
-
const activeElement = document.activeElement;
|
|
618
|
-
const focusWasInPanel = elements.manualTransitionPanel.contains?.(activeElement) ?? false;
|
|
619
|
-
state.manualTransition = reconcileManualTransition(state.manualTransition, task);
|
|
620
|
-
const pending = manualTransitionPending();
|
|
621
|
-
const expanded = Boolean(state.manualTransition);
|
|
622
|
-
elements.moreTaskActions.disabled = pending;
|
|
623
|
-
elements.moreTaskActions.textContent = expanded ? "←" : "…";
|
|
624
|
-
elements.moreTaskActions.setAttribute(
|
|
625
|
-
"aria-label",
|
|
626
|
-
expanded ? "Hide more task actions" : "More task actions",
|
|
627
|
-
);
|
|
628
|
-
elements.moreTaskActions.setAttribute("title", expanded ? "Hide more task actions" : "More task actions");
|
|
629
|
-
elements.moreTaskActions.setAttribute("aria-expanded", String(expanded));
|
|
630
|
-
elements.closeDialog.disabled = pending;
|
|
631
|
-
elements.manualTransitionPanel.hidden = state.manualTransition === null;
|
|
632
|
-
elements.manualTransitionPanel.setAttribute("aria-busy", String(pending));
|
|
633
|
-
elements.copyTaskId.disabled = pending || !task.id;
|
|
634
|
-
elements.markTaskCompleted.hidden = !canManuallyTransitionTask(task, "completed");
|
|
635
|
-
elements.markTaskFailed.hidden = !canManuallyTransitionTask(task, "failed");
|
|
636
|
-
elements.markTaskCompleted.disabled = pending;
|
|
637
|
-
elements.markTaskFailed.disabled = pending;
|
|
638
|
-
const archivePending = state.archivePendingThreadIds.has(task.threadId);
|
|
639
|
-
const archived = state.archivedThreadIds.has(task.threadId);
|
|
640
|
-
elements.archiveTask.disabled = pending || archivePending || archived;
|
|
641
|
-
elements.archiveTask.textContent = archived ? "Archived" : archivePending ? "Archiving…" : "Archive chat";
|
|
642
|
-
elements.archiveTask.setAttribute(
|
|
643
|
-
"aria-label",
|
|
644
|
-
archived
|
|
645
|
-
? `${task.title} is archived in Codex`
|
|
646
|
-
: archivePending
|
|
647
|
-
? `Archiving ${task.title} in Codex`
|
|
648
|
-
: `Archive ${task.title} in Codex`,
|
|
649
|
-
);
|
|
650
|
-
elements.manualTransitionStatus.hidden = !pending;
|
|
651
|
-
elements.manualTransitionStatus.textContent = pending ? "Saving task state…" : "";
|
|
652
|
-
const error = state.manualTransition?.error ?? "";
|
|
653
|
-
elements.manualTransitionError.hidden = !error;
|
|
654
|
-
elements.manualTransitionError.textContent = error;
|
|
655
|
-
if (!state.manualTransition) {
|
|
656
|
-
elements.closeDialog.disabled = false;
|
|
657
|
-
if (focusWasInPanel) elements.moreTaskActions.focus();
|
|
658
|
-
return;
|
|
659
|
-
}
|
|
660
|
-
if (pending) {
|
|
661
|
-
focusManualTransitionStatus(elements.manualTransitionPanel);
|
|
662
|
-
} else {
|
|
663
|
-
restoreTaskActionMenuFocus(
|
|
664
|
-
elements.manualTransitionPanel,
|
|
665
|
-
activeElement,
|
|
666
|
-
elements.moreTaskActions,
|
|
667
|
-
);
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
async function submitManualTransition(targetStatus, event) {
|
|
672
|
-
const task = state.selectedTask;
|
|
673
|
-
if (
|
|
674
|
-
!task
|
|
675
|
-
|| !canManuallyTransitionTask(task, targetStatus)
|
|
676
|
-
|| manualTransitionPending()
|
|
677
|
-
) return;
|
|
678
|
-
const previous = state.manualTransition;
|
|
679
|
-
const actionId = previous?.targetStatus === targetStatus && previous.actionId
|
|
680
|
-
? previous.actionId
|
|
681
|
-
: crypto.randomUUID();
|
|
682
|
-
const attempt = {
|
|
683
|
-
...previous,
|
|
684
|
-
taskId: task.id,
|
|
685
|
-
stage: "pending",
|
|
686
|
-
targetStatus,
|
|
687
|
-
actionId,
|
|
688
|
-
expected: previous?.expected ?? manualTransitionExpectedState(task),
|
|
689
|
-
error: null,
|
|
690
|
-
};
|
|
691
|
-
state.manualTransition = attempt;
|
|
692
|
-
renderManualTransition(task);
|
|
693
|
-
const result = await manuallyTransitionTaskFromControl(
|
|
694
|
-
event,
|
|
695
|
-
{ ...task, ...attempt.expected },
|
|
696
|
-
targetStatus,
|
|
697
|
-
attempt.actionId,
|
|
698
|
-
);
|
|
699
|
-
const current = reconcileManualTransitionResponse({
|
|
700
|
-
requestTask: task,
|
|
701
|
-
expected: attempt.expected,
|
|
702
|
-
responseTask: result.task,
|
|
703
|
-
selectedTask: state.selectedTask,
|
|
704
|
-
});
|
|
705
|
-
if (result.ok) {
|
|
706
|
-
state.manualTransition = null;
|
|
707
|
-
if (current === result.task) replaceCurrentTask(result.task);
|
|
708
|
-
renderDialog(current);
|
|
709
|
-
render();
|
|
710
|
-
elements.dialogTitle.focus?.();
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
if (current === result.task) replaceCurrentTask(result.task);
|
|
714
|
-
if (
|
|
715
|
-
!canManuallyTransitionTask(current)
|
|
716
|
-
|| !taskMatchesManualTransitionExpected(current, attempt.expected)
|
|
717
|
-
) {
|
|
718
|
-
state.manualTransition = null;
|
|
719
|
-
renderDialog(current);
|
|
720
|
-
render();
|
|
721
|
-
showMessage("This task changed. Review its current state before trying again.");
|
|
722
|
-
elements.dialogTitle.focus?.();
|
|
723
|
-
return;
|
|
724
|
-
}
|
|
725
|
-
state.manualTransition = {
|
|
726
|
-
...attempt,
|
|
727
|
-
stage: "choose",
|
|
728
|
-
error: result.message,
|
|
729
|
-
actionId: result.code === "stale_task" ? crypto.randomUUID() : attempt.actionId,
|
|
730
|
-
};
|
|
731
|
-
renderDialog(current);
|
|
732
|
-
elements.manualTransitionError.focus();
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
function renderDialog(task, { highlightTurnRef = null } = {}) {
|
|
736
|
-
if (state.selectedTask?.id !== task.id) {
|
|
737
|
-
copyTaskIdGeneration += 1;
|
|
738
|
-
clearTimeout(copyTaskIdTimer);
|
|
739
|
-
copyTaskIdTimer = null;
|
|
740
|
-
}
|
|
741
|
-
const preservedResults = state.selectedTask?.id === task.id
|
|
742
|
-
? state.selectedTask.results
|
|
743
|
-
: null;
|
|
744
|
-
const preservedUsage = state.selectedTask?.id === task.id
|
|
745
|
-
? state.selectedTask.usage
|
|
746
|
-
: null;
|
|
747
|
-
const detailedTask = {
|
|
748
|
-
...task,
|
|
749
|
-
turns: mergeProjectedTurns(task, state.selectedTask?.turns ?? []),
|
|
750
|
-
results: task.results ?? preservedResults ?? [],
|
|
751
|
-
usage: task.usage ?? preservedUsage ?? null,
|
|
752
|
-
};
|
|
753
|
-
const focusedElement = elements.dialogResults.contains?.(document.activeElement)
|
|
754
|
-
? document.activeElement
|
|
755
|
-
: null;
|
|
756
|
-
let focusedTurn = focusedElement;
|
|
757
|
-
while (focusedTurn && focusedTurn !== elements.dialogResults && !focusedTurn.dataset?.turnRef) {
|
|
758
|
-
focusedTurn = focusedTurn.parentNode;
|
|
759
|
-
}
|
|
760
|
-
const focusedTurnRef = focusedTurn?.dataset?.turnRef ?? null;
|
|
761
|
-
const focusedDescendantKey = focusedElement?.dataset?.focusKey ?? null;
|
|
762
|
-
const focusedDescendantPath = focusedTurn && focusedElement
|
|
763
|
-
? descendantPath(focusedTurn, focusedElement)
|
|
764
|
-
: null;
|
|
765
|
-
state.selectedTask = detailedTask;
|
|
766
|
-
renderProject(elements.dialogProject, task);
|
|
767
|
-
elements.dialogTitle.textContent = task.title;
|
|
768
|
-
elements.dialogRelatedLinks.replaceChildren(...relatedGitHubLinks(detailedTask).children);
|
|
769
|
-
elements.dialogRelatedLinks.setAttribute(
|
|
770
|
-
"aria-label",
|
|
771
|
-
`Related GitHub links for ${task.title}`,
|
|
772
|
-
);
|
|
773
|
-
elements.dialogRelatedLinks.hidden = (
|
|
774
|
-
(task.relatedGitHubLinks?.length ?? 0) === 0 && !task.relatedGitHubLinksTruncated
|
|
775
|
-
);
|
|
776
|
-
const timeline = turnTimeline(detailedTask, { highlightTurnRef });
|
|
777
|
-
elements.dialogResults.replaceChildren(...timeline);
|
|
778
|
-
const highlightedTurn = timeline.find((item) => item.dataset.turnRef === highlightTurnRef) ?? null;
|
|
779
|
-
const replacedFocusedTurn = timeline.find((item) => item.dataset.turnRef === focusedTurnRef) ?? null;
|
|
780
|
-
if (replacedFocusedTurn) {
|
|
781
|
-
const pathMatchedElement = descendantAtPath(replacedFocusedTurn, focusedDescendantPath);
|
|
782
|
-
const replacedFocusedElement = (
|
|
783
|
-
pathMatchedElement?.dataset?.focusKey === focusedDescendantKey
|
|
784
|
-
? pathMatchedElement
|
|
785
|
-
: null
|
|
786
|
-
) ?? descendantWithFocusKey(replacedFocusedTurn, focusedDescendantKey);
|
|
787
|
-
if (replacedFocusedElement && replacedFocusedElement !== replacedFocusedTurn) {
|
|
788
|
-
replacedFocusedElement.focus?.();
|
|
789
|
-
} else {
|
|
790
|
-
replacedFocusedTurn.tabIndex = -1;
|
|
791
|
-
replacedFocusedTurn.focus?.();
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
const currentTurnRef = task.turnRef ?? task.latestTurn?.turnRef ?? null;
|
|
795
|
-
const preservedUsageIsPriorGeneration = task.usage == null
|
|
796
|
-
&& preservedUsage != null
|
|
797
|
-
&& currentTurnRef != null
|
|
798
|
-
&& preservedUsage.generationTurnRef !== currentTurnRef;
|
|
799
|
-
const taskUsage = detailedTask.usage?.task
|
|
800
|
-
&& (detailedTask.usage.status === "available" || task.status === "working")
|
|
801
|
-
&& (!preservedUsageIsPriorGeneration || task.status === "working")
|
|
802
|
-
? {
|
|
803
|
-
status: "available",
|
|
804
|
-
...detailedTask.usage.task,
|
|
805
|
-
knownSoFar: task.status === "working",
|
|
806
|
-
}
|
|
807
|
-
: detailedTask.usage?.status === "calculating" || preservedUsageIsPriorGeneration
|
|
808
|
-
? { ...detailedTask.usage, status: task.status === "working" ? "pending" : "calculating" }
|
|
809
|
-
: detailedTask.usage ?? {
|
|
810
|
-
status: task.status === "working"
|
|
811
|
-
? "pending"
|
|
812
|
-
: task.threadId
|
|
813
|
-
? "calculating"
|
|
814
|
-
: "unavailable",
|
|
815
|
-
};
|
|
816
|
-
elements.dialogUsage.replaceChildren(usagePresentation(
|
|
817
|
-
taskUsage,
|
|
818
|
-
{ wholeTask: true },
|
|
819
|
-
));
|
|
820
|
-
elements.dialogInstruction.textContent = task.instruction;
|
|
821
|
-
elements.copyTaskId.disabled = !task.id;
|
|
822
|
-
setCopyTaskIdLabel("Copy Task ID");
|
|
823
|
-
elements.dialogMetadata.replaceChildren(
|
|
824
|
-
...detailRow("Current status", taskStatusLabel(task)),
|
|
825
|
-
...detailRow("Current turn ref", task.turnRef),
|
|
826
|
-
...detailRow("Current Codex turn ID", task.turnId),
|
|
827
|
-
...detailRow("Last result status", task.lastResult?.status?.replaceAll("_", " ")),
|
|
828
|
-
...detailRow("Last result turn ref", task.lastResult?.turnRef),
|
|
829
|
-
...detailRow("Last result Codex turn ID", task.lastResult?.turnId),
|
|
830
|
-
...detailRow("Last result updated", timestampControl(task.lastResult?.updatedAt, {
|
|
831
|
-
accessibleName: `Last result updated time for ${task.title}`,
|
|
832
|
-
key: `detail:${task.id}:last-result-updated`,
|
|
833
|
-
})),
|
|
834
|
-
...detailRow("Task ID", task.id),
|
|
835
|
-
...detailRow("Thread ID", task.threadId),
|
|
836
|
-
...detailRow("Project path", task.project.path),
|
|
837
|
-
...detailRow("Created", timestampControl(task.createdAt, {
|
|
838
|
-
accessibleName: `Created time for ${task.title}`,
|
|
839
|
-
key: `detail:${task.id}:created`,
|
|
840
|
-
})),
|
|
841
|
-
...detailRow("Updated", timestampControl(
|
|
842
|
-
task.meaningfulUpdatedAt ?? task.updatedAt ?? task.createdAt,
|
|
843
|
-
{
|
|
844
|
-
accessibleName: `Updated time for ${task.title}`,
|
|
845
|
-
key: `detail:${task.id}:updated`,
|
|
846
|
-
},
|
|
847
|
-
)),
|
|
848
|
-
...detailRow("Updated by", task.updatedBy),
|
|
849
|
-
);
|
|
850
|
-
configureOpenTaskControl(elements.openProject, `Open ${task.title} in Codex`);
|
|
851
|
-
const canArchive = canArchiveTask(task);
|
|
852
|
-
const archived = state.archivedThreadIds.has(task.threadId);
|
|
853
|
-
const archivePending = state.archivePendingThreadIds.has(task.threadId);
|
|
854
|
-
elements.archiveTask.hidden = !canArchive;
|
|
855
|
-
elements.archiveTask.disabled = archived || archivePending;
|
|
856
|
-
elements.archiveTask.textContent = archived ? "Archived" : archivePending ? "Archiving…" : "Archive chat";
|
|
857
|
-
elements.archiveTask.setAttribute(
|
|
858
|
-
"aria-label",
|
|
859
|
-
archived ? `${task.title} is archived in Codex` : `Archive ${task.title} in Codex`,
|
|
860
|
-
);
|
|
861
|
-
renderManualTransition(detailedTask);
|
|
862
|
-
return highlightedTurn;
|
|
863
|
-
}
|
|
864
|
-
|
|
865
|
-
async function openDialog(task, {
|
|
866
|
-
focus = false,
|
|
867
|
-
highlightTurnRef = null,
|
|
868
|
-
preserveNavigation = false,
|
|
869
|
-
} = {}) {
|
|
870
|
-
if (focus) {
|
|
871
|
-
state.detailNavigation = {
|
|
872
|
-
focusPending: true,
|
|
873
|
-
highlightTurnRef,
|
|
874
|
-
taskId: task.id,
|
|
875
|
-
};
|
|
876
|
-
} else if (!preserveNavigation) state.detailNavigation = null;
|
|
877
|
-
const navigation = state.detailNavigation?.taskId === task.id
|
|
878
|
-
? state.detailNavigation
|
|
879
|
-
: null;
|
|
880
|
-
const activeHighlightTurnRef = navigation?.highlightTurnRef ?? highlightTurnRef;
|
|
881
|
-
const requestGeneration = ++detailRequestGeneration;
|
|
882
|
-
renderDialog(task, { highlightTurnRef: activeHighlightTurnRef });
|
|
883
|
-
if (!elements.dialog.open) {
|
|
884
|
-
elements.dialog.showModal();
|
|
885
|
-
renderNotifications();
|
|
886
|
-
}
|
|
887
|
-
const load = async (attempt = 0) => {
|
|
888
|
-
try {
|
|
889
|
-
const response = await fetch(`/api/tasks/${encodeURIComponent(task.id)}`);
|
|
890
|
-
if (!response.ok) throw new Error("Task details are unavailable.");
|
|
891
|
-
const detail = await response.json();
|
|
892
|
-
if (
|
|
893
|
-
requestGeneration === detailRequestGeneration
|
|
894
|
-
&& state.selectedTask?.id === task.id
|
|
895
|
-
&& elements.dialog.open
|
|
896
|
-
) {
|
|
897
|
-
const activeNavigation = state.detailNavigation?.taskId === task.id
|
|
898
|
-
? state.detailNavigation
|
|
899
|
-
: null;
|
|
900
|
-
const highlightedTurn = renderDialog(detail.task, {
|
|
901
|
-
highlightTurnRef: activeNavigation?.highlightTurnRef ?? activeHighlightTurnRef,
|
|
902
|
-
});
|
|
903
|
-
if (activeNavigation?.focusPending) {
|
|
904
|
-
(highlightedTurn ?? elements.dialogTitle).focus?.();
|
|
905
|
-
activeNavigation.focusPending = false;
|
|
906
|
-
}
|
|
907
|
-
if (usageStillCalculating(detail.task) && attempt < MAX_USAGE_POLL_ATTEMPTS) {
|
|
908
|
-
setTimeout(() => load(attempt + 1), USAGE_POLL_INTERVAL_MS);
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
} catch {
|
|
912
|
-
if (
|
|
913
|
-
requestGeneration === detailRequestGeneration
|
|
914
|
-
&& state.selectedTask?.id === task.id
|
|
915
|
-
&& elements.dialog.open
|
|
916
|
-
) {
|
|
917
|
-
showMessage("Task activity timeline is temporarily unavailable.");
|
|
918
|
-
const activeNavigation = state.detailNavigation?.taskId === task.id
|
|
919
|
-
? state.detailNavigation
|
|
920
|
-
: null;
|
|
921
|
-
if (activeNavigation?.focusPending) {
|
|
922
|
-
elements.dialogTitle.focus?.();
|
|
923
|
-
activeNavigation.focusPending = false;
|
|
924
|
-
}
|
|
925
|
-
}
|
|
926
|
-
}
|
|
927
|
-
};
|
|
928
|
-
await load();
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
function taskCard(task) {
|
|
932
|
-
const article = document.createElement("article");
|
|
933
|
-
article.className = "task-card";
|
|
934
|
-
const heading = document.createElement("div");
|
|
935
|
-
heading.className = "task-heading";
|
|
936
|
-
const title = document.createElement("button");
|
|
937
|
-
title.type = "button";
|
|
938
|
-
title.className = "task-title";
|
|
939
|
-
title.textContent = task.title;
|
|
940
|
-
title.addEventListener("click", () => openDialog(task));
|
|
941
|
-
const badge = document.createElement("span");
|
|
942
|
-
badge.className = `status status-${task.status ?? "unresolved"}`;
|
|
943
|
-
badge.textContent = taskStatusLabel(task);
|
|
944
|
-
heading.append(title, badge);
|
|
945
|
-
const project = document.createElement("p");
|
|
946
|
-
project.className = "task-project";
|
|
947
|
-
renderProject(project, task);
|
|
948
|
-
const summary = document.createElement("p");
|
|
949
|
-
summary.className = "task-summary";
|
|
950
|
-
const latest = latestTurnPresentation(task);
|
|
951
|
-
const requestLabel = document.createElement("strong");
|
|
952
|
-
requestLabel.textContent = "Request";
|
|
953
|
-
const request = document.createElement("span");
|
|
954
|
-
request.className = "preserve-lines";
|
|
955
|
-
appendLinkedText(request, latest.requestSummary, task);
|
|
956
|
-
const resultLabel = document.createElement("strong");
|
|
957
|
-
resultLabel.textContent = "Result";
|
|
958
|
-
const result = document.createElement("span");
|
|
959
|
-
result.className = `preserve-lines${latest.resultStatus === "working" ? " text-shimmer" : ""}`;
|
|
960
|
-
if (latest.resultStatus === "working") renderTextShimmer(result, latest.resultSummary);
|
|
961
|
-
else appendLinkedText(result, latest.resultSummary, task);
|
|
962
|
-
summary.replaceChildren(requestLabel, request, resultLabel, result);
|
|
963
|
-
const relatedLinks = relatedGitHubLinks(task, { compact: true });
|
|
964
|
-
const time = timestampControl(
|
|
965
|
-
task.meaningfulUpdatedAt ?? task.updatedAt ?? task.createdAt,
|
|
966
|
-
{
|
|
967
|
-
accessibleName: `Updated time for ${task.title}`,
|
|
968
|
-
key: `list:${task.id}:updated`,
|
|
969
|
-
prefix: "Updated ",
|
|
970
|
-
},
|
|
971
|
-
);
|
|
972
|
-
const footer = document.createElement("div");
|
|
973
|
-
footer.className = "task-footer";
|
|
974
|
-
const openTask = document.createElement("button");
|
|
975
|
-
openTask.type = "button";
|
|
976
|
-
openTask.className = "secondary-button task-open";
|
|
977
|
-
configureOpenTaskControl(openTask, `Open ${task.title} in Codex`);
|
|
978
|
-
openTask.addEventListener("click", (event) => openTaskFromControl(event, task.id, {
|
|
979
|
-
showMessage,
|
|
980
|
-
}));
|
|
981
|
-
footer.append(time, openTask);
|
|
982
|
-
article.append(heading, project, summary, relatedLinks, footer);
|
|
983
|
-
return article;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
function render() {
|
|
987
|
-
const project = elements.projectFilter.value;
|
|
988
|
-
const status = elements.statusFilter.querySelector("input:checked")?.value ?? "";
|
|
989
|
-
const date = elements.dateFilter.value;
|
|
990
|
-
const now = Date.now();
|
|
991
|
-
const contextualTasks = filterTasks(state.tasks, { project, date, now });
|
|
992
|
-
const visible = filterTasks(contextualTasks, { status, now });
|
|
993
|
-
const counts = statusFilterCounts(state.tasks, { project, date, now });
|
|
994
|
-
for (const label of elements.statusFilter.querySelectorAll("[data-status-label]")) {
|
|
995
|
-
const value = label.dataset.statusLabel;
|
|
996
|
-
label.textContent = statusFilterText(value, counts[value]);
|
|
997
|
-
}
|
|
998
|
-
elements.taskList.replaceChildren(...visible.map(taskCard));
|
|
999
|
-
elements.emptyState.hidden = visible.length > 0;
|
|
1000
|
-
elements.taskCount.textContent = `${visible.length} of ${state.tasks.length} task${state.tasks.length === 1 ? "" : "s"}`;
|
|
1001
|
-
clearTimeout(dateRefreshTimer);
|
|
1002
|
-
const refreshDelay = nextDateFilterRefreshDelay(contextualTasks, date, now);
|
|
1003
|
-
dateRefreshTimer = refreshDelay === null ? null : setTimeout(render, refreshDelay);
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
|
-
function selectStatusFromKeyboard(event) {
|
|
1007
|
-
if (event.target?.name !== "status-filter") return;
|
|
1008
|
-
const inputs = [...elements.statusFilter.querySelectorAll('input[name="status-filter"]')];
|
|
1009
|
-
const currentIndex = inputs.indexOf(event.target);
|
|
1010
|
-
if (currentIndex < 0) return;
|
|
1011
|
-
let nextIndex = currentIndex;
|
|
1012
|
-
if (["ArrowRight", "ArrowDown"].includes(event.key)) nextIndex = (currentIndex + 1) % inputs.length;
|
|
1013
|
-
else if (["ArrowLeft", "ArrowUp"].includes(event.key)) {
|
|
1014
|
-
nextIndex = (currentIndex - 1 + inputs.length) % inputs.length;
|
|
1015
|
-
} else if (event.key === "Home") nextIndex = 0;
|
|
1016
|
-
else if (event.key === "End") nextIndex = inputs.length - 1;
|
|
1017
|
-
else if (![" ", "Enter"].includes(event.key)) return;
|
|
1018
|
-
event.preventDefault();
|
|
1019
|
-
for (const input of inputs) input.checked = false;
|
|
1020
|
-
inputs[nextIndex].checked = true;
|
|
1021
|
-
inputs[nextIndex].focus();
|
|
1022
|
-
render();
|
|
1023
|
-
}
|
|
1024
|
-
|
|
1025
|
-
function applySnapshot(snapshot) {
|
|
1026
|
-
const reconciled = reconcileNotifications({
|
|
1027
|
-
initialized: state.initialized,
|
|
1028
|
-
notifications: state.notifications,
|
|
1029
|
-
seenIds: state.seenNotificationIds,
|
|
1030
|
-
signatures: state.signatures,
|
|
1031
|
-
}, snapshot.tasks);
|
|
1032
|
-
state.tasks = snapshot.tasks;
|
|
1033
|
-
state.signatures = reconciled.signatures;
|
|
1034
|
-
state.notifications = reconciled.notifications;
|
|
1035
|
-
state.seenNotificationIds = reconciled.seenIds;
|
|
1036
|
-
state.initialized = true;
|
|
1037
|
-
if (snapshot.healthy === false) {
|
|
1038
|
-
showMessage("The task log is temporarily unavailable. Showing the last valid snapshot.");
|
|
1039
|
-
} else {
|
|
1040
|
-
elements.dashboardMessage.hidden = true;
|
|
1041
|
-
}
|
|
1042
|
-
replaceOptions(
|
|
1043
|
-
elements.projectFilter,
|
|
1044
|
-
[...new Set(state.tasks.map((task) => task.project.name))].sort(),
|
|
1045
|
-
"All projects",
|
|
1046
|
-
);
|
|
1047
|
-
if (state.selectedTask) {
|
|
1048
|
-
const updated = state.tasks.find((task) => task.id === state.selectedTask.id);
|
|
1049
|
-
if (updated && elements.dialog.open) openDialog(updated, { preserveNavigation: true });
|
|
1050
|
-
}
|
|
1051
|
-
renderNotifications(reconciled.additions);
|
|
1052
|
-
render();
|
|
1053
|
-
}
|
|
1054
|
-
|
|
1055
|
-
function showMessage(message) {
|
|
1056
|
-
elements.dashboardMessageText.textContent = message;
|
|
1057
|
-
elements.dashboardMessage.hidden = false;
|
|
1058
|
-
}
|
|
1059
|
-
|
|
1060
|
-
void loadDashboardVersion();
|
|
1061
|
-
|
|
1062
|
-
const events = new EventSource("/api/events");
|
|
1063
|
-
events.addEventListener("open", () => setConnection(true));
|
|
1064
|
-
events.addEventListener("error", () => setConnection(false));
|
|
1065
|
-
events.addEventListener("snapshot", (event) => {
|
|
1066
|
-
setConnection(true);
|
|
1067
|
-
applySnapshot(JSON.parse(event.data));
|
|
1068
|
-
});
|
|
1069
|
-
events.addEventListener("dashboard-error", (event) => {
|
|
1070
|
-
showMessage(JSON.parse(event.data).message);
|
|
1071
|
-
});
|
|
1072
|
-
|
|
1073
|
-
elements.projectFilter.addEventListener("change", render);
|
|
1074
|
-
elements.statusFilter.addEventListener("change", render);
|
|
1075
|
-
elements.statusFilter.addEventListener("keydown", selectStatusFromKeyboard);
|
|
1076
|
-
elements.dateFilter.addEventListener("change", render);
|
|
1077
|
-
elements.dismissDashboardMessage.addEventListener("click", () => {
|
|
1078
|
-
elements.dashboardMessage.hidden = true;
|
|
1079
|
-
});
|
|
1080
|
-
elements.clearNotifications.addEventListener("click", () => {
|
|
1081
|
-
state.notifications = clearNotifications();
|
|
1082
|
-
renderNotifications();
|
|
1083
|
-
});
|
|
1084
|
-
elements.closeDialog.addEventListener("click", () => {
|
|
1085
|
-
if (!manualTransitionPending()) elements.dialog.close();
|
|
1086
|
-
});
|
|
1087
|
-
elements.dialog.addEventListener("click", (event) => {
|
|
1088
|
-
if (event.target === elements.dialog && !manualTransitionPending()) elements.dialog.close();
|
|
1089
|
-
});
|
|
1090
|
-
elements.dialog.addEventListener("keydown", (event) => {
|
|
1091
|
-
handleManualTransitionEscape(event, {
|
|
1092
|
-
active: Boolean(state.manualTransition),
|
|
1093
|
-
pending: manualTransitionPending(),
|
|
1094
|
-
cancel: () => resetManualTransition({ focus: true }),
|
|
1095
|
-
});
|
|
1096
|
-
});
|
|
1097
|
-
elements.dialog.addEventListener("cancel", (event) => {
|
|
1098
|
-
if (!state.manualTransition) return;
|
|
1099
|
-
event.preventDefault();
|
|
1100
|
-
if (!manualTransitionPending()) resetManualTransition({ focus: true });
|
|
1101
|
-
});
|
|
1102
|
-
elements.dialog.addEventListener("close", () => {
|
|
1103
|
-
if (!manualTransitionPending()) state.manualTransition = null;
|
|
1104
|
-
state.detailNavigation = null;
|
|
1105
|
-
renderNotifications();
|
|
1106
|
-
});
|
|
1107
|
-
elements.moreTaskActions.addEventListener("click", () => {
|
|
1108
|
-
const task = state.selectedTask;
|
|
1109
|
-
if (!task || manualTransitionPending()) return;
|
|
1110
|
-
if (state.manualTransition) {
|
|
1111
|
-
resetManualTransition({ focus: true });
|
|
1112
|
-
return;
|
|
1113
|
-
}
|
|
1114
|
-
state.manualTransition = {
|
|
1115
|
-
taskId: task.id,
|
|
1116
|
-
stage: "choose",
|
|
1117
|
-
expected: manualTransitionExpectedState(task),
|
|
1118
|
-
};
|
|
1119
|
-
renderManualTransition(task);
|
|
1120
|
-
elements.copyTaskId.focus();
|
|
1121
|
-
});
|
|
1122
|
-
elements.markTaskCompleted.addEventListener("click", (event) => {
|
|
1123
|
-
return submitManualTransition("completed", event);
|
|
1124
|
-
});
|
|
1125
|
-
elements.markTaskFailed.addEventListener("click", (event) => {
|
|
1126
|
-
return submitManualTransition("failed", event);
|
|
1127
|
-
});
|
|
1128
|
-
elements.copyTaskId.addEventListener("click", async () => {
|
|
1129
|
-
const taskId = state.selectedTask?.id;
|
|
1130
|
-
if (!taskId) return;
|
|
1131
|
-
const copyGeneration = ++copyTaskIdGeneration;
|
|
1132
|
-
clearTimeout(copyTaskIdTimer);
|
|
1133
|
-
copyTaskIdTimer = null;
|
|
1134
|
-
try {
|
|
1135
|
-
await navigator.clipboard.writeText(taskId);
|
|
1136
|
-
if (copyGeneration !== copyTaskIdGeneration || state.selectedTask?.id !== taskId) return;
|
|
1137
|
-
setCopyTaskIdLabel("Task ID copied");
|
|
1138
|
-
copyTaskIdTimer = setTimeout(() => {
|
|
1139
|
-
if (copyGeneration === copyTaskIdGeneration && state.selectedTask?.id === taskId) {
|
|
1140
|
-
setCopyTaskIdLabel("Copy Task ID");
|
|
1141
|
-
}
|
|
1142
|
-
copyTaskIdTimer = null;
|
|
1143
|
-
}, 1_500);
|
|
1144
|
-
} catch {
|
|
1145
|
-
if (copyGeneration === copyTaskIdGeneration && state.selectedTask?.id === taskId) {
|
|
1146
|
-
showMessage("Clipboard access is unavailable. Copy the Task ID from the metadata below.");
|
|
1147
|
-
}
|
|
1148
|
-
}
|
|
1149
|
-
});
|
|
1150
|
-
elements.openProject.addEventListener("click", async (event) => {
|
|
1151
|
-
if (!state.selectedTask) return;
|
|
1152
|
-
await openTaskFromControl(event, state.selectedTask.id, { showMessage });
|
|
1153
|
-
});
|
|
1154
|
-
elements.archiveTask.addEventListener("click", async (event) => {
|
|
1155
|
-
const task = state.selectedTask;
|
|
1156
|
-
if (
|
|
1157
|
-
!task
|
|
1158
|
-
|| !canArchiveTask(task)
|
|
1159
|
-
|| state.archivePendingThreadIds.has(task.threadId)
|
|
1160
|
-
) return;
|
|
1161
|
-
state.archivePendingThreadIds.add(task.threadId);
|
|
1162
|
-
renderManualTransition(task);
|
|
1163
|
-
try {
|
|
1164
|
-
await archiveTaskFromControl(event, task, {
|
|
1165
|
-
onArchived: (threadId) => state.archivedThreadIds.add(threadId),
|
|
1166
|
-
showUpdate: (message, options) => showArchiveUpdate(task, message, options),
|
|
1167
|
-
});
|
|
1168
|
-
} finally {
|
|
1169
|
-
state.archivePendingThreadIds.delete(task.threadId);
|
|
1170
|
-
if (state.selectedTask?.id === task.id) renderDialog(state.selectedTask);
|
|
1171
|
-
}
|
|
1172
|
-
});
|