parallel-codex-tui 0.2.9 → 0.3.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/README.md +22 -7
- package/dist/bootstrap.js +8 -1
- package/dist/cli.js +18 -3
- package/dist/core/role-configuration.js +238 -0
- package/dist/core/session-manager.js +287 -4
- package/dist/domain/schemas.js +3 -1
- package/dist/orchestrator/orchestrator.js +135 -11
- package/dist/tui/App.js +615 -16
- package/dist/tui/AppShell.js +7 -1
- package/dist/tui/InputBar.js +35 -5
- package/dist/tui/MainConversationsView.js +28 -8
- package/dist/tui/RoleConfigurationView.js +96 -0
- package/dist/tui/StatusDetailView.js +9 -6
- package/dist/tui/keyboard.js +11 -0
- package/dist/tui/role-configuration-state.js +74 -0
- package/dist/version.js +1 -1
- package/dist/workers/native-attach.js +5 -1
- package/package.json +1 -1
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import {
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
3
|
+
import { cp, mkdir, mkdtemp, open, readdir, rename, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import { basename, dirname, join } from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline";
|
|
4
6
|
import { z } from "zod";
|
|
5
7
|
import { appendJsonLine, appendText, ensureDir, pathExists, readJson, readRecentJsonLines, readTextIfExists, removeIfExists, writeJson, writeText } from "./file-store.js";
|
|
6
8
|
import { runWithLeaseFinalization } from "./lease-finalization.js";
|
|
@@ -183,7 +185,7 @@ export class SessionManager {
|
|
|
183
185
|
}
|
|
184
186
|
return readJson(path, MainConversationStateSchema);
|
|
185
187
|
}
|
|
186
|
-
async listMainConversations(limit = 100) {
|
|
188
|
+
async listMainConversations(limit = 100, options = {}) {
|
|
187
189
|
const boundedLimit = Number.isFinite(limit)
|
|
188
190
|
? Math.min(500, Math.max(0, Math.trunc(limit)))
|
|
189
191
|
: 100;
|
|
@@ -223,7 +225,13 @@ export class SessionManager {
|
|
|
223
225
|
return summary;
|
|
224
226
|
};
|
|
225
227
|
for (const archive of archives) {
|
|
226
|
-
ensureConversation(archive.id, archive.created_at, archive.last_activated_at);
|
|
228
|
+
const conversation = ensureConversation(archive.id, archive.created_at, archive.last_activated_at);
|
|
229
|
+
if (archive.title) {
|
|
230
|
+
conversation.title = archive.title;
|
|
231
|
+
}
|
|
232
|
+
if (archive.archived_at) {
|
|
233
|
+
conversation.archivedAt = archive.archived_at;
|
|
234
|
+
}
|
|
227
235
|
}
|
|
228
236
|
for (const record of records) {
|
|
229
237
|
const id = record.conversation_id ?? null;
|
|
@@ -254,7 +262,9 @@ export class SessionManager {
|
|
|
254
262
|
conversation.nativeSessionCount = archived.size;
|
|
255
263
|
}));
|
|
256
264
|
return [...conversations.values()]
|
|
265
|
+
.filter((conversation) => options.includeArchived || !conversation.archivedAt || conversation.current)
|
|
257
266
|
.sort((left, right) => (Number(right.current) - Number(left.current)
|
|
267
|
+
|| Number(Boolean(left.archivedAt)) - Number(Boolean(right.archivedAt))
|
|
258
268
|
|| right.lastActivityAt.localeCompare(left.lastActivityAt)
|
|
259
269
|
|| mainConversationScopeKey(right.id).localeCompare(mainConversationScopeKey(left.id))))
|
|
260
270
|
.slice(0, boundedLimit);
|
|
@@ -292,11 +302,14 @@ export class SessionManager {
|
|
|
292
302
|
await ensureDir(main.dir);
|
|
293
303
|
const lease = await this.claimTaskRunLease(main.dir);
|
|
294
304
|
return runWithLeaseFinalization("Main conversation restore", lease, async () => {
|
|
295
|
-
const conversations = await this.listMainConversations(500);
|
|
305
|
+
const conversations = await this.listMainConversations(500, { includeArchived: true });
|
|
296
306
|
const target = conversations.find((conversation) => conversation.id === id);
|
|
297
307
|
if (!target) {
|
|
298
308
|
throw new Error(`Main conversation not found: ${id ?? "legacy"}`);
|
|
299
309
|
}
|
|
310
|
+
if (target.archivedAt) {
|
|
311
|
+
throw new Error(`Cannot restore archived Main conversation ${id ?? "legacy"}. Unarchive it first.`);
|
|
312
|
+
}
|
|
300
313
|
const current = await this.readMainConversationState();
|
|
301
314
|
const currentId = current?.id ?? null;
|
|
302
315
|
if (currentId === id) {
|
|
@@ -331,6 +344,140 @@ export class SessionManager {
|
|
|
331
344
|
return { conversation: restored, restoredNativeSessions, changed: true };
|
|
332
345
|
});
|
|
333
346
|
}
|
|
347
|
+
async renameMainConversation(conversationId, title) {
|
|
348
|
+
const normalizedTitle = normalizeMainConversationTitle(title);
|
|
349
|
+
return this.withMainConversationManagementLease(conversationId, "rename", async (main, conversation) => {
|
|
350
|
+
const metadata = await this.mainConversationArchiveMetadata(conversation);
|
|
351
|
+
await this.writeMainConversationArchive({ ...metadata, title: normalizedTitle });
|
|
352
|
+
await this.appendEvent(main, "main.conversation_renamed", `Renamed ${conversation.id ?? "legacy conversation"} to ${normalizedTitle}`);
|
|
353
|
+
return this.requireMainConversationSummary(conversation.id);
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
async setMainConversationArchived(conversationId, archived) {
|
|
357
|
+
return this.withMainConversationManagementLease(conversationId, archived ? "archive" : "unarchive", async (main, conversation) => {
|
|
358
|
+
if (archived && conversation.current) {
|
|
359
|
+
throw new Error("Cannot archive the current Main conversation. Restore another conversation first.");
|
|
360
|
+
}
|
|
361
|
+
if (Boolean(conversation.archivedAt) === archived) {
|
|
362
|
+
return conversation;
|
|
363
|
+
}
|
|
364
|
+
const metadata = await this.mainConversationArchiveMetadata(conversation);
|
|
365
|
+
const next = archived
|
|
366
|
+
? MainConversationArchiveSchema.parse({
|
|
367
|
+
...metadata,
|
|
368
|
+
archived_at: this.now().toISOString()
|
|
369
|
+
})
|
|
370
|
+
: withoutMainConversationArchiveTime(metadata);
|
|
371
|
+
await writeJson(join(this.mainConversationArchiveDir(conversation.id), "meta.json"), next);
|
|
372
|
+
await this.appendEvent(main, archived ? "main.conversation_archived" : "main.conversation_unarchived", archived
|
|
373
|
+
? `Archived ${conversation.id ?? "legacy conversation"}`
|
|
374
|
+
: `Unarchived ${conversation.id ?? "legacy conversation"}`);
|
|
375
|
+
return this.requireMainConversationSummary(conversation.id);
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
async deleteMainConversation(conversationId) {
|
|
379
|
+
await this.withMainConversationManagementLease(conversationId, "delete", async (main, conversation) => {
|
|
380
|
+
if (conversation.current) {
|
|
381
|
+
throw new Error("Cannot delete the current Main conversation. Restore another conversation first.");
|
|
382
|
+
}
|
|
383
|
+
const staging = await mkdtemp(join(main.dir, ".conversation-delete-"));
|
|
384
|
+
const chatPath = join(main.dir, "chat.jsonl");
|
|
385
|
+
const replacementPath = join(staging, "chat.next.jsonl");
|
|
386
|
+
const originalPath = join(staging, "chat.original.jsonl");
|
|
387
|
+
const archivePath = this.mainConversationArchiveDir(conversation.id);
|
|
388
|
+
const stagedArchivePath = join(staging, "archive");
|
|
389
|
+
let archiveMoved = false;
|
|
390
|
+
let originalMoved = false;
|
|
391
|
+
let replacementInstalled = false;
|
|
392
|
+
try {
|
|
393
|
+
await writeConversationChatFile(chatPath, replacementPath, conversation.id, "exclude");
|
|
394
|
+
if (await pathExists(archivePath)) {
|
|
395
|
+
await rename(archivePath, stagedArchivePath);
|
|
396
|
+
archiveMoved = true;
|
|
397
|
+
}
|
|
398
|
+
if (await pathExists(chatPath)) {
|
|
399
|
+
await rename(chatPath, originalPath);
|
|
400
|
+
originalMoved = true;
|
|
401
|
+
}
|
|
402
|
+
await rename(replacementPath, chatPath);
|
|
403
|
+
replacementInstalled = true;
|
|
404
|
+
await this.appendEvent(main, "main.conversation_deleted", `Deleted ${conversation.id ?? "legacy conversation"}`);
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
const rollbackErrors = [];
|
|
408
|
+
if (replacementInstalled) {
|
|
409
|
+
try {
|
|
410
|
+
await removeIfExists(chatPath);
|
|
411
|
+
}
|
|
412
|
+
catch (rollbackError) {
|
|
413
|
+
rollbackErrors.push(rollbackError);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
if (originalMoved) {
|
|
417
|
+
try {
|
|
418
|
+
await rename(originalPath, chatPath);
|
|
419
|
+
}
|
|
420
|
+
catch (rollbackError) {
|
|
421
|
+
rollbackErrors.push(rollbackError);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (archiveMoved) {
|
|
425
|
+
try {
|
|
426
|
+
await rename(stagedArchivePath, archivePath);
|
|
427
|
+
}
|
|
428
|
+
catch (rollbackError) {
|
|
429
|
+
rollbackErrors.push(rollbackError);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
await rm(staging, { force: true, recursive: true }).catch(() => undefined);
|
|
433
|
+
if (rollbackErrors.length > 0) {
|
|
434
|
+
throw new Error(`Main conversation deletion failed and rollback also failed: ${errorMessage(error)}`, { cause: new AggregateError([error, ...rollbackErrors]) });
|
|
435
|
+
}
|
|
436
|
+
throw error;
|
|
437
|
+
}
|
|
438
|
+
await rm(staging, { force: true, recursive: true }).catch(() => undefined);
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
async exportMainConversation(conversationId) {
|
|
442
|
+
return this.withMainConversationManagementLease(conversationId, "export", async (main, conversation) => {
|
|
443
|
+
const createdAt = this.now().toISOString();
|
|
444
|
+
const exportsRoot = join(this.projectRoot, this.dataDir, "exports");
|
|
445
|
+
await ensureDir(exportsRoot);
|
|
446
|
+
const scope = conversation.id ?? "main-legacy";
|
|
447
|
+
const staging = await mkdtemp(join(exportsRoot, `.${scope}-`));
|
|
448
|
+
const suffix = basename(staging).slice(-6);
|
|
449
|
+
const stamp = createdAt.replace(/[^0-9]/g, "").slice(0, 14);
|
|
450
|
+
const destination = join(exportsRoot, `${scope}-${stamp}-${suffix}`);
|
|
451
|
+
try {
|
|
452
|
+
const messageCount = await writeConversationChatFile(join(main.dir, "chat.jsonl"), join(staging, "chat.jsonl"), conversation.id, "include");
|
|
453
|
+
const nativeSessions = await this.mainConversationNativeSessions(conversation.id, conversation.current);
|
|
454
|
+
for (const session of nativeSessions) {
|
|
455
|
+
await writeJson(join(staging, "native-sessions", `${session.worker_id}.json`), session);
|
|
456
|
+
}
|
|
457
|
+
await writeJson(join(staging, "manifest.json"), {
|
|
458
|
+
format: "parallel-codex-main-conversation-export-v1",
|
|
459
|
+
exported_at: createdAt,
|
|
460
|
+
source_workspace: this.projectRoot,
|
|
461
|
+
conversation: {
|
|
462
|
+
id: conversation.id,
|
|
463
|
+
title: conversation.title,
|
|
464
|
+
created_at: conversation.createdAt,
|
|
465
|
+
last_activity_at: conversation.lastActivityAt,
|
|
466
|
+
...(conversation.archivedAt ? { archived_at: conversation.archivedAt } : {})
|
|
467
|
+
},
|
|
468
|
+
message_count: messageCount,
|
|
469
|
+
native_session_count: nativeSessions.length
|
|
470
|
+
});
|
|
471
|
+
await this.appendEvent(main, "main.conversation_exported", `Exported ${conversation.id ?? "legacy conversation"}`);
|
|
472
|
+
await rename(staging, destination);
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
await rm(staging, { force: true, recursive: true });
|
|
476
|
+
throw error;
|
|
477
|
+
}
|
|
478
|
+
return { conversationId: conversation.id, path: destination, createdAt };
|
|
479
|
+
});
|
|
480
|
+
}
|
|
334
481
|
async reconcileInterruptedMainSession() {
|
|
335
482
|
const main = this.taskFromId("main");
|
|
336
483
|
if (!(await pathExists(main.dir))) {
|
|
@@ -924,6 +1071,24 @@ export class SessionManager {
|
|
|
924
1071
|
await this.clearWorkerStatusNativeSession(worker);
|
|
925
1072
|
await this.index?.deleteNativeSession(this.taskIdFromWorkerDir(worker.dir), record.worker_id);
|
|
926
1073
|
}
|
|
1074
|
+
async mainConversationArchiveMetadata(conversation) {
|
|
1075
|
+
const existing = await readMainConversationArchiveIfValid(join(this.mainConversationArchiveDir(conversation.id), "meta.json"));
|
|
1076
|
+
return MainConversationArchiveSchema.parse({
|
|
1077
|
+
...existing,
|
|
1078
|
+
version: 1,
|
|
1079
|
+
id: conversation.id,
|
|
1080
|
+
created_at: existing?.created_at ?? conversation.createdAt,
|
|
1081
|
+
last_activated_at: existing?.last_activated_at ?? conversation.lastActivityAt
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
async requireMainConversationSummary(conversationId) {
|
|
1085
|
+
const conversation = (await this.listMainConversations(500, { includeArchived: true }))
|
|
1086
|
+
.find((entry) => entry.id === conversationId);
|
|
1087
|
+
if (!conversation) {
|
|
1088
|
+
throw new Error(`Main conversation disappeared from its catalog: ${conversationId ?? "legacy"}`);
|
|
1089
|
+
}
|
|
1090
|
+
return conversation;
|
|
1091
|
+
}
|
|
927
1092
|
mainConversationArchiveDir(conversationId) {
|
|
928
1093
|
return join(this.mainSessionDir(), MAIN_CONVERSATION_ARCHIVE_DIRECTORY, mainConversationScopeKey(conversationId));
|
|
929
1094
|
}
|
|
@@ -949,6 +1114,7 @@ export class SessionManager {
|
|
|
949
1114
|
const path = join(this.mainConversationArchiveDir(parsed.id), "meta.json");
|
|
950
1115
|
const existing = await readMainConversationArchiveIfValid(path);
|
|
951
1116
|
await writeJson(path, MainConversationArchiveSchema.parse({
|
|
1117
|
+
...existing,
|
|
952
1118
|
...parsed,
|
|
953
1119
|
created_at: existing && existing.created_at < parsed.created_at
|
|
954
1120
|
? existing.created_at
|
|
@@ -1045,6 +1211,39 @@ export class SessionManager {
|
|
|
1045
1211
|
}
|
|
1046
1212
|
return ids;
|
|
1047
1213
|
}
|
|
1214
|
+
async mainConversationNativeSessions(conversationId, includeActive) {
|
|
1215
|
+
const sessions = new Map();
|
|
1216
|
+
const archiveDir = join(this.mainConversationArchiveDir(conversationId), "native-sessions");
|
|
1217
|
+
if (await pathExists(archiveDir)) {
|
|
1218
|
+
for (const entry of await readdir(archiveDir, { withFileTypes: true })) {
|
|
1219
|
+
if (!entry.isFile() || !entry.name.endsWith(".json")) {
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
const record = await readNativeSessionIfValid(join(archiveDir, entry.name));
|
|
1223
|
+
if (record?.scope === "main"
|
|
1224
|
+
&& record.role === "main"
|
|
1225
|
+
&& safeMainWorkerId(record.worker_id)
|
|
1226
|
+
&& entry.name === `${record.worker_id}.json`) {
|
|
1227
|
+
sessions.set(record.worker_id, record);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
if (includeActive) {
|
|
1232
|
+
const mainDir = this.mainSessionDir();
|
|
1233
|
+
for (const entry of await readdir(mainDir, { withFileTypes: true })) {
|
|
1234
|
+
if (!entry.isDirectory() || entry.name === MAIN_CONVERSATION_ARCHIVE_DIRECTORY) {
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
const record = await this.readNativeSession({ dir: join(mainDir, entry.name) });
|
|
1238
|
+
if (record?.scope === "main"
|
|
1239
|
+
&& record.role === "main"
|
|
1240
|
+
&& safeMainWorkerId(record.worker_id)) {
|
|
1241
|
+
sessions.set(record.worker_id, record);
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
return [...sessions.values()].sort((left, right) => left.worker_id.localeCompare(right.worker_id));
|
|
1246
|
+
}
|
|
1048
1247
|
async appendEvent(task, type, message) {
|
|
1049
1248
|
const event = {
|
|
1050
1249
|
time: this.now().toISOString(),
|
|
@@ -1069,6 +1268,20 @@ export class SessionManager {
|
|
|
1069
1268
|
return run(task, meta);
|
|
1070
1269
|
});
|
|
1071
1270
|
}
|
|
1271
|
+
async withMainConversationManagementLease(conversationId, operation, run) {
|
|
1272
|
+
const id = conversationId === null ? null : ConversationIdSchema.parse(conversationId);
|
|
1273
|
+
const main = this.taskFromId("main");
|
|
1274
|
+
await ensureDir(main.dir);
|
|
1275
|
+
const lease = await this.claimTaskRunLease(main.dir);
|
|
1276
|
+
return runWithLeaseFinalization(`Main conversation ${operation}`, lease, async () => {
|
|
1277
|
+
const conversation = (await this.listMainConversations(500, { includeArchived: true }))
|
|
1278
|
+
.find((entry) => entry.id === id);
|
|
1279
|
+
if (!conversation) {
|
|
1280
|
+
throw new Error(`Main conversation not found: ${id ?? "legacy"}`);
|
|
1281
|
+
}
|
|
1282
|
+
return run(main, conversation);
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1072
1285
|
assertTerminalTask(meta, operation) {
|
|
1073
1286
|
if (!TERMINAL_TASK_STATES.has(meta.status)) {
|
|
1074
1287
|
throw new Error(`Cannot ${operation} task ${meta.id} while it is ${meta.status}.`);
|
|
@@ -1620,6 +1833,10 @@ function nextConversationId(baseId, previousId) {
|
|
|
1620
1833
|
function mainConversationScopeKey(conversationId) {
|
|
1621
1834
|
return conversationId ?? LEGACY_MAIN_CONVERSATION_DIRECTORY;
|
|
1622
1835
|
}
|
|
1836
|
+
function withoutMainConversationArchiveTime(metadata) {
|
|
1837
|
+
const { archived_at: _archivedAt, ...active } = metadata;
|
|
1838
|
+
return MainConversationArchiveSchema.parse(active);
|
|
1839
|
+
}
|
|
1623
1840
|
function mainConversationTitle(text, conversationId) {
|
|
1624
1841
|
const sanitized = sanitizePersistedMainMessage("user", text)
|
|
1625
1842
|
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
@@ -1634,9 +1851,75 @@ function mainConversationTitle(text, conversationId) {
|
|
|
1634
1851
|
? `${characters.slice(0, 77).join("")}...`
|
|
1635
1852
|
: sanitized;
|
|
1636
1853
|
}
|
|
1854
|
+
function normalizeMainConversationTitle(title) {
|
|
1855
|
+
const normalized = title
|
|
1856
|
+
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
|
1857
|
+
.replace(/[\u0000-\u001f\u007f]/g, " ")
|
|
1858
|
+
.replace(/\s+/g, " ")
|
|
1859
|
+
.trim();
|
|
1860
|
+
if (!normalized) {
|
|
1861
|
+
throw new Error("Main conversation title cannot be empty.");
|
|
1862
|
+
}
|
|
1863
|
+
if (Array.from(normalized).length > 160) {
|
|
1864
|
+
throw new Error("Main conversation title cannot exceed 160 characters.");
|
|
1865
|
+
}
|
|
1866
|
+
return normalized;
|
|
1867
|
+
}
|
|
1637
1868
|
function safeMainWorkerId(workerId) {
|
|
1638
1869
|
return SAFE_MAIN_WORKER_ID.test(workerId) && workerId !== "." && workerId !== "..";
|
|
1639
1870
|
}
|
|
1871
|
+
async function writeConversationChatFile(sourcePath, destinationPath, conversationId, mode) {
|
|
1872
|
+
await ensureDir(dirname(destinationPath));
|
|
1873
|
+
const output = await open(destinationPath, "wx");
|
|
1874
|
+
let matched = 0;
|
|
1875
|
+
let buffer = "";
|
|
1876
|
+
try {
|
|
1877
|
+
if (!(await pathExists(sourcePath))) {
|
|
1878
|
+
await output.close();
|
|
1879
|
+
return 0;
|
|
1880
|
+
}
|
|
1881
|
+
const input = createReadStream(sourcePath, { encoding: "utf8" });
|
|
1882
|
+
const lines = createInterface({ input, crlfDelay: Infinity });
|
|
1883
|
+
for await (const line of lines) {
|
|
1884
|
+
const belongs = chatLineBelongsToMainConversation(line, conversationId);
|
|
1885
|
+
if (belongs) {
|
|
1886
|
+
matched += 1;
|
|
1887
|
+
}
|
|
1888
|
+
if ((mode === "include") !== belongs) {
|
|
1889
|
+
continue;
|
|
1890
|
+
}
|
|
1891
|
+
buffer += `${line}\n`;
|
|
1892
|
+
if (Buffer.byteLength(buffer, "utf8") >= 64 * 1024) {
|
|
1893
|
+
await output.write(buffer);
|
|
1894
|
+
buffer = "";
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
if (buffer) {
|
|
1898
|
+
await output.write(buffer);
|
|
1899
|
+
}
|
|
1900
|
+
await output.close();
|
|
1901
|
+
return matched;
|
|
1902
|
+
}
|
|
1903
|
+
catch (error) {
|
|
1904
|
+
await output.close().catch(() => undefined);
|
|
1905
|
+
await removeIfExists(destinationPath).catch(() => undefined);
|
|
1906
|
+
throw error;
|
|
1907
|
+
}
|
|
1908
|
+
}
|
|
1909
|
+
function chatLineBelongsToMainConversation(line, conversationId) {
|
|
1910
|
+
try {
|
|
1911
|
+
const parsed = ChatRecordSchema.safeParse(JSON.parse(line));
|
|
1912
|
+
if (!parsed.success || parsed.data.task_id) {
|
|
1913
|
+
return false;
|
|
1914
|
+
}
|
|
1915
|
+
return conversationId
|
|
1916
|
+
? parsed.data.conversation_id === conversationId
|
|
1917
|
+
: !parsed.data.conversation_id;
|
|
1918
|
+
}
|
|
1919
|
+
catch {
|
|
1920
|
+
return false;
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1640
1923
|
function normalizeTaskTitle(title) {
|
|
1641
1924
|
const normalized = title
|
|
1642
1925
|
.replace(/\x1b\[[0-?]*[ -/]*[@-~]/g, "")
|
package/dist/domain/schemas.js
CHANGED
|
@@ -209,7 +209,9 @@ export const MainConversationArchiveSchema = z.object({
|
|
|
209
209
|
version: z.literal(1),
|
|
210
210
|
id: ConversationIdSchema.nullable(),
|
|
211
211
|
created_at: z.string().datetime(),
|
|
212
|
-
last_activated_at: z.string().datetime()
|
|
212
|
+
last_activated_at: z.string().datetime(),
|
|
213
|
+
title: z.string().min(1).max(160).optional(),
|
|
214
|
+
archived_at: z.string().datetime().optional()
|
|
213
215
|
});
|
|
214
216
|
export const ChatRecordSchema = z.object({
|
|
215
217
|
time: z.string().datetime(),
|