taskchef 5.9.0 → 5.10.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 +24 -14
- package/src/dashboard/index.html +9 -1
- package/src/dashboard/state.js +39 -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,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
findCurrentTask,
|
|
3
|
+
KNOWN_TASK_STATUSES,
|
|
4
|
+
nextDateFilterRefreshDelay,
|
|
3
5
|
reconcileNotifications,
|
|
6
|
+
taskStatusLabel,
|
|
7
|
+
taskWithinDateFilter,
|
|
4
8
|
} from "./state.js";
|
|
5
9
|
|
|
6
10
|
const state = {
|
|
@@ -10,6 +14,7 @@ const state = {
|
|
|
10
14
|
initialized: false,
|
|
11
15
|
selectedTask: null,
|
|
12
16
|
};
|
|
17
|
+
let dateRefreshTimer = null;
|
|
13
18
|
|
|
14
19
|
const elements = {
|
|
15
20
|
clearNotifications: document.querySelector("#clear-notifications"),
|
|
@@ -18,6 +23,7 @@ const elements = {
|
|
|
18
23
|
connectionLabel: document.querySelector("#connection-label"),
|
|
19
24
|
copyThreadId: document.querySelector("#copy-thread-id"),
|
|
20
25
|
dashboardMessage: document.querySelector("#dashboard-message"),
|
|
26
|
+
dateFilter: document.querySelector("#date-filter"),
|
|
21
27
|
dialog: document.querySelector("#task-dialog"),
|
|
22
28
|
dialogInstruction: document.querySelector("#dialog-instruction"),
|
|
23
29
|
dialogMetadata: document.querySelector("#dialog-metadata"),
|
|
@@ -26,7 +32,7 @@ const elements = {
|
|
|
26
32
|
dialogTitle: document.querySelector("#dialog-title"),
|
|
27
33
|
emptyState: document.querySelector("#empty-state"),
|
|
28
34
|
notifications: document.querySelector("#notifications"),
|
|
29
|
-
openProject: document.querySelector("#open-
|
|
35
|
+
openProject: document.querySelector("#open-codex"),
|
|
30
36
|
projectFilter: document.querySelector("#project-filter"),
|
|
31
37
|
statusFilter: document.querySelector("#status-filter"),
|
|
32
38
|
taskCount: document.querySelector("#task-count"),
|
|
@@ -34,10 +40,6 @@ const elements = {
|
|
|
34
40
|
toastList: document.querySelector("#toast-list"),
|
|
35
41
|
};
|
|
36
42
|
|
|
37
|
-
function statusLabel(task) {
|
|
38
|
-
return task.status === null ? "unresolved" : task.status.replaceAll("_", " ");
|
|
39
|
-
}
|
|
40
|
-
|
|
41
43
|
function formatTime(value) {
|
|
42
44
|
if (!value) return "—";
|
|
43
45
|
return new Intl.DateTimeFormat(undefined, {
|
|
@@ -78,7 +80,7 @@ function notificationToast(notification) {
|
|
|
78
80
|
const title = document.createElement("strong");
|
|
79
81
|
title.textContent = notification.kind === "new" ? "New task" : "Task updated";
|
|
80
82
|
const description = document.createElement("span");
|
|
81
|
-
description.textContent = `${task.title} · ${
|
|
83
|
+
description.textContent = `${task.title} · ${taskStatusLabel(task)}`;
|
|
82
84
|
text.append(title, description);
|
|
83
85
|
text.addEventListener("click", () => {
|
|
84
86
|
const current = findCurrentTask(state.tasks, notification.taskId);
|
|
@@ -121,13 +123,15 @@ function openDialog(task) {
|
|
|
121
123
|
elements.dialogInstruction.textContent = task.instruction;
|
|
122
124
|
elements.copyThreadId.disabled = !task.threadId;
|
|
123
125
|
elements.dialogMetadata.replaceChildren(
|
|
124
|
-
...detailRow("Status",
|
|
126
|
+
...detailRow("Status", taskStatusLabel(task)),
|
|
125
127
|
...detailRow("Task ID", task.id),
|
|
126
128
|
...detailRow("Thread ID", task.threadId),
|
|
127
129
|
...detailRow("Turn ID", task.turnId),
|
|
128
130
|
...detailRow("Project path", task.project.path),
|
|
129
131
|
...detailRow("Created", formatTime(task.createdAt)),
|
|
130
|
-
...detailRow("Updated", formatTime(
|
|
132
|
+
...detailRow("Updated", formatTime(
|
|
133
|
+
task.meaningfulUpdatedAt ?? task.updatedAt ?? task.createdAt,
|
|
134
|
+
)),
|
|
131
135
|
...detailRow("Updated by", task.updatedBy),
|
|
132
136
|
);
|
|
133
137
|
if (!elements.dialog.open) elements.dialog.showModal();
|
|
@@ -145,7 +149,7 @@ function taskCard(task) {
|
|
|
145
149
|
title.addEventListener("click", () => openDialog(task));
|
|
146
150
|
const badge = document.createElement("span");
|
|
147
151
|
badge.className = `status status-${task.status ?? "unresolved"}`;
|
|
148
|
-
badge.textContent =
|
|
152
|
+
badge.textContent = taskStatusLabel(task);
|
|
149
153
|
heading.append(title, badge);
|
|
150
154
|
const project = document.createElement("p");
|
|
151
155
|
project.className = "task-project";
|
|
@@ -154,8 +158,8 @@ function taskCard(task) {
|
|
|
154
158
|
summary.className = "task-summary";
|
|
155
159
|
summary.textContent = task.summary ?? "No semantic result reported yet.";
|
|
156
160
|
const time = document.createElement("time");
|
|
157
|
-
time.dateTime = task.updatedAt ?? task.createdAt;
|
|
158
|
-
time.textContent = `Updated ${formatTime(
|
|
161
|
+
time.dateTime = task.meaningfulUpdatedAt ?? task.updatedAt ?? task.createdAt;
|
|
162
|
+
time.textContent = `Updated ${formatTime(time.dateTime)}`;
|
|
159
163
|
article.append(heading, project, summary, time);
|
|
160
164
|
return article;
|
|
161
165
|
}
|
|
@@ -163,12 +167,17 @@ function taskCard(task) {
|
|
|
163
167
|
function render() {
|
|
164
168
|
const project = elements.projectFilter.value;
|
|
165
169
|
const status = elements.statusFilter.value;
|
|
170
|
+
const date = elements.dateFilter.value;
|
|
166
171
|
const visible = state.tasks.filter((task) =>
|
|
167
172
|
(!project || task.project.name === project)
|
|
168
|
-
&& (!status ||
|
|
173
|
+
&& (!status || taskStatusLabel(task) === status)
|
|
174
|
+
&& taskWithinDateFilter(task, date));
|
|
169
175
|
elements.taskList.replaceChildren(...visible.map(taskCard));
|
|
170
176
|
elements.emptyState.hidden = visible.length > 0;
|
|
171
177
|
elements.taskCount.textContent = `${visible.length} of ${state.tasks.length} task${state.tasks.length === 1 ? "" : "s"}`;
|
|
178
|
+
clearTimeout(dateRefreshTimer);
|
|
179
|
+
const refreshDelay = nextDateFilterRefreshDelay(visible, date);
|
|
180
|
+
dateRefreshTimer = refreshDelay === null ? null : setTimeout(render, refreshDelay);
|
|
172
181
|
}
|
|
173
182
|
|
|
174
183
|
function applySnapshot(snapshot) {
|
|
@@ -193,7 +202,7 @@ function applySnapshot(snapshot) {
|
|
|
193
202
|
);
|
|
194
203
|
replaceOptions(
|
|
195
204
|
elements.statusFilter,
|
|
196
|
-
[...new Set(state.tasks.map(
|
|
205
|
+
[...new Set([...KNOWN_TASK_STATUSES, ...state.tasks.map(taskStatusLabel)])],
|
|
197
206
|
"All statuses",
|
|
198
207
|
);
|
|
199
208
|
if (state.selectedTask) {
|
|
@@ -222,6 +231,7 @@ events.addEventListener("dashboard-error", (event) => {
|
|
|
222
231
|
|
|
223
232
|
elements.projectFilter.addEventListener("change", render);
|
|
224
233
|
elements.statusFilter.addEventListener("change", render);
|
|
234
|
+
elements.dateFilter.addEventListener("change", render);
|
|
225
235
|
elements.clearNotifications.addEventListener("click", () => {
|
|
226
236
|
state.notifications = [];
|
|
227
237
|
renderNotifications();
|
|
@@ -244,7 +254,7 @@ elements.openProject.addEventListener("click", async () => {
|
|
|
244
254
|
if (!state.selectedTask) return;
|
|
245
255
|
elements.openProject.disabled = true;
|
|
246
256
|
try {
|
|
247
|
-
const response = await fetch(`/api/tasks/${encodeURIComponent(state.selectedTask.id)}/open-
|
|
257
|
+
const response = await fetch(`/api/tasks/${encodeURIComponent(state.selectedTask.id)}/open-codex`, {
|
|
248
258
|
method: "POST",
|
|
249
259
|
});
|
|
250
260
|
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,43 @@
|
|
|
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 taskWithinDateFilter(task, filter, now = Date.now()) {
|
|
25
|
+
const windowMs = DATE_WINDOWS_MS.get(filter);
|
|
26
|
+
if (windowMs === null) return true;
|
|
27
|
+
if (windowMs === undefined) return false;
|
|
28
|
+
const meaningfulTime = taskMeaningfulTime(task);
|
|
29
|
+
return Number.isFinite(meaningfulTime) && meaningfulTime >= now - windowMs;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function nextDateFilterRefreshDelay(tasks, filter, now = Date.now()) {
|
|
33
|
+
const windowMs = DATE_WINDOWS_MS.get(filter);
|
|
34
|
+
if (windowMs === null || windowMs === undefined) return null;
|
|
35
|
+
const nextCutoff = tasks
|
|
36
|
+
.map((task) => taskMeaningfulTime(task) + windowMs - now)
|
|
37
|
+
.filter((delay) => Number.isFinite(delay) && delay >= 0)
|
|
38
|
+
.reduce((minimum, delay) => Math.min(minimum, delay), Number.POSITIVE_INFINITY);
|
|
39
|
+
return Number.isFinite(nextCutoff) ? nextCutoff + 1 : null;
|
|
40
|
+
}
|
|
2
41
|
|
|
3
42
|
export function taskSignature(task) {
|
|
4
43
|
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;
|