taskchef 7.6.0 → 7.7.1
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 +1 -1
- package/README.md +8 -0
- package/assets/CODEX_ASSET_NOTICE.md +22 -0
- package/assets/codex-app-dark.png +0 -0
- package/assets/codex-app-light.png +0 -0
- package/docs/images/notification-event-snapshots.jpg +0 -0
- package/docs/images/result-history-dashboard.jpg +0 -0
- package/docs/spec.md +28 -0
- package/docs/workflows.md +22 -0
- package/package.json +2 -1
- package/src/dashboard/app.js +70 -16
- package/src/dashboard/index.html +6 -2
- package/src/dashboard/state.js +135 -23
- package/src/dashboard/styles.css +5 -1
- package/src/dashboard.js +2 -1
- package/assets/codex.svg +0 -3
package/README.md
CHANGED
|
@@ -180,6 +180,12 @@ Task and result times are relative through 29 days (with minute detail for the
|
|
|
180
180
|
first six hours), then use a locale-aware calendar date. Each time is a keyboard-
|
|
181
181
|
accessible toggle for its full locale-aware date and time, and one shared
|
|
182
182
|
30-second timer keeps relative labels current without reloading the page.
|
|
183
|
+
The Updates panel captures immutable event-time lifecycle notices. It identifies
|
|
184
|
+
them by task, turn when available, and lifecycle event rather than dashboard
|
|
185
|
+
revision, so reconnects and non-semantic rewrites do not replay a notice and a
|
|
186
|
+
later task state cannot rewrite an older notice. A notice remains readable if
|
|
187
|
+
its task disappears; selecting it then explains that current details are no
|
|
188
|
+
longer available.
|
|
183
189
|
It does not mutate TaskChef data and prints its local URL.
|
|
184
190
|
When a compatible foreground dashboard already owns port 3210,
|
|
185
191
|
`ensure_dashboard` reuses it but does not take ownership. If an unknown,
|
|
@@ -191,6 +197,8 @@ The health endpoint contains only a fixed service marker, health schema,
|
|
|
191
197
|
TaskChef version, dashboard-server version, and canonical workspace. It exposes
|
|
192
198
|
no task data, credentials, environment variables, process control, or secrets.
|
|
193
199
|
|
|
200
|
+

|
|
201
|
+
|
|
194
202
|

|
|
195
203
|
|
|
196
204
|
## Common recovery
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Codex app icon assets
|
|
2
|
+
|
|
3
|
+
`codex-app-dark.png` and `codex-app-light.png` are unmodified Codex app icon
|
|
4
|
+
assets from OpenAI's official macOS application bundle. The source bundle was
|
|
5
|
+
ChatGPT/Codex `26.818.61809` (build `7019`), bundle identifier
|
|
6
|
+
`com.openai.codex`, signed with Apple developer team identifier `2DC432GLL2`.
|
|
7
|
+
The files were copied from these bundle-relative resource paths:
|
|
8
|
+
|
|
9
|
+
- `Contents/Resources/icon-codex-dark-color.png`
|
|
10
|
+
- `Contents/Resources/icon-codex-light.png`
|
|
11
|
+
|
|
12
|
+
The application itself selects these files as the dark and light Codex dock
|
|
13
|
+
icon previews. Their SHA-256 digests are:
|
|
14
|
+
|
|
15
|
+
- `codex-app-dark.png`: `69fb4384e161be8a20dcb94a9ac34aea4fbfaeb67514110a71e7b0732eccb0fc`
|
|
16
|
+
- `codex-app-light.png`: `de7d43f3386105ab20952958c2c25beb0d903e2aeb6e1aef57c49a648c0d1c07`
|
|
17
|
+
|
|
18
|
+
The Codex icon is an OpenAI trademark and copyrighted asset. It is not covered
|
|
19
|
+
by TaskChef's MIT license. TaskChef uses it unchanged and only beside the
|
|
20
|
+
visible **Open task** label to identify an action that opens the corresponding
|
|
21
|
+
task in OpenAI Codex. Use remains subject to the [OpenAI Design
|
|
22
|
+
Guidelines](https://openai.com/brand/), including OpenAI's Marks usage terms.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/docs/spec.md
CHANGED
|
@@ -327,6 +327,34 @@ current configuration before use.
|
|
|
327
327
|
Snapshot and SSE list payloads MUST omit full `results` history and include the
|
|
328
328
|
derived latest-result projection. The bounded per-task detail endpoint MAY
|
|
329
329
|
return the full validated task so the dialog can render history newest first.
|
|
330
|
+
Dashboard notifications MUST capture an immutable event-time projection of the
|
|
331
|
+
task title, lifecycle state and event, turn ID when present, event timestamp,
|
|
332
|
+
and relevant concise summary. Rendering MUST NOT resolve historical notice text
|
|
333
|
+
from the task's later current state. Notice identity and deduplication MUST use
|
|
334
|
+
task ID, turn ID, and lifecycle event; a creation notice without a turn ID MUST
|
|
335
|
+
fall back to task ID plus the immutable creation timestamp. Dashboard revision
|
|
336
|
+
MUST NOT be the sole identity. Identical snapshots, SSE reconnects, idempotent
|
|
337
|
+
reports, schema normalization, and non-semantic rewrites MUST NOT add notices.
|
|
338
|
+
Distinct working and semantic-result events for one turn MAY each be retained.
|
|
339
|
+
When one compact snapshot first exposes both a latest semantic result and a
|
|
340
|
+
newer working turn, the browser MUST reconcile both events. Temporary task
|
|
341
|
+
absence MUST NOT discard the prior semantic signature or turn a later
|
|
342
|
+
reappearance into another creation event. The first page snapshot MUST establish
|
|
343
|
+
a quiet baseline rather than replay existing history. After that baseline, a
|
|
344
|
+
new task first observed with a turn or semantic result MUST retain its creation
|
|
345
|
+
event and each lifecycle event observable in that compact projection.
|
|
346
|
+
|
|
347
|
+
Working with no prior result SHOULD be labeled as a task start. Working on a
|
|
348
|
+
newer turn while a prior result remains projected SHOULD be labeled as a
|
|
349
|
+
follow-up start. Creation, completion, input-needed, and failure labels MUST be
|
|
350
|
+
distinct. The Updates panel MUST remain bounded and support individual dismiss
|
|
351
|
+
and clear-all without resetting replay protection. A retained notice whose task
|
|
352
|
+
is absent from the current list MUST remain readable; selecting it MUST NOT
|
|
353
|
+
navigate or mutate data and SHOULD explain that current details are unavailable.
|
|
354
|
+
The retained notice list MUST NOT be a live region that re-announces old notices
|
|
355
|
+
when it rerenders. A separate polite status region MUST announce only newly
|
|
356
|
+
reconciled events. Each notice control's accessible description MUST include
|
|
357
|
+
its displayed summary when present, event time, and missing-task state.
|
|
330
358
|
|
|
331
359
|
## Task-log migration
|
|
332
360
|
|
package/docs/workflows.md
CHANGED
|
@@ -284,6 +284,28 @@ unnecessary history. A read-only per-task endpoint returns full history only
|
|
|
284
284
|
when the dialog opens. Historical project paths are untrusted until matched
|
|
285
285
|
against current configuration.
|
|
286
286
|
|
|
287
|
+
The browser derives one immutable Updates-panel snapshot when it first observes
|
|
288
|
+
a semantic lifecycle transition. The snapshot keeps its event-time title,
|
|
289
|
+
state/event, turn ID, timestamp, and relevant summary. Its identity combines
|
|
290
|
+
task ID, turn ID, and event, with creation time as the fallback when creation
|
|
291
|
+
has no turn. A separate seen-identity set outlives the bounded visible notices,
|
|
292
|
+
individual dismissals, and Clear all, preventing replay after reconnect,
|
|
293
|
+
normalization, or disappearance and reappearance. Per-task semantic signatures
|
|
294
|
+
also remain as tombstones while a task is absent. When one compact snapshot
|
|
295
|
+
first reveals a latest semantic result plus a newer working turn, the browser
|
|
296
|
+
captures both. The first page snapshot is a quiet baseline so opening or
|
|
297
|
+
refreshing the dashboard does not replay existing history. If a task is first
|
|
298
|
+
observed in a progressed state after that baseline, creation is retained
|
|
299
|
+
alongside the observable result and working events. The current task list is
|
|
300
|
+
used only as the navigation target: if the task is gone, the notice stays
|
|
301
|
+
readable and selection reports that current details are unavailable.
|
|
302
|
+
|
|
303
|
+
The retained toast list is not live. Reconciliation passes only its newly added
|
|
304
|
+
snapshots to a separate polite status region, so reconnect, dismissal, Clear
|
|
305
|
+
all, and ordinary rerendering do not re-announce retained history. Toast action
|
|
306
|
+
labels remain concise while `aria-describedby` connects the visible summary,
|
|
307
|
+
event time, and missing-task explanation for assistive technology.
|
|
308
|
+
|
|
287
309
|
## Schema 4/5 migration
|
|
288
310
|
|
|
289
311
|
`taskchef workspace migrate` acquires the same workspace lock as lifecycle
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "taskchef",
|
|
3
|
-
"version": "7.
|
|
3
|
+
"version": "7.7.1",
|
|
4
4
|
"description": "A non-blocking interactive dispatcher for visible Codex tasks.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Favo Yang",
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
"docs/spec.md",
|
|
27
27
|
"docs/workflows.md",
|
|
28
28
|
"docs/firstmate-taskchef-comparison.md",
|
|
29
|
+
"docs/images/notification-event-snapshots.jpg",
|
|
29
30
|
"docs/images/result-history-dashboard.jpg",
|
|
30
31
|
"index.js",
|
|
31
32
|
"mcp",
|
package/src/dashboard/app.js
CHANGED
|
@@ -1,19 +1,24 @@
|
|
|
1
1
|
import {
|
|
2
|
+
clearNotifications,
|
|
3
|
+
dismissNotification,
|
|
2
4
|
findCurrentTask,
|
|
3
5
|
KNOWN_TASK_STATUSES,
|
|
4
6
|
nextDateFilterRefreshDelay,
|
|
7
|
+
notificationDismissLabel,
|
|
8
|
+
notificationOpenLabel,
|
|
5
9
|
notificationTitle,
|
|
6
10
|
reconcileNotifications,
|
|
7
11
|
taskStatusLabel,
|
|
8
12
|
taskWithinDateFilter,
|
|
9
13
|
} from "./state.js";
|
|
10
14
|
import { openTaskFromControl } from "./actions.js";
|
|
11
|
-
import { RelativeTimeController, parsedTimestamp } from "./time.js";
|
|
15
|
+
import { formatRelativeTime, RelativeTimeController, parsedTimestamp } from "./time.js";
|
|
12
16
|
|
|
13
17
|
const state = {
|
|
14
18
|
tasks: [],
|
|
15
19
|
signatures: new Map(),
|
|
16
20
|
notifications: [],
|
|
21
|
+
seenNotificationIds: new Set(),
|
|
17
22
|
initialized: false,
|
|
18
23
|
selectedTask: null,
|
|
19
24
|
};
|
|
@@ -39,6 +44,7 @@ const elements = {
|
|
|
39
44
|
dismissDashboardMessage: document.querySelector("#dismiss-dashboard-message"),
|
|
40
45
|
emptyState: document.querySelector("#empty-state"),
|
|
41
46
|
notifications: document.querySelector("#notifications"),
|
|
47
|
+
notificationAnnouncer: document.querySelector("#notification-announcer"),
|
|
42
48
|
openProject: document.querySelector("#open-codex"),
|
|
43
49
|
projectFilter: document.querySelector("#project-filter"),
|
|
44
50
|
statusFilter: document.querySelector("#status-filter"),
|
|
@@ -46,6 +52,7 @@ const elements = {
|
|
|
46
52
|
taskList: document.querySelector("#task-list"),
|
|
47
53
|
toastList: document.querySelector("#toast-list"),
|
|
48
54
|
};
|
|
55
|
+
let notificationDescriptionSerial = 0;
|
|
49
56
|
|
|
50
57
|
function timestampControl(value, { accessibleName, key, prefix = "" }) {
|
|
51
58
|
if (!parsedTimestamp(value)) {
|
|
@@ -71,10 +78,19 @@ function timestampControl(value, { accessibleName, key, prefix = "" }) {
|
|
|
71
78
|
}
|
|
72
79
|
|
|
73
80
|
function codexIcon() {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
const picture = document.createElement("picture");
|
|
82
|
+
picture.className = "codex-icon";
|
|
83
|
+
picture.setAttribute("aria-hidden", "true");
|
|
84
|
+
const dark = document.createElement("source");
|
|
85
|
+
dark.srcset = "/assets/codex-app-dark.png";
|
|
86
|
+
dark.media = "(prefers-color-scheme: dark)";
|
|
87
|
+
const image = document.createElement("img");
|
|
88
|
+
image.src = "/assets/codex-app-light.png";
|
|
89
|
+
image.alt = "";
|
|
90
|
+
image.width = 16;
|
|
91
|
+
image.height = 16;
|
|
92
|
+
picture.append(dark, image);
|
|
93
|
+
return picture;
|
|
78
94
|
}
|
|
79
95
|
|
|
80
96
|
function configureOpenTaskControl(control, ariaLabel) {
|
|
@@ -108,17 +124,37 @@ function replaceOptions(select, values, allLabel) {
|
|
|
108
124
|
|
|
109
125
|
function notificationToast(notification) {
|
|
110
126
|
const task = findCurrentTask(state.tasks, notification.taskId);
|
|
111
|
-
if (!task) return null;
|
|
112
127
|
const toast = document.createElement("div");
|
|
113
|
-
toast.className = "toast"
|
|
128
|
+
toast.className = `toast${task ? "" : " toast-missing"}`;
|
|
114
129
|
const text = document.createElement("button");
|
|
115
130
|
text.type = "button";
|
|
116
131
|
text.className = "toast-content";
|
|
132
|
+
text.setAttribute("aria-label", notificationOpenLabel(notification, Boolean(task)));
|
|
133
|
+
const describedBy = [];
|
|
117
134
|
const title = document.createElement("strong");
|
|
118
|
-
title.textContent = notificationTitle(
|
|
135
|
+
title.textContent = notificationTitle(notification);
|
|
119
136
|
const description = document.createElement("span");
|
|
120
|
-
description.textContent =
|
|
137
|
+
description.textContent = notification.title;
|
|
121
138
|
text.append(title, description);
|
|
139
|
+
if (notification.summary) {
|
|
140
|
+
const summary = document.createElement("span");
|
|
141
|
+
summary.className = "toast-summary preserve-lines";
|
|
142
|
+
summary.id = `notification-description-${notificationDescriptionSerial += 1}`;
|
|
143
|
+
summary.textContent = notification.summary;
|
|
144
|
+
text.append(summary);
|
|
145
|
+
describedBy.push(summary.id);
|
|
146
|
+
}
|
|
147
|
+
const metadata = document.createElement("span");
|
|
148
|
+
metadata.className = "toast-metadata";
|
|
149
|
+
metadata.id = `notification-description-${notificationDescriptionSerial += 1}`;
|
|
150
|
+
const timestamp = document.createElement("time");
|
|
151
|
+
timestamp.dateTime = notification.timestamp ?? "";
|
|
152
|
+
timestamp.textContent = formatRelativeTime(notification.timestamp);
|
|
153
|
+
metadata.append(timestamp);
|
|
154
|
+
if (!task) metadata.append(" · Task no longer available");
|
|
155
|
+
text.append(metadata);
|
|
156
|
+
describedBy.push(metadata.id);
|
|
157
|
+
text.setAttribute("aria-describedby", describedBy.join(" "));
|
|
122
158
|
text.addEventListener("click", () => {
|
|
123
159
|
const current = findCurrentTask(state.tasks, notification.taskId);
|
|
124
160
|
if (current) openDialog(current);
|
|
@@ -127,21 +163,37 @@ function notificationToast(notification) {
|
|
|
127
163
|
const dismiss = document.createElement("button");
|
|
128
164
|
dismiss.type = "button";
|
|
129
165
|
dismiss.className = "icon-button";
|
|
130
|
-
dismiss.setAttribute("aria-label",
|
|
166
|
+
dismiss.setAttribute("aria-label", notificationDismissLabel(notification));
|
|
167
|
+
dismiss.setAttribute("aria-describedby", describedBy.join(" "));
|
|
131
168
|
dismiss.textContent = "×";
|
|
132
169
|
dismiss.addEventListener("click", () => {
|
|
133
|
-
state.notifications = state.notifications
|
|
170
|
+
state.notifications = dismissNotification(state.notifications, notification.id);
|
|
134
171
|
renderNotifications();
|
|
135
172
|
});
|
|
136
173
|
toast.append(text, dismiss);
|
|
137
174
|
return toast;
|
|
138
175
|
}
|
|
139
176
|
|
|
140
|
-
function
|
|
177
|
+
function notificationAnnouncement(notification) {
|
|
178
|
+
return [
|
|
179
|
+
notificationTitle(notification),
|
|
180
|
+
notification.title,
|
|
181
|
+
notification.summary,
|
|
182
|
+
notification.turnId ? `Turn ${notification.turnId}` : null,
|
|
183
|
+
formatRelativeTime(notification.timestamp),
|
|
184
|
+
].filter(Boolean).join(". ");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function renderNotifications(additions = []) {
|
|
141
188
|
elements.toastList.replaceChildren(
|
|
142
|
-
...state.notifications.map(notificationToast)
|
|
189
|
+
...state.notifications.map(notificationToast),
|
|
143
190
|
);
|
|
144
191
|
elements.notifications.hidden = state.notifications.length === 0;
|
|
192
|
+
if (additions.length > 0) {
|
|
193
|
+
elements.notificationAnnouncer.textContent = additions
|
|
194
|
+
.map(notificationAnnouncement)
|
|
195
|
+
.join(". ");
|
|
196
|
+
}
|
|
145
197
|
}
|
|
146
198
|
|
|
147
199
|
function detailRow(term, value) {
|
|
@@ -311,11 +363,13 @@ function applySnapshot(snapshot) {
|
|
|
311
363
|
const reconciled = reconcileNotifications({
|
|
312
364
|
initialized: state.initialized,
|
|
313
365
|
notifications: state.notifications,
|
|
366
|
+
seenIds: state.seenNotificationIds,
|
|
314
367
|
signatures: state.signatures,
|
|
315
|
-
}, snapshot.tasks
|
|
368
|
+
}, snapshot.tasks);
|
|
316
369
|
state.tasks = snapshot.tasks;
|
|
317
370
|
state.signatures = reconciled.signatures;
|
|
318
371
|
state.notifications = reconciled.notifications;
|
|
372
|
+
state.seenNotificationIds = reconciled.seenIds;
|
|
319
373
|
state.initialized = true;
|
|
320
374
|
if (snapshot.healthy === false) {
|
|
321
375
|
showMessage("The task log is temporarily unavailable. Showing the last valid snapshot.");
|
|
@@ -336,7 +390,7 @@ function applySnapshot(snapshot) {
|
|
|
336
390
|
const updated = state.tasks.find((task) => task.id === state.selectedTask.id);
|
|
337
391
|
if (updated && elements.dialog.open) openDialog(updated);
|
|
338
392
|
}
|
|
339
|
-
renderNotifications();
|
|
393
|
+
renderNotifications(reconciled.additions);
|
|
340
394
|
render();
|
|
341
395
|
}
|
|
342
396
|
|
|
@@ -363,7 +417,7 @@ elements.dismissDashboardMessage.addEventListener("click", () => {
|
|
|
363
417
|
elements.dashboardMessage.hidden = true;
|
|
364
418
|
});
|
|
365
419
|
elements.clearNotifications.addEventListener("click", () => {
|
|
366
|
-
state.notifications =
|
|
420
|
+
state.notifications = clearNotifications();
|
|
367
421
|
renderNotifications();
|
|
368
422
|
});
|
|
369
423
|
elements.closeDialog.addEventListener("click", () => elements.dialog.close());
|
package/src/dashboard/index.html
CHANGED
|
@@ -71,7 +71,8 @@
|
|
|
71
71
|
<strong>Updates</strong>
|
|
72
72
|
<button id="clear-notifications" class="text-button" type="button">Clear all</button>
|
|
73
73
|
</div>
|
|
74
|
-
<
|
|
74
|
+
<p id="notification-announcer" class="visually-hidden" role="status" aria-live="polite"></p>
|
|
75
|
+
<div id="toast-list" class="toast-list"></div>
|
|
75
76
|
</aside>
|
|
76
77
|
|
|
77
78
|
<dialog id="task-dialog" aria-labelledby="dialog-title">
|
|
@@ -84,7 +85,10 @@
|
|
|
84
85
|
</div>
|
|
85
86
|
<div class="dialog-actions">
|
|
86
87
|
<button id="open-codex" class="primary-button task-action" type="button" aria-label="Open this task in Codex">
|
|
87
|
-
<
|
|
88
|
+
<picture class="codex-icon" aria-hidden="true">
|
|
89
|
+
<source srcset="/assets/codex-app-dark.png" media="(prefers-color-scheme: dark)">
|
|
90
|
+
<img src="/assets/codex-app-light.png" alt="" width="16" height="16">
|
|
91
|
+
</picture>
|
|
88
92
|
<span>Open task</span>
|
|
89
93
|
</button>
|
|
90
94
|
<button id="copy-thread-id" class="secondary-button" type="button">Copy thread ID</button>
|
package/src/dashboard/state.js
CHANGED
|
@@ -7,6 +7,16 @@ export const KNOWN_TASK_STATUSES = [
|
|
|
7
7
|
"unresolved",
|
|
8
8
|
];
|
|
9
9
|
|
|
10
|
+
const NOTIFICATION_TITLES = new Map([
|
|
11
|
+
["created", "Task created"],
|
|
12
|
+
["task_started", "Task started"],
|
|
13
|
+
["follow_up_started", "Follow-up started"],
|
|
14
|
+
["completed", "Task completed"],
|
|
15
|
+
["needs_input", "Task needs input"],
|
|
16
|
+
["failed", "Task failed"],
|
|
17
|
+
["unresolved", "Task updated"],
|
|
18
|
+
]);
|
|
19
|
+
|
|
10
20
|
const DATE_WINDOWS_MS = new Map([
|
|
11
21
|
["24h", 24 * 60 * 60 * 1_000],
|
|
12
22
|
["7d", 7 * 24 * 60 * 60 * 1_000],
|
|
@@ -21,8 +31,8 @@ export function taskStatusLabel(task) {
|
|
|
21
31
|
return task.status === null ? "unresolved" : task.status.replaceAll("_", " ");
|
|
22
32
|
}
|
|
23
33
|
|
|
24
|
-
export function notificationTitle(
|
|
25
|
-
return
|
|
34
|
+
export function notificationTitle(notification) {
|
|
35
|
+
return NOTIFICATION_TITLES.get(notification.event) ?? "Task updated";
|
|
26
36
|
}
|
|
27
37
|
|
|
28
38
|
export function taskWithinDateFilter(task, filter, now = Date.now()) {
|
|
@@ -44,41 +54,143 @@ export function nextDateFilterRefreshDelay(tasks, filter, now = Date.now()) {
|
|
|
44
54
|
}
|
|
45
55
|
|
|
46
56
|
export function taskSignature(task) {
|
|
47
|
-
return JSON.stringify([
|
|
48
|
-
task.threadId,
|
|
49
|
-
task.status,
|
|
50
|
-
task.summary,
|
|
51
|
-
task.turnId,
|
|
52
|
-
task.updatedAt,
|
|
53
|
-
task.updatedBy,
|
|
54
|
-
task.lastResult,
|
|
55
|
-
]);
|
|
57
|
+
return JSON.stringify([task.id, task.turnId ?? null, task.status ?? "unresolved"]);
|
|
56
58
|
}
|
|
57
59
|
|
|
58
60
|
export function findCurrentTask(tasks, taskId) {
|
|
59
61
|
return tasks.find((task) => task.id === taskId) ?? null;
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
function lifecycleEvent(task) {
|
|
65
|
+
if (task.status === "working") {
|
|
66
|
+
return task.lastResult ? "follow_up_started" : "task_started";
|
|
67
|
+
}
|
|
68
|
+
return task.status ?? "unresolved";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function notificationIdentity(task, event) {
|
|
72
|
+
if (event === "created") {
|
|
73
|
+
return JSON.stringify([task.id, null, event, task.createdAt ?? null]);
|
|
74
|
+
}
|
|
75
|
+
return JSON.stringify([task.id, task.turnId ?? null, event]);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function eventTimestamp(task, event) {
|
|
79
|
+
if (event === "created") return task.createdAt ?? task.updatedAt ?? null;
|
|
80
|
+
if (
|
|
81
|
+
task.lastResult?.status === task.status
|
|
82
|
+
&& task.lastResult?.turnId === task.turnId
|
|
83
|
+
) {
|
|
84
|
+
return task.lastResult.updatedAt;
|
|
85
|
+
}
|
|
86
|
+
return task.updatedAt ?? task.createdAt ?? null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function eventSummary(task, event) {
|
|
90
|
+
if (!["completed", "needs_input", "failed"].includes(event)) return null;
|
|
91
|
+
if (
|
|
92
|
+
task.lastResult?.status === task.status
|
|
93
|
+
&& task.lastResult?.turnId === task.turnId
|
|
94
|
+
) {
|
|
95
|
+
return task.lastResult.summary;
|
|
96
|
+
}
|
|
97
|
+
return task.summary ?? null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function notificationSnapshot(task, event = lifecycleEvent(task)) {
|
|
101
|
+
const created = event === "created";
|
|
102
|
+
return Object.freeze({
|
|
103
|
+
id: notificationIdentity(task, event),
|
|
104
|
+
taskId: task.id,
|
|
105
|
+
title: task.title,
|
|
106
|
+
status: created ? "working" : task.status,
|
|
107
|
+
event,
|
|
108
|
+
turnId: created ? null : task.turnId ?? null,
|
|
109
|
+
timestamp: eventTimestamp(task, event),
|
|
110
|
+
summary: eventSummary(task, event),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function resultNotificationSnapshot(task) {
|
|
115
|
+
const result = task.lastResult;
|
|
116
|
+
if (!result) return null;
|
|
117
|
+
return Object.freeze({
|
|
118
|
+
id: notificationIdentity({ ...task, turnId: result.turnId }, result.status),
|
|
119
|
+
taskId: task.id,
|
|
120
|
+
title: task.title,
|
|
121
|
+
status: result.status,
|
|
122
|
+
event: result.status,
|
|
123
|
+
turnId: result.turnId ?? null,
|
|
124
|
+
timestamp: result.updatedAt,
|
|
125
|
+
summary: result.summary,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function notificationOpenLabel(notification, available) {
|
|
130
|
+
const action = available ? "Open current task details" : "Show task availability";
|
|
131
|
+
return `${action} for ${notification.title}: ${notificationTitle(notification)}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function notificationDismissLabel(notification) {
|
|
135
|
+
return `Dismiss ${notificationTitle(notification)} notification for ${notification.title}`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function dismissNotification(notifications, notificationId) {
|
|
139
|
+
return notifications.filter(({ id }) => id !== notificationId);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function clearNotifications() {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
|
|
62
146
|
export function reconcileNotifications(
|
|
63
|
-
{ initialized, notifications, signatures },
|
|
147
|
+
{ initialized, notifications, seenIds = new Set(), signatures },
|
|
64
148
|
tasks,
|
|
65
|
-
revision,
|
|
66
149
|
) {
|
|
67
|
-
const nextSignatures = new Map(
|
|
68
|
-
|
|
150
|
+
const nextSignatures = new Map(signatures);
|
|
151
|
+
for (const task of tasks) nextSignatures.set(task.id, taskSignature(task));
|
|
152
|
+
const nextSeenIds = new Set(seenIds);
|
|
153
|
+
if (!initialized) {
|
|
154
|
+
for (const task of tasks) {
|
|
155
|
+
nextSeenIds.add(notificationSnapshot(task, "created").id);
|
|
156
|
+
const resultNotification = resultNotificationSnapshot(task);
|
|
157
|
+
if (resultNotification) nextSeenIds.add(resultNotification.id);
|
|
158
|
+
nextSeenIds.add(notificationSnapshot(task).id);
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
additions: [],
|
|
162
|
+
notifications,
|
|
163
|
+
seenIds: nextSeenIds,
|
|
164
|
+
signatures: nextSignatures,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
69
167
|
const additions = [];
|
|
70
168
|
for (const task of tasks) {
|
|
71
|
-
|
|
72
|
-
if (!signatures.has(task.id))
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
169
|
+
const candidates = [];
|
|
170
|
+
if (!signatures.has(task.id)) {
|
|
171
|
+
const resultNotification = resultNotificationSnapshot(task);
|
|
172
|
+
if (task.turnId || task.status !== "working" || resultNotification) {
|
|
173
|
+
candidates.push(notificationSnapshot(task));
|
|
174
|
+
if (resultNotification) candidates.push(resultNotification);
|
|
175
|
+
}
|
|
176
|
+
candidates.push(notificationSnapshot(task, "created"));
|
|
177
|
+
} else if (signatures.get(task.id) !== nextSignatures.get(task.id)) {
|
|
178
|
+
candidates.push(notificationSnapshot(task));
|
|
179
|
+
const resultNotification = resultNotificationSnapshot(task);
|
|
180
|
+
if (resultNotification) candidates.push(resultNotification);
|
|
181
|
+
}
|
|
182
|
+
for (const notification of candidates) {
|
|
183
|
+
if (!nextSeenIds.has(notification.id)) additions.push(notification);
|
|
184
|
+
nextSeenIds.add(notification.id);
|
|
185
|
+
}
|
|
186
|
+
const resultNotification = resultNotificationSnapshot(task);
|
|
187
|
+
if (resultNotification) nextSeenIds.add(resultNotification.id);
|
|
188
|
+
nextSeenIds.add(notificationSnapshot(task).id);
|
|
79
189
|
}
|
|
80
190
|
return {
|
|
191
|
+
additions,
|
|
81
192
|
notifications: [...additions, ...notifications].slice(0, MAX_NOTIFICATIONS),
|
|
193
|
+
seenIds: nextSeenIds,
|
|
82
194
|
signatures: nextSignatures,
|
|
83
195
|
};
|
|
84
196
|
}
|
package/src/dashboard/styles.css
CHANGED
|
@@ -84,7 +84,8 @@ time, .timestamp-missing { font-size: 0.78rem; }
|
|
|
84
84
|
.task-footer { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
|
85
85
|
.task-open { flex: none; }
|
|
86
86
|
.task-action { display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-height: 32px; padding: 5px 9px; font-size: 0.82rem; line-height: 1.2; white-space: nowrap; }
|
|
87
|
-
.codex-icon { display:
|
|
87
|
+
.codex-icon { display: block; flex: none; width: 16px; height: 16px; }
|
|
88
|
+
.codex-icon img { display: block; width: 100%; height: 100%; object-fit: contain; }
|
|
88
89
|
|
|
89
90
|
.status { flex: none; padding: 4px 9px; border-radius: 999px; background: var(--surface-muted); color: var(--muted); font-size: 0.72rem; font-weight: 750; text-transform: capitalize; }
|
|
90
91
|
.status-completed { background: var(--accent-soft); color: var(--accent); }
|
|
@@ -105,6 +106,9 @@ time, .timestamp-missing { font-size: 0.78rem; }
|
|
|
105
106
|
.toast { display: flex; align-items: flex-start; gap: 8px; padding: 10px; border-radius: 7px; background: var(--surface-muted); }
|
|
106
107
|
.toast-content { display: grid; flex: 1; gap: 2px; padding: 0; border: 0; background: none; text-align: left; cursor: pointer; }
|
|
107
108
|
.toast-content span { color: var(--muted); font-size: 0.85rem; }
|
|
109
|
+
.toast-content .toast-summary { color: var(--text); }
|
|
110
|
+
.toast-content .toast-metadata { font-size: 0.75rem; }
|
|
111
|
+
.toast-missing { border: 1px dashed var(--border); }
|
|
108
112
|
|
|
109
113
|
.text-button, .icon-button { border: 0; background: none; cursor: pointer; }
|
|
110
114
|
.text-button { color: var(--accent); font-size: 0.82rem; font-weight: 700; }
|
package/src/dashboard.js
CHANGED
|
@@ -48,7 +48,8 @@ const STATIC_FILES = new Map([
|
|
|
48
48
|
["/styles.css", [path.join(STATIC_ROOT, "styles.css"), "text/css; charset=utf-8"]],
|
|
49
49
|
["/assets/taskchef-dark.svg", [path.join(ASSET_ROOT, "taskchef-dark.svg"), "image/svg+xml"]],
|
|
50
50
|
["/assets/taskchef.svg", [path.join(ASSET_ROOT, "taskchef.svg"), "image/svg+xml"]],
|
|
51
|
-
["/assets/codex.
|
|
51
|
+
["/assets/codex-app-dark.png", [path.join(ASSET_ROOT, "codex-app-dark.png"), "image/png"]],
|
|
52
|
+
["/assets/codex-app-light.png", [path.join(ASSET_ROOT, "codex-app-light.png"), "image/png"]],
|
|
52
53
|
]);
|
|
53
54
|
|
|
54
55
|
export function dashboardAuthority(host, port) {
|
package/assets/codex.svg
DELETED
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
|
|
2
|
-
<path fill="currentColor" d="M3 1.5h10A1.5 1.5 0 0 1 14.5 3v10a1.5 1.5 0 0 1-1.5 1.5H3A1.5 1.5 0 0 1 1.5 13V3A1.5 1.5 0 0 1 3 1.5Zm0 1a.5.5 0 0 0-.5.5v10a.5.5 0 0 0 .5.5h10a.5.5 0 0 0 .5-.5V3a.5.5 0 0 0-.5-.5H3Zm1.15 3.1.7-.7L7.95 8l-3.1 3.1-.7-.7L6.55 8l-2.4-2.4ZM8 10h4v1H8v-1Z"/>
|
|
3
|
-
</svg>
|