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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "5.11.1",
3
+ "version": "5.12.0",
4
4
  "description": "Dispatch work from a data-only workspace to visible Codex project tasks.",
5
5
  "author": {
6
6
  "name": "Favo Yang",
package/README.md CHANGED
@@ -174,10 +174,9 @@ executors report results:
174
174
  taskchef dashboard
175
175
  ```
176
176
 
177
- Open the printed capability URL, which starts with `http://127.0.0.1:3210` and
178
- contains a one-time launch token. Do not share it: the browser exchanges it for
179
- a local session cookie before loading task data, then removes the token from the
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, requires the
195
- unguessable launch capability before serving instructions or results, validates
196
- the same task schema as the CLI, retains its last valid snapshot if the log
197
- becomes invalid, and never writes dispatcher-workspace files. It limits event
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskchef",
3
- "version": "5.11.1",
3
+ "version": "5.12.0",
4
4
  "description": "A non-blocking interactive dispatcher for visible Codex tasks.",
5
5
  "license": "MIT",
6
6
  "author": "Favo Yang",
@@ -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
+ }
@@ -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
- article.append(heading, project, summary, time);
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
- elements.openProject.disabled = true;
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
  });
@@ -12,7 +12,7 @@
12
12
  <header class="site-header">
13
13
  <div>
14
14
  <p class="eyebrow">TaskChef</p>
15
- <h1>Task dashboard</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">
@@ -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 session validation failed." });
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}/?token=${encodeURIComponent(capabilityToken)}`,
571
+ url: `${allowedOrigin}/`,
604
572
  monitor,
605
573
  get eventClientCount() { return clients.size; },
606
574
  async close() {