parallel-codex-tui 0.2.9 → 0.2.10
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 +5 -5
- package/dist/cli.js +8 -2
- package/dist/core/session-manager.js +287 -4
- package/dist/domain/schemas.js +3 -1
- package/dist/tui/App.js +220 -14
- package/dist/tui/InputBar.js +8 -2
- package/dist/tui/MainConversationsView.js +28 -8
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Built with Codex-assisted development.
|
|
|
6
6
|
|
|
7
7
|
## Current Release
|
|
8
8
|
|
|
9
|
-
`v0.2.
|
|
9
|
+
`v0.2.10` is available from [npm](https://www.npmjs.com/package/parallel-codex-tui/v/0.2.10) and as a [GitHub Release](https://github.com/allendred/parallel-codex-tui/releases/tag/v0.2.10). `Ctrl+T`, then `C`, opens every persisted Main conversation and now provides the same durable management lifecycle as Task sessions: rename, archive, unarchive, scoped export, and confirmed deletion. Restoring one switches the exact `conversation_id`, reloads only that conversation's file-backed history, and restores its archived native Codex or Claude session ids when they are available. `Ctrl+N` still creates an explicit, durable Main conversation boundary without deleting chat, Tasks, or Worker logs.
|
|
10
10
|
|
|
11
11
|
A real Claude acceptance retires the first native session, inserts 16 newer records so the secret is absent from the inline transcript, then proves a fresh Claude session can read the scoped snapshot and recover it. The same live TUI presses `Ctrl+N`, verifies the replacement session is retired, starts another native session whose prompt and archive exclude the secret, and then switches between two isolated workspaces. The bounded rollover fallback remains included: every Main call rebuilds recent context from canonical `chat.jsonl`, so a fresh Agent still receives the latest dialogue inside its current scope.
|
|
12
12
|
|
|
@@ -25,7 +25,7 @@ Highlights:
|
|
|
25
25
|
- Named Worker Providers support Codex-compatible, Claude-compatible, OpenAI-compatible, Anthropic-compatible, and custom generic commands with independent role, model, environment, permission, resume, and interactive settings.
|
|
26
26
|
- Worker overview, Feature board, collaboration timeline, Task and Main conversation centers, status details, rendered Markdown/Diff/error logs, Unicode search, keyboard navigation, mouse scrolling, and configurable themes share one terminal UI system.
|
|
27
27
|
|
|
28
|
-
Release acceptance includes a real three-Feature Tetris task with parallel Actor/Critic waves and final integration review. A clean `v0.2.5` task also ran Codex Judge and Actor, a buffered Claude Critic that independently executed `node --test` and `npm test`, atomic integration, and a resumed Codex Judge that independently passed all seven acceptance criteria. Real Codex and Claude probes both proved fresh and same-session resume calls; Codex fresh and resume runs executed workspace writes with root-level `-a never`, and Claude automated sessions executed safe Bash tools with `auto` permissions. The semantic Router completed a live classification, and one TUI completed Main calls in two workspaces before restoring the first workspace without leaking chat state. PTY coverage runs in Apple Terminal, tmux, and Zellij profiles at narrow and wide sizes, including status/log equivalence, preserving the native output tail across status-detail round trips, and proving both inline and on-demand file memory after native-session rollover. The deterministic repository suite contains 1,
|
|
28
|
+
Release acceptance includes a real three-Feature Tetris task with parallel Actor/Critic waves and final integration review. A clean `v0.2.5` task also ran Codex Judge and Actor, a buffered Claude Critic that independently executed `node --test` and `npm test`, atomic integration, and a resumed Codex Judge that independently passed all seven acceptance criteria. Real Codex and Claude probes both proved fresh and same-session resume calls; Codex fresh and resume runs executed workspace writes with root-level `-a never`, and Claude automated sessions executed safe Bash tools with `auto` permissions. The semantic Router completed a live classification, and one TUI completed Main calls in two workspaces before restoring the first workspace without leaking chat state. PTY coverage runs in Apple Terminal, tmux, and Zellij profiles at narrow and wide sizes, including status/log equivalence, preserving the native output tail across status-detail round trips, and proving both inline and on-demand file memory after native-session rollover. The deterministic repository suite contains 1,311 tests across 130 files: 1,310 pass by default, while one quota-consuming real-Agent test is skipped and passes through `npm run test:real-agents`.
|
|
29
29
|
|
|
30
30
|
Real Provider probes depend on valid local CLI credentials. In particular, authenticate the Claude CLI before selecting a Claude-compatible Worker, then run `parallel-codex-tui --doctor --probe-agents` to prove fresh and resumed calls on that machine.
|
|
31
31
|
|
|
@@ -469,7 +469,7 @@ From Worker overview, press `C` to open the file-backed Actor/Critic collaborati
|
|
|
469
469
|
|
|
470
470
|
`Ctrl+T` opens the workspace's persisted Task sessions without exiting the outer TUI. The list shows status, creation time, turn and Worker counts, plus the number of distinct native sessions after deduplicating reused `engine + session_id` bindings; `>` marks the selected row and `*` marks the active Task. Use Up/Down, PageUp/PageDown, the mouse wheel, or `Tab` to select, Enter to restore, `C` to open Main conversations, `I` to inspect the complete session hierarchy, `R` to rename with Unicode-safe cursor editing, `A` to archive or unarchive, `D` twice to confirm deletion, `E` to export, `H` to show or hide archived sessions, `Ctrl+N` to leave the active Task and start a fresh Main conversation, and `Esc` to return without losing the chat draft. Archived sessions are hidden by default, cannot be restored until unarchived, and are excluded from automatic startup selection. Archive, delete, and export require a terminal Task with no live task lease; the active Task cannot be archived or deleted. Exports are complete file-backed snapshots under `.parallel-codex/exports/<task>-<timestamp>/` with a versioned manifest and without transient lease files. Restoring a Task reloads its route, Workers, retry state, and recorded native session ids. The selected active Task is stored in `session-index.sqlite` and survives process and workspace switches; `Ctrl+N` persists an intentionally empty active-Task context instead of reopening an older Task on restart. SQLite schema changes run as ordered, transactional migrations. Existing catalogs receive a pre-migration snapshot, each successful startup/reindex refreshes a healthy backup, and an integrity failure restores that backup or rebuilds the catalog from authoritative Task files while preserving the corrupt copy for inspection. `meta.json` and the Worker files remain the rebuild authority.
|
|
471
471
|
|
|
472
|
-
The Main conversation list shows each scoped title, message count, distinct native session count, last activity, current `*` marker, and selected `>` marker. Use the same Up/Down, PageUp/PageDown, mouse-wheel, or `Tab` navigation; Enter restores the selected conversation, `N` starts a new one, `T` returns to Tasks, and `Esc` returns to chat. Restoration preserves every Task and Worker log, switches `conversation.json`, and rebuilds Main history from records carrying only the selected `conversation_id` (or the pre-boundary legacy scope). Before switching, active Main native sessions are archived and retired; archived bindings for the selected conversation are then restored for best-effort native resume. A brand-new empty legacy scope is not added to the catalog.
|
|
472
|
+
The Main conversation list shows each scoped title, message count, distinct native session count, last activity, current `*` marker, and selected `>` marker. Use the same Up/Down, PageUp/PageDown, mouse-wheel, or `Tab` navigation; Enter restores the selected conversation, `R` renames it with Unicode-safe cursor editing, `A` archives or unarchives it, `D` twice confirms deletion, `E` exports it, `H` shows or hides archived conversations, `N` starts a new one, `T` returns to Tasks, and `Esc` returns to chat. Archived conversations are hidden by default and cannot be restored until unarchived; the current Main conversation cannot be archived or deleted. A Main export is a versioned, conversation-scoped snapshot under `.parallel-codex/exports/<conversation>-<timestamp>/` containing only that conversation's chat rows and matching native sessions. Confirmed deletion streams the shared chat JSONL into an atomic replacement, preserves Task chat and malformed evidence, and restores the original chat and conversation archive if publication fails. Restoration preserves every Task and Worker log, switches `conversation.json`, and rebuilds Main history from records carrying only the selected `conversation_id` (or the pre-boundary legacy scope). Before switching, active Main native sessions are archived and retired; archived bindings for the selected conversation are then restored for best-effort native resume. A brand-new empty legacy scope is not added to the catalog.
|
|
473
473
|
|
|
474
474
|
The Task detail view reads authoritative task files and presents `Project -> Task -> Turn -> Worker -> Native session`, including each request, role, engine, configured model, Worker state, native session id, working directory, and last activity time. Historical Workers from earlier turns remain selectable after same-task follow-ups. Use Up/Down or `Tab` to select a Worker, Enter to open its retained log, `C` or `Ctrl+O` to continue the original native session, `B` to ask the native CLI to fork it, `R` to refresh, and `Esc` to return to the Task list. Continue and fork restore the selected Worker's recorded cwd and writable roots while the outer TUI stays open. Codex and Claude have built-in fork commands; a generic CLI exposes `B` only when `interactive.forkArgs` is configured. The child fork is owned and persisted by the native CLI; the indexed parent session remains unchanged.
|
|
475
475
|
|
|
@@ -523,12 +523,12 @@ The release job installs npm `^11.5.1`, runs on Node `24.15.x`, publishes the pr
|
|
|
523
523
|
To publish a release, update `package.json` and `src/version.ts` to the same version, then push a matching tag:
|
|
524
524
|
|
|
525
525
|
```bash
|
|
526
|
-
VERSION=0.2.
|
|
526
|
+
VERSION=0.2.10
|
|
527
527
|
git tag "v$VERSION"
|
|
528
528
|
git push origin "v$VERSION"
|
|
529
529
|
```
|
|
530
530
|
|
|
531
|
-
You can also run the Release workflow manually and enter the same tag value. The release tag must match `package.json`; for example, package version `0.2.
|
|
531
|
+
You can also run the Release workflow manually and enter the same tag value. The release tag must match `package.json`; for example, package version `0.2.10` requires tag `v0.2.10`. Published tags such as `v0.2.9` are immutable and must not be moved or reused.
|
|
532
532
|
|
|
533
533
|
## Publishing Hygiene
|
|
534
534
|
|
package/dist/cli.js
CHANGED
|
@@ -127,11 +127,17 @@ async function main() {
|
|
|
127
127
|
records,
|
|
128
128
|
policy: routerDiagnosticsPolicy(latestConfig.router)
|
|
129
129
|
};
|
|
130
|
-
}, loadTaskSessions: (options) => state.runtime.index.listTasks(100, options), loadMainConversations: () => state.runtime.sessions.listMainConversations(100), activateMainConversation: async (conversationId) => {
|
|
130
|
+
}, loadTaskSessions: (options) => state.runtime.index.listTasks(100, options), loadMainConversations: (options) => state.runtime.sessions.listMainConversations(100, options), activateMainConversation: async (conversationId) => {
|
|
131
131
|
const restored = await state.runtime.sessions.activateMainConversation(conversationId);
|
|
132
132
|
await state.runtime.index.setActiveTaskId(null);
|
|
133
133
|
return restored;
|
|
134
|
-
},
|
|
134
|
+
}, renameMainConversation: async (conversationId, title) => {
|
|
135
|
+
await state.runtime.sessions.renameMainConversation(conversationId, title);
|
|
136
|
+
}, setMainConversationArchived: async (conversationId, archived) => {
|
|
137
|
+
await state.runtime.sessions.setMainConversationArchived(conversationId, archived);
|
|
138
|
+
}, deleteMainConversation: async (conversationId) => {
|
|
139
|
+
await state.runtime.sessions.deleteMainConversation(conversationId);
|
|
140
|
+
}, exportMainConversation: async (conversationId) => (await state.runtime.sessions.exportMainConversation(conversationId)).path, loadTaskSessionDetails: (task) => loadPersistedTaskSessionDetails({
|
|
135
141
|
task,
|
|
136
142
|
taskDir: state.runtime.sessions.taskFromId(task.id).dir,
|
|
137
143
|
modelNames: Object.fromEntries(Object.entries(state.runtime.config.workers).map(([id, worker]) => [id, worker.model.name]))
|
|
@@ -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(),
|
package/dist/tui/App.js
CHANGED
|
@@ -50,7 +50,7 @@ const EMPTY_WORKER_NAVIGATION_TARGETS = {
|
|
|
50
50
|
errorOffsets: [],
|
|
51
51
|
diffOffsets: []
|
|
52
52
|
};
|
|
53
|
-
export function App({ config, orchestrator, cwd, initialTaskId = null, initialRoute = null, initialWorkers, initialCanRetryTask = false, initialMessages = [], persistChatMessage, reloadConfig, workspaceChoices = [], switchWorkspace, loadRouterDiagnostics, loadTaskSessions, loadMainConversations, activateMainConversation, loadTaskSessionDetails, renameTaskSession, setTaskSessionArchived, deleteTaskSession, exportTaskSession, exportDiagnostics, loadCollaborationTimeline, activateTaskSession, startMainConversation, prepareNativeAttach, prepareNativeFork, startNativeAttach, copyToClipboard = copyTextToClipboard, shutdownSignal }) {
|
|
53
|
+
export function App({ config, orchestrator, cwd, initialTaskId = null, initialRoute = null, initialWorkers, initialCanRetryTask = false, initialMessages = [], persistChatMessage, reloadConfig, workspaceChoices = [], switchWorkspace, loadRouterDiagnostics, loadTaskSessions, loadMainConversations, activateMainConversation, renameMainConversation, setMainConversationArchived, deleteMainConversation, exportMainConversation, loadTaskSessionDetails, renameTaskSession, setTaskSessionArchived, deleteTaskSession, exportTaskSession, exportDiagnostics, loadCollaborationTimeline, activateTaskSession, startMainConversation, prepareNativeAttach, prepareNativeFork, startNativeAttach, copyToClipboard = copyTextToClipboard, shutdownSignal }) {
|
|
54
54
|
configureTuiTheme({
|
|
55
55
|
theme: config.ui.theme,
|
|
56
56
|
colors: config.ui.colors
|
|
@@ -101,6 +101,8 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
101
101
|
const [sessionCenterMode, setSessionCenterMode] = useState("tasks");
|
|
102
102
|
const [mainConversations, setMainConversations] = useState([]);
|
|
103
103
|
const [selectedMainConversationIndex, setSelectedMainConversationIndex] = useState(0);
|
|
104
|
+
const [mainConversationsIncludeArchived, setMainConversationsIncludeArchived] = useState(false);
|
|
105
|
+
const [mainConversationAction, setMainConversationAction] = useState(null);
|
|
104
106
|
const [taskSessionsLoading, setTaskSessionsLoading] = useState(false);
|
|
105
107
|
const [taskSessionsError, setTaskSessionsError] = useState(null);
|
|
106
108
|
const [taskSessionsNotice, setTaskSessionsNotice] = useState(null);
|
|
@@ -156,6 +158,8 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
156
158
|
const sessionCenterModeRef = useRef("tasks");
|
|
157
159
|
const mainConversationsRef = useRef(mainConversations);
|
|
158
160
|
const selectedMainConversationIndexRef = useRef(selectedMainConversationIndex);
|
|
161
|
+
const mainConversationsIncludeArchivedRef = useRef(mainConversationsIncludeArchived);
|
|
162
|
+
const mainConversationActionRef = useRef(mainConversationAction);
|
|
159
163
|
const taskSessionsLoadingRef = useRef(taskSessionsLoading);
|
|
160
164
|
const taskSessionsIncludeArchivedRef = useRef(taskSessionsIncludeArchived);
|
|
161
165
|
const taskSessionActionRef = useRef(taskSessionAction);
|
|
@@ -188,7 +192,12 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
188
192
|
const activateSelectedTaskSessionRef = useRef(activateSelectedTaskSession);
|
|
189
193
|
const refreshTaskSessionsRef = useRef(refreshTaskSessions);
|
|
190
194
|
const openMainConversationsRef = useRef(openMainConversations);
|
|
195
|
+
const refreshMainConversationsRef = useRef(refreshMainConversations);
|
|
191
196
|
const restoreSelectedMainConversationRef = useRef(restoreSelectedMainConversation);
|
|
197
|
+
const renameSelectedMainConversationRef = useRef(renameSelectedMainConversation);
|
|
198
|
+
const archiveSelectedMainConversationRef = useRef(archiveSelectedMainConversation);
|
|
199
|
+
const deleteSelectedMainConversationRef = useRef(deleteSelectedMainConversation);
|
|
200
|
+
const exportSelectedMainConversationRef = useRef(exportSelectedMainConversation);
|
|
192
201
|
const renameSelectedTaskSessionRef = useRef(renameSelectedTaskSession);
|
|
193
202
|
const archiveSelectedTaskSessionRef = useRef(archiveSelectedTaskSession);
|
|
194
203
|
const deleteSelectedTaskSessionRef = useRef(deleteSelectedTaskSession);
|
|
@@ -352,6 +361,12 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
352
361
|
useEffect(() => {
|
|
353
362
|
selectedMainConversationIndexRef.current = selectedMainConversationIndex;
|
|
354
363
|
}, [selectedMainConversationIndex]);
|
|
364
|
+
useEffect(() => {
|
|
365
|
+
mainConversationsIncludeArchivedRef.current = mainConversationsIncludeArchived;
|
|
366
|
+
}, [mainConversationsIncludeArchived]);
|
|
367
|
+
useEffect(() => {
|
|
368
|
+
mainConversationActionRef.current = mainConversationAction;
|
|
369
|
+
}, [mainConversationAction]);
|
|
355
370
|
useEffect(() => {
|
|
356
371
|
taskSessionsLoadingRef.current = taskSessionsLoading;
|
|
357
372
|
}, [taskSessionsLoading]);
|
|
@@ -424,7 +439,12 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
424
439
|
activateSelectedTaskSessionRef.current = activateSelectedTaskSession;
|
|
425
440
|
refreshTaskSessionsRef.current = refreshTaskSessions;
|
|
426
441
|
openMainConversationsRef.current = openMainConversations;
|
|
442
|
+
refreshMainConversationsRef.current = refreshMainConversations;
|
|
427
443
|
restoreSelectedMainConversationRef.current = restoreSelectedMainConversation;
|
|
444
|
+
renameSelectedMainConversationRef.current = renameSelectedMainConversation;
|
|
445
|
+
archiveSelectedMainConversationRef.current = archiveSelectedMainConversation;
|
|
446
|
+
deleteSelectedMainConversationRef.current = deleteSelectedMainConversation;
|
|
447
|
+
exportSelectedMainConversationRef.current = exportSelectedMainConversation;
|
|
428
448
|
renameSelectedTaskSessionRef.current = renameSelectedTaskSession;
|
|
429
449
|
archiveSelectedTaskSessionRef.current = archiveSelectedTaskSession;
|
|
430
450
|
deleteSelectedTaskSessionRef.current = deleteSelectedTaskSession;
|
|
@@ -875,7 +895,41 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
875
895
|
return;
|
|
876
896
|
}
|
|
877
897
|
if (sessionCenterModeRef.current === "conversations") {
|
|
898
|
+
const conversationAction = mainConversationActionRef.current;
|
|
899
|
+
if (conversationAction) {
|
|
900
|
+
if (chunk === "\x1b") {
|
|
901
|
+
updateMainConversationAction(null);
|
|
902
|
+
setTaskSessionsError(null);
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
if (taskSessionsLoadingRef.current) {
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
if (conversationAction.type === "rename") {
|
|
909
|
+
const update = applyChatInputChunk(conversationAction.value, chunk, conversationAction.cursor);
|
|
910
|
+
if (update.submit !== null) {
|
|
911
|
+
void renameSelectedMainConversationRef.current({
|
|
912
|
+
...conversationAction,
|
|
913
|
+
value: update.submit,
|
|
914
|
+
cursor: Array.from(update.submit).length
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
else {
|
|
918
|
+
updateMainConversationAction({
|
|
919
|
+
...conversationAction,
|
|
920
|
+
value: update.value,
|
|
921
|
+
cursor: update.cursor
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
return;
|
|
925
|
+
}
|
|
926
|
+
if (chunk === "d" || chunk === "D") {
|
|
927
|
+
void deleteSelectedMainConversationRef.current(conversationAction);
|
|
928
|
+
}
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
878
931
|
if (isTaskSessionsShortcut(chunk, {}) || chunk === "\x1b") {
|
|
932
|
+
updateMainConversationAction(null);
|
|
879
933
|
setTaskSessionsError(null);
|
|
880
934
|
setTaskSessionsNotice(null);
|
|
881
935
|
viewRef.current = taskSessionsReturnViewRef.current;
|
|
@@ -890,6 +944,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
890
944
|
return;
|
|
891
945
|
}
|
|
892
946
|
if (chunk === "t" || chunk === "T") {
|
|
947
|
+
updateMainConversationAction(null);
|
|
893
948
|
sessionCenterModeRef.current = "tasks";
|
|
894
949
|
setSessionCenterMode("tasks");
|
|
895
950
|
setTaskSessionsError(null);
|
|
@@ -901,6 +956,63 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
901
956
|
void openMainConversationsRef.current();
|
|
902
957
|
return;
|
|
903
958
|
}
|
|
959
|
+
const selectedConversation = mainConversationsRef.current[selectedMainConversationIndexRef.current];
|
|
960
|
+
if (chunk === "r" || chunk === "R") {
|
|
961
|
+
if (selectedConversation) {
|
|
962
|
+
updateMainConversationAction({
|
|
963
|
+
type: "rename",
|
|
964
|
+
conversationId: selectedConversation.id,
|
|
965
|
+
title: selectedConversation.title,
|
|
966
|
+
value: selectedConversation.title,
|
|
967
|
+
cursor: Array.from(selectedConversation.title).length
|
|
968
|
+
});
|
|
969
|
+
setTaskSessionsError(null);
|
|
970
|
+
setTaskSessionsNotice(null);
|
|
971
|
+
}
|
|
972
|
+
return;
|
|
973
|
+
}
|
|
974
|
+
if (chunk === "a" || chunk === "A") {
|
|
975
|
+
void archiveSelectedMainConversationRef.current();
|
|
976
|
+
return;
|
|
977
|
+
}
|
|
978
|
+
if (chunk === "d" || chunk === "D") {
|
|
979
|
+
if (selectedConversation) {
|
|
980
|
+
if (selectedConversation.current) {
|
|
981
|
+
setTaskSessionsError("Restore another Main conversation before deleting the current one");
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
updateMainConversationAction({
|
|
985
|
+
type: "delete",
|
|
986
|
+
conversationId: selectedConversation.id,
|
|
987
|
+
title: selectedConversation.title
|
|
988
|
+
});
|
|
989
|
+
setTaskSessionsError(null);
|
|
990
|
+
setTaskSessionsNotice(null);
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
if (chunk === "e" || chunk === "E") {
|
|
996
|
+
void exportSelectedMainConversationRef.current();
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
if (chunk === "h" || chunk === "H") {
|
|
1000
|
+
const includeArchived = !mainConversationsIncludeArchivedRef.current;
|
|
1001
|
+
mainConversationsIncludeArchivedRef.current = includeArchived;
|
|
1002
|
+
setMainConversationsIncludeArchived(includeArchived);
|
|
1003
|
+
setTaskSessionsError(null);
|
|
1004
|
+
setTaskSessionsNotice(null);
|
|
1005
|
+
void (async () => {
|
|
1006
|
+
await refreshMainConversationsRef.current(selectedConversation?.id);
|
|
1007
|
+
if (sessionCenterModeRef.current === "conversations"
|
|
1008
|
+
&& mainConversationsIncludeArchivedRef.current === includeArchived) {
|
|
1009
|
+
setTaskSessionsNotice(includeArchived
|
|
1010
|
+
? "Archived conversations shown"
|
|
1011
|
+
: "Archived conversations hidden");
|
|
1012
|
+
}
|
|
1013
|
+
})();
|
|
1014
|
+
return;
|
|
1015
|
+
}
|
|
904
1016
|
if (chunk === "\r" || chunk === "\n") {
|
|
905
1017
|
void restoreSelectedMainConversationRef.current();
|
|
906
1018
|
return;
|
|
@@ -2285,6 +2397,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
2285
2397
|
setTaskSessionDetailSelectedWorkerIndex(0);
|
|
2286
2398
|
setTaskSessionDetailNotice(null);
|
|
2287
2399
|
updateTaskSessionAction(null);
|
|
2400
|
+
updateMainConversationAction(null);
|
|
2288
2401
|
sessionCenterModeRef.current = "tasks";
|
|
2289
2402
|
setSessionCenterMode("tasks");
|
|
2290
2403
|
await refreshTaskSessions(activeTaskIdRef.current);
|
|
@@ -2347,24 +2460,32 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
2347
2460
|
taskSessionDetailsRef.current = null;
|
|
2348
2461
|
setTaskSessionDetails(null);
|
|
2349
2462
|
updateTaskSessionAction(null);
|
|
2463
|
+
updateMainConversationAction(null);
|
|
2350
2464
|
setTaskSessionsError(null);
|
|
2351
2465
|
setTaskSessionsNotice(null);
|
|
2352
|
-
|
|
2353
|
-
|
|
2466
|
+
await refreshMainConversations(previousId);
|
|
2467
|
+
}
|
|
2468
|
+
async function refreshMainConversations(preferredConversationId, manageLoading = true) {
|
|
2469
|
+
if (manageLoading) {
|
|
2470
|
+
taskSessionsLoadingRef.current = true;
|
|
2471
|
+
setTaskSessionsLoading(true);
|
|
2472
|
+
}
|
|
2354
2473
|
const sequence = taskSessionsLoadSequenceRef.current + 1;
|
|
2355
2474
|
taskSessionsLoadSequenceRef.current = sequence;
|
|
2356
2475
|
try {
|
|
2357
2476
|
if (!loadMainConversations) {
|
|
2358
2477
|
throw new Error("Main conversations are unavailable");
|
|
2359
2478
|
}
|
|
2360
|
-
const conversations = await loadMainConversations(
|
|
2479
|
+
const conversations = await loadMainConversations({
|
|
2480
|
+
includeArchived: mainConversationsIncludeArchivedRef.current
|
|
2481
|
+
});
|
|
2361
2482
|
if (taskSessionsLoadSequenceRef.current !== sequence) {
|
|
2362
2483
|
return;
|
|
2363
2484
|
}
|
|
2364
2485
|
mainConversationsRef.current = conversations;
|
|
2365
2486
|
setMainConversations(conversations);
|
|
2366
|
-
const preservedIndex =
|
|
2367
|
-
? conversations.findIndex((conversation) => conversation.id ===
|
|
2487
|
+
const preservedIndex = preferredConversationId !== undefined
|
|
2488
|
+
? conversations.findIndex((conversation) => conversation.id === preferredConversationId)
|
|
2368
2489
|
: -1;
|
|
2369
2490
|
const currentIndex = conversations.findIndex((conversation) => conversation.current);
|
|
2370
2491
|
const selectedIndex = preservedIndex >= 0
|
|
@@ -2381,7 +2502,7 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
2381
2502
|
}
|
|
2382
2503
|
}
|
|
2383
2504
|
finally {
|
|
2384
|
-
if (taskSessionsLoadSequenceRef.current === sequence) {
|
|
2505
|
+
if (manageLoading && taskSessionsLoadSequenceRef.current === sequence) {
|
|
2385
2506
|
taskSessionsLoadingRef.current = false;
|
|
2386
2507
|
setTaskSessionsLoading(false);
|
|
2387
2508
|
}
|
|
@@ -2405,8 +2526,8 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
2405
2526
|
}
|
|
2406
2527
|
const result = await activateMainConversation(selected.id);
|
|
2407
2528
|
if (!result.changed) {
|
|
2408
|
-
await openMainConversations();
|
|
2409
2529
|
setTaskSessionsNotice(`Already current · ${result.conversation.title}`);
|
|
2530
|
+
await refreshMainConversations(result.conversation.id, false);
|
|
2410
2531
|
return;
|
|
2411
2532
|
}
|
|
2412
2533
|
await activateTaskSession?.(null);
|
|
@@ -2424,6 +2545,79 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
2424
2545
|
setTaskSessionsLoading(false);
|
|
2425
2546
|
}
|
|
2426
2547
|
}
|
|
2548
|
+
function updateMainConversationAction(next) {
|
|
2549
|
+
mainConversationActionRef.current = next;
|
|
2550
|
+
setMainConversationAction(next);
|
|
2551
|
+
}
|
|
2552
|
+
async function runMainConversationOperation(run, notice, preferredConversationId) {
|
|
2553
|
+
if (taskSessionsLoadingRef.current) {
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
taskSessionsLoadingRef.current = true;
|
|
2557
|
+
setTaskSessionsLoading(true);
|
|
2558
|
+
setTaskSessionsError(null);
|
|
2559
|
+
setTaskSessionsNotice(null);
|
|
2560
|
+
try {
|
|
2561
|
+
const result = await run();
|
|
2562
|
+
updateMainConversationAction(null);
|
|
2563
|
+
await refreshMainConversations(preferredConversationId, false);
|
|
2564
|
+
taskSessionsLoadingRef.current = false;
|
|
2565
|
+
setTaskSessionsLoading(false);
|
|
2566
|
+
setTaskSessionsNotice(typeof notice === "function" ? notice(result) : notice);
|
|
2567
|
+
}
|
|
2568
|
+
catch (error) {
|
|
2569
|
+
taskSessionsLoadingRef.current = false;
|
|
2570
|
+
setTaskSessionsLoading(false);
|
|
2571
|
+
setTaskSessionsError(error instanceof Error ? error.message : String(error));
|
|
2572
|
+
}
|
|
2573
|
+
finally {
|
|
2574
|
+
taskSessionsLoadingRef.current = false;
|
|
2575
|
+
setTaskSessionsLoading(false);
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
async function renameSelectedMainConversation(action) {
|
|
2579
|
+
await runMainConversationOperation(async () => {
|
|
2580
|
+
if (!renameMainConversation) {
|
|
2581
|
+
throw new Error("Main conversation rename is unavailable");
|
|
2582
|
+
}
|
|
2583
|
+
const title = action.value.trim();
|
|
2584
|
+
await renameMainConversation(action.conversationId, title);
|
|
2585
|
+
return title;
|
|
2586
|
+
}, (title) => `Renamed · ${title}`, action.conversationId);
|
|
2587
|
+
}
|
|
2588
|
+
async function archiveSelectedMainConversation() {
|
|
2589
|
+
const selected = mainConversationsRef.current[selectedMainConversationIndexRef.current];
|
|
2590
|
+
if (!selected) {
|
|
2591
|
+
return;
|
|
2592
|
+
}
|
|
2593
|
+
const archived = !selected.archivedAt;
|
|
2594
|
+
await runMainConversationOperation(async () => {
|
|
2595
|
+
if (!setMainConversationArchived) {
|
|
2596
|
+
throw new Error("Main conversation archive is unavailable");
|
|
2597
|
+
}
|
|
2598
|
+
await setMainConversationArchived(selected.id, archived);
|
|
2599
|
+
}, archived ? `Archived · ${selected.title}` : `Unarchived · ${selected.title}`, selected.id);
|
|
2600
|
+
}
|
|
2601
|
+
async function deleteSelectedMainConversation(action) {
|
|
2602
|
+
await runMainConversationOperation(async () => {
|
|
2603
|
+
if (!deleteMainConversation) {
|
|
2604
|
+
throw new Error("Main conversation deletion is unavailable");
|
|
2605
|
+
}
|
|
2606
|
+
await deleteMainConversation(action.conversationId);
|
|
2607
|
+
}, `Deleted · ${action.title}`);
|
|
2608
|
+
}
|
|
2609
|
+
async function exportSelectedMainConversation() {
|
|
2610
|
+
const selected = mainConversationsRef.current[selectedMainConversationIndexRef.current];
|
|
2611
|
+
if (!selected) {
|
|
2612
|
+
return;
|
|
2613
|
+
}
|
|
2614
|
+
await runMainConversationOperation(async () => {
|
|
2615
|
+
if (!exportMainConversation) {
|
|
2616
|
+
throw new Error("Main conversation export is unavailable");
|
|
2617
|
+
}
|
|
2618
|
+
return exportMainConversation(selected.id);
|
|
2619
|
+
}, (path) => `Exported · ${path}`, selected.id);
|
|
2620
|
+
}
|
|
2427
2621
|
function updateTaskSessionAction(next) {
|
|
2428
2622
|
taskSessionActionRef.current = next;
|
|
2429
2623
|
setTaskSessionAction(next);
|
|
@@ -2907,15 +3101,27 @@ export function App({ config, orchestrator, cwd, initialTaskId = null, initialRo
|
|
|
2907
3101
|
if (view === "workspace") {
|
|
2908
3102
|
return (_jsx(Box, { flexDirection: "column", height: terminalSize.rows, children: _jsx(WorkspacePicker, { cwd: cwd, choices: workspaceChoices, terminalHeight: terminalSize.rows, terminalWidth: terminalWidth, onCancel: closeWorkspacePicker, onSelect: (workspace) => void selectWorkspace(workspace) }) }));
|
|
2909
3103
|
}
|
|
2910
|
-
return (_jsx(AppShell, { view: view, cwd: cwd, taskId: activeTaskId, statusText: [visibleTaskStatus, visibleRouteSummary, configChange?.compact].filter(Boolean).join(" | "), contentHeight: contentHeight, showStatusBar: config.ui.showStatusBar, input: _jsx(InputBar, { mode: view === "worker" && workerSearch.open ? "worker-search" : view, ready: inputReady, busy: busy, routeFallback: Boolean(routeFallbackPrompt), collaborationDetail: collaborationDetailOpen, collaborationUnresolved: collaborationUnresolvedOnly, collaborationBack: collaborationReturnViewRef.current, featureCanCancel: featureCanCancel, featureCanPause: featureCanCancel, featureCanReassign: featureCanReassign, featureCancelConfirm: featureCancelPrompt?.action === "cancel", featurePauseConfirm: featureCancelPrompt?.action === "pause", featureAssignment: Boolean(featureAssignmentPrompt), taskSessionAction:
|
|
2911
|
-
?
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
|
|
3104
|
+
return (_jsx(AppShell, { view: view, cwd: cwd, taskId: activeTaskId, statusText: [visibleTaskStatus, visibleRouteSummary, configChange?.compact].filter(Boolean).join(" | "), contentHeight: contentHeight, showStatusBar: config.ui.showStatusBar, input: _jsx(InputBar, { mode: view === "worker" && workerSearch.open ? "worker-search" : view, ready: inputReady, busy: busy, routeFallback: Boolean(routeFallbackPrompt), collaborationDetail: collaborationDetailOpen, collaborationUnresolved: collaborationUnresolvedOnly, collaborationBack: collaborationReturnViewRef.current, featureCanCancel: featureCanCancel, featureCanPause: featureCanCancel, featureCanReassign: featureCanReassign, featureCancelConfirm: featureCancelPrompt?.action === "cancel", featurePauseConfirm: featureCancelPrompt?.action === "pause", featureAssignment: Boolean(featureAssignmentPrompt), taskSessionAction: sessionCenterMode === "conversations"
|
|
3105
|
+
? mainConversationAction?.type === "rename"
|
|
3106
|
+
? { type: "rename", value: mainConversationAction.value, cursor: mainConversationAction.cursor }
|
|
3107
|
+
: mainConversationAction?.type === "delete"
|
|
3108
|
+
? { type: "delete", title: mainConversationAction.title }
|
|
3109
|
+
: null
|
|
3110
|
+
: taskSessionAction?.type === "rename"
|
|
3111
|
+
? { type: "rename", value: taskSessionAction.value, cursor: taskSessionAction.cursor }
|
|
3112
|
+
: taskSessionAction?.type === "delete"
|
|
3113
|
+
? { type: "delete", title: taskSessionAction.title }
|
|
3114
|
+
: null, taskSessionsIncludeArchived: sessionCenterMode === "conversations"
|
|
3115
|
+
? mainConversationsIncludeArchived
|
|
3116
|
+
: taskSessionsIncludeArchived, mainConversationSessions: sessionCenterMode === "conversations" && !taskSessionDetails, taskSessionDetail: Boolean(taskSessionDetails), taskSessionDetailHasNative: taskSessionDetailHasNative, taskSessionDetailCanFork: taskSessionDetailCanFork, canRetry: canRetryTask, hasWorkers: workers.length > 0, hasActiveTask: Boolean(activeTaskId), hasTaskResult: hasTaskResult, taskResultExpanded: taskResultExpanded, chatScrollOffset: chatScrollOffset, chatMaxScrollOffset: chatMaxScrollOffset, nativeClosed: view === "native" && nativeAttach?.closedCode !== null, searchMatchIndex: workerSearch.matchIndex, searchMatchCount: workerNavigationTargets.searchOffsets.length, clipboardNotice: clipboardNotice, value: workerSearch.open && view === "worker"
|
|
2915
3117
|
? workerSearch.query
|
|
2916
3118
|
: view === "native" || view === "router" || view === "workers" || view === "features" || view === "sessions" || view === "collaboration" || view === "status" ? "" : input, cursor: workerSearch.open && view === "worker"
|
|
2917
3119
|
? workerSearch.cursor
|
|
2918
|
-
: view === "chat" ? inputCursor : undefined, terminalWidth: terminalWidth, onChange: view === "native" ? setNativeInput : setInput, onSubmit: view === "native" ? undefined : submit }), error: attachError, children: view === "status" ? (_jsx(StatusDetailView, { cwd: cwd, taskId: activeTaskId, mode: activeMode, busy: busy, canRetry: canRetryTask, taskStatus: visibleTaskStatus, routeStatus: visibleRouteStatus, routeReason: routePending ? undefined : lastRoute?.reason, pairing: config.pairing, configStatus: configChange?.detail, configRestartRequired: configChange?.kind === "restart", workers: workers, selectedWorkerIndex: selectedWorkerIndex, height: contentHeight, terminalWidth: terminalWidth })) : view === "native" ? (_jsx(NativeAttachView, { attach: nativeAttach, viewportHeight: contentHeight })) : view === "sessions" ? (taskSessionDetails ? (_jsx(TaskSessionDetailView, { details: taskSessionDetails, selectedWorkerIndex: taskSessionDetailSelectedWorkerIndex, loading: taskSessionsLoading, error: taskSessionsError, notice: taskSessionDetailNotice, height: contentHeight, terminalWidth: terminalWidth })) : sessionCenterMode === "conversations" ? (_jsx(MainConversationsView, { conversations: mainConversations, selectedIndex: selectedMainConversationIndex,
|
|
3120
|
+
: view === "chat" ? inputCursor : undefined, terminalWidth: terminalWidth, onChange: view === "native" ? setNativeInput : setInput, onSubmit: view === "native" ? undefined : submit }), error: attachError, children: view === "status" ? (_jsx(StatusDetailView, { cwd: cwd, taskId: activeTaskId, mode: activeMode, busy: busy, canRetry: canRetryTask, taskStatus: visibleTaskStatus, routeStatus: visibleRouteStatus, routeReason: routePending ? undefined : lastRoute?.reason, pairing: config.pairing, configStatus: configChange?.detail, configRestartRequired: configChange?.kind === "restart", workers: workers, selectedWorkerIndex: selectedWorkerIndex, height: contentHeight, terminalWidth: terminalWidth })) : view === "native" ? (_jsx(NativeAttachView, { attach: nativeAttach, viewportHeight: contentHeight })) : view === "sessions" ? (taskSessionDetails ? (_jsx(TaskSessionDetailView, { details: taskSessionDetails, selectedWorkerIndex: taskSessionDetailSelectedWorkerIndex, loading: taskSessionsLoading, error: taskSessionsError, notice: taskSessionDetailNotice, height: contentHeight, terminalWidth: terminalWidth })) : sessionCenterMode === "conversations" ? (_jsx(MainConversationsView, { conversations: mainConversations, selectedIndex: selectedMainConversationIndex, includeArchived: mainConversationsIncludeArchived, notice: taskSessionsNotice, action: mainConversationAction?.type === "rename"
|
|
3121
|
+
? { type: "rename", title: mainConversationAction.title }
|
|
3122
|
+
: mainConversationAction?.type === "delete"
|
|
3123
|
+
? { type: "delete", title: mainConversationAction.title }
|
|
3124
|
+
: null, loading: taskSessionsLoading, error: taskSessionsError, height: contentHeight, terminalWidth: terminalWidth })) : (_jsx(TaskSessionsView, { tasks: taskSessions, activeTaskId: activeTaskId, selectedIndex: selectedTaskSessionIndex, includeArchived: taskSessionsIncludeArchived, notice: taskSessionsNotice, action: taskSessionAction?.type === "rename"
|
|
2919
3125
|
? { type: "rename", title: taskSessionAction.title }
|
|
2920
3126
|
: taskSessionAction?.type === "delete"
|
|
2921
3127
|
? { type: "delete", title: taskSessionAction.title }
|
package/dist/tui/InputBar.js
CHANGED
|
@@ -40,7 +40,7 @@ export function InputBar({ mode, ready = true, busy = false, routeFallback = fal
|
|
|
40
40
|
return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(`${hints.label}${hints.detail}`), fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: hints.label }), hints.detail ? _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.muted, children: hints.detail }) : null] }));
|
|
41
41
|
}
|
|
42
42
|
if (mainConversationSessions) {
|
|
43
|
-
const hints = mainConversationSessionsInputHints(terminalWidth);
|
|
43
|
+
const hints = mainConversationSessionsInputHints(terminalWidth, taskSessionsIncludeArchived);
|
|
44
44
|
return (_jsxs(InputRail, { terminalWidth: terminalWidth, textWidth: displayWidth(`${hints.label}${hints.detail}`), fill: fillRail, children: [_jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.accent, bold: true, children: hints.label }), hints.detail ? _jsx(Text, { backgroundColor: TUI_THEME.rail, color: TUI_THEME.muted, children: hints.detail }) : null] }));
|
|
45
45
|
}
|
|
46
46
|
const hints = taskSessionsInputHints(terminalWidth, taskSessionsIncludeArchived);
|
|
@@ -588,8 +588,14 @@ function taskSessionsInputHints(width, includeArchived) {
|
|
|
588
588
|
{ label: "s", detail: "" }
|
|
589
589
|
]);
|
|
590
590
|
}
|
|
591
|
-
function mainConversationSessionsInputHints(width) {
|
|
591
|
+
export function mainConversationSessionsInputHints(width, includeArchived) {
|
|
592
|
+
const archivedAction = includeArchived ? "H hide archived" : "H archived";
|
|
592
593
|
return selectInputHints(width, [
|
|
594
|
+
{ label: "conversations", detail: ` · Up/Dn select · Enter restore · R rename · A archive · D delete · E export · ${archivedAction} · N new · T tasks · Esc back` },
|
|
595
|
+
{ label: "conversations", detail: " · Up/Dn select · Enter restore · R rename · A archive · D delete · E export · N new · T tasks · Esc back" },
|
|
596
|
+
{ label: "conversations", detail: " · Up/Dn select · Enter restore · R rename · A archive · D delete · N new · T tasks · Esc back" },
|
|
597
|
+
{ label: "conversations", detail: " · Up/Dn select · Enter restore · R rename · A archive · N new · T tasks · Esc back" },
|
|
598
|
+
{ label: "conversations", detail: " · Up/Dn select · Enter restore · R rename · N new · T tasks · Esc back" },
|
|
593
599
|
{ label: "conversations", detail: " · Up/Dn select · Enter restore · N new · T tasks · Esc back" },
|
|
594
600
|
{ label: "conversations", detail: " · Up/Dn select · Enter restore · T tasks · Esc back" },
|
|
595
601
|
{ label: "conversations", detail: " · Up/Dn select · Enter restore · Esc back" },
|
|
@@ -2,10 +2,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
3
|
import { compactEndByDisplayWidth, displayWidth } from "./display-width.js";
|
|
4
4
|
import { TUI_THEME } from "./theme.js";
|
|
5
|
-
export function MainConversationsView({ conversations, selectedIndex, notice = null, loading = false, error = null, height = 20, terminalWidth = process.stdout.columns || 120 }) {
|
|
5
|
+
export function MainConversationsView({ conversations, selectedIndex, includeArchived = false, notice = null, action = null, loading = false, error = null, height = 20, terminalWidth = process.stdout.columns || 120 }) {
|
|
6
6
|
const viewportHeight = Math.max(1, height);
|
|
7
7
|
const width = mainConversationsContentWidth(terminalWidth);
|
|
8
|
-
const lines = mainConversationsDisplayLines(conversations, selectedIndex, viewportHeight, terminalWidth, { notice, loading, error });
|
|
8
|
+
const lines = mainConversationsDisplayLines(conversations, selectedIndex, viewportHeight, terminalWidth, { includeArchived, notice, action, loading, error });
|
|
9
9
|
const blankRows = Math.max(0, viewportHeight - lines.length);
|
|
10
10
|
return (_jsxs(Box, { flexDirection: "column", height: viewportHeight, children: [lines.map((line, index) => (_jsx(MainConversationRow, { line: line, width: width }, `${line.conversationIndex ?? line.tone}-${index}`))), Array.from({ length: blankRows }, (_, index) => (_jsx(Text, { backgroundColor: TUI_THEME.surface, children: " ".repeat(width) }, `main-conversation-fill-${index}`)))] }));
|
|
11
11
|
}
|
|
@@ -13,15 +13,26 @@ export function mainConversationsDisplayLines(conversations, selectedIndex, heig
|
|
|
13
13
|
const viewportHeight = Math.max(1, Math.trunc(height));
|
|
14
14
|
const width = mainConversationsContentWidth(terminalWidth);
|
|
15
15
|
const lines = [{
|
|
16
|
-
text: fitMainConversationCandidates([
|
|
16
|
+
text: fitMainConversationCandidates([
|
|
17
|
+
state.includeArchived ? "Main conversations · archived shown" : "Main conversations",
|
|
18
|
+
state.includeArchived ? "Conversations · all" : "Conversations",
|
|
19
|
+
"Chats",
|
|
20
|
+
"C"
|
|
21
|
+
], width),
|
|
17
22
|
tone: "heading"
|
|
18
23
|
}];
|
|
19
24
|
if (viewportHeight >= 3) {
|
|
20
25
|
const messages = conversations.reduce((sum, conversation) => sum + conversation.messageCount, 0);
|
|
21
26
|
const nativeSessions = conversations.reduce((sum, conversation) => sum + conversation.nativeSessionCount, 0);
|
|
27
|
+
const archived = conversations.filter((conversation) => conversation.archivedAt).length;
|
|
22
28
|
lines.push({
|
|
23
29
|
text: fitMainConversationCandidates([
|
|
24
|
-
|
|
30
|
+
[
|
|
31
|
+
`${conversations.length} ${conversations.length === 1 ? "conversation" : "conversations"}`,
|
|
32
|
+
`${messages} messages`,
|
|
33
|
+
`${nativeSessions} native`,
|
|
34
|
+
...(archived > 0 ? [`${archived} archived`] : [])
|
|
35
|
+
].join(" · "),
|
|
25
36
|
`${conversations.length} conversations · ${messages} messages`,
|
|
26
37
|
`${conversations.length} conversations`,
|
|
27
38
|
`${conversations.length} chats`
|
|
@@ -29,7 +40,15 @@ export function mainConversationsDisplayLines(conversations, selectedIndex, heig
|
|
|
29
40
|
tone: "muted"
|
|
30
41
|
});
|
|
31
42
|
}
|
|
32
|
-
if (viewportHeight >= 4 && state.
|
|
43
|
+
if (viewportHeight >= 4 && state.action) {
|
|
44
|
+
lines.push({
|
|
45
|
+
text: fitMainConversationText(state.action.type === "rename"
|
|
46
|
+
? `rename · ${safeMainConversationText(state.action.title)} · Enter save · Esc cancel`
|
|
47
|
+
: `delete · ${safeMainConversationText(state.action.title)} · press D again · Esc cancel`, width),
|
|
48
|
+
tone: state.action.type === "delete" ? "danger" : "active"
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
else if (viewportHeight >= 4 && state.notice) {
|
|
33
52
|
lines.push({ text: fitMainConversationText(state.notice, width), tone: "success" });
|
|
34
53
|
}
|
|
35
54
|
const slots = Math.max(0, viewportHeight - lines.length);
|
|
@@ -88,14 +107,15 @@ function mainConversationRowText(conversation, selected, width) {
|
|
|
88
107
|
const title = safeMainConversationText(conversation.title);
|
|
89
108
|
const messages = `${conversation.messageCount} ${conversation.messageCount === 1 ? "message" : "messages"}`;
|
|
90
109
|
const native = `${conversation.nativeSessionCount} native`;
|
|
110
|
+
const status = conversation.archivedAt ? "archived" : null;
|
|
91
111
|
const date = conversation.lastActivityAt.slice(5, 16).replace("T", " ");
|
|
92
112
|
const scope = conversation.id
|
|
93
113
|
? `#${conversation.id.replace(/^conversation-/, "")}`
|
|
94
114
|
: "legacy";
|
|
95
115
|
return fitMainConversationCandidates([
|
|
96
|
-
[marker + title, messages, native, date].join(" · "),
|
|
97
|
-
[marker + title, messages, native].join(" · "),
|
|
98
|
-
[marker + title, messages].join(" · "),
|
|
116
|
+
[marker + title, status, messages, native, date].filter(Boolean).join(" · "),
|
|
117
|
+
[marker + title, status, messages, native].filter(Boolean).join(" · "),
|
|
118
|
+
[marker + title, status, messages].filter(Boolean).join(" · "),
|
|
99
119
|
[marker + title, date].join(" · "),
|
|
100
120
|
[marker + scope, messages].join(" · "),
|
|
101
121
|
marker.trimEnd()
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const version = "0.2.
|
|
1
|
+
export const version = "0.2.10";
|