@alsdd/t3-import 0.0.0-stage → 0.1.1-alsd.1

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.
Files changed (97) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/LICENSE +21 -0
  3. package/README.md +141 -2
  4. package/assets/fork-demo.gif +0 -0
  5. package/assets/fork-demo.png +0 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +399 -0
  8. package/dist/cli.js.map +1 -0
  9. package/dist/core/checkpoint.d.ts +25 -0
  10. package/dist/core/checkpoint.js +82 -0
  11. package/dist/core/checkpoint.js.map +1 -0
  12. package/dist/core/errors.d.ts +12 -0
  13. package/dist/core/errors.js +17 -0
  14. package/dist/core/errors.js.map +1 -0
  15. package/dist/core/jsonl.d.ts +7 -0
  16. package/dist/core/jsonl.js +37 -0
  17. package/dist/core/jsonl.js.map +1 -0
  18. package/dist/core/providers.d.ts +6 -0
  19. package/dist/core/providers.js +17 -0
  20. package/dist/core/providers.js.map +1 -0
  21. package/dist/core/types.d.ts +247 -0
  22. package/dist/core/types.js +4 -0
  23. package/dist/core/types.js.map +1 -0
  24. package/dist/core/util.d.ts +14 -0
  25. package/dist/core/util.js +61 -0
  26. package/dist/core/util.js.map +1 -0
  27. package/dist/sources/claude.d.ts +6 -0
  28. package/dist/sources/claude.js +576 -0
  29. package/dist/sources/claude.js.map +1 -0
  30. package/dist/sources/codex.d.ts +11 -0
  31. package/dist/sources/codex.js +603 -0
  32. package/dist/sources/codex.js.map +1 -0
  33. package/dist/sources/codexAppServer.d.ts +24 -0
  34. package/dist/sources/codexAppServer.js +190 -0
  35. package/dist/sources/codexAppServer.js.map +1 -0
  36. package/dist/sources/files.d.ts +1 -0
  37. package/dist/sources/files.js +24 -0
  38. package/dist/sources/files.js.map +1 -0
  39. package/dist/sources/index.d.ts +3 -0
  40. package/dist/sources/index.js +15 -0
  41. package/dist/sources/index.js.map +1 -0
  42. package/dist/sources/opencode.d.ts +6 -0
  43. package/dist/sources/opencode.js +443 -0
  44. package/dist/sources/opencode.js.map +1 -0
  45. package/dist/target/cache.d.ts +14 -0
  46. package/dist/target/cache.js +155 -0
  47. package/dist/target/cache.js.map +1 -0
  48. package/dist/target/config.d.ts +22 -0
  49. package/dist/target/config.js +90 -0
  50. package/dist/target/config.js.map +1 -0
  51. package/dist/target/destination.d.ts +21 -0
  52. package/dist/target/destination.js +77 -0
  53. package/dist/target/destination.js.map +1 -0
  54. package/dist/target/importer.d.ts +86 -0
  55. package/dist/target/importer.js +597 -0
  56. package/dist/target/importer.js.map +1 -0
  57. package/dist/target/launch.d.ts +6 -0
  58. package/dist/target/launch.js +77 -0
  59. package/dist/target/launch.js.map +1 -0
  60. package/dist/target/ledger.d.ts +35 -0
  61. package/dist/target/ledger.js +174 -0
  62. package/dist/target/ledger.js.map +1 -0
  63. package/dist/target/reconciliation.d.ts +60 -0
  64. package/dist/target/reconciliation.js +259 -0
  65. package/dist/target/reconciliation.js.map +1 -0
  66. package/dist/target/replace.d.ts +9 -0
  67. package/dist/target/replace.js +341 -0
  68. package/dist/target/replace.js.map +1 -0
  69. package/dist/target/schema.d.ts +22 -0
  70. package/dist/target/schema.js +108 -0
  71. package/dist/target/schema.js.map +1 -0
  72. package/dist/target/sync.d.ts +34 -0
  73. package/dist/target/sync.js +879 -0
  74. package/dist/target/sync.js.map +1 -0
  75. package/dist/target/syncAll.d.ts +4 -0
  76. package/dist/target/syncAll.js +98 -0
  77. package/dist/target/syncAll.js.map +1 -0
  78. package/dist/target/v2.d.ts +18 -0
  79. package/dist/target/v2.js +267 -0
  80. package/dist/target/v2.js.map +1 -0
  81. package/dist/target/v2Profiles.d.ts +5 -0
  82. package/dist/target/v2Profiles.js +14 -0
  83. package/dist/target/v2Profiles.js.map +1 -0
  84. package/dist/tui.d.ts +49 -0
  85. package/dist/tui.js +560 -0
  86. package/dist/tui.js.map +1 -0
  87. package/package.json +93 -4
  88. package/vendor/t3-v010/LICENSE +21 -0
  89. package/vendor/t3-v010/THIRD-PARTY-NOTICES.txt +2274 -0
  90. package/vendor/t3-v010/reference.json +6 -0
  91. package/vendor/t3-v010/runtime.mjs +60364 -0
  92. package/vendor/t3-v010/schema-columns.json +568 -0
  93. package/vendor/t3-v011/LICENSE +21 -0
  94. package/vendor/t3-v011/THIRD-PARTY-NOTICES.txt +2274 -0
  95. package/vendor/t3-v011/reference.json +6 -0
  96. package/vendor/t3-v011/runtime.mjs +60993 -0
  97. package/vendor/t3-v011/schema-columns.json +526 -0
package/dist/tui.d.ts ADDED
@@ -0,0 +1,49 @@
1
+ import React from "react";
2
+ import type { ConversationReplacePreview, ConversationSyncPreview, ReplaceSelection, SourceName, TargetPaths } from "./core/types.js";
3
+ import { type DestinationOptions } from "./target/destination.js";
4
+ export type Screen = "preflight" | "blocked" | "source" | "workspace" | "provider" | "threads" | "destination" | "review" | "running" | "result" | "launching" | "launch-error" | "error";
5
+ export interface TuiDestinationChoice {
6
+ label: string;
7
+ options?: DestinationOptions;
8
+ }
9
+ export declare function sourceLabel(source: SourceName): string;
10
+ /** Only offer destinations that the importer's canonical policy accepts. */
11
+ export declare function discoverTuiDestinations(paths: TargetPaths, workspace: string): TuiDestinationChoice[];
12
+ export declare function escapeDestination(screen: Screen, history: Screen[]): Screen | undefined;
13
+ interface TuiProps {
14
+ paths: TargetPaths;
15
+ initialSource?: SourceName;
16
+ initialWorkspace?: string;
17
+ initialProvider?: string;
18
+ mode?: "auto" | "sync" | "replace";
19
+ resetCache?: boolean;
20
+ }
21
+ export declare function helpTextForScreen(screen: Screen, searching?: boolean): string | undefined;
22
+ export declare function selectionMarker(isSelected: boolean, isDisabled: boolean): string;
23
+ export declare function StartT3Prompt({ index }: {
24
+ index: number;
25
+ }): React.JSX.Element;
26
+ export declare function ReplacementReview({ workspace, selections, previews, counts, resetCache, }: {
27
+ workspace: string | undefined;
28
+ selections: ReplaceSelection[];
29
+ previews: Map<string, ConversationReplacePreview>;
30
+ counts: {
31
+ tasks: number;
32
+ turns: number;
33
+ events: number;
34
+ attachments: number;
35
+ };
36
+ resetCache: boolean;
37
+ }): React.JSX.Element;
38
+ export declare function conversationStatusText(preview: ConversationSyncPreview | undefined, checking: boolean): string;
39
+ export declare function replacementStatusText(preview: ConversationReplacePreview | undefined, checking: boolean): string;
40
+ export declare function ConversationStatusDetails({ preview, replacement, checking, error, mode, review }: {
41
+ preview?: ConversationSyncPreview | undefined;
42
+ replacement?: ConversationReplacePreview | undefined;
43
+ checking: boolean;
44
+ error?: string | undefined;
45
+ mode?: "auto" | "sync" | "replace";
46
+ review?: boolean;
47
+ }): React.JSX.Element;
48
+ export declare function ImportTui({ paths, initialSource, initialWorkspace, initialProvider, mode, resetCache }: TuiProps): React.JSX.Element;
49
+ export {};
package/dist/tui.js ADDED
@@ -0,0 +1,560 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useMemo, useState } from "react";
3
+ import Database from "better-sqlite3";
4
+ import { Box, Text, useApp, useInput } from "ink";
5
+ import { sourceAdapter } from "./sources/index.js";
6
+ import { compactThreadForImport, importConversations } from "./target/importer.js";
7
+ import { inspectConversationSync, syncConversations } from "./target/sync.js";
8
+ import { listProviderInstances } from "./target/config.js";
9
+ import { inspectRuntime } from "./target/schema.js";
10
+ import { launchT3Code } from "./target/launch.js";
11
+ import { resetT3RebuildableCache } from "./target/cache.js";
12
+ import { inspectConversationReplacement, replaceConversations } from "./target/replace.js";
13
+ import { importDestination } from "./target/destination.js";
14
+ import { sourceNames } from "./core/providers.js";
15
+ import { canonicalPath } from "./core/util.js";
16
+ export function sourceLabel(source) {
17
+ if (source === "codex")
18
+ return "Codex";
19
+ if (source === "claude")
20
+ return "Claude Code";
21
+ return "OpenCode";
22
+ }
23
+ /** Only offer destinations that the importer's canonical policy accepts. */
24
+ export function discoverTuiDestinations(paths, workspace) {
25
+ const db = new Database(paths.dbPath, { readonly: true, fileMustExist: true });
26
+ try {
27
+ const rows = db.prepare("SELECT project_id projectId, title, workspace_root workspaceRoot FROM projection_projects WHERE deleted_at IS NULL ORDER BY title COLLATE NOCASE, project_id").all();
28
+ const matching = rows.filter((row) => canonicalPath(row.workspaceRoot) === canonicalPath(workspace));
29
+ const choices = [{ label: matching.length ? "Use the matching workspace project (default)" : "Create or use the workspace project (default)" }];
30
+ for (const row of matching) {
31
+ try {
32
+ importDestination(db, paths, workspace, { targetProject: row.projectId });
33
+ choices.push({ label: `${row.title} · ${row.workspaceRoot}`, options: { targetProject: row.projectId } });
34
+ }
35
+ catch {
36
+ // A project that fails the importer's destination invariant is not selectable.
37
+ }
38
+ }
39
+ try {
40
+ importDestination(db, paths, workspace, { noProject: true });
41
+ choices.push({ label: "No project (T3 scratch)", options: { noProject: true } });
42
+ }
43
+ catch {
44
+ // Native scratch is only offered when the canonical policy permits it.
45
+ }
46
+ return choices;
47
+ }
48
+ finally {
49
+ db.close();
50
+ }
51
+ }
52
+ export function escapeDestination(screen, history) {
53
+ if (["source", "blocked", "result", "launch-error", "error"].includes(screen))
54
+ return undefined;
55
+ return history.at(-1);
56
+ }
57
+ export function helpTextForScreen(screen, searching = false) {
58
+ if (searching)
59
+ return "Type to search Backspace delete Enter apply Esc cancel";
60
+ if (screen === "threads")
61
+ return "↑/↓ or j/k move Space select a all / search Enter continue Esc back/quit q quit";
62
+ if (screen === "source" || screen === "workspace" || screen === "provider" || screen === "destination")
63
+ return "↑/↓ or j/k move Enter choose Esc back/quit q quit";
64
+ if (screen === "review")
65
+ return "Enter confirm Esc back q quit";
66
+ if (screen === "result")
67
+ return "↑/↓ or j/k move Enter confirm q quit";
68
+ return undefined;
69
+ }
70
+ function Help({ screen, searching }) {
71
+ const text = helpTextForScreen(screen, searching);
72
+ return text ? _jsx(Text, { dimColor: true, children: text }) : null;
73
+ }
74
+ export function selectionMarker(isSelected, isDisabled) {
75
+ if (isDisabled)
76
+ return "⊘";
77
+ return isSelected ? "◉" : "○";
78
+ }
79
+ function Menu({ items, index, selected, disabled }) {
80
+ return _jsx(Box, { flexDirection: "column", children: items.map((item, itemIndex) => {
81
+ const isDisabled = disabled?.has(itemIndex) ?? false;
82
+ return _jsxs(Text, { dimColor: isDisabled, ...(itemIndex === index && !isDisabled ? { color: "cyan" } : {}), children: [itemIndex === index ? "›" : " ", " ", selected ? selectionMarker(selected.has(itemIndex), isDisabled) : "", " ", item] }, `${itemIndex}:${item}`);
83
+ }) });
84
+ }
85
+ export function StartT3Prompt({ index }) {
86
+ return _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Start T3 Code now?" }), _jsx(Menu, { items: ["Yes", "No"], index: index }), _jsx(Text, { dimColor: true, children: "Press Enter to confirm." })] });
87
+ }
88
+ export function ReplacementReview({ workspace, selections, previews, counts, resetCache, }) {
89
+ const taskWord = counts.tasks === 1 ? "task" : "tasks";
90
+ return _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Review replacement" }), _jsxs(Text, { children: ["Workspace: ", workspace] }), _jsx(Box, { marginTop: 1, flexDirection: "column", children: selections.map((selection) => {
91
+ const preview = previews.get(selection.conversation.summary.id);
92
+ return _jsxs(Box, { flexDirection: "column", children: [_jsx(Text, { bold: true, children: selection.conversation.summary.title }), _jsxs(Text, { children: ["Old T3 task: ", preview?.oldThreadId] }), _jsxs(Text, { children: ["New T3 task: ", preview?.newThreadId] })] }, `replacement-review:${selection.conversation.summary.id}`);
93
+ }) }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: ["The new ", taskWord, " will contain:"] }), _jsxs(Text, { children: [" ", counts.turns, " turns \u00B7 ", counts.events, " events \u00B7 ", counts.attachments, " attachments"] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsxs(Text, { children: ["The provider, model, and resume connection will transfer to the new ", taskWord, "."] }), _jsxs(Text, { children: ["The old ", taskWord, " will disappear from T3 but remain in its event history."] })] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [_jsx(Text, { children: "When you confirm, t3-import will automatically:" }), _jsx(Text, { children: " \u2022 Create a verified backup of the T3 database" }), _jsxs(Text, { children: [" ", resetCache ? "• Back up and reset T3's UI cache" : "• Keep T3's existing UI cache (--no-cache-reset)"] })] }), _jsx(Text, { color: "yellow", children: "T3 must remain closed." }), _jsx(Text, { bold: true, children: "Press Enter to replace, or Escape to cancel." })] });
94
+ }
95
+ export function conversationStatusText(preview, checking) {
96
+ if (!preview)
97
+ return checking ? "checking…" : "unavailable";
98
+ if (preview.status === "new")
99
+ return "new";
100
+ if (preview.status === "active-only")
101
+ return "active turn only";
102
+ if (preview.status === "syncable") {
103
+ const added = `${preview.newTurns} new turn${preview.newTurns === 1 ? "" : "s"}`;
104
+ const statuses = [];
105
+ if (preview.newInterruptedTurns)
106
+ statuses.push(preview.newTurns === 1 ? "interrupted" : `${preview.newInterruptedTurns} interrupted`);
107
+ if (preview.newFailedTurns)
108
+ statuses.push(preview.newTurns === 1 ? "failed" : `${preview.newFailedTurns} failed`);
109
+ const detail = statuses.length ? ` · ${statuses.join(", ")}` : "";
110
+ return preview.adoptedTurns ? `${added}${detail}, ${preview.adoptedTurns} adopted` : `${added}${detail}`;
111
+ }
112
+ if (preview.status === "up-to-date")
113
+ return `up to date${preview.ignoredActiveTurns ? ` · ${preview.ignoredActiveTurns} active turn${preview.ignoredActiveTurns === 1 ? "" : "s"} ignored` : ""}`;
114
+ if (preview.status === "history-diverged")
115
+ return "history changed";
116
+ if (preview.status === "branch-sync-unsupported")
117
+ return "branch sync unsupported";
118
+ return "target missing";
119
+ }
120
+ export function replacementStatusText(preview, checking) {
121
+ if (!preview)
122
+ return checking ? "checking…" : "unavailable";
123
+ if (preview.status === "replaceable")
124
+ return "replace available";
125
+ if (preview.status === "already-current")
126
+ return "already current";
127
+ if (preview.status === "active-source")
128
+ return "active source";
129
+ if (preview.status === "branch-replace-unsupported")
130
+ return "branch replacement unsupported";
131
+ if (preview.status === "not-imported")
132
+ return "not imported";
133
+ return "target missing";
134
+ }
135
+ export function ConversationStatusDetails({ preview, replacement, checking, error, mode = "auto", review = false }) {
136
+ let action;
137
+ if (error)
138
+ action = "Unavailable: inspection failed. Resolve the error and reopen this workspace.";
139
+ else if (mode === "replace")
140
+ action = replacement?.selectable
141
+ ? "Select to review replacement. Existing history will be replaced only after confirmation."
142
+ : checking && !replacement ? "Checking replacement eligibility…" : "Replacement is unavailable for this conversation.";
143
+ else if (!preview)
144
+ action = checking ? "Checking source history and target identity…" : "No reconciliation preview is available.";
145
+ else if (preview.status === "new")
146
+ action = mode === "sync"
147
+ ? "No eligible sync target was found. Use import to review a new conversation."
148
+ : "Select to import a new conversation. No eligible existing target was found.";
149
+ else if (preview.status === "syncable")
150
+ action = "Select to append the settled missing turns. Existing messages and local titles are preserved.";
151
+ else if (preview.status === "up-to-date")
152
+ action = "No settled messages need importing.";
153
+ else if (preview.status === "active-only")
154
+ action = "Wait for the source turn to finish, then reopen this workspace to check again.";
155
+ else if (preview.status === "history-diverged")
156
+ action = replacement?.selectable && mode === "auto"
157
+ ? "Conflict: select to review replacement. Histories will not be merged automatically."
158
+ : "Conflict: inspect the differing histories before choosing replacement.";
159
+ else if (preview.status === "branch-sync-unsupported")
160
+ action = "Conflict: branches require separate inspection; automatic sync is unavailable.";
161
+ else
162
+ action = "The previous target is missing. Inspect or restore it before retrying.";
163
+ if (review && preview?.status === "new")
164
+ action = "Will import a new conversation after confirmation.";
165
+ if (review && preview?.status === "syncable")
166
+ action = `Will append ${preview.newTurns} settled turn(s) after confirmation. Existing messages and local titles are preserved.`;
167
+ const warnings = [...new Set([...(preview?.warnings ?? []), ...(replacement?.warnings ?? [])])];
168
+ return _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { children: action }), preview?.ignoredActiveTurns ? _jsxs(Text, { color: "yellow", children: [preview.ignoredActiveTurns, " unfinished source turn(s) excluded; only settled history is eligible."] }) : null, error ? _jsxs(Text, { color: "yellow", children: ["Reason: ", error] }) : null, warnings.map((warning) => _jsxs(Text, { color: "yellow", children: ["Reason: ", warning] }, warning))] });
169
+ }
170
+ export function ImportTui({ paths, initialSource, initialWorkspace, initialProvider, mode = "auto", resetCache = false }) {
171
+ const app = useApp();
172
+ const [screen, setScreen] = useState("preflight");
173
+ const [screenHistory, setScreenHistory] = useState([]);
174
+ const [runtime, setRuntime] = useState();
175
+ const [source, setSource] = useState(initialSource ?? "codex");
176
+ const [summaries, setSummaries] = useState([]);
177
+ const [workspace, setWorkspace] = useState();
178
+ const [provider, setProvider] = useState(initialProvider);
179
+ const [destinationChoices, setDestinationChoices] = useState([]);
180
+ const [destinationError, setDestinationError] = useState();
181
+ const [destinationOptions, setDestinationOptions] = useState({});
182
+ const [index, setIndex] = useState(0);
183
+ const [selected, setSelected] = useState(new Set());
184
+ const [conversations, setConversations] = useState(new Map());
185
+ const [syncPreviews, setSyncPreviews] = useState(new Map());
186
+ const [replacePreviews, setReplacePreviews] = useState(new Map());
187
+ const [pendingStatuses, setPendingStatuses] = useState(0);
188
+ const [inspectionErrors, setInspectionErrors] = useState(new Map());
189
+ const [preparedImports, setPreparedImports] = useState([]);
190
+ const [preparedSync, setPreparedSync] = useState([]);
191
+ const [preparedReplacements, setPreparedReplacements] = useState([]);
192
+ const [searching, setSearching] = useState(false);
193
+ const [query, setQuery] = useState("");
194
+ const [result, setResult] = useState();
195
+ const [syncResult, setSyncResult] = useState();
196
+ const [replaceResult, setReplaceResult] = useState();
197
+ const [error, setError] = useState();
198
+ const [launchError, setLaunchError] = useState();
199
+ const [cacheWarning, setCacheWarning] = useState();
200
+ const checkingStatuses = pendingStatuses > 0;
201
+ const navigate = useCallback((next) => {
202
+ setScreenHistory((history) => [...history, screen]);
203
+ setScreen(next);
204
+ setIndex(0);
205
+ }, [screen]);
206
+ const workspaces = useMemo(() => [...new Map(summaries.map((summary) => [summary.workspace.toLowerCase(), summary.workspace])).values()], [summaries]);
207
+ const providers = useMemo(() => listProviderInstances(paths, source), [paths, source]);
208
+ const visibleThreads = useMemo(() => summaries
209
+ .filter((summary) => !workspace || summary.workspace.toLowerCase() === workspace.toLowerCase())
210
+ .filter((summary) => mode !== "replace" || !replacePreviews.has(summary.id) || replacePreviews.get(summary.id)?.previouslyImported)
211
+ .filter((summary) => !query || summary.title.toLowerCase().includes(query.toLowerCase()) || summary.id.includes(query)), [mode, query, replacePreviews, summaries, workspace]);
212
+ const operationFor = useCallback((summary) => {
213
+ const preview = syncPreviews.get(summary.id);
214
+ const replacement = replacePreviews.get(summary.id);
215
+ if (mode === "replace")
216
+ return replacement?.selectable ? "replace" : undefined;
217
+ if (mode === "sync")
218
+ return preview?.status === "syncable" ? "regular" : undefined;
219
+ if (replacement?.selectable && preview?.status === "history-diverged")
220
+ return "replace";
221
+ return preview?.selectable ? "regular" : undefined;
222
+ }, [mode, replacePreviews, syncPreviews]);
223
+ const canSelect = useCallback((summary) => {
224
+ const operation = operationFor(summary);
225
+ if (!operation)
226
+ return false;
227
+ const selectedOperation = [...selected].map((id) => summaries.find((item) => item.id === id)).filter((item) => Boolean(item)).map(operationFor).find(Boolean);
228
+ return !selectedOperation || selectedOperation === operation;
229
+ }, [operationFor, selected, summaries]);
230
+ const selectedVisible = useMemo(() => new Set(visibleThreads.flatMap((summary, itemIndex) => selected.has(summary.id) ? [itemIndex] : [])), [selected, visibleThreads]);
231
+ const disabledVisible = useMemo(() => new Set(visibleThreads.flatMap((summary, itemIndex) => canSelect(summary) ? [] : [itemIndex])), [canSelect, visibleThreads]);
232
+ const statusCounts = useMemo(() => {
233
+ const counts = { new: 0, syncable: 0, current: 0, active: 0, conflicts: 0, unavailable: 0 };
234
+ for (const summary of visibleThreads) {
235
+ if (inspectionErrors.has(summary.id)) {
236
+ counts.unavailable += 1;
237
+ continue;
238
+ }
239
+ const status = syncPreviews.get(summary.id)?.status;
240
+ if (status === "new")
241
+ counts.new += 1;
242
+ else if (status === "syncable")
243
+ counts.syncable += 1;
244
+ else if (status === "up-to-date")
245
+ counts.current += 1;
246
+ else if (status === "active-only")
247
+ counts.active += 1;
248
+ else if (status)
249
+ counts.conflicts += 1;
250
+ }
251
+ return counts;
252
+ }, [visibleThreads, syncPreviews, inspectionErrors]);
253
+ const importPreview = useMemo(() => preparedImports.reduce((counts, selection) => {
254
+ for (const sourceThread of selection.conversation.threads) {
255
+ const thread = compactThreadForImport(sourceThread);
256
+ counts.tasks += 1;
257
+ counts.turns += thread.turns.length;
258
+ counts.messages += thread.turns.reduce((sum, turn) => sum + 1 + turn.assistant.length, 0);
259
+ counts.activities += thread.turns.reduce((sum, turn) => sum + turn.activities.length, 0) + (thread.currentBranch ? 0 : 1);
260
+ counts.attachments += thread.turns.reduce((sum, turn) => sum + turn.user.attachments.length, 0);
261
+ }
262
+ return counts;
263
+ }, { tasks: 0, turns: 0, messages: 0, activities: 0, attachments: 0 }), [preparedImports]);
264
+ const syncPreview = useMemo(() => preparedSync.reduce((counts, selection) => {
265
+ const preview = syncPreviews.get(selection.conversation.summary.id);
266
+ counts.tasks += 1;
267
+ counts.turns += preview?.newTurns ?? 0;
268
+ counts.adopted += preview?.adoptedTurns ?? 0;
269
+ counts.titles += preview?.titleChanged ? 1 : 0;
270
+ return counts;
271
+ }, { tasks: 0, turns: 0, adopted: 0, titles: 0 }), [preparedSync, syncPreviews]);
272
+ const replacementPreview = useMemo(() => preparedReplacements.reduce((counts, selection) => {
273
+ const preview = replacePreviews.get(selection.conversation.summary.id);
274
+ counts.tasks += 1;
275
+ counts.turns += preview?.turns ?? 0;
276
+ counts.events += preview?.events ?? 0;
277
+ counts.attachments += preview?.attachments ?? 0;
278
+ return counts;
279
+ }, { tasks: 0, turns: 0, events: 0, attachments: 0 }), [preparedReplacements, replacePreviews]);
280
+ const checkRuntime = useCallback(() => {
281
+ setScreenHistory([]);
282
+ setScreen("preflight");
283
+ inspectRuntime(paths)
284
+ .then((state) => {
285
+ setRuntime(state);
286
+ setScreen(state.live ? "blocked" : initialSource ? "workspace" : "source");
287
+ })
288
+ .catch((cause) => {
289
+ setError(`Unable to check T3 runtime: ${cause instanceof Error ? cause.message : String(cause)}`);
290
+ setScreen("error");
291
+ });
292
+ }, [initialSource, paths]);
293
+ useEffect(() => { checkRuntime(); }, [checkRuntime]);
294
+ useEffect(() => {
295
+ if (screen !== "workspace")
296
+ return;
297
+ let cancelled = false;
298
+ setIndex(0);
299
+ sourceAdapter(source).discover({}).then((items) => {
300
+ if (cancelled)
301
+ return;
302
+ setSummaries(items);
303
+ const selectedWorkspace = initialWorkspace
304
+ ? items.find((item) => item.workspace.toLowerCase() === initialWorkspace.toLowerCase())?.workspace
305
+ : undefined;
306
+ if (selectedWorkspace) {
307
+ setWorkspace(selectedWorkspace);
308
+ setProvider(initialProvider ?? providers[0]);
309
+ // An explicit --workspace bypasses the chooser and its navigation history.
310
+ setScreen("threads");
311
+ }
312
+ }).catch((cause) => { if (!cancelled) {
313
+ setError(cause instanceof Error ? cause.message : String(cause));
314
+ setScreen("error");
315
+ } });
316
+ return () => { cancelled = true; };
317
+ }, [screen, source]);
318
+ useEffect(() => {
319
+ if (screen !== "threads" || !workspace)
320
+ return;
321
+ let cancelled = false;
322
+ const items = summaries.filter((summary) => summary.workspace.toLowerCase() === workspace.toLowerCase());
323
+ const queue = [...items];
324
+ setPendingStatuses(items.length);
325
+ setSelected(new Set());
326
+ setConversations(new Map());
327
+ setSyncPreviews(new Map());
328
+ setReplacePreviews(new Map());
329
+ setInspectionErrors(new Map());
330
+ const worker = async () => {
331
+ while (!cancelled) {
332
+ const summary = queue.shift();
333
+ if (!summary)
334
+ return;
335
+ try {
336
+ const conversation = await sourceAdapter(source).load(summary, { workspace });
337
+ const preview = mode === "replace" ? undefined : await inspectConversationSync(conversation, paths);
338
+ const replacement = mode === "replace" || preview?.status === "history-diverged"
339
+ ? await inspectConversationReplacement(conversation, paths)
340
+ : undefined;
341
+ if (cancelled)
342
+ return;
343
+ setConversations((current) => new Map(current).set(summary.id, conversation));
344
+ if (preview)
345
+ setSyncPreviews((current) => new Map(current).set(summary.id, preview));
346
+ if (replacement)
347
+ setReplacePreviews((current) => new Map(current).set(summary.id, replacement));
348
+ setSummaries((current) => current.map((item) => item.id === summary.id
349
+ ? { ...item, title: conversation.summary.title, updatedAt: conversation.summary.updatedAt, status: conversation.summary.status }
350
+ : item));
351
+ }
352
+ catch (cause) {
353
+ if (!cancelled)
354
+ setInspectionErrors((current) => new Map(current).set(summary.id, cause instanceof Error ? cause.message : String(cause)));
355
+ }
356
+ finally {
357
+ if (!cancelled)
358
+ setPendingStatuses((count) => Math.max(0, count - 1));
359
+ }
360
+ }
361
+ };
362
+ void Promise.all(Array.from({ length: Math.min(3, items.length) }, () => worker()));
363
+ return () => { cancelled = true; };
364
+ }, [mode, paths, screen, source, workspace]);
365
+ useEffect(() => {
366
+ if (screen !== "destination" || !workspace)
367
+ return;
368
+ try {
369
+ setDestinationChoices(discoverTuiDestinations(paths, workspace));
370
+ setDestinationError(undefined);
371
+ setDestinationOptions({});
372
+ setIndex(0);
373
+ }
374
+ catch (cause) {
375
+ setDestinationError(cause instanceof Error ? cause.message : String(cause));
376
+ setDestinationChoices([]);
377
+ }
378
+ }, [paths, screen, workspace]);
379
+ const move = (delta, length) => setIndex((current) => length === 0 ? 0 : (current + delta + length) % length);
380
+ useInput((input, key) => {
381
+ if (screen === "running")
382
+ return;
383
+ if (searching) {
384
+ if (key.escape || key.return)
385
+ setSearching(false);
386
+ else if (key.backspace || key.delete)
387
+ setQuery((value) => value.slice(0, -1));
388
+ else if (!key.ctrl && !key.meta && input)
389
+ setQuery((value) => value + input);
390
+ return;
391
+ }
392
+ if (input === "q") {
393
+ app.exit();
394
+ return;
395
+ }
396
+ if (screen === "blocked" && (input === "r" || key.return)) {
397
+ checkRuntime();
398
+ return;
399
+ }
400
+ if (key.upArrow || input === "k") {
401
+ move(-1, screen === "source" ? sourceNames.length : screen === "result" ? 2 : screen === "workspace" ? workspaces.length : screen === "provider" ? providers.length : screen === "destination" ? destinationChoices.length : visibleThreads.length);
402
+ return;
403
+ }
404
+ if (key.downArrow || input === "j") {
405
+ move(1, screen === "source" ? sourceNames.length : screen === "result" ? 2 : screen === "workspace" ? workspaces.length : screen === "provider" ? providers.length : screen === "destination" ? destinationChoices.length : visibleThreads.length);
406
+ return;
407
+ }
408
+ if (key.escape) {
409
+ const previous = escapeDestination(screen, screenHistory);
410
+ if (!previous) {
411
+ app.exit();
412
+ return;
413
+ }
414
+ if (screen === "provider" && previous === "threads") {
415
+ setPreparedImports([]);
416
+ setPreparedSync([]);
417
+ setPreparedReplacements([]);
418
+ }
419
+ setScreenHistory((history) => history.slice(0, -1));
420
+ setScreen(previous);
421
+ setIndex(0);
422
+ return;
423
+ }
424
+ if (screen === "source" && key.return) {
425
+ setSource(sourceNames[index]);
426
+ setSelected(new Set());
427
+ setPreparedImports([]);
428
+ setPreparedSync([]);
429
+ setPreparedReplacements([]);
430
+ navigate("workspace");
431
+ return;
432
+ }
433
+ if (screen === "workspace" && key.return && workspaces[index]) {
434
+ setWorkspace(workspaces[index]);
435
+ setProvider(initialProvider ?? providers[0]);
436
+ navigate("threads");
437
+ return;
438
+ }
439
+ if (screen === "provider" && key.return && providers[index]) {
440
+ setProvider(providers[index]);
441
+ navigate(preparedImports.length ? "destination" : "threads");
442
+ return;
443
+ }
444
+ if (screen === "destination" && key.return && destinationChoices[index]) {
445
+ setDestinationOptions(destinationChoices[index].options ?? {});
446
+ navigate("review");
447
+ return;
448
+ }
449
+ if (screen === "threads") {
450
+ if (input === "/") {
451
+ setSearching(true);
452
+ return;
453
+ }
454
+ if (input === "a") {
455
+ const currentOperation = visibleThreads[index] ? operationFor(visibleThreads[index]) : undefined;
456
+ const operation = currentOperation ?? visibleThreads.map(operationFor).find(Boolean);
457
+ const selectable = visibleThreads.filter((summary) => canSelect(summary) && operationFor(summary) === operation);
458
+ setSelected((value) => selectable.every((summary) => value.has(summary.id))
459
+ ? new Set([...value].filter((id) => !selectable.some((summary) => summary.id === id)))
460
+ : new Set([...value, ...selectable.map((summary) => summary.id)]));
461
+ return;
462
+ }
463
+ if (input === " " && visibleThreads[index] && canSelect(visibleThreads[index])) {
464
+ const id = visibleThreads[index].id;
465
+ setSelected((value) => { const next = new Set(value); if (next.has(id))
466
+ next.delete(id);
467
+ else
468
+ next.add(id); return next; });
469
+ return;
470
+ }
471
+ if (key.return && selected.size > 0) {
472
+ const chosen = summaries.filter((summary) => selected.has(summary.id));
473
+ const imports = [];
474
+ const syncs = [];
475
+ const replacements = [];
476
+ for (const summary of chosen) {
477
+ const conversation = conversations.get(summary.id);
478
+ const preview = syncPreviews.get(summary.id);
479
+ if (!conversation)
480
+ continue;
481
+ if (operationFor(summary) === "replace")
482
+ replacements.push({ conversation });
483
+ else if (preview?.status === "new" && mode === "auto")
484
+ imports.push({ conversation, resume: true });
485
+ else if (preview?.status === "syncable")
486
+ syncs.push({ conversation });
487
+ }
488
+ setPreparedImports(imports);
489
+ setPreparedSync(syncs);
490
+ setPreparedReplacements(replacements);
491
+ if (imports.length > 0) {
492
+ setDestinationOptions({});
493
+ navigate(!initialProvider && providers.length > 1 ? "provider" : "destination");
494
+ }
495
+ else
496
+ navigate("review");
497
+ return;
498
+ }
499
+ }
500
+ if (screen === "review" && key.return) {
501
+ setScreen("running");
502
+ const run = async () => {
503
+ const imported = preparedImports.length
504
+ ? await importConversations(preparedImports, paths, { dryRun: false, resume: true, ...(provider ? { providerInstance: provider } : {}), ...destinationOptions })
505
+ : undefined;
506
+ const synced = preparedSync.length
507
+ ? await syncConversations(preparedSync, paths, { dryRun: false, allowProjectionBacklog: Boolean(imported?.results.some((item) => item.status === "imported")) })
508
+ : undefined;
509
+ const replaced = preparedReplacements.length
510
+ ? await replaceConversations(preparedReplacements, paths, { dryRun: false })
511
+ : undefined;
512
+ const changed = Boolean(imported?.results.some((item) => item.status === "imported")) || Boolean(synced?.results.some((item) => item.status === "synced")) || Boolean(replaced?.results.some((item) => item.status === "replaced"));
513
+ if (resetCache && changed) {
514
+ try {
515
+ const cache = await resetT3RebuildableCache(paths);
516
+ const holder = replaced ?? synced ?? imported;
517
+ if (holder) {
518
+ holder.cache = cache;
519
+ holder.warnings.push(...cache.warnings);
520
+ }
521
+ }
522
+ catch (cause) {
523
+ setCacheWarning(`T3 data was updated, but its IndexedDB cache could not be reset: ${cause instanceof Error ? cause.message : String(cause)}`);
524
+ }
525
+ }
526
+ setResult(imported);
527
+ setSyncResult(synced);
528
+ setReplaceResult(replaced);
529
+ setIndex(0);
530
+ setScreen("result");
531
+ };
532
+ run().catch((cause) => { setError(cause instanceof Error ? cause.message : String(cause)); setScreen("error"); });
533
+ }
534
+ if (screen === "result" && key.return) {
535
+ if (index === 1) {
536
+ app.exit();
537
+ return;
538
+ }
539
+ setScreen("launching");
540
+ launchT3Code().then(() => app.exit()).catch((cause) => {
541
+ setLaunchError(cause instanceof Error ? cause.message : String(cause));
542
+ setScreen("launch-error");
543
+ });
544
+ return;
545
+ }
546
+ if ((screen === "launch-error" || screen === "error") && key.return)
547
+ app.exit();
548
+ });
549
+ return _jsxs(Box, { flexDirection: "column", paddingX: 1, children: [_jsx(Text, { bold: true, color: "cyan", children: "t3-import" }), _jsxs(Text, { dimColor: true, children: ["Target: ", paths.dbPath] }), _jsxs(Box, { marginTop: 1, flexDirection: "column", children: [screen === "preflight" && _jsx(Text, { children: "Checking whether T3 is running\u2026" }), screen === "blocked" && _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: "red", children: "T3 is running" }), _jsxs(Text, { children: ["Close T3 before importing, syncing, or replacing", runtime?.pid ? ` (detected PID ${runtime.pid})` : "", "."] }), runtime?.origin ? _jsxs(Text, { dimColor: true, children: ["Runtime: ", runtime.origin] }) : null, _jsx(Text, { children: "Press r or Enter to check again, or q to quit." })] }), screen === "source" && _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Choose a source" }), _jsx(Menu, { items: sourceNames.map(sourceLabel), index: index })] }), screen === "workspace" && _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Choose a workspace" }), summaries.length === 0 ? _jsxs(Text, { children: ["Discovering ", source, " conversations\u2026"] }) : _jsx(Menu, { items: workspaces, index: index })] }), screen === "provider" && _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Choose a T3 provider instance" }), _jsx(Menu, { items: providers, index: index })] }), screen === "destination" && _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Choose a T3 destination for new imports" }), _jsx(Text, { dimColor: true, children: "Available choices preserve the source working directory." }), destinationError ? _jsx(Text, { color: "red", children: destinationError }) : destinationChoices.length ? _jsx(Menu, { items: destinationChoices.map((choice) => choice.label), index: index }) : _jsx(Text, { children: "Checking available destinations\u2026" })] }), screen === "threads" && _jsxs(_Fragment, { children: [_jsxs(Text, { bold: true, children: [mode === "sync" ? "Select conversations to sync" : mode === "replace" ? "Select conversations to replace" : "Select conversations", " \u2014 ", workspace] }), searching || query ? _jsxs(Text, { color: "yellow", children: ["Search: ", query, _jsx(Text, { dimColor: true, children: "_" })] }) : null, _jsx(Menu, { items: visibleThreads.map((item) => {
550
+ const syncText = conversationStatusText(syncPreviews.get(item.id), checkingStatuses);
551
+ const replacement = replacePreviews.get(item.id);
552
+ const status = mode === "replace"
553
+ ? replacementStatusText(replacement, checkingStatuses)
554
+ : syncPreviews.get(item.id)?.status === "history-diverged" && replacement?.selectable
555
+ ? "history changed · replace available"
556
+ : syncText;
557
+ return `${item.title} ${status}`;
558
+ }), index: index, selected: selectedVisible, disabled: disabledVisible }), mode !== "replace" ? _jsxs(Text, { dimColor: true, children: ["New: ", statusCounts.new, " · To sync: ", statusCounts.syncable, " · Current: ", statusCounts.current, " · Active only: ", statusCounts.active, " · Conflicts: ", statusCounts.conflicts, " · Unavailable: ", statusCounts.unavailable] }) : null, visibleThreads[index] ? _jsx(ConversationStatusDetails, { preview: syncPreviews.get(visibleThreads[index].id), replacement: replacePreviews.get(visibleThreads[index].id), checking: checkingStatuses, error: inspectionErrors.get(visibleThreads[index].id), mode: mode }) : null, pendingStatuses > 0 ? _jsxs(Text, { dimColor: true, children: ["Checking ", pendingStatuses, " conversation", pendingStatuses === 1 ? "" : "s", "\u2026"] }) : null, visibleThreads.length === 0 && _jsx(Text, { color: "yellow", children: "No matching conversations." })] }), screen === "review" && _jsx(_Fragment, { children: preparedReplacements.length ? _jsx(_Fragment, { children: _jsx(ReplacementReview, { workspace: workspace, selections: preparedReplacements, previews: replacePreviews, counts: replacementPreview, resetCache: resetCache }) }) : _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, children: "Review changes" }), _jsxs(Text, { children: ["Source: ", source, " \u00B7 Workspace: ", workspace] }), preparedImports.length ? _jsxs(_Fragment, { children: [_jsxs(Text, { children: ["Source working directory: ", workspace] }), _jsxs(Text, { children: ["Effective T3 working directory: ", workspace] }), _jsxs(Text, { children: ["Destination: ", destinationChoices.find((choice) => JSON.stringify(choice.options ?? {}) === JSON.stringify(destinationOptions))?.label ?? "Use workspace project automatically"] })] }) : null, _jsxs(Text, { children: ["New tasks: ", importPreview.tasks, " \u00B7 synchronized tasks: ", syncPreview.tasks] }), _jsxs(Text, { children: ["Imported turns: ", importPreview.turns, " \u00B7 added turns: ", syncPreview.turns, " \u00B7 adopted turns: ", syncPreview.adopted] }), _jsxs(Text, { children: ["Messages: ", importPreview.messages, " \u00B7 activities: ", importPreview.activities, " \u00B7 attachments: ", importPreview.attachments] }), _jsxs(Text, { children: ["Title updates: ", syncPreview.titles] }), [...preparedImports, ...preparedSync].map((selection) => _jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsx(Text, { bold: true, children: selection.conversation.summary.title }), _jsx(ConversationStatusDetails, { preview: syncPreviews.get(selection.conversation.summary.id), checking: false, mode: mode, review: true })] }, selection.conversation.summary.id)), preparedImports.length ? _jsxs(Text, { children: ["Provider for new tasks: ", provider] }) : null, resetCache ? _jsx(Text, { children: "T3 IndexedDB cache: back up and reset after successful changes" }) : _jsx(Text, { children: "T3 IndexedDB cache: keep existing cache" }), _jsx(Text, { color: "yellow", children: "T3 must remain closed. A verified backup will be created before database changes." }), _jsx(Text, { bold: true, children: "Press Enter to continue, Escape to go back." })] }) }), screen === "running" && _jsx(Text, { children: "Backing up and reconciling conversations\u2026" }), screen === "result" && (result || syncResult || replaceResult) && _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: "green", children: "Complete" }), result ? _jsxs(Text, { children: ["Import backup: ", result.backup ?? "not needed"] }) : null, result?.results.map((item) => _jsxs(Text, { children: [item.status === "imported" ? "✓" : "•", " ", item.title, " \u2014 ", item.status, ", ", item.turns, " turns, ", item.events, " events"] }, `import:${item.threadId}`)), syncResult ? _jsxs(Text, { children: ["Sync backup: ", syncResult.backup ?? "not needed"] }) : null, syncResult?.results.map((item) => _jsxs(Text, { children: [item.status === "synced" ? "✓" : "•", " ", item.title, " \u2014 ", item.status, ", ", item.turnsAdded, " added, ", item.turnsAdopted, " adopted, ", item.events, " events"] }, `sync:${item.sourceKey}`)), replaceResult ? _jsxs(Text, { children: ["Replacement backup: ", replaceResult.backup ?? "not needed"] }) : null, replaceResult?.results.map((item) => _jsxs(Text, { children: [item.status === "replaced" ? "✓" : "•", " ", item.title, " \u2014 ", item.status, item.oldThreadId ? `, old ${item.oldThreadId}` : "", item.newThreadId ? `, new ${item.newThreadId}` : "", ", ", item.turns, " turns, ", item.events, " events"] }, `replace:${item.sourceKey}`)), (replaceResult?.cache ?? syncResult?.cache ?? result?.cache) ? _jsxs(Text, { children: ["Cache backup: ", (replaceResult?.cache ?? syncResult?.cache ?? result?.cache)?.backup ?? "cache not found"] }) : null, cacheWarning ? _jsx(Text, { color: "yellow", children: cacheWarning }) : null, _jsx(StartT3Prompt, { index: index })] }), screen === "launching" && _jsx(Text, { children: "Starting T3 Code\u2026" }), screen === "launch-error" && _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: "red", children: "Could not start T3 Code" }), _jsx(Text, { children: launchError }), _jsx(Text, { dimColor: true, children: "Press Enter to exit and start it manually." })] }), screen === "error" && _jsxs(_Fragment, { children: [_jsx(Text, { bold: true, color: "red", children: "Operation failed" }), _jsx(Text, { children: error }), _jsx(Text, { dimColor: true, children: "Press Enter to exit." })] })] }), helpTextForScreen(screen, searching) ? _jsx(Box, { marginTop: 1, children: _jsx(Help, { screen: screen, searching: searching }) }) : null] });
559
+ }
560
+ //# sourceMappingURL=tui.js.map