taskchef 5.11.1 → 5.12.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 +1 -1
- package/README.md +11 -8
- package/package.json +1 -1
- package/src/dashboard/actions.js +17 -0
- package/src/dashboard/app.js +15 -12
- package/src/dashboard/index.html +1 -1
- package/src/dashboard/styles.css +4 -0
- package/src/dashboard.js +3 -35
package/README.md
CHANGED
|
@@ -174,10 +174,9 @@ executors report results:
|
|
|
174
174
|
taskchef dashboard
|
|
175
175
|
```
|
|
176
176
|
|
|
177
|
-
Open the printed
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
address bar. Use `--port <number>` to choose another port and `--workspace
|
|
177
|
+
Open the printed local URL, which is `http://127.0.0.1:3210/` by default. The
|
|
178
|
+
same server can be open in the Codex in-app browser and an external browser at
|
|
179
|
+
the same time. Use `--port <number>` to choose another port and `--workspace
|
|
181
180
|
<path>` to override the normal workspace resolution. Press Ctrl+C in the
|
|
182
181
|
terminal to stop the server.
|
|
183
182
|
|
|
@@ -191,10 +190,14 @@ The dashboard:
|
|
|
191
190
|
- opens the recorded task directly in Codex when it has a supported UUID thread
|
|
192
191
|
ID, with a project-opening fallback for unresolved or legacy identities.
|
|
193
192
|
|
|
194
|
-
The server binds only to the numeric IPv4 loopback interface
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
193
|
+
The server binds only to the numeric IPv4 loopback interface and rejects
|
|
194
|
+
non-loopback configuration. It intentionally does not authenticate browser
|
|
195
|
+
sessions, so any local process or browser that can reach the port can read the
|
|
196
|
+
dashboard data. Keep the server running only while needed and do not expose it
|
|
197
|
+
through a proxy or tunnel. State-changing actions still require an exact Host
|
|
198
|
+
and same-origin request. The server validates the same task schema as the CLI,
|
|
199
|
+
retains its last valid snapshot if the log becomes invalid, and never writes
|
|
200
|
+
dispatcher-workspace files. It limits event
|
|
198
201
|
streams and disconnects slow clients instead of buffering snapshots without
|
|
199
202
|
bound. It watches the workspace directory
|
|
200
203
|
so TaskChef's atomic log replacement remains visible, uses a low-frequency file
|
package/package.json
CHANGED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export async function openTaskFromControl(event, taskId, {
|
|
2
|
+
fetchAction = globalThis.fetch,
|
|
3
|
+
showMessage,
|
|
4
|
+
} = {}) {
|
|
5
|
+
event.stopPropagation();
|
|
6
|
+
const control = event.currentTarget;
|
|
7
|
+
control.disabled = true;
|
|
8
|
+
try {
|
|
9
|
+
const response = await fetchAction(`/api/tasks/${encodeURIComponent(taskId)}/open-codex`, {
|
|
10
|
+
method: "POST",
|
|
11
|
+
});
|
|
12
|
+
const result = await response.json();
|
|
13
|
+
showMessage(result.message);
|
|
14
|
+
} finally {
|
|
15
|
+
control.disabled = false;
|
|
16
|
+
}
|
|
17
|
+
}
|
package/src/dashboard/app.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
taskStatusLabel,
|
|
8
8
|
taskWithinDateFilter,
|
|
9
9
|
} from "./state.js";
|
|
10
|
+
import { openTaskFromControl } from "./actions.js";
|
|
10
11
|
|
|
11
12
|
const state = {
|
|
12
13
|
tasks: [],
|
|
@@ -161,7 +162,18 @@ function taskCard(task) {
|
|
|
161
162
|
const time = document.createElement("time");
|
|
162
163
|
time.dateTime = task.meaningfulUpdatedAt ?? task.updatedAt ?? task.createdAt;
|
|
163
164
|
time.textContent = `Updated ${formatTime(time.dateTime)}`;
|
|
164
|
-
|
|
165
|
+
const footer = document.createElement("div");
|
|
166
|
+
footer.className = "task-footer";
|
|
167
|
+
const openTask = document.createElement("button");
|
|
168
|
+
openTask.type = "button";
|
|
169
|
+
openTask.className = "secondary-button task-open";
|
|
170
|
+
openTask.textContent = "Open task";
|
|
171
|
+
openTask.setAttribute("aria-label", `Open ${task.title} in Codex`);
|
|
172
|
+
openTask.addEventListener("click", (event) => openTaskFromControl(event, task.id, {
|
|
173
|
+
showMessage,
|
|
174
|
+
}));
|
|
175
|
+
footer.append(time, openTask);
|
|
176
|
+
article.append(heading, project, summary, footer);
|
|
165
177
|
return article;
|
|
166
178
|
}
|
|
167
179
|
|
|
@@ -251,16 +263,7 @@ elements.copyThreadId.addEventListener("click", async () => {
|
|
|
251
263
|
showMessage("Clipboard access is unavailable. Copy the thread ID from the metadata below.");
|
|
252
264
|
}
|
|
253
265
|
});
|
|
254
|
-
elements.openProject.addEventListener("click", async () => {
|
|
266
|
+
elements.openProject.addEventListener("click", async (event) => {
|
|
255
267
|
if (!state.selectedTask) return;
|
|
256
|
-
|
|
257
|
-
try {
|
|
258
|
-
const response = await fetch(`/api/tasks/${encodeURIComponent(state.selectedTask.id)}/open-codex`, {
|
|
259
|
-
method: "POST",
|
|
260
|
-
});
|
|
261
|
-
const result = await response.json();
|
|
262
|
-
showMessage(result.message);
|
|
263
|
-
} finally {
|
|
264
|
-
elements.openProject.disabled = false;
|
|
265
|
-
}
|
|
268
|
+
await openTaskFromControl(event, state.selectedTask.id, { showMessage });
|
|
266
269
|
});
|
package/src/dashboard/index.html
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<header class="site-header">
|
|
13
13
|
<div>
|
|
14
14
|
<p class="eyebrow">TaskChef</p>
|
|
15
|
-
<h1>
|
|
15
|
+
<h1>TaskChef dashboard</h1>
|
|
16
16
|
<p class="subtitle">Latest delegated work, updated as TaskChef reports change.</p>
|
|
17
17
|
</div>
|
|
18
18
|
<div class="connection" role="status" aria-live="polite">
|
package/src/dashboard/styles.css
CHANGED
|
@@ -74,6 +74,8 @@ select { min-width: 180px; padding: 9px 34px 9px 11px; border: 1px solid var(--b
|
|
|
74
74
|
.task-project { margin: 3px 0 12px; font-size: 0.86rem; }
|
|
75
75
|
.task-summary { max-width: 78ch; margin-bottom: 14px; }
|
|
76
76
|
time { font-size: 0.78rem; }
|
|
77
|
+
.task-footer { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
|
78
|
+
.task-open { flex: none; }
|
|
77
79
|
|
|
78
80
|
.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; }
|
|
79
81
|
.status-completed { background: var(--accent-soft); color: var(--accent); }
|
|
@@ -122,6 +124,8 @@ pre { max-height: 280px; margin: 0; padding: 14px; overflow: auto; border-radius
|
|
|
122
124
|
.toolbar label, .toolbar select { width: 100%; }
|
|
123
125
|
.task-count { margin: 0; }
|
|
124
126
|
.task-heading { align-items: flex-start; }
|
|
127
|
+
.task-footer { align-items: flex-start; flex-direction: column; }
|
|
128
|
+
.task-open { width: 100%; }
|
|
125
129
|
.dialog-actions { align-items: stretch; flex-direction: column; }
|
|
126
130
|
.metadata { grid-template-columns: 1fr; }
|
|
127
131
|
.metadata dt { padding-bottom: 0; border-bottom: 0; }
|
package/src/dashboard.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { randomBytes } from "node:crypto";
|
|
2
1
|
import { EventEmitter } from "node:events";
|
|
3
2
|
import { constants, watch } from "node:fs";
|
|
4
3
|
import { open, readFile, realpath, stat } from "node:fs/promises";
|
|
@@ -38,6 +37,7 @@ const CONTENT_SECURITY_POLICY = [
|
|
|
38
37
|
|
|
39
38
|
const STATIC_FILES = new Map([
|
|
40
39
|
["/", ["index.html", "text/html; charset=utf-8"]],
|
|
40
|
+
["/actions.js", ["actions.js", "text/javascript; charset=utf-8"]],
|
|
41
41
|
["/app.js", ["app.js", "text/javascript; charset=utf-8"]],
|
|
42
42
|
["/state.js", ["state.js", "text/javascript; charset=utf-8"]],
|
|
43
43
|
["/styles.css", ["styles.css", "text/css; charset=utf-8"]],
|
|
@@ -383,14 +383,6 @@ export function createSseClient(response, {
|
|
|
383
383
|
return client;
|
|
384
384
|
}
|
|
385
385
|
|
|
386
|
-
function requestSessionCookie(request, cookieName) {
|
|
387
|
-
return request.headers.cookie
|
|
388
|
-
?.split(";")
|
|
389
|
-
.map((part) => part.trim())
|
|
390
|
-
.find((part) => part.startsWith(`${cookieName}=`))
|
|
391
|
-
?.slice(cookieName.length + 1) ?? null;
|
|
392
|
-
}
|
|
393
|
-
|
|
394
386
|
function publicMonitorError() {
|
|
395
387
|
return {
|
|
396
388
|
message: "The task log is temporarily unavailable. Showing the last valid snapshot.",
|
|
@@ -418,10 +410,6 @@ export async function createDashboardServer({
|
|
|
418
410
|
const monitor = new DashboardMonitor(workspace, monitorOptions);
|
|
419
411
|
await monitor.start();
|
|
420
412
|
const clients = new Set();
|
|
421
|
-
const capabilityToken = randomBytes(32).toString("base64url");
|
|
422
|
-
let launchToken = capabilityToken;
|
|
423
|
-
const sessionToken = randomBytes(32).toString("base64url");
|
|
424
|
-
const sessionCookieName = `taskchef_session_${randomBytes(12).toString("hex")}`;
|
|
425
413
|
let allowedAuthority;
|
|
426
414
|
let allowedOrigin;
|
|
427
415
|
|
|
@@ -447,26 +435,6 @@ export async function createDashboardServer({
|
|
|
447
435
|
sendJson(response, 421, { message: "Misdirected request." });
|
|
448
436
|
return;
|
|
449
437
|
}
|
|
450
|
-
const authenticated = requestSessionCookie(request, sessionCookieName) === sessionToken;
|
|
451
|
-
if (
|
|
452
|
-
(method === "GET" || method === "HEAD")
|
|
453
|
-
&& launchToken !== null
|
|
454
|
-
&& url.pathname === "/"
|
|
455
|
-
&& url.searchParams.get("token") === launchToken
|
|
456
|
-
) {
|
|
457
|
-
launchToken = null;
|
|
458
|
-
response.writeHead(303, {
|
|
459
|
-
...securityHeaders("text/plain; charset=utf-8"),
|
|
460
|
-
Location: "/",
|
|
461
|
-
"Set-Cookie": `${sessionCookieName}=${sessionToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
462
|
-
});
|
|
463
|
-
response.end("Opening TaskChef dashboard.\n");
|
|
464
|
-
return;
|
|
465
|
-
}
|
|
466
|
-
if (!authenticated) {
|
|
467
|
-
sendJson(response, 401, { message: "Dashboard launch capability required." });
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
438
|
if (method !== "GET" && method !== "HEAD" && method !== "POST") {
|
|
471
439
|
response.writeHead(405, { Allow: "GET, HEAD, POST" });
|
|
472
440
|
response.end();
|
|
@@ -514,7 +482,7 @@ export async function createDashboardServer({
|
|
|
514
482
|
const taskMatch = url.pathname.match(/^\/api\/tasks\/([a-zA-Z0-9._-]+)\/open-codex$/);
|
|
515
483
|
if (taskMatch && method === "POST") {
|
|
516
484
|
if (request.headers.origin !== allowedOrigin) {
|
|
517
|
-
sendJson(response, 403, { message: "Dashboard
|
|
485
|
+
sendJson(response, 403, { message: "Dashboard origin validation failed." });
|
|
518
486
|
return;
|
|
519
487
|
}
|
|
520
488
|
const task = monitor.tasks.find((candidate) => candidate.id === taskMatch[1]);
|
|
@@ -600,7 +568,7 @@ export async function createDashboardServer({
|
|
|
600
568
|
host,
|
|
601
569
|
port: boundPort,
|
|
602
570
|
origin: allowedOrigin,
|
|
603
|
-
url: `${allowedOrigin}
|
|
571
|
+
url: `${allowedOrigin}/`,
|
|
604
572
|
monitor,
|
|
605
573
|
get eventClientCount() { return clients.size; },
|
|
606
574
|
async close() {
|