codex-session-manager 0.1.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.
@@ -0,0 +1,821 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const node_process_1 = __importDefault(require("node:process"));
7
+ const session_store_1 = require("./session-store");
8
+ const ui_utils_1 = require("./ui-utils");
9
+ const preview_1 = require("./preview");
10
+ const state = {
11
+ sessions: [],
12
+ filtered: [],
13
+ searchQuery: "",
14
+ archiveFilter: "active",
15
+ selectedIndex: 0,
16
+ statusMessage: "",
17
+ inputState: null,
18
+ selectedPaths: new Set(),
19
+ tagIndex: [],
20
+ showDetails: true,
21
+ showHelp: false,
22
+ sortOrder: "desc",
23
+ detailCache: new Map(),
24
+ detailState: { filePath: null, loading: false },
25
+ };
26
+ const DEFAULT_ROWS = 24;
27
+ const DEFAULT_COLUMNS = 80;
28
+ const TAG_SUGGESTION_LIMIT = 5;
29
+ const PREVIEW_CHAR_LIMIT = 240;
30
+ const DETAIL_MIN_COLUMNS = 80;
31
+ const LIST_WIDTH_RATIO = 0.45;
32
+ const LIST_MIN_WIDTH = 30;
33
+ const DETAIL_MIN_WIDTH = 20;
34
+ const DETAIL_SEPARATOR_WIDTH = 3;
35
+ const codexPaths = (0, session_store_1.getDefaultPaths)();
36
+ function setStatus(message) {
37
+ state.statusMessage = message;
38
+ }
39
+ function clearScreen() {
40
+ node_process_1.default.stdout.write("\x1b[2J\x1b[0f");
41
+ }
42
+ function hideCursor() {
43
+ node_process_1.default.stdout.write("\x1b[?25l");
44
+ }
45
+ function showCursor() {
46
+ node_process_1.default.stdout.write("\x1b[?25h");
47
+ }
48
+ function truncate(text, width) {
49
+ if (text.length <= width) {
50
+ return text;
51
+ }
52
+ if (width <= 3) {
53
+ return text.slice(0, width);
54
+ }
55
+ return `${text.slice(0, width - 3)}...`;
56
+ }
57
+ function padRight(text, width) {
58
+ const clipped = truncate(text, width);
59
+ if (clipped.length >= width) {
60
+ return clipped;
61
+ }
62
+ return `${clipped}${" ".repeat(width - clipped.length)}`;
63
+ }
64
+ function wrapText(text, width) {
65
+ if (width <= 0) {
66
+ return [];
67
+ }
68
+ const words = text.split(" ");
69
+ const lines = [];
70
+ let current = "";
71
+ for (const word of words) {
72
+ if (!word) {
73
+ continue;
74
+ }
75
+ if (word.length > width) {
76
+ if (current) {
77
+ lines.push(current);
78
+ current = "";
79
+ }
80
+ let remaining = word;
81
+ while (remaining.length > width) {
82
+ lines.push(remaining.slice(0, width));
83
+ remaining = remaining.slice(width);
84
+ }
85
+ current = remaining;
86
+ continue;
87
+ }
88
+ if (!current) {
89
+ current = word;
90
+ continue;
91
+ }
92
+ if (current.length + 1 + word.length <= width) {
93
+ current = `${current} ${word}`;
94
+ }
95
+ else {
96
+ lines.push(current);
97
+ current = word;
98
+ }
99
+ }
100
+ if (current) {
101
+ lines.push(current);
102
+ }
103
+ if (lines.length === 0) {
104
+ lines.push("");
105
+ }
106
+ return lines.map((line) => truncate(line, width));
107
+ }
108
+ function formatSessionLine(session, selected, current, width) {
109
+ const cursor = current ? ">" : " ";
110
+ const marker = selected ? "[x]" : "[ ]";
111
+ const tagText = session.tags.length ? ` tags:${session.tags.join(",")}` : "";
112
+ const archiveText = session.archived ? " [archived]" : "";
113
+ const dateText = session.dateLabel ? ` ${session.dateLabel}` : "";
114
+ return truncate(`${cursor} ${marker} ${session.displayName}${tagText}${archiveText}${dateText}`, width);
115
+ }
116
+ function getListWindow(listLength, viewSize) {
117
+ if (listLength <= viewSize) {
118
+ return [0, listLength];
119
+ }
120
+ const half = Math.floor(viewSize / 2);
121
+ let start = state.selectedIndex - half;
122
+ if (start < 0) {
123
+ start = 0;
124
+ }
125
+ if (start + viewSize > listLength) {
126
+ start = Math.max(0, listLength - viewSize);
127
+ }
128
+ return [start, Math.min(listLength, start + viewSize)];
129
+ }
130
+ function pushWrapped(lines, text, width) {
131
+ lines.push(...wrapText(text, width));
132
+ }
133
+ function buildDetailLines(session, width) {
134
+ const lines = [];
135
+ pushWrapped(lines, "Details", width);
136
+ lines.push("");
137
+ pushWrapped(lines, `Name: ${session.displayName}`, width);
138
+ if (session.title) {
139
+ pushWrapped(lines, `Title: ${session.title}`, width);
140
+ }
141
+ pushWrapped(lines, `Status: ${session.archived ? "archived" : "active"}`, width);
142
+ if (session.timestamp) {
143
+ pushWrapped(lines, `Timestamp: ${session.timestamp}`, width);
144
+ }
145
+ if (session.cwd) {
146
+ pushWrapped(lines, `Cwd: ${session.cwd}`, width);
147
+ }
148
+ if (session.tags.length) {
149
+ pushWrapped(lines, `Tags: ${session.tags.join(", ")}`, width);
150
+ }
151
+ if (session.id) {
152
+ pushWrapped(lines, `Id: ${session.id}`, width);
153
+ }
154
+ if (session.originator) {
155
+ pushWrapped(lines, `Originator: ${session.originator}`, width);
156
+ }
157
+ if (session.cliVersion) {
158
+ pushWrapped(lines, `CLI: ${session.cliVersion}`, width);
159
+ }
160
+ if (session.modelProvider) {
161
+ pushWrapped(lines, `Model: ${session.modelProvider}`, width);
162
+ }
163
+ if (session.git?.repositoryUrl) {
164
+ pushWrapped(lines, `Repo: ${session.git.repositoryUrl}`, width);
165
+ }
166
+ if (session.git?.branch) {
167
+ pushWrapped(lines, `Branch: ${session.git.branch}`, width);
168
+ }
169
+ if (session.git?.commitHash) {
170
+ pushWrapped(lines, `Commit: ${session.git.commitHash}`, width);
171
+ }
172
+ lines.push("");
173
+ pushWrapped(lines, "Preview:", width);
174
+ const cache = state.detailCache;
175
+ if (state.detailState.loading && state.detailState.filePath === session.filePath) {
176
+ pushWrapped(lines, "(loading preview...)", width);
177
+ }
178
+ else if (cache.has(session.filePath)) {
179
+ const preview = cache.get(session.filePath);
180
+ if (preview) {
181
+ pushWrapped(lines, "First:", width);
182
+ pushWrapped(lines, preview.first, width);
183
+ lines.push("");
184
+ pushWrapped(lines, "Last:", width);
185
+ pushWrapped(lines, preview.last, width);
186
+ }
187
+ else {
188
+ pushWrapped(lines, "(no preview available)", width);
189
+ }
190
+ }
191
+ else {
192
+ pushWrapped(lines, "(preview unavailable)", width);
193
+ }
194
+ return lines;
195
+ }
196
+ function renderListWithDetails(listLines, detailLines, listWidth, detailWidth, listSize) {
197
+ const result = [];
198
+ const separator = " | ";
199
+ for (let i = 0; i < listSize; i += 1) {
200
+ const left = padRight(listLines[i] ?? "", listWidth);
201
+ const right = padRight(detailLines[i] ?? "", detailWidth);
202
+ result.push(`${left}${separator}${right}`);
203
+ }
204
+ return result;
205
+ }
206
+ function render() {
207
+ const rows = typeof node_process_1.default.stdout.rows === "number" ? node_process_1.default.stdout.rows : DEFAULT_ROWS;
208
+ const cols = typeof node_process_1.default.stdout.columns === "number"
209
+ ? node_process_1.default.stdout.columns
210
+ : DEFAULT_COLUMNS;
211
+ if (state.showHelp) {
212
+ clearScreen();
213
+ const helpLines = [
214
+ "Help (toggle with h or ?)",
215
+ "",
216
+ "Navigation",
217
+ " up/down (j/k): move focus",
218
+ " p/n: page up/down",
219
+ " g or Home: jump to top",
220
+ " G or End: jump to bottom",
221
+ "",
222
+ "Filter and view",
223
+ " /: search",
224
+ " f: show filter (active/archived/active+archived)",
225
+ " s: sort order (desc/asc)",
226
+ " d: toggle details pane",
227
+ "",
228
+ "Selection",
229
+ " space or Tab: toggle selected session",
230
+ " A: select all visible",
231
+ " I (capital i): invert selection (visible)",
232
+ " C: clear selection",
233
+ "",
234
+ "Actions",
235
+ " r: rename session",
236
+ " t: edit tags",
237
+ " a: toggle archive focused",
238
+ " B: toggle archive selected",
239
+ "",
240
+ "Other",
241
+ " h or ?: toggle help",
242
+ " q: quit",
243
+ ];
244
+ const output = helpLines.map((line) => truncate(line, cols));
245
+ node_process_1.default.stdout.write(output.join("\n"));
246
+ return;
247
+ }
248
+ const hasStatus = Boolean(state.statusMessage);
249
+ const tagSuggestions = state.inputState?.kind === "tags"
250
+ ? (0, ui_utils_1.getTagSuggestions)(state.inputState.value, state.tagIndex, TAG_SUGGESTION_LIMIT)
251
+ : [];
252
+ const suggestionLine = tagSuggestions.length
253
+ ? `Suggestions: ${tagSuggestions.join(", ")} (tab to autocomplete)`
254
+ : "";
255
+ const footerLines = 1 + (suggestionLine ? 1 : 0);
256
+ const headerLines = 4 + (hasStatus ? 1 : 0);
257
+ const listSize = Math.max(1, rows - headerLines - footerLines);
258
+ clearScreen();
259
+ const lines = [];
260
+ const searchLabel = state.searchQuery.trim();
261
+ const searchDisplay = searchLabel ? ` ${searchLabel}` : "";
262
+ const showLabel = state.archiveFilter === "all" ? "active+archived" : state.archiveFilter;
263
+ lines.push(truncate(`Search (/):${searchDisplay} | Show (f): ${showLabel} | Sort (s): ${state.sortOrder} | Details (d): ${state.showDetails ? "on" : "off"} | Help (h/?)`, cols));
264
+ lines.push(truncate(`Selected (space): ${state.selectedPaths.size} | Current: ${state.filtered.length ? state.selectedIndex + 1 : 0}/${state.filtered.length} | Showing: ${state.filtered.length}/${state.sessions.length} | Nav: ↑/↓ | Page: p/n | Top/Bottom: g/G | Quit: q`, cols));
265
+ lines.push(truncate("Actions: rename (r) | tags (t) | toggle archive focused (a) | toggle archive selected (B)", cols));
266
+ lines.push(truncate("Selection: select all (A) | invert (I) | clear (C)", cols));
267
+ if (hasStatus) {
268
+ lines.push(`Status: ${state.statusMessage}`);
269
+ }
270
+ const [start, end] = getListWindow(state.filtered.length, listSize);
271
+ const listLines = [];
272
+ if (state.filtered.length === 0) {
273
+ listLines.push("(no sessions match the filter)");
274
+ }
275
+ else {
276
+ for (let i = start; i < end; i += 1) {
277
+ const session = state.filtered[i];
278
+ listLines.push(formatSessionLine(session, state.selectedPaths.has(session.filePath), i === state.selectedIndex, cols));
279
+ }
280
+ }
281
+ const useDetails = state.showDetails && cols >= DETAIL_MIN_COLUMNS;
282
+ if (useDetails) {
283
+ const separatorWidth = DETAIL_SEPARATOR_WIDTH;
284
+ const minDetailWidth = DETAIL_MIN_WIDTH;
285
+ let listWidth = Math.floor(cols * LIST_WIDTH_RATIO);
286
+ listWidth = Math.max(LIST_MIN_WIDTH, listWidth);
287
+ listWidth = Math.min(listWidth, cols - separatorWidth - minDetailWidth);
288
+ const detailWidth = cols - listWidth - separatorWidth;
289
+ const session = currentSession();
290
+ const detailLines = session
291
+ ? buildDetailLines(session, detailWidth)
292
+ : ["(no session selected)"];
293
+ const combined = renderListWithDetails(listLines.map((line) => truncate(line, listWidth)), detailLines, listWidth, detailWidth, listSize);
294
+ lines.push(...combined);
295
+ }
296
+ else {
297
+ for (let i = 0; i < listSize; i += 1) {
298
+ lines.push(listLines[i] ?? "");
299
+ }
300
+ }
301
+ if (suggestionLine) {
302
+ lines.push(truncate(suggestionLine, cols));
303
+ }
304
+ if (state.inputState) {
305
+ lines.push(truncate(`${state.inputState.prompt}${state.inputState.value}`, cols));
306
+ }
307
+ else {
308
+ lines.push("");
309
+ }
310
+ node_process_1.default.stdout.write(lines.join("\n"));
311
+ }
312
+ function applyFilters() {
313
+ const filtered = (0, session_store_1.filterSessions)(state.sessions, state.searchQuery, state.archiveFilter);
314
+ state.filtered = (0, session_store_1.sortSessionsByDate)(filtered, state.sortOrder);
315
+ if (state.selectedIndex >= state.filtered.length) {
316
+ state.selectedIndex = Math.max(0, state.filtered.length - 1);
317
+ }
318
+ }
319
+ function pruneSelection() {
320
+ const valid = new Set(state.sessions.map((session) => session.filePath));
321
+ for (const selected of state.selectedPaths) {
322
+ if (!valid.has(selected)) {
323
+ state.selectedPaths.delete(selected);
324
+ }
325
+ }
326
+ }
327
+ async function refreshSessions() {
328
+ state.sessions = await (0, session_store_1.loadSessions)(codexPaths);
329
+ state.tagIndex = (0, ui_utils_1.buildTagIndex)(state.sessions);
330
+ pruneSelection();
331
+ applyFilters();
332
+ }
333
+ function currentSession() {
334
+ if (state.filtered.length === 0) {
335
+ return null;
336
+ }
337
+ return state.filtered[state.selectedIndex] ?? null;
338
+ }
339
+ function startInput(kind, prompt, value, onSubmit) {
340
+ state.inputState = { kind, prompt, value, onSubmit };
341
+ }
342
+ function toggleSelection(session) {
343
+ if (state.selectedPaths.has(session.filePath)) {
344
+ state.selectedPaths.delete(session.filePath);
345
+ }
346
+ else {
347
+ state.selectedPaths.add(session.filePath);
348
+ }
349
+ }
350
+ function selectAllVisible() {
351
+ for (const session of state.filtered) {
352
+ state.selectedPaths.add(session.filePath);
353
+ }
354
+ }
355
+ function invertSelectionVisible() {
356
+ for (const session of state.filtered) {
357
+ toggleSelection(session);
358
+ }
359
+ }
360
+ function clearSelection() {
361
+ state.selectedPaths.clear();
362
+ }
363
+ function selectedSessions() {
364
+ const map = new Map(state.sessions.map((session) => [session.filePath, session]));
365
+ return Array.from(state.selectedPaths)
366
+ .map((filePath) => map.get(filePath))
367
+ .filter((session) => Boolean(session));
368
+ }
369
+ async function handleRename(session) {
370
+ startInput("rename", "Rename to: ", session.title ?? "", async (value) => {
371
+ await (0, session_store_1.updateSessionMetadata)(session.filePath, { title: value });
372
+ setStatus(value.trim() ? "Renamed session." : "Cleared session name.");
373
+ await refreshSessions();
374
+ });
375
+ }
376
+ async function handleTags(session) {
377
+ const existing = session.tags.join(", ");
378
+ startInput("tags", "Tags (comma separated): ", existing, async (value) => {
379
+ const tags = (0, session_store_1.parseTagsInput)(value);
380
+ await (0, session_store_1.updateSessionMetadata)(session.filePath, { tags });
381
+ setStatus(tags.length ? "Updated tags." : "Cleared tags.");
382
+ await refreshSessions();
383
+ });
384
+ }
385
+ async function handleArchiveToggle(session) {
386
+ const targetArchived = !session.archived;
387
+ const wasSelected = state.selectedPaths.has(session.filePath);
388
+ const newPath = await (0, session_store_1.setArchiveStatus)(session, targetArchived, codexPaths);
389
+ if (wasSelected) {
390
+ state.selectedPaths.delete(session.filePath);
391
+ state.selectedPaths.add(newPath);
392
+ }
393
+ setStatus(targetArchived ? "Archived session." : "Restored session.");
394
+ await refreshSessions();
395
+ render();
396
+ }
397
+ async function toggleSelectedArchive() {
398
+ const selected = selectedSessions();
399
+ if (selected.length === 0) {
400
+ setStatus("No sessions selected.");
401
+ maybeLoadDetails();
402
+ render();
403
+ return;
404
+ }
405
+ let archivedCount = 0;
406
+ let restoredCount = 0;
407
+ let failed = 0;
408
+ for (const session of selected) {
409
+ try {
410
+ const targetArchived = !session.archived;
411
+ await (0, session_store_1.setArchiveStatus)(session, targetArchived, codexPaths);
412
+ if (targetArchived) {
413
+ archivedCount += 1;
414
+ }
415
+ else {
416
+ restoredCount += 1;
417
+ }
418
+ }
419
+ catch {
420
+ failed += 1;
421
+ }
422
+ }
423
+ await refreshSessions();
424
+ clearSelection();
425
+ const parts = [];
426
+ if (archivedCount > 0) {
427
+ parts.push(`archived ${archivedCount}`);
428
+ }
429
+ if (restoredCount > 0) {
430
+ parts.push(`restored ${restoredCount}`);
431
+ }
432
+ if (failed > 0) {
433
+ parts.push(`failed ${failed}`);
434
+ }
435
+ const summary = parts.length ? parts.join(", ") : "no changes";
436
+ setStatus(`Toggle selected: ${summary}.`);
437
+ maybeLoadDetails();
438
+ render();
439
+ }
440
+ function toggleArchiveFilter() {
441
+ if (state.archiveFilter === "all") {
442
+ state.archiveFilter = "active";
443
+ }
444
+ else if (state.archiveFilter === "active") {
445
+ state.archiveFilter = "archived";
446
+ }
447
+ else {
448
+ state.archiveFilter = "all";
449
+ }
450
+ applyFilters();
451
+ }
452
+ function toggleSortOrder() {
453
+ state.sortOrder = state.sortOrder === "desc" ? "asc" : "desc";
454
+ applyFilters();
455
+ }
456
+ function toggleDetails() {
457
+ state.showDetails = !state.showDetails;
458
+ }
459
+ function toggleHelp() {
460
+ state.showHelp = !state.showHelp;
461
+ }
462
+ function getListSize() {
463
+ const rows = typeof node_process_1.default.stdout.rows === "number" ? node_process_1.default.stdout.rows : DEFAULT_ROWS;
464
+ const hasStatus = Boolean(state.statusMessage);
465
+ const headerLines = 4 + (hasStatus ? 1 : 0);
466
+ const footerLines = 1;
467
+ return Math.max(1, rows - headerLines - footerLines);
468
+ }
469
+ function maybeLoadDetails() {
470
+ if (!state.showDetails) {
471
+ state.detailState = { filePath: null, loading: false };
472
+ return;
473
+ }
474
+ const session = currentSession();
475
+ if (!session) {
476
+ state.detailState = { filePath: null, loading: false };
477
+ return;
478
+ }
479
+ const cached = state.detailCache.has(session.filePath);
480
+ if (state.detailState.filePath === session.filePath && cached) {
481
+ return;
482
+ }
483
+ state.detailState = { filePath: session.filePath, loading: !cached };
484
+ if (cached) {
485
+ return;
486
+ }
487
+ void (0, preview_1.readMessagePreviews)(session.filePath, PREVIEW_CHAR_LIMIT)
488
+ .then((preview) => {
489
+ state.detailCache.set(session.filePath, preview ?? null);
490
+ if (state.detailState.filePath === session.filePath) {
491
+ state.detailState.loading = false;
492
+ render();
493
+ }
494
+ })
495
+ .catch((error) => {
496
+ state.detailCache.set(session.filePath, null);
497
+ setStatus(error instanceof Error ? error.message : "Failed to load preview.");
498
+ state.detailState.loading = false;
499
+ render();
500
+ });
501
+ }
502
+ function exit() {
503
+ showCursor();
504
+ if (node_process_1.default.stdin.isTTY) {
505
+ node_process_1.default.stdin.setRawMode(false);
506
+ node_process_1.default.stdin.pause();
507
+ }
508
+ clearScreen();
509
+ }
510
+ function handleInputKey(key) {
511
+ if (!state.inputState) {
512
+ return;
513
+ }
514
+ if (key === "enter") {
515
+ const submit = state.inputState.onSubmit;
516
+ const value = state.inputState.value;
517
+ state.inputState = null;
518
+ Promise.resolve(submit(value))
519
+ .catch((error) => {
520
+ setStatus(error instanceof Error ? error.message : "Action failed.");
521
+ })
522
+ .finally(() => {
523
+ render();
524
+ });
525
+ return;
526
+ }
527
+ if (key === "escape") {
528
+ state.inputState = null;
529
+ return;
530
+ }
531
+ if (key === "backspace") {
532
+ state.inputState.value = state.inputState.value.slice(0, -1);
533
+ return;
534
+ }
535
+ if (key === "tab" && state.inputState.kind === "tags") {
536
+ const suggestions = (0, ui_utils_1.getTagSuggestions)(state.inputState.value, state.tagIndex, TAG_SUGGESTION_LIMIT);
537
+ if (suggestions.length) {
538
+ state.inputState.value = (0, ui_utils_1.applyTagSuggestion)(state.inputState.value, suggestions[0]);
539
+ }
540
+ return;
541
+ }
542
+ if (key.startsWith("char:")) {
543
+ const char = key.slice(5);
544
+ if (char >= " " && char !== "\x7f") {
545
+ state.inputState.value += char;
546
+ }
547
+ }
548
+ }
549
+ function handleListKey(key) {
550
+ if (key === "up") {
551
+ state.selectedIndex = Math.max(0, state.selectedIndex - 1);
552
+ return;
553
+ }
554
+ if (key === "down") {
555
+ state.selectedIndex = Math.min(Math.max(0, state.filtered.length - 1), state.selectedIndex + 1);
556
+ return;
557
+ }
558
+ if (key === "page-up") {
559
+ const pageSize = getListSize();
560
+ state.selectedIndex = Math.max(0, state.selectedIndex - pageSize);
561
+ return;
562
+ }
563
+ if (key === "page-down") {
564
+ const pageSize = getListSize();
565
+ state.selectedIndex = Math.min(Math.max(0, state.filtered.length - 1), state.selectedIndex + pageSize);
566
+ return;
567
+ }
568
+ if (key === "quit") {
569
+ exit();
570
+ node_process_1.default.exit(0);
571
+ }
572
+ if (key === "search") {
573
+ startInput("search", "Search: ", state.searchQuery, async (value) => {
574
+ state.searchQuery = value;
575
+ applyFilters();
576
+ });
577
+ return;
578
+ }
579
+ if (key === "filter") {
580
+ toggleArchiveFilter();
581
+ return;
582
+ }
583
+ if (key === "toggle-details") {
584
+ toggleDetails();
585
+ return;
586
+ }
587
+ if (key === "sort") {
588
+ toggleSortOrder();
589
+ return;
590
+ }
591
+ if (key === "help") {
592
+ toggleHelp();
593
+ return;
594
+ }
595
+ if (key === "top") {
596
+ state.selectedIndex = 0;
597
+ return;
598
+ }
599
+ if (key === "bottom") {
600
+ state.selectedIndex = Math.max(0, state.filtered.length - 1);
601
+ return;
602
+ }
603
+ if (key === "select-all") {
604
+ selectAllVisible();
605
+ return;
606
+ }
607
+ if (key === "invert-selection") {
608
+ invertSelectionVisible();
609
+ return;
610
+ }
611
+ if (key === "clear-selection") {
612
+ clearSelection();
613
+ return;
614
+ }
615
+ if (key === "bulk-archive") {
616
+ void toggleSelectedArchive().catch((error) => {
617
+ setStatus(error instanceof Error ? error.message : "Toggle selected failed.");
618
+ render();
619
+ });
620
+ return;
621
+ }
622
+ const session = currentSession();
623
+ if (!session) {
624
+ return;
625
+ }
626
+ if (key === "toggle-selection") {
627
+ toggleSelection(session);
628
+ return;
629
+ }
630
+ if (key === "rename") {
631
+ void handleRename(session);
632
+ return;
633
+ }
634
+ if (key === "tags") {
635
+ void handleTags(session);
636
+ return;
637
+ }
638
+ if (key === "archive") {
639
+ void handleArchiveToggle(session).catch((error) => {
640
+ setStatus(error instanceof Error ? error.message : "Archive failed.");
641
+ render();
642
+ });
643
+ }
644
+ }
645
+ function handleKey(key) {
646
+ if (state.showHelp && key !== "help" && key !== "quit") {
647
+ return;
648
+ }
649
+ if (state.inputState) {
650
+ handleInputKey(key);
651
+ }
652
+ else {
653
+ handleListKey(key);
654
+ }
655
+ if (!state.inputState && !state.showHelp) {
656
+ maybeLoadDetails();
657
+ }
658
+ render();
659
+ }
660
+ function handleData(data) {
661
+ const text = data.toString("utf8");
662
+ if (text === "\u0003") {
663
+ exit();
664
+ node_process_1.default.exit(0);
665
+ }
666
+ if (state.inputState) {
667
+ if (text === "\x1b") {
668
+ handleKey("escape");
669
+ return;
670
+ }
671
+ if (text === "\r") {
672
+ handleKey("enter");
673
+ return;
674
+ }
675
+ if (text === "\t") {
676
+ handleKey("tab");
677
+ return;
678
+ }
679
+ if (text === "\x7f") {
680
+ handleKey("backspace");
681
+ return;
682
+ }
683
+ for (const char of text) {
684
+ handleKey(`char:${char}`);
685
+ }
686
+ return;
687
+ }
688
+ if (text === "\x1b[A") {
689
+ handleKey("up");
690
+ return;
691
+ }
692
+ if (text === "\x1b[B") {
693
+ handleKey("down");
694
+ return;
695
+ }
696
+ if (text === "\x1b[H" || text === "\x1b[1~" || text === "\x1b[7~") {
697
+ handleKey("top");
698
+ return;
699
+ }
700
+ if (text === "\x1b[F" || text === "\x1b[4~" || text === "\x1b[8~") {
701
+ handleKey("bottom");
702
+ return;
703
+ }
704
+ if (text === "\x1b") {
705
+ handleKey("escape");
706
+ return;
707
+ }
708
+ if (text === "\r") {
709
+ handleKey("enter");
710
+ return;
711
+ }
712
+ if (text === "\t") {
713
+ handleKey("toggle-selection");
714
+ return;
715
+ }
716
+ if (text === "\x7f") {
717
+ handleKey("backspace");
718
+ return;
719
+ }
720
+ for (const char of text) {
721
+ if (char === " ") {
722
+ handleKey("toggle-selection");
723
+ continue;
724
+ }
725
+ if (char === "/") {
726
+ handleKey("search");
727
+ continue;
728
+ }
729
+ if (char === "f") {
730
+ handleKey("filter");
731
+ continue;
732
+ }
733
+ if (char === "r") {
734
+ handleKey("rename");
735
+ continue;
736
+ }
737
+ if (char === "t") {
738
+ handleKey("tags");
739
+ continue;
740
+ }
741
+ if (char === "a") {
742
+ handleKey("archive");
743
+ continue;
744
+ }
745
+ if (char === "B") {
746
+ handleKey("bulk-archive");
747
+ continue;
748
+ }
749
+ if (char === "d") {
750
+ handleKey("toggle-details");
751
+ continue;
752
+ }
753
+ if (char === "s") {
754
+ handleKey("sort");
755
+ continue;
756
+ }
757
+ if (char === "g") {
758
+ handleKey("top");
759
+ continue;
760
+ }
761
+ if (char === "G") {
762
+ handleKey("bottom");
763
+ continue;
764
+ }
765
+ if (char === "h" || char === "?") {
766
+ handleKey("help");
767
+ continue;
768
+ }
769
+ if (char === "j") {
770
+ handleKey("down");
771
+ continue;
772
+ }
773
+ if (char === "k") {
774
+ handleKey("up");
775
+ continue;
776
+ }
777
+ if (char === "p") {
778
+ handleKey("page-up");
779
+ continue;
780
+ }
781
+ if (char === "n") {
782
+ handleKey("page-down");
783
+ continue;
784
+ }
785
+ if (char === "A") {
786
+ handleKey("select-all");
787
+ continue;
788
+ }
789
+ if (char === "I") {
790
+ handleKey("invert-selection");
791
+ continue;
792
+ }
793
+ if (char === "C") {
794
+ handleKey("clear-selection");
795
+ continue;
796
+ }
797
+ if (char === "q") {
798
+ handleKey("quit");
799
+ continue;
800
+ }
801
+ handleKey(`char:${char}`);
802
+ }
803
+ }
804
+ async function main() {
805
+ if (!node_process_1.default.stdin.isTTY) {
806
+ console.error("This tool requires a TTY.");
807
+ node_process_1.default.exit(1);
808
+ }
809
+ hideCursor();
810
+ node_process_1.default.stdin.setRawMode(true);
811
+ node_process_1.default.stdin.resume();
812
+ node_process_1.default.stdin.on("data", handleData);
813
+ await refreshSessions();
814
+ maybeLoadDetails();
815
+ render();
816
+ }
817
+ main().catch((error) => {
818
+ exit();
819
+ console.error(error instanceof Error ? error.message : String(error));
820
+ node_process_1.default.exit(1);
821
+ });