taskchef 5.9.0 → 5.11.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 +24 -11
- package/index.js +2 -0
- package/package.json +1 -1
- package/src/codex-app.js +36 -3
- package/src/dashboard/app.js +26 -15
- package/src/dashboard/index.html +9 -1
- package/src/dashboard/state.js +43 -0
- package/src/dashboard.js +23 -5
package/README.md
CHANGED
|
@@ -181,11 +181,12 @@ terminal to stop the server.
|
|
|
181
181
|
The dashboard:
|
|
182
182
|
|
|
183
183
|
- orders tasks by their latest semantic or identity update;
|
|
184
|
-
- filters by project and
|
|
184
|
+
- filters by project, status, and latest update window (24 hours, 7 days, or
|
|
185
|
+
all time);
|
|
185
186
|
- shows dismissible notifications when tasks are added or changed;
|
|
186
187
|
- reveals the original instruction, latest semantic result, and task metadata;
|
|
187
|
-
- opens
|
|
188
|
-
|
|
188
|
+
- opens the recorded task directly in Codex when it has a supported UUID thread
|
|
189
|
+
ID, with a project-opening fallback for unresolved or legacy identities.
|
|
189
190
|
|
|
190
191
|
The server binds only to the numeric IPv4 loopback interface, requires the
|
|
191
192
|
unguessable launch capability before serving instructions or results, validates
|
|
@@ -200,14 +201,26 @@ the server or browser, the dashboard rejects logs above 16 MiB, histories above
|
|
|
200
201
|
2,000 tasks, and unusually large display fields while retaining the last valid
|
|
201
202
|
snapshot. The data CLI remains unaffected by these display limits.
|
|
202
203
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
204
|
+
Date windows advance while the page is open, even when the task file is idle.
|
|
205
|
+
They use persisted semantic/identity timestamps and identity changes observed by
|
|
206
|
+
the current server. Legacy schema-v1 identity resolutions did not record an
|
|
207
|
+
update timestamp, so after a dashboard restart those rare historical records
|
|
208
|
+
fall back to their creation time.
|
|
209
|
+
|
|
210
|
+
The dashboard uses Codex's registered `codex://threads/<thread-id>` desktop
|
|
211
|
+
route for direct navigation. A task-level refresh would require native Codex
|
|
212
|
+
metadata tools that are available to the report skill, not to a standalone
|
|
213
|
+
browser page. TaskChef also does not submit replies from the dashboard: `codex
|
|
214
|
+
resume <session> [prompt]` starts an interactive CLI session and may execute the
|
|
215
|
+
prompt immediately, rather than opening a reviewed draft in the desktop app.
|
|
216
|
+
Refresh and reply integrations remain deferred until Codex exposes supported
|
|
217
|
+
browser-facing contracts for those actions.
|
|
218
|
+
|
|
219
|
+
TaskChef task records do not contain model token usage, and the supported Codex
|
|
220
|
+
task metadata surface does not expose it to this local server. The dashboard
|
|
221
|
+
therefore does not estimate tokens or inspect private Codex session logs. Token
|
|
222
|
+
usage can be added later if Codex exposes a supported per-task usage field or
|
|
223
|
+
TaskChef executors begin reporting a structured usage value.
|
|
211
224
|
|
|
212
225
|
### Manage configured projects
|
|
213
226
|
|
package/index.js
CHANGED
package/package.json
CHANGED
package/src/codex-app.js
CHANGED
|
@@ -5,6 +5,7 @@ import { promisify } from "node:util";
|
|
|
5
5
|
|
|
6
6
|
const execFile = promisify(execFileCallback);
|
|
7
7
|
const CODEX_COMMAND_TIMEOUT_MS = 10_000;
|
|
8
|
+
const CODEX_THREAD_ID_PATTERN = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/i;
|
|
8
9
|
|
|
9
10
|
function runCodex(run, filePath, args) {
|
|
10
11
|
return run(filePath, args, { timeout: CODEX_COMMAND_TIMEOUT_MS, killSignal: "SIGKILL" });
|
|
@@ -26,6 +27,10 @@ function pathCandidates(env) {
|
|
|
26
27
|
.map((directory) => path.join(directory, process.platform === "win32" ? "codex.exe" : "codex"));
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
function isDesktopBundleCandidate(filePath) {
|
|
31
|
+
return filePath.includes(`${path.sep}Contents${path.sep}Resources${path.sep}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
async function supportsAppCommand(filePath, run) {
|
|
30
35
|
try {
|
|
31
36
|
const { stdout, stderr } = await runCodex(run, filePath, ["app", "--help"]);
|
|
@@ -50,8 +55,16 @@ export async function discoverCodexCli({
|
|
|
50
55
|
return { path: candidate, source: explicit !== null ? "explicit" : "environment" };
|
|
51
56
|
}
|
|
52
57
|
|
|
58
|
+
const candidates = pathCandidates(env);
|
|
59
|
+
for (const pathCandidate of candidates.filter(isDesktopBundleCandidate)) {
|
|
60
|
+
const candidate = await executable(pathCandidate);
|
|
61
|
+
if (candidate && await supportsAppCommand(candidate, run)) {
|
|
62
|
+
return { path: candidate, source: "desktop-path" };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
53
66
|
let candidate = null;
|
|
54
|
-
for (const pathCandidate of
|
|
67
|
+
for (const pathCandidate of candidates) {
|
|
55
68
|
candidate = await executable(pathCandidate);
|
|
56
69
|
if (candidate) break;
|
|
57
70
|
}
|
|
@@ -59,8 +72,7 @@ export async function discoverCodexCli({
|
|
|
59
72
|
if (!(await supportsAppCommand(candidate, run))) {
|
|
60
73
|
throw new Error(`Codex CLI does not support the app command: ${candidate}`);
|
|
61
74
|
}
|
|
62
|
-
|
|
63
|
-
return { path: candidate, source: bundled ? "desktop-path" : "path" };
|
|
75
|
+
return { path: candidate, source: "path" };
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
export async function openWorkspaceInCodex(workspace, options = {}) {
|
|
@@ -75,3 +87,24 @@ export async function openWorkspaceInCodex(workspace, options = {}) {
|
|
|
75
87
|
workspace,
|
|
76
88
|
};
|
|
77
89
|
}
|
|
90
|
+
|
|
91
|
+
export function isCodexThreadDeepLinkId(threadId) {
|
|
92
|
+
return typeof threadId === "string" && CODEX_THREAD_ID_PATTERN.test(threadId);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function openThreadInCodex(threadId, options = {}) {
|
|
96
|
+
if (!isCodexThreadDeepLinkId(threadId)) {
|
|
97
|
+
throw new Error("Codex thread ID is not supported by the desktop deep link");
|
|
98
|
+
}
|
|
99
|
+
const run = options.run ?? execFile;
|
|
100
|
+
const platform = options.platform ?? process.platform;
|
|
101
|
+
const url = `codex://threads/${encodeURIComponent(threadId)}`;
|
|
102
|
+
if (platform === "darwin") {
|
|
103
|
+
await runCodex(run, "/usr/bin/open", [url]);
|
|
104
|
+
} else if (platform === "win32") {
|
|
105
|
+
await runCodex(run, "rundll32.exe", ["url.dll,FileProtocolHandler", url]);
|
|
106
|
+
} else {
|
|
107
|
+
await runCodex(run, "xdg-open", [url]);
|
|
108
|
+
}
|
|
109
|
+
return { status: "requested", mechanism: "codex-deep-link", threadId, url };
|
|
110
|
+
}
|
package/src/dashboard/app.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
findCurrentTask,
|
|
3
|
+
KNOWN_TASK_STATUSES,
|
|
4
|
+
nextDateFilterRefreshDelay,
|
|
5
|
+
notificationTitle,
|
|
3
6
|
reconcileNotifications,
|
|
7
|
+
taskStatusLabel,
|
|
8
|
+
taskWithinDateFilter,
|
|
4
9
|
} from "./state.js";
|
|
5
10
|
|
|
6
11
|
const state = {
|
|
@@ -10,6 +15,7 @@ const state = {
|
|
|
10
15
|
initialized: false,
|
|
11
16
|
selectedTask: null,
|
|
12
17
|
};
|
|
18
|
+
let dateRefreshTimer = null;
|
|
13
19
|
|
|
14
20
|
const elements = {
|
|
15
21
|
clearNotifications: document.querySelector("#clear-notifications"),
|
|
@@ -18,6 +24,7 @@ const elements = {
|
|
|
18
24
|
connectionLabel: document.querySelector("#connection-label"),
|
|
19
25
|
copyThreadId: document.querySelector("#copy-thread-id"),
|
|
20
26
|
dashboardMessage: document.querySelector("#dashboard-message"),
|
|
27
|
+
dateFilter: document.querySelector("#date-filter"),
|
|
21
28
|
dialog: document.querySelector("#task-dialog"),
|
|
22
29
|
dialogInstruction: document.querySelector("#dialog-instruction"),
|
|
23
30
|
dialogMetadata: document.querySelector("#dialog-metadata"),
|
|
@@ -26,7 +33,7 @@ const elements = {
|
|
|
26
33
|
dialogTitle: document.querySelector("#dialog-title"),
|
|
27
34
|
emptyState: document.querySelector("#empty-state"),
|
|
28
35
|
notifications: document.querySelector("#notifications"),
|
|
29
|
-
openProject: document.querySelector("#open-
|
|
36
|
+
openProject: document.querySelector("#open-codex"),
|
|
30
37
|
projectFilter: document.querySelector("#project-filter"),
|
|
31
38
|
statusFilter: document.querySelector("#status-filter"),
|
|
32
39
|
taskCount: document.querySelector("#task-count"),
|
|
@@ -34,10 +41,6 @@ const elements = {
|
|
|
34
41
|
toastList: document.querySelector("#toast-list"),
|
|
35
42
|
};
|
|
36
43
|
|
|
37
|
-
function statusLabel(task) {
|
|
38
|
-
return task.status === null ? "unresolved" : task.status.replaceAll("_", " ");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
44
|
function formatTime(value) {
|
|
42
45
|
if (!value) return "—";
|
|
43
46
|
return new Intl.DateTimeFormat(undefined, {
|
|
@@ -76,9 +79,9 @@ function notificationToast(notification) {
|
|
|
76
79
|
text.type = "button";
|
|
77
80
|
text.className = "toast-content";
|
|
78
81
|
const title = document.createElement("strong");
|
|
79
|
-
title.textContent = notification.kind
|
|
82
|
+
title.textContent = notificationTitle(task, notification.kind);
|
|
80
83
|
const description = document.createElement("span");
|
|
81
|
-
description.textContent = `${task.title} · ${
|
|
84
|
+
description.textContent = `${task.title} · ${taskStatusLabel(task)}`;
|
|
82
85
|
text.append(title, description);
|
|
83
86
|
text.addEventListener("click", () => {
|
|
84
87
|
const current = findCurrentTask(state.tasks, notification.taskId);
|
|
@@ -121,13 +124,15 @@ function openDialog(task) {
|
|
|
121
124
|
elements.dialogInstruction.textContent = task.instruction;
|
|
122
125
|
elements.copyThreadId.disabled = !task.threadId;
|
|
123
126
|
elements.dialogMetadata.replaceChildren(
|
|
124
|
-
...detailRow("Status",
|
|
127
|
+
...detailRow("Status", taskStatusLabel(task)),
|
|
125
128
|
...detailRow("Task ID", task.id),
|
|
126
129
|
...detailRow("Thread ID", task.threadId),
|
|
127
130
|
...detailRow("Turn ID", task.turnId),
|
|
128
131
|
...detailRow("Project path", task.project.path),
|
|
129
132
|
...detailRow("Created", formatTime(task.createdAt)),
|
|
130
|
-
...detailRow("Updated", formatTime(
|
|
133
|
+
...detailRow("Updated", formatTime(
|
|
134
|
+
task.meaningfulUpdatedAt ?? task.updatedAt ?? task.createdAt,
|
|
135
|
+
)),
|
|
131
136
|
...detailRow("Updated by", task.updatedBy),
|
|
132
137
|
);
|
|
133
138
|
if (!elements.dialog.open) elements.dialog.showModal();
|
|
@@ -145,7 +150,7 @@ function taskCard(task) {
|
|
|
145
150
|
title.addEventListener("click", () => openDialog(task));
|
|
146
151
|
const badge = document.createElement("span");
|
|
147
152
|
badge.className = `status status-${task.status ?? "unresolved"}`;
|
|
148
|
-
badge.textContent =
|
|
153
|
+
badge.textContent = taskStatusLabel(task);
|
|
149
154
|
heading.append(title, badge);
|
|
150
155
|
const project = document.createElement("p");
|
|
151
156
|
project.className = "task-project";
|
|
@@ -154,8 +159,8 @@ function taskCard(task) {
|
|
|
154
159
|
summary.className = "task-summary";
|
|
155
160
|
summary.textContent = task.summary ?? "No semantic result reported yet.";
|
|
156
161
|
const time = document.createElement("time");
|
|
157
|
-
time.dateTime = task.updatedAt ?? task.createdAt;
|
|
158
|
-
time.textContent = `Updated ${formatTime(
|
|
162
|
+
time.dateTime = task.meaningfulUpdatedAt ?? task.updatedAt ?? task.createdAt;
|
|
163
|
+
time.textContent = `Updated ${formatTime(time.dateTime)}`;
|
|
159
164
|
article.append(heading, project, summary, time);
|
|
160
165
|
return article;
|
|
161
166
|
}
|
|
@@ -163,12 +168,17 @@ function taskCard(task) {
|
|
|
163
168
|
function render() {
|
|
164
169
|
const project = elements.projectFilter.value;
|
|
165
170
|
const status = elements.statusFilter.value;
|
|
171
|
+
const date = elements.dateFilter.value;
|
|
166
172
|
const visible = state.tasks.filter((task) =>
|
|
167
173
|
(!project || task.project.name === project)
|
|
168
|
-
&& (!status ||
|
|
174
|
+
&& (!status || taskStatusLabel(task) === status)
|
|
175
|
+
&& taskWithinDateFilter(task, date));
|
|
169
176
|
elements.taskList.replaceChildren(...visible.map(taskCard));
|
|
170
177
|
elements.emptyState.hidden = visible.length > 0;
|
|
171
178
|
elements.taskCount.textContent = `${visible.length} of ${state.tasks.length} task${state.tasks.length === 1 ? "" : "s"}`;
|
|
179
|
+
clearTimeout(dateRefreshTimer);
|
|
180
|
+
const refreshDelay = nextDateFilterRefreshDelay(visible, date);
|
|
181
|
+
dateRefreshTimer = refreshDelay === null ? null : setTimeout(render, refreshDelay);
|
|
172
182
|
}
|
|
173
183
|
|
|
174
184
|
function applySnapshot(snapshot) {
|
|
@@ -193,7 +203,7 @@ function applySnapshot(snapshot) {
|
|
|
193
203
|
);
|
|
194
204
|
replaceOptions(
|
|
195
205
|
elements.statusFilter,
|
|
196
|
-
[...new Set(state.tasks.map(
|
|
206
|
+
[...new Set([...KNOWN_TASK_STATUSES, ...state.tasks.map(taskStatusLabel)])],
|
|
197
207
|
"All statuses",
|
|
198
208
|
);
|
|
199
209
|
if (state.selectedTask) {
|
|
@@ -222,6 +232,7 @@ events.addEventListener("dashboard-error", (event) => {
|
|
|
222
232
|
|
|
223
233
|
elements.projectFilter.addEventListener("change", render);
|
|
224
234
|
elements.statusFilter.addEventListener("change", render);
|
|
235
|
+
elements.dateFilter.addEventListener("change", render);
|
|
225
236
|
elements.clearNotifications.addEventListener("click", () => {
|
|
226
237
|
state.notifications = [];
|
|
227
238
|
renderNotifications();
|
|
@@ -244,7 +255,7 @@ elements.openProject.addEventListener("click", async () => {
|
|
|
244
255
|
if (!state.selectedTask) return;
|
|
245
256
|
elements.openProject.disabled = true;
|
|
246
257
|
try {
|
|
247
|
-
const response = await fetch(`/api/tasks/${encodeURIComponent(state.selectedTask.id)}/open-
|
|
258
|
+
const response = await fetch(`/api/tasks/${encodeURIComponent(state.selectedTask.id)}/open-codex`, {
|
|
248
259
|
method: "POST",
|
|
249
260
|
});
|
|
250
261
|
const result = await response.json();
|
package/src/dashboard/index.html
CHANGED
|
@@ -35,6 +35,14 @@
|
|
|
35
35
|
<option value="">All statuses</option>
|
|
36
36
|
</select>
|
|
37
37
|
</label>
|
|
38
|
+
<label>
|
|
39
|
+
Updated
|
|
40
|
+
<select id="date-filter">
|
|
41
|
+
<option value="24h">Latest 24 hours</option>
|
|
42
|
+
<option value="7d">Latest 7 days</option>
|
|
43
|
+
<option value="all" selected>All time</option>
|
|
44
|
+
</select>
|
|
45
|
+
</label>
|
|
38
46
|
<p id="task-count" class="task-count" aria-live="polite"></p>
|
|
39
47
|
</section>
|
|
40
48
|
|
|
@@ -66,7 +74,7 @@
|
|
|
66
74
|
<button id="close-dialog" class="icon-button" type="button" aria-label="Close task details">×</button>
|
|
67
75
|
</div>
|
|
68
76
|
<div class="dialog-actions">
|
|
69
|
-
<button id="open-
|
|
77
|
+
<button id="open-codex" class="primary-button" type="button">Open task in Codex</button>
|
|
70
78
|
<button id="copy-thread-id" class="secondary-button" type="button">Copy thread ID</button>
|
|
71
79
|
</div>
|
|
72
80
|
<section>
|
package/src/dashboard/state.js
CHANGED
|
@@ -1,4 +1,47 @@
|
|
|
1
1
|
export const MAX_NOTIFICATIONS = 50;
|
|
2
|
+
export const KNOWN_TASK_STATUSES = [
|
|
3
|
+
"working",
|
|
4
|
+
"needs input",
|
|
5
|
+
"completed",
|
|
6
|
+
"failed",
|
|
7
|
+
"unresolved",
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
const DATE_WINDOWS_MS = new Map([
|
|
11
|
+
["24h", 24 * 60 * 60 * 1_000],
|
|
12
|
+
["7d", 7 * 24 * 60 * 60 * 1_000],
|
|
13
|
+
["all", null],
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function taskMeaningfulTime(task) {
|
|
17
|
+
return Date.parse(task.meaningfulUpdatedAt ?? task.updatedAt ?? task.createdAt);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function taskStatusLabel(task) {
|
|
21
|
+
return task.status === null ? "unresolved" : task.status.replaceAll("_", " ");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function notificationTitle(task, kind) {
|
|
25
|
+
return kind === "new" ? "New task" : `Task ${taskStatusLabel(task)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function taskWithinDateFilter(task, filter, now = Date.now()) {
|
|
29
|
+
const windowMs = DATE_WINDOWS_MS.get(filter);
|
|
30
|
+
if (windowMs === null) return true;
|
|
31
|
+
if (windowMs === undefined) return false;
|
|
32
|
+
const meaningfulTime = taskMeaningfulTime(task);
|
|
33
|
+
return Number.isFinite(meaningfulTime) && meaningfulTime >= now - windowMs;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function nextDateFilterRefreshDelay(tasks, filter, now = Date.now()) {
|
|
37
|
+
const windowMs = DATE_WINDOWS_MS.get(filter);
|
|
38
|
+
if (windowMs === null || windowMs === undefined) return null;
|
|
39
|
+
const nextCutoff = tasks
|
|
40
|
+
.map((task) => taskMeaningfulTime(task) + windowMs - now)
|
|
41
|
+
.filter((delay) => Number.isFinite(delay) && delay >= 0)
|
|
42
|
+
.reduce((minimum, delay) => Math.min(minimum, delay), Number.POSITIVE_INFINITY);
|
|
43
|
+
return Number.isFinite(nextCutoff) ? nextCutoff + 1 : null;
|
|
44
|
+
}
|
|
2
45
|
|
|
3
46
|
export function taskSignature(task) {
|
|
4
47
|
return JSON.stringify([
|
package/src/dashboard.js
CHANGED
|
@@ -6,7 +6,11 @@ import http from "node:http";
|
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { fileURLToPath } from "node:url";
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
isCodexThreadDeepLinkId,
|
|
11
|
+
openThreadInCodex,
|
|
12
|
+
openWorkspaceInCodex,
|
|
13
|
+
} from "./codex-app.js";
|
|
10
14
|
import {
|
|
11
15
|
canonicalDirectory,
|
|
12
16
|
canonicalGitRoot,
|
|
@@ -184,7 +188,12 @@ export class DashboardMonitor extends EventEmitter {
|
|
|
184
188
|
revision: this.revision,
|
|
185
189
|
generatedAt: new Date().toISOString(),
|
|
186
190
|
healthy: !this.unhealthy,
|
|
187
|
-
tasks: this.tasks
|
|
191
|
+
tasks: this.tasks.map((task) => ({
|
|
192
|
+
...task,
|
|
193
|
+
meaningfulUpdatedAt: new Date(
|
|
194
|
+
meaningfulUpdateTime(task, this.observedUpdateTimes),
|
|
195
|
+
).toISOString(),
|
|
196
|
+
})),
|
|
188
197
|
};
|
|
189
198
|
}
|
|
190
199
|
|
|
@@ -395,6 +404,7 @@ export async function createDashboardServer({
|
|
|
395
404
|
port = 3210,
|
|
396
405
|
monitorOptions = {},
|
|
397
406
|
openProject = null,
|
|
407
|
+
openThread = null,
|
|
398
408
|
} = {}) {
|
|
399
409
|
if (!LOOPBACK_HOSTS.has(host)) {
|
|
400
410
|
throw new Error("dashboard host must be a loopback address");
|
|
@@ -501,7 +511,7 @@ export async function createDashboardServer({
|
|
|
501
511
|
return;
|
|
502
512
|
}
|
|
503
513
|
|
|
504
|
-
const taskMatch = url.pathname.match(/^\/api\/tasks\/([a-zA-Z0-9._-]+)\/open-
|
|
514
|
+
const taskMatch = url.pathname.match(/^\/api\/tasks\/([a-zA-Z0-9._-]+)\/open-codex$/);
|
|
505
515
|
if (taskMatch && method === "POST") {
|
|
506
516
|
if (request.headers.origin !== allowedOrigin) {
|
|
507
517
|
sendJson(response, 403, { message: "Dashboard session validation failed." });
|
|
@@ -513,6 +523,12 @@ export async function createDashboardServer({
|
|
|
513
523
|
return;
|
|
514
524
|
}
|
|
515
525
|
try {
|
|
526
|
+
if (isCodexThreadDeepLinkId(task.threadId)) {
|
|
527
|
+
if (openThread) await openThread(task.threadId);
|
|
528
|
+
else await openThreadInCodex(task.threadId);
|
|
529
|
+
sendJson(response, 202, { message: "Opened this task in Codex." });
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
516
532
|
const trustedProject = (await readConfig(monitor.workspace, { checkPaths: false })).projects
|
|
517
533
|
.find((project) => project.path === task.project.path);
|
|
518
534
|
if (!trustedProject) {
|
|
@@ -533,11 +549,13 @@ export async function createDashboardServer({
|
|
|
533
549
|
if (openProject) await openProject(canonicalProjectPath);
|
|
534
550
|
else await openWorkspaceInCodex(canonicalProjectPath);
|
|
535
551
|
sendJson(response, 202, {
|
|
536
|
-
message:
|
|
552
|
+
message: task.threadId
|
|
553
|
+
? "Opened the project in Codex; this legacy thread ID cannot use direct navigation."
|
|
554
|
+
: "Opened the project in Codex; this task does not yet have a thread ID.",
|
|
537
555
|
});
|
|
538
556
|
} catch {
|
|
539
557
|
sendJson(response, 503, {
|
|
540
|
-
message: "Codex could not be opened.
|
|
558
|
+
message: "Codex could not be opened. Open the project and select the recorded thread instead.",
|
|
541
559
|
});
|
|
542
560
|
}
|
|
543
561
|
return;
|