@akiojin/gwt 2.0.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.
Files changed (132) hide show
  1. package/README.ja.md +323 -0
  2. package/README.md +347 -0
  3. package/bin/gwt.js +5 -0
  4. package/package.json +125 -0
  5. package/src/claude-history.ts +717 -0
  6. package/src/claude.ts +292 -0
  7. package/src/cli/ui/__tests__/SKIPPED_TESTS.md +119 -0
  8. package/src/cli/ui/__tests__/acceptance/branchList.acceptance.test.tsx.skip +239 -0
  9. package/src/cli/ui/__tests__/acceptance/navigation.acceptance.test.tsx +214 -0
  10. package/src/cli/ui/__tests__/acceptance/realtimeUpdate.acceptance.test.tsx.skip +219 -0
  11. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +183 -0
  12. package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +313 -0
  13. package/src/cli/ui/__tests__/components/App.test.tsx +270 -0
  14. package/src/cli/ui/__tests__/components/common/Confirm.test.tsx +66 -0
  15. package/src/cli/ui/__tests__/components/common/ErrorBoundary.test.tsx +103 -0
  16. package/src/cli/ui/__tests__/components/common/Input.test.tsx +92 -0
  17. package/src/cli/ui/__tests__/components/common/LoadingIndicator.test.tsx +127 -0
  18. package/src/cli/ui/__tests__/components/common/Select.memo.test.tsx +264 -0
  19. package/src/cli/ui/__tests__/components/common/Select.test.tsx +246 -0
  20. package/src/cli/ui/__tests__/components/parts/Footer.test.tsx +62 -0
  21. package/src/cli/ui/__tests__/components/parts/Header.test.tsx +54 -0
  22. package/src/cli/ui/__tests__/components/parts/ScrollableList.test.tsx +68 -0
  23. package/src/cli/ui/__tests__/components/parts/Stats.test.tsx +135 -0
  24. package/src/cli/ui/__tests__/components/screens/AIToolSelectorScreen.test.tsx +153 -0
  25. package/src/cli/ui/__tests__/components/screens/BranchCreatorScreen.test.tsx +215 -0
  26. package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +293 -0
  27. package/src/cli/ui/__tests__/components/screens/ExecutionModeSelectorScreen.test.tsx +161 -0
  28. package/src/cli/ui/__tests__/components/screens/PRCleanupScreen.test.tsx +215 -0
  29. package/src/cli/ui/__tests__/components/screens/SessionSelectorScreen.test.tsx +99 -0
  30. package/src/cli/ui/__tests__/components/screens/WorktreeManagerScreen.test.tsx +127 -0
  31. package/src/cli/ui/__tests__/hooks/useGitData.test.ts.skip +228 -0
  32. package/src/cli/ui/__tests__/hooks/useScreenState.test.ts +146 -0
  33. package/src/cli/ui/__tests__/hooks/useTerminalSize.test.ts +98 -0
  34. package/src/cli/ui/__tests__/integration/branchList.test.tsx.skip +253 -0
  35. package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +306 -0
  36. package/src/cli/ui/__tests__/integration/navigation.test.tsx +405 -0
  37. package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx +505 -0
  38. package/src/cli/ui/__tests__/integration/realtimeUpdate.test.tsx.skip +216 -0
  39. package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +180 -0
  40. package/src/cli/ui/__tests__/performance/useMemoOptimization.test.tsx +237 -0
  41. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +775 -0
  42. package/src/cli/ui/__tests__/utils/statisticsCalculator.test.ts +243 -0
  43. package/src/cli/ui/components/App.tsx +793 -0
  44. package/src/cli/ui/components/common/Confirm.tsx +40 -0
  45. package/src/cli/ui/components/common/ErrorBoundary.tsx +57 -0
  46. package/src/cli/ui/components/common/Input.tsx +36 -0
  47. package/src/cli/ui/components/common/LoadingIndicator.tsx +95 -0
  48. package/src/cli/ui/components/common/Select.tsx +216 -0
  49. package/src/cli/ui/components/parts/Footer.tsx +41 -0
  50. package/src/cli/ui/components/parts/Header.test.tsx +85 -0
  51. package/src/cli/ui/components/parts/Header.tsx +63 -0
  52. package/src/cli/ui/components/parts/MergeStatusList.tsx +75 -0
  53. package/src/cli/ui/components/parts/ProgressBar.tsx +73 -0
  54. package/src/cli/ui/components/parts/ScrollableList.tsx +24 -0
  55. package/src/cli/ui/components/parts/Stats.tsx +67 -0
  56. package/src/cli/ui/components/screens/AIToolSelectorScreen.tsx +116 -0
  57. package/src/cli/ui/components/screens/BatchMergeProgressScreen.tsx +70 -0
  58. package/src/cli/ui/components/screens/BatchMergeResultScreen.tsx +104 -0
  59. package/src/cli/ui/components/screens/BranchCreatorScreen.tsx +213 -0
  60. package/src/cli/ui/components/screens/BranchListScreen.tsx +299 -0
  61. package/src/cli/ui/components/screens/ExecutionModeSelectorScreen.tsx +149 -0
  62. package/src/cli/ui/components/screens/PRCleanupScreen.tsx +167 -0
  63. package/src/cli/ui/components/screens/SessionSelectorScreen.tsx +100 -0
  64. package/src/cli/ui/components/screens/WorktreeManagerScreen.tsx +117 -0
  65. package/src/cli/ui/hooks/useBatchMerge.ts +96 -0
  66. package/src/cli/ui/hooks/useGitData.ts +157 -0
  67. package/src/cli/ui/hooks/useScreenState.ts +44 -0
  68. package/src/cli/ui/hooks/useTerminalSize.ts +33 -0
  69. package/src/cli/ui/screens/BranchActionSelectorScreen.tsx +102 -0
  70. package/src/cli/ui/screens/__tests__/BranchActionSelectorScreen.test.tsx +151 -0
  71. package/src/cli/ui/types.ts +295 -0
  72. package/src/cli/ui/utils/baseBranch.ts +34 -0
  73. package/src/cli/ui/utils/branchFormatter.ts +222 -0
  74. package/src/cli/ui/utils/statisticsCalculator.ts +44 -0
  75. package/src/codex.ts +139 -0
  76. package/src/config/builtin-tools.ts +44 -0
  77. package/src/config/constants.ts +100 -0
  78. package/src/config/env-history.ts +45 -0
  79. package/src/config/index.ts +204 -0
  80. package/src/config/tools.ts +293 -0
  81. package/src/git.ts +1102 -0
  82. package/src/github.ts +158 -0
  83. package/src/index.test.ts +87 -0
  84. package/src/index.ts +684 -0
  85. package/src/index.ts.backup +1543 -0
  86. package/src/launcher.ts +142 -0
  87. package/src/repositories/git.repository.ts +129 -0
  88. package/src/repositories/github.repository.ts +83 -0
  89. package/src/repositories/worktree.repository.ts +69 -0
  90. package/src/services/BatchMergeService.ts +251 -0
  91. package/src/services/WorktreeOrchestrator.ts +115 -0
  92. package/src/services/__tests__/BatchMergeService.test.ts +518 -0
  93. package/src/services/__tests__/WorktreeOrchestrator.test.ts +258 -0
  94. package/src/services/dependency-installer.ts +199 -0
  95. package/src/services/git.service.ts +113 -0
  96. package/src/services/github.service.ts +61 -0
  97. package/src/services/worktree.service.ts +66 -0
  98. package/src/types/api.ts +241 -0
  99. package/src/types/tools.ts +235 -0
  100. package/src/utils/spinner.ts +54 -0
  101. package/src/utils/terminal.ts +272 -0
  102. package/src/utils.test.ts +43 -0
  103. package/src/utils.ts +60 -0
  104. package/src/web/client/index.html +12 -0
  105. package/src/web/client/src/components/BranchGraph.tsx +231 -0
  106. package/src/web/client/src/components/EnvEditor.tsx +145 -0
  107. package/src/web/client/src/components/Terminal.tsx +137 -0
  108. package/src/web/client/src/hooks/useBranches.ts +41 -0
  109. package/src/web/client/src/hooks/useConfig.ts +31 -0
  110. package/src/web/client/src/hooks/useSessions.ts +59 -0
  111. package/src/web/client/src/hooks/useWorktrees.ts +47 -0
  112. package/src/web/client/src/index.css +834 -0
  113. package/src/web/client/src/lib/api.ts +184 -0
  114. package/src/web/client/src/lib/websocket.ts +174 -0
  115. package/src/web/client/src/main.tsx +29 -0
  116. package/src/web/client/src/pages/BranchDetailPage.tsx +847 -0
  117. package/src/web/client/src/pages/BranchListPage.tsx +264 -0
  118. package/src/web/client/src/pages/ConfigManagementPage.tsx +203 -0
  119. package/src/web/client/src/router.tsx +27 -0
  120. package/src/web/client/vite.config.ts +21 -0
  121. package/src/web/server/env/importer.ts +54 -0
  122. package/src/web/server/index.ts +74 -0
  123. package/src/web/server/pty/manager.ts +189 -0
  124. package/src/web/server/routes/branches.ts +126 -0
  125. package/src/web/server/routes/config.ts +220 -0
  126. package/src/web/server/routes/index.ts +37 -0
  127. package/src/web/server/routes/sessions.ts +130 -0
  128. package/src/web/server/routes/worktrees.ts +108 -0
  129. package/src/web/server/services/branches.ts +368 -0
  130. package/src/web/server/services/worktrees.ts +85 -0
  131. package/src/web/server/websocket/handler.ts +180 -0
  132. package/src/worktree.ts +703 -0
@@ -0,0 +1,717 @@
1
+ import { homedir } from "node:os";
2
+ import { readdir, readFile, stat } from "node:fs/promises";
3
+ import path from "node:path";
4
+
5
+ /**
6
+ * Claude Code conversation session information
7
+ */
8
+ export interface ClaudeConversation {
9
+ id: string;
10
+ sessionId?: string; // Claude Code session ID for --resume command
11
+ title: string;
12
+ lastActivity: number;
13
+ messageCount: number;
14
+ projectPath: string;
15
+ filePath: string;
16
+ summary?: string;
17
+ }
18
+
19
+ /**
20
+ * Message structure for conversation details
21
+ */
22
+ export interface ClaudeMessage {
23
+ role: "user" | "assistant";
24
+ content: string | Array<{ text: string; type?: string }>;
25
+ timestamp?: number;
26
+ }
27
+
28
+ /**
29
+ * Detailed conversation with full message history
30
+ */
31
+ export interface DetailedClaudeConversation extends ClaudeConversation {
32
+ messages: ClaudeMessage[];
33
+ }
34
+
35
+ /**
36
+ * Claude Code history manager error
37
+ */
38
+ export class ClaudeHistoryError extends Error {
39
+ constructor(
40
+ message: string,
41
+ public cause?: unknown,
42
+ ) {
43
+ super(message);
44
+ this.name = "ClaudeHistoryError";
45
+ }
46
+ }
47
+
48
+ /**
49
+ * Get Claude Code configuration directory
50
+ */
51
+ function getClaudeConfigDir(): string {
52
+ return path.join(homedir(), ".claude");
53
+ }
54
+
55
+ /**
56
+ * Get Claude Code projects directory
57
+ */
58
+ function getClaudeProjectsDir(): string {
59
+ return path.join(getClaudeConfigDir(), "projects");
60
+ }
61
+
62
+ /**
63
+ * Check if Claude Code is configured on this system
64
+ */
65
+ export async function isClaudeHistoryAvailable(): Promise<boolean> {
66
+ try {
67
+ const projectsDir = getClaudeProjectsDir();
68
+ const stats = await stat(projectsDir);
69
+ return stats.isDirectory();
70
+ } catch {
71
+ return false;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Parse a JSONL conversation file
77
+ */
78
+ async function parseConversationFile(
79
+ filePath: string,
80
+ ): Promise<ClaudeConversation | null> {
81
+ try {
82
+ const content = await readFile(filePath, "utf-8");
83
+ const lines = content
84
+ .trim()
85
+ .split("\n")
86
+ .filter((line) => line.trim());
87
+
88
+ if (lines.length === 0) {
89
+ return null;
90
+ }
91
+
92
+ // Parse messages to extract information
93
+ const messages = lines
94
+ .map((line) => {
95
+ try {
96
+ return JSON.parse(line);
97
+ } catch {
98
+ return null;
99
+ }
100
+ })
101
+ .filter(Boolean);
102
+
103
+ if (messages.length === 0) {
104
+ return null;
105
+ }
106
+
107
+ // Extract conversation metadata
108
+ const firstMessage = messages[0];
109
+ const lastMessage = messages[messages.length - 1];
110
+
111
+ // Extract session ID from messages (look for session_id, id, or conversation_id fields)
112
+ let sessionId: string | undefined;
113
+ for (const message of messages) {
114
+ if (message.session_id) {
115
+ sessionId = message.session_id;
116
+ break;
117
+ } else if (message.conversation_id) {
118
+ sessionId = message.conversation_id;
119
+ break;
120
+ } else if (
121
+ message.id &&
122
+ typeof message.id === "string" &&
123
+ message.id.length > 10
124
+ ) {
125
+ // If ID looks like a session ID (longer string), use it
126
+ sessionId = message.id;
127
+ break;
128
+ }
129
+ }
130
+
131
+ // If no session ID found in messages, try to extract from filename
132
+ if (!sessionId) {
133
+ const fileName = path.basename(filePath, ".jsonl");
134
+ // Look for UUID-like patterns in filename
135
+ const uuidMatch = fileName.match(
136
+ /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i,
137
+ );
138
+ if (uuidMatch) {
139
+ sessionId = uuidMatch[1];
140
+ } else if (fileName.length > 20) {
141
+ // Use filename as session ID if it's long enough
142
+ sessionId = fileName;
143
+ }
144
+ }
145
+
146
+ // Generate conversation title from first user message or file name
147
+ let title = "Untitled Conversation";
148
+
149
+ // Debug: Log raw messages for investigation
150
+ if (process.env.DEBUG_CLAUDE_HISTORY || process.env.CLAUDE_WORKTREE_DEBUG) {
151
+ console.log(`
152
+ [DEBUG] ===== Processing file: ${filePath} =====`);
153
+ console.log(
154
+ `[DEBUG] File basename: ${path.basename(filePath, ".jsonl")}`,
155
+ );
156
+ console.log(`[DEBUG] Message count: ${messages.length}`);
157
+
158
+ // Log first 3 messages in detail
159
+ console.log(`[DEBUG] First 3 messages:`);
160
+ messages.slice(0, 3).forEach((msg, idx) => {
161
+ console.log(`[DEBUG] Message ${idx + 1}:`);
162
+ console.log(` - Type: ${typeof msg}`);
163
+ console.log(` - Keys: ${Object.keys(msg).join(", ")}`);
164
+ console.log(` - Role: ${msg.role || "undefined"}`);
165
+ console.log(` - Content type: ${typeof msg.content}`);
166
+ if (msg.content) {
167
+ if (typeof msg.content === "string") {
168
+ console.log(
169
+ ` - Content preview: ${msg.content.substring(0, 100)}...`,
170
+ );
171
+ } else if (Array.isArray(msg.content)) {
172
+ console.log(
173
+ ` - Content is array with ${msg.content.length} items`,
174
+ );
175
+ if (msg.content[0]) {
176
+ console.log(` - First item type: ${typeof msg.content[0]}`);
177
+ console.log(
178
+ ` - First item keys: ${typeof msg.content[0] === "object" ? Object.keys(msg.content[0]).join(", ") : "N/A"}`,
179
+ );
180
+ }
181
+ } else {
182
+ console.log(
183
+ ` - Content is object with keys: ${Object.keys(msg.content).join(", ")}`,
184
+ );
185
+ }
186
+ }
187
+ console.log("");
188
+ });
189
+ }
190
+
191
+ // Find last user message - Claude Code uses different message structure
192
+ const lastUserMessage = messages
193
+ .slice()
194
+ .reverse()
195
+ .find(
196
+ (msg) =>
197
+ // Claude Code format: type='message' + userType='user'
198
+ (msg.type === "message" && msg.userType === "user") ||
199
+ // Nested format: type='user' with message.role='user'
200
+ (msg.type === "user" && msg.message && msg.message.role === "user") ||
201
+ // Legacy format
202
+ msg.role === "user" ||
203
+ msg.role === "human" ||
204
+ (msg.sender && msg.sender === "human") ||
205
+ (!msg.role && msg.content), // fallback for messages without explicit role
206
+ );
207
+
208
+ if (lastUserMessage) {
209
+ let extractedContent = "";
210
+
211
+ // Extract content based on Claude Code's actual structure
212
+ let messageContent = null;
213
+
214
+ // For Claude Code format: msg.message.content
215
+ if (lastUserMessage.message && lastUserMessage.message.content) {
216
+ messageContent = lastUserMessage.message.content;
217
+ }
218
+ // For direct message field (string)
219
+ else if (
220
+ lastUserMessage.message &&
221
+ typeof lastUserMessage.message === "string"
222
+ ) {
223
+ messageContent = lastUserMessage.message;
224
+ }
225
+ // For legacy content field
226
+ else if (lastUserMessage.content) {
227
+ messageContent = lastUserMessage.content;
228
+ }
229
+
230
+ // Handle different content formats that Claude Code might use
231
+ if (typeof messageContent === "string") {
232
+ extractedContent = messageContent;
233
+ } else if (Array.isArray(messageContent)) {
234
+ // Handle array of content blocks
235
+ for (const block of messageContent) {
236
+ if (typeof block === "string") {
237
+ extractedContent = block;
238
+ break;
239
+ } else if (block && typeof block === "object") {
240
+ // Handle content blocks with type and text properties
241
+ if (block.text && typeof block.text === "string") {
242
+ extractedContent = block.text;
243
+ break;
244
+ } else if (block.content && typeof block.content === "string") {
245
+ extractedContent = block.content;
246
+ break;
247
+ }
248
+ }
249
+ }
250
+ } else if (messageContent && typeof messageContent === "object") {
251
+ // Handle single content object
252
+ if (messageContent.text) {
253
+ extractedContent = messageContent.text;
254
+ } else if (messageContent.content) {
255
+ extractedContent = messageContent.content;
256
+ }
257
+ }
258
+
259
+ // Clean and format the extracted content
260
+ if (extractedContent) {
261
+ // Remove system prompts or meta information
262
+ const cleanContent = extractedContent
263
+ .replace(/^(<.*?>|System:|Assistant:|Human:|User:)/i, "")
264
+ .replace(/<\/[^>]+>/g, "") // Remove closing tags like </local-command-stdout>
265
+ .replace(/^(Result of calling|Error:|Warning:|DEBUG:|LOG:)/i, "") // Remove system messages
266
+ .replace(/^(Tool executed|Command executed|Output:)/i, "") // Remove tool output indicators
267
+ .replace(/^\s*[-#*•]\s*/gm, "") // Remove list markers
268
+ .replace(/^https?:\/\/[^\s]+$/gm, "") // Remove standalone URLs
269
+ .trim();
270
+
271
+ // Extract first meaningful line
272
+ const firstLine = cleanContent.split("\n")[0]?.trim() || "";
273
+
274
+ // Validate that the content is meaningful
275
+ if (
276
+ firstLine.length > 5 &&
277
+ !firstLine.match(/^(no content|undefined|null|\(no content\))$/i) &&
278
+ !firstLine.includes("</") && // Avoid HTML-like content
279
+ !firstLine.match(/^[^a-zA-Z]*$/) // Must contain some letters
280
+ ) {
281
+ title =
282
+ firstLine.length > 60
283
+ ? firstLine.substring(0, 57) + "..."
284
+ : firstLine;
285
+ }
286
+
287
+ // Debug: Log title extraction
288
+ if (process.env.DEBUG_CLAUDE_HISTORY) {
289
+ console.log(
290
+ `[DEBUG] Extracted title: "${title}" from content: "${extractedContent.substring(0, 100)}..."`,
291
+ );
292
+ console.log(
293
+ `[DEBUG] lastUserMessage structure:`,
294
+ JSON.stringify(lastUserMessage, null, 2).substring(0, 500),
295
+ );
296
+ }
297
+ }
298
+ }
299
+
300
+ // If still no good title, try alternative extraction methods
301
+ if (!title || title === "Untitled Conversation") {
302
+ // Try to extract from filename patterns
303
+ const fileName = path.basename(filePath, ".jsonl");
304
+
305
+ // Remove timestamp patterns and use remaining text
306
+ const cleanFileName = fileName.replace(
307
+ /^\d{4}-\d{2}-\d{2}_\d{2}-\d{2}-\d{2}_/,
308
+ "",
309
+ );
310
+
311
+ if (
312
+ cleanFileName &&
313
+ cleanFileName.length > 0 &&
314
+ !cleanFileName.match(/^[0-9a-f-]+$/i)
315
+ ) {
316
+ // Only use filename if it's not just a UUID
317
+ title = cleanFileName.replace(/[-_]/g, " ").trim();
318
+ title = title.charAt(0).toUpperCase() + title.slice(1);
319
+ } else {
320
+ // Fallback: try to extract from any message content
321
+ let foundTitle = false;
322
+
323
+ // Try last messages first - more relevant for current context
324
+ for (const msg of messages.slice(-10).reverse()) {
325
+ // Check last 10 messages in reverse order
326
+ if (msg && msg.content) {
327
+ let content = "";
328
+
329
+ // Extract content regardless of format
330
+ if (typeof msg.content === "string") {
331
+ content = msg.content;
332
+ } else if (Array.isArray(msg.content)) {
333
+ for (const item of msg.content) {
334
+ if (typeof item === "string") {
335
+ content = item;
336
+ break;
337
+ } else if (item && typeof item === "object") {
338
+ content =
339
+ item.text ||
340
+ item.content ||
341
+ JSON.stringify(item).substring(0, 100);
342
+ if (content) break;
343
+ }
344
+ }
345
+ } else if (typeof msg.content === "object") {
346
+ content =
347
+ msg.content.text ||
348
+ msg.content.content ||
349
+ JSON.stringify(msg.content).substring(0, 100);
350
+ }
351
+
352
+ // Clean and extract meaningful text
353
+ if (content && content.length > 10) {
354
+ // Remove common prefixes and clean up
355
+ const cleaned = content
356
+ .replace(
357
+ /^(Human:|Assistant:|User:|System:|<.*?>|\[.*?\])/gi,
358
+ "",
359
+ )
360
+ .replace(/^\s*[-#*•]\s*/gm, "") // Remove list markers
361
+ .trim();
362
+
363
+ if (cleaned.length > 10) {
364
+ // Get first sentence or line
365
+ const firstSentence =
366
+ cleaned.match(/^[^.!?\n]{10,60}/)?.[0] ||
367
+ cleaned.substring(0, 50);
368
+ if (firstSentence && firstSentence.length > 10) {
369
+ title =
370
+ firstSentence.trim() +
371
+ (firstSentence.length === 50 ? "..." : "");
372
+ foundTitle = true;
373
+ break;
374
+ }
375
+ }
376
+ }
377
+ }
378
+ }
379
+
380
+ // If still no title, use generic title
381
+ if (!foundTitle) {
382
+ // We'll use the file stats later for the date
383
+ title = `Conversation (${messages.length} messages)`;
384
+ }
385
+ }
386
+ }
387
+
388
+ // Get file stats for last activity time
389
+ const stats = await stat(filePath);
390
+
391
+ // Extract project path from file path
392
+ const projectsDir = getClaudeProjectsDir();
393
+ const relativePath = path.relative(projectsDir, filePath);
394
+ const projectPath = path.dirname(relativePath);
395
+
396
+ const result: ClaudeConversation = {
397
+ id: path.basename(filePath, ".jsonl"),
398
+ title: title,
399
+ lastActivity: stats.mtime.getTime(),
400
+ messageCount: messages.length,
401
+ projectPath: projectPath === "." ? "root" : projectPath,
402
+ filePath: filePath,
403
+ summary: generateSummary(messages),
404
+ };
405
+
406
+ // Only add sessionId if it exists
407
+ if (sessionId) {
408
+ result.sessionId = sessionId;
409
+ }
410
+
411
+ return result;
412
+ } catch (error) {
413
+ console.error(`Failed to parse conversation file ${filePath}:`, error);
414
+ return null;
415
+ }
416
+ }
417
+
418
+ /**
419
+ * Get detailed conversation with all messages
420
+ */
421
+ export async function getDetailedConversation(
422
+ conversation: ClaudeConversation,
423
+ ): Promise<DetailedClaudeConversation | null> {
424
+ try {
425
+ const content = await readFile(conversation.filePath, "utf-8");
426
+ const lines = content
427
+ .trim()
428
+ .split("\n")
429
+ .filter((line) => line.trim());
430
+
431
+ if (lines.length === 0) {
432
+ return null;
433
+ }
434
+
435
+ // Parse all messages
436
+ const messages: ClaudeMessage[] = lines
437
+ .map((line) => {
438
+ try {
439
+ const parsed = JSON.parse(line);
440
+
441
+ // Extract role and content based on Claude Code's actual structure
442
+ let role: "user" | "assistant" = "user";
443
+ let content = "";
444
+
445
+ // Determine role based on Claude Code's structure
446
+ if (parsed.type === "message" && parsed.userType === "user") {
447
+ role = "user";
448
+ } else if (parsed.type === "user") {
449
+ role = "user";
450
+ } else if (parsed.type === "assistant") {
451
+ role = "assistant";
452
+ } else if (parsed.message && parsed.message.role === "user") {
453
+ role = "user";
454
+ } else if (parsed.message && parsed.message.role === "assistant") {
455
+ role = "assistant";
456
+ } else if (parsed.role === "user" || parsed.role === "human") {
457
+ role = "user";
458
+ } else if (parsed.role === "assistant") {
459
+ role = "assistant";
460
+ } else {
461
+ // Default based on message structure
462
+ role = "assistant";
463
+ }
464
+
465
+ // Extract content based on Claude Code's structure
466
+ if (parsed.message && parsed.message.content) {
467
+ // For Claude Code format: msg.message.content
468
+ const messageContent = parsed.message.content;
469
+ if (typeof messageContent === "string") {
470
+ content = messageContent;
471
+ } else if (Array.isArray(messageContent)) {
472
+ // Handle array of content blocks
473
+ for (const block of messageContent) {
474
+ if (typeof block === "string") {
475
+ content = block;
476
+ break;
477
+ } else if (block && typeof block === "object") {
478
+ // Claude Code format: {type: "text", text: "..."}
479
+ if (
480
+ block.type === "text" &&
481
+ block.text &&
482
+ typeof block.text === "string"
483
+ ) {
484
+ content = block.text;
485
+ break;
486
+ } else if (block.type === "tool_use" && block.name) {
487
+ // Display tool usage
488
+ content = `🔧 Used tool: ${block.name}`;
489
+ break;
490
+ } else if (block.text && typeof block.text === "string") {
491
+ content = block.text;
492
+ break;
493
+ } else if (
494
+ block.content &&
495
+ typeof block.content === "string"
496
+ ) {
497
+ content = block.content;
498
+ break;
499
+ }
500
+ }
501
+ }
502
+ }
503
+ } else if (parsed.message && typeof parsed.message === "string") {
504
+ // For direct message field (string)
505
+ content = parsed.message;
506
+ } else if (parsed.content) {
507
+ // For legacy content field
508
+ if (typeof parsed.content === "string") {
509
+ content = parsed.content;
510
+ } else if (Array.isArray(parsed.content)) {
511
+ for (const block of parsed.content) {
512
+ if (typeof block === "string") {
513
+ content = block;
514
+ break;
515
+ } else if (block && typeof block === "object") {
516
+ // Claude Code format: {type: "text", text: "..."}
517
+ if (
518
+ block.type === "text" &&
519
+ block.text &&
520
+ typeof block.text === "string"
521
+ ) {
522
+ content = block.text;
523
+ break;
524
+ } else if (block.type === "tool_use" && block.name) {
525
+ // Display tool usage
526
+ content = `🔧 Used tool: ${block.name}`;
527
+ break;
528
+ } else if (block.text && typeof block.text === "string") {
529
+ content = block.text;
530
+ break;
531
+ }
532
+ }
533
+ }
534
+ }
535
+ }
536
+
537
+ return {
538
+ role,
539
+ content,
540
+ timestamp: parsed.timestamp || Date.now(),
541
+ };
542
+ } catch {
543
+ return null;
544
+ }
545
+ })
546
+ .filter(Boolean) as ClaudeMessage[];
547
+
548
+ if (messages.length === 0) {
549
+ return null;
550
+ }
551
+
552
+ return {
553
+ ...conversation,
554
+ messages,
555
+ };
556
+ } catch (error) {
557
+ console.error(`Failed to get detailed conversation:`, error);
558
+ return null;
559
+ }
560
+ }
561
+
562
+ /**
563
+ * Generate a summary from conversation messages
564
+ */
565
+ function generateSummary(messages: any[]): string {
566
+ // Find user messages with flexible role matching
567
+ const userMessages = messages
568
+ .filter(
569
+ (msg) =>
570
+ msg.role === "user" ||
571
+ msg.role === "human" ||
572
+ (msg.sender && msg.sender === "human") ||
573
+ (!msg.role && msg.content),
574
+ )
575
+ .slice(0, 3);
576
+
577
+ const topics = userMessages
578
+ .map((msg) => {
579
+ let content = "";
580
+
581
+ // Handle different content formats
582
+ if (typeof msg.content === "string") {
583
+ content = msg.content;
584
+ } else if (Array.isArray(msg.content)) {
585
+ // Handle array of content blocks
586
+ for (const block of msg.content) {
587
+ if (typeof block === "string") {
588
+ content = block;
589
+ break;
590
+ } else if (block && typeof block === "object") {
591
+ if (block.text && typeof block.text === "string") {
592
+ content = block.text;
593
+ break;
594
+ } else if (block.content && typeof block.content === "string") {
595
+ content = block.content;
596
+ break;
597
+ }
598
+ }
599
+ }
600
+ } else if (msg.content && typeof msg.content === "object") {
601
+ if (msg.content.text) {
602
+ content = msg.content.text;
603
+ } else if (msg.content.content) {
604
+ content = msg.content.content;
605
+ }
606
+ }
607
+
608
+ const firstLine = content.split("\n")[0]?.trim() || "";
609
+ return firstLine.length > 30
610
+ ? firstLine.substring(0, 27) + "..."
611
+ : firstLine;
612
+ })
613
+ .filter((topic) => topic.length > 0);
614
+
615
+ return topics.length > 0 ? topics.join(" • ") : "No summary available";
616
+ }
617
+
618
+ /**
619
+ * Get all Claude Code conversations
620
+ */
621
+ export async function getAllClaudeConversations(): Promise<
622
+ ClaudeConversation[]
623
+ > {
624
+ if (!(await isClaudeHistoryAvailable())) {
625
+ throw new ClaudeHistoryError(
626
+ "Claude Code history is not available on this system",
627
+ );
628
+ }
629
+
630
+ try {
631
+ const conversations: ClaudeConversation[] = [];
632
+ const projectsDir = getClaudeProjectsDir();
633
+
634
+ // Recursively scan for .jsonl files
635
+ await scanDirectoryForConversations(projectsDir, conversations);
636
+
637
+ // Sort by last activity (most recent first)
638
+ conversations.sort((a, b) => b.lastActivity - a.lastActivity);
639
+
640
+ return conversations;
641
+ } catch (error) {
642
+ throw new ClaudeHistoryError(
643
+ "Failed to scan Claude Code conversations",
644
+ error,
645
+ );
646
+ }
647
+ }
648
+
649
+ /**
650
+ * Recursively scan directory for conversation files
651
+ */
652
+ async function scanDirectoryForConversations(
653
+ dirPath: string,
654
+ conversations: ClaudeConversation[],
655
+ ): Promise<void> {
656
+ try {
657
+ const entries = await readdir(dirPath, { withFileTypes: true });
658
+
659
+ for (const entry of entries) {
660
+ const fullPath = path.join(dirPath, entry.name);
661
+
662
+ if (entry.isDirectory()) {
663
+ // Recursively scan subdirectories
664
+ await scanDirectoryForConversations(fullPath, conversations);
665
+ } else if (entry.isFile() && entry.name.endsWith(".jsonl")) {
666
+ // Parse conversation file
667
+ const conversation = await parseConversationFile(fullPath);
668
+ if (conversation) {
669
+ conversations.push(conversation);
670
+ }
671
+ }
672
+ }
673
+ } catch (error) {
674
+ // Continue scanning even if one directory fails
675
+ console.error(`Failed to scan directory ${dirPath}:`, error);
676
+ }
677
+ }
678
+
679
+ /**
680
+ * Get conversations filtered by project/worktree path
681
+ */
682
+ export async function getConversationsForProject(
683
+ worktreePath: string,
684
+ ): Promise<ClaudeConversation[]> {
685
+ const allConversations = await getAllClaudeConversations();
686
+
687
+ // Extract project name from worktree path
688
+ const projectName = path.basename(worktreePath);
689
+
690
+ return allConversations.filter((conversation) => {
691
+ // Match by project path or conversation mentions the project
692
+ return (
693
+ conversation.projectPath.includes(projectName) ||
694
+ conversation.title.toLowerCase().includes(projectName.toLowerCase()) ||
695
+ conversation.summary?.toLowerCase().includes(projectName.toLowerCase())
696
+ );
697
+ });
698
+ }
699
+
700
+ /**
701
+ * Launch Claude Code with a specific conversation
702
+ */
703
+ export async function launchClaudeWithConversation(
704
+ worktreePath: string,
705
+ conversation: ClaudeConversation,
706
+ options: { skipPermissions?: boolean } = {},
707
+ ): Promise<void> {
708
+ const { launchClaudeCode } = await import("./claude.js");
709
+
710
+ // Launch Claude Code in the worktree with the conversation file
711
+ // Note: This might need adjustment based on how Claude Code handles specific conversation files
712
+ // For now, we'll use the standard launch and let Claude Code handle the session
713
+ await launchClaudeCode(worktreePath, {
714
+ mode: "resume",
715
+ skipPermissions: options.skipPermissions ?? false,
716
+ });
717
+ }