@akiojin/gwt 4.11.6 → 4.12.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 (172) hide show
  1. package/bin/gwt.js +1 -1
  2. package/dist/claude.d.ts +1 -0
  3. package/dist/claude.d.ts.map +1 -1
  4. package/dist/claude.js +50 -24
  5. package/dist/claude.js.map +1 -1
  6. package/dist/cli/ui/App.solid.d.ts.map +1 -1
  7. package/dist/cli/ui/App.solid.js +247 -49
  8. package/dist/cli/ui/App.solid.js.map +1 -1
  9. package/dist/cli/ui/components/solid/QuickStartStep.d.ts.map +1 -1
  10. package/dist/cli/ui/components/solid/QuickStartStep.js +35 -22
  11. package/dist/cli/ui/components/solid/QuickStartStep.js.map +1 -1
  12. package/dist/cli/ui/components/solid/SelectInput.d.ts.map +1 -1
  13. package/dist/cli/ui/components/solid/SelectInput.js +2 -1
  14. package/dist/cli/ui/components/solid/SelectInput.js.map +1 -1
  15. package/dist/cli/ui/components/solid/WizardController.d.ts.map +1 -1
  16. package/dist/cli/ui/components/solid/WizardController.js +19 -11
  17. package/dist/cli/ui/components/solid/WizardController.js.map +1 -1
  18. package/dist/cli/ui/components/solid/WizardSteps.d.ts.map +1 -1
  19. package/dist/cli/ui/components/solid/WizardSteps.js +26 -69
  20. package/dist/cli/ui/components/solid/WizardSteps.js.map +1 -1
  21. package/dist/cli/ui/core/theme.d.ts +9 -0
  22. package/dist/cli/ui/core/theme.d.ts.map +1 -1
  23. package/dist/cli/ui/core/theme.js +21 -0
  24. package/dist/cli/ui/core/theme.js.map +1 -1
  25. package/dist/cli/ui/screens/solid/BranchListScreen.d.ts +9 -2
  26. package/dist/cli/ui/screens/solid/BranchListScreen.d.ts.map +1 -1
  27. package/dist/cli/ui/screens/solid/BranchListScreen.js +101 -28
  28. package/dist/cli/ui/screens/solid/BranchListScreen.js.map +1 -1
  29. package/dist/cli/ui/screens/solid/ConfirmScreen.d.ts +2 -1
  30. package/dist/cli/ui/screens/solid/ConfirmScreen.d.ts.map +1 -1
  31. package/dist/cli/ui/screens/solid/ConfirmScreen.js +11 -3
  32. package/dist/cli/ui/screens/solid/ConfirmScreen.js.map +1 -1
  33. package/dist/cli/ui/screens/solid/EnvironmentScreen.d.ts.map +1 -1
  34. package/dist/cli/ui/screens/solid/EnvironmentScreen.js +9 -10
  35. package/dist/cli/ui/screens/solid/EnvironmentScreen.js.map +1 -1
  36. package/dist/cli/ui/screens/solid/LogScreen.d.ts +7 -1
  37. package/dist/cli/ui/screens/solid/LogScreen.d.ts.map +1 -1
  38. package/dist/cli/ui/screens/solid/LogScreen.js +254 -16
  39. package/dist/cli/ui/screens/solid/LogScreen.js.map +1 -1
  40. package/dist/cli/ui/screens/solid/ProfileEnvScreen.d.ts.map +1 -1
  41. package/dist/cli/ui/screens/solid/ProfileEnvScreen.js +8 -5
  42. package/dist/cli/ui/screens/solid/ProfileEnvScreen.js.map +1 -1
  43. package/dist/cli/ui/screens/solid/SelectorScreen.d.ts.map +1 -1
  44. package/dist/cli/ui/screens/solid/SelectorScreen.js +12 -4
  45. package/dist/cli/ui/screens/solid/SelectorScreen.js.map +1 -1
  46. package/dist/cli/ui/types.d.ts +1 -0
  47. package/dist/cli/ui/types.d.ts.map +1 -1
  48. package/dist/cli/ui/utils/branchFormatter.d.ts +1 -0
  49. package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
  50. package/dist/cli/ui/utils/branchFormatter.js +29 -7
  51. package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
  52. package/dist/cli/ui/utils/continueSession.d.ts +14 -0
  53. package/dist/cli/ui/utils/continueSession.d.ts.map +1 -1
  54. package/dist/cli/ui/utils/continueSession.js +61 -3
  55. package/dist/cli/ui/utils/continueSession.js.map +1 -1
  56. package/dist/cli/ui/utils/versionCache.d.ts +37 -0
  57. package/dist/cli/ui/utils/versionCache.d.ts.map +1 -0
  58. package/dist/cli/ui/utils/versionCache.js +70 -0
  59. package/dist/cli/ui/utils/versionCache.js.map +1 -0
  60. package/dist/cli/ui/utils/versionFetcher.d.ts +41 -0
  61. package/dist/cli/ui/utils/versionFetcher.d.ts.map +1 -0
  62. package/dist/cli/ui/utils/versionFetcher.js +89 -0
  63. package/dist/cli/ui/utils/versionFetcher.js.map +1 -0
  64. package/dist/codex.d.ts +1 -0
  65. package/dist/codex.d.ts.map +1 -1
  66. package/dist/codex.js +48 -19
  67. package/dist/codex.js.map +1 -1
  68. package/dist/config/index.d.ts.map +1 -1
  69. package/dist/config/index.js +10 -1
  70. package/dist/config/index.js.map +1 -1
  71. package/dist/gemini.d.ts +1 -0
  72. package/dist/gemini.d.ts.map +1 -1
  73. package/dist/gemini.js +36 -3
  74. package/dist/gemini.js.map +1 -1
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +32 -2
  77. package/dist/index.js.map +1 -1
  78. package/dist/launcher.d.ts.map +1 -1
  79. package/dist/launcher.js +43 -8
  80. package/dist/launcher.js.map +1 -1
  81. package/dist/logging/agentOutput.d.ts +21 -0
  82. package/dist/logging/agentOutput.d.ts.map +1 -0
  83. package/dist/logging/agentOutput.js +164 -0
  84. package/dist/logging/agentOutput.js.map +1 -0
  85. package/dist/logging/formatter.d.ts.map +1 -1
  86. package/dist/logging/formatter.js +18 -4
  87. package/dist/logging/formatter.js.map +1 -1
  88. package/dist/logging/logger.d.ts.map +1 -1
  89. package/dist/logging/logger.js +2 -0
  90. package/dist/logging/logger.js.map +1 -1
  91. package/dist/logging/reader.d.ts +21 -0
  92. package/dist/logging/reader.d.ts.map +1 -1
  93. package/dist/logging/reader.js +79 -0
  94. package/dist/logging/reader.js.map +1 -1
  95. package/dist/opentui/index.solid.js +2306 -653
  96. package/dist/services/dependency-installer.js +2 -2
  97. package/dist/services/dependency-installer.js.map +1 -1
  98. package/dist/utils/session/common.d.ts +8 -0
  99. package/dist/utils/session/common.d.ts.map +1 -1
  100. package/dist/utils/session/common.js +22 -0
  101. package/dist/utils/session/common.js.map +1 -1
  102. package/dist/utils/session/parsers/claude.d.ts +10 -4
  103. package/dist/utils/session/parsers/claude.d.ts.map +1 -1
  104. package/dist/utils/session/parsers/claude.js +64 -18
  105. package/dist/utils/session/parsers/claude.js.map +1 -1
  106. package/dist/utils/session/parsers/codex.d.ts.map +1 -1
  107. package/dist/utils/session/parsers/codex.js +48 -28
  108. package/dist/utils/session/parsers/codex.js.map +1 -1
  109. package/dist/utils/session/parsers/gemini.d.ts.map +1 -1
  110. package/dist/utils/session/parsers/gemini.js +43 -6
  111. package/dist/utils/session/parsers/gemini.js.map +1 -1
  112. package/dist/utils/session/parsers/opencode.d.ts.map +1 -1
  113. package/dist/utils/session/parsers/opencode.js +43 -6
  114. package/dist/utils/session/parsers/opencode.js.map +1 -1
  115. package/dist/utils/session/types.d.ts +7 -0
  116. package/dist/utils/session/types.d.ts.map +1 -1
  117. package/dist/web/client/src/components/ui/alert.d.ts +1 -1
  118. package/dist/worktree.d.ts +4 -1
  119. package/dist/worktree.d.ts.map +1 -1
  120. package/dist/worktree.js +21 -15
  121. package/dist/worktree.js.map +1 -1
  122. package/package.json +2 -1
  123. package/src/claude.ts +64 -28
  124. package/src/cli/ui/App.solid.tsx +324 -51
  125. package/src/cli/ui/__tests__/solid/AppSolid.cleanup.test.tsx +830 -1
  126. package/src/cli/ui/__tests__/solid/BranchListScreen.test.tsx +105 -5
  127. package/src/cli/ui/__tests__/solid/ConfirmScreen.test.tsx +77 -0
  128. package/src/cli/ui/__tests__/solid/LogScreen.test.tsx +351 -0
  129. package/src/cli/ui/__tests__/solid/components/QuickStartStep.test.tsx +73 -2
  130. package/src/cli/ui/__tests__/solid/components/WizardSteps.test.tsx +4 -1
  131. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +72 -45
  132. package/src/cli/ui/components/solid/QuickStartStep.tsx +35 -23
  133. package/src/cli/ui/components/solid/SearchInput.tsx +1 -1
  134. package/src/cli/ui/components/solid/SelectInput.tsx +4 -0
  135. package/src/cli/ui/components/solid/WizardController.tsx +20 -11
  136. package/src/cli/ui/components/solid/WizardSteps.tsx +29 -86
  137. package/src/cli/ui/core/theme.ts +32 -0
  138. package/src/cli/ui/hooks/solid/useAsyncOperation.ts +8 -6
  139. package/src/cli/ui/hooks/solid/useGitOperations.ts +6 -5
  140. package/src/cli/ui/screens/solid/BranchListScreen.tsx +135 -32
  141. package/src/cli/ui/screens/solid/ConfirmScreen.tsx +20 -8
  142. package/src/cli/ui/screens/solid/EnvironmentScreen.tsx +22 -20
  143. package/src/cli/ui/screens/solid/LogScreen.tsx +364 -35
  144. package/src/cli/ui/screens/solid/ProfileEnvScreen.tsx +19 -15
  145. package/src/cli/ui/screens/solid/SelectorScreen.tsx +25 -14
  146. package/src/cli/ui/screens/solid/SettingsScreen.tsx +5 -3
  147. package/src/cli/ui/types.ts +1 -0
  148. package/src/cli/ui/utils/__tests__/branchFormatter.test.ts +53 -6
  149. package/src/cli/ui/utils/branchFormatter.ts +35 -7
  150. package/src/cli/ui/utils/continueSession.ts +90 -3
  151. package/src/cli/ui/utils/versionCache.ts +93 -0
  152. package/src/cli/ui/utils/versionFetcher.ts +120 -0
  153. package/src/codex.ts +62 -20
  154. package/src/config/__tests__/saveSession.test.ts +2 -2
  155. package/src/config/index.ts +11 -1
  156. package/src/gemini.ts +50 -4
  157. package/src/index.test.ts +16 -10
  158. package/src/index.ts +38 -1
  159. package/src/launcher.ts +49 -8
  160. package/src/logging/agentOutput.ts +216 -0
  161. package/src/logging/formatter.ts +23 -4
  162. package/src/logging/logger.ts +2 -0
  163. package/src/logging/reader.ts +117 -0
  164. package/src/services/__tests__/BatchMergeService.test.ts +34 -14
  165. package/src/services/dependency-installer.ts +2 -2
  166. package/src/utils/session/common.ts +28 -0
  167. package/src/utils/session/parsers/claude.ts +79 -29
  168. package/src/utils/session/parsers/codex.ts +50 -26
  169. package/src/utils/session/parsers/gemini.ts +46 -5
  170. package/src/utils/session/parsers/opencode.ts +46 -5
  171. package/src/utils/session/types.ts +4 -0
  172. package/src/worktree.ts +28 -15
@@ -246,10 +246,839 @@ describe("AppSolid cleanup command", () => {
246
246
 
247
247
  try {
248
248
  const frame = testSetup.captureCharFrame();
249
- expect(frame).toMatch(/\[ \] w {2,}feature\/safe/);
249
+ expect(frame).toMatch(/\[ \] w o feature\/safe/);
250
250
  expect(frame).toContain("[ ] w ! feature/unsafe");
251
251
  } finally {
252
252
  testSetup.renderer.destroy();
253
253
  }
254
254
  });
255
+
256
+ it("updates safety icons as each branch check completes", async () => {
257
+ let releaseSecond: (() => void) | null = null;
258
+ const progressGate = new Promise<void>((resolve) => {
259
+ releaseSecond = resolve;
260
+ });
261
+ const getCleanupStatusMock = mock(
262
+ async ({
263
+ onProgress,
264
+ }: { onProgress?: (status: { branch: string }) => void } = {}) => {
265
+ const firstStatus = {
266
+ worktreePath: "/tmp/first",
267
+ branch: "feature/first",
268
+ hasUncommittedChanges: false,
269
+ hasUnpushedCommits: false,
270
+ cleanupType: "worktree-and-branch",
271
+ hasRemoteBranch: true,
272
+ hasUniqueCommits: false,
273
+ hasUpstream: true,
274
+ upstream: "origin/feature/first",
275
+ isAccessible: true,
276
+ reasons: ["no-diff-with-base"],
277
+ };
278
+ const secondStatus = {
279
+ worktreePath: "/tmp/second",
280
+ branch: "feature/second",
281
+ hasUncommittedChanges: false,
282
+ hasUnpushedCommits: false,
283
+ cleanupType: "worktree-and-branch",
284
+ hasRemoteBranch: true,
285
+ hasUniqueCommits: true,
286
+ hasUpstream: true,
287
+ upstream: "origin/feature/second",
288
+ isAccessible: true,
289
+ reasons: ["remote-synced"],
290
+ };
291
+
292
+ onProgress?.(firstStatus);
293
+ await progressGate;
294
+ onProgress?.(secondStatus);
295
+ return [firstStatus, secondStatus];
296
+ },
297
+ );
298
+
299
+ mock.module?.("../../../../worktree.js", () => ({
300
+ listAdditionalWorktrees: mock(async () => []),
301
+ repairWorktrees: mock(async () => ({
302
+ repairedCount: 0,
303
+ failedCount: 0,
304
+ failures: [],
305
+ })),
306
+ removeWorktree: mock(async () => {}),
307
+ getCleanupStatus: getCleanupStatusMock,
308
+ isProtectedBranchName: mock(() => false),
309
+ }));
310
+
311
+ mock.module?.("../../../../git.js", () => ({
312
+ getRepositoryRoot: mock(async () => "/repo"),
313
+ getAllBranches: mock(async () => []),
314
+ getLocalBranches: mock(async () => []),
315
+ getCurrentBranch: mock(async () => "main"),
316
+ deleteBranch: mock(async () => {}),
317
+ }));
318
+
319
+ mock.module?.("../../../../config/index.js", () => ({
320
+ getConfig: mock(async () => ({ defaultBaseBranch: "main" })),
321
+ getLastToolUsageMap: mock(async () => new Map()),
322
+ loadSession: mock(async () => null),
323
+ }));
324
+
325
+ mock.module?.("../../../../config/tools.js", () => ({
326
+ getAllCodingAgents: mock(async () => [
327
+ { id: "codex-cli", displayName: "Codex CLI" },
328
+ ]),
329
+ }));
330
+
331
+ mock.module?.("../../../../config/profiles.js", () => ({
332
+ loadProfiles: mock(async () => ({ profiles: {}, activeProfile: null })),
333
+ createProfile: mock(async () => {}),
334
+ updateProfile: mock(async () => {}),
335
+ deleteProfile: mock(async () => {}),
336
+ setActiveProfile: mock(async () => {}),
337
+ }));
338
+
339
+ const { AppSolid } = await import("../../App.solid.js");
340
+
341
+ const firstBranch = createBranch({
342
+ name: "feature/first",
343
+ label: "feature/first",
344
+ value: "feature/first",
345
+ worktree: { path: "/tmp/first", locked: false, prunable: false },
346
+ });
347
+ const secondBranch = createBranch({
348
+ name: "feature/second",
349
+ label: "feature/second",
350
+ value: "feature/second",
351
+ worktree: { path: "/tmp/second", locked: false, prunable: false },
352
+ });
353
+
354
+ const testSetup = await testRender(
355
+ () => (
356
+ <AppSolid
357
+ branches={[firstBranch, secondBranch]}
358
+ stats={makeStats({ localCount: 2, worktreeCount: 2 })}
359
+ version={null}
360
+ toolStatuses={[]}
361
+ />
362
+ ),
363
+ { width: 80, height: 24 },
364
+ );
365
+ await testSetup.renderOnce();
366
+ await new Promise((resolve) => setTimeout(resolve, 0));
367
+ await testSetup.renderOnce();
368
+
369
+ try {
370
+ let frame = testSetup.captureCharFrame();
371
+ expect(frame).toMatch(/\[ \] w o feature\/first/);
372
+ expect(frame).toMatch(/\[ \] w [-\\|/] feature\/second/);
373
+
374
+ releaseSecond?.();
375
+ await new Promise((resolve) => setTimeout(resolve, 0));
376
+ await testSetup.renderOnce();
377
+
378
+ frame = testSetup.captureCharFrame();
379
+ expect(frame).toMatch(/\[ \] w \* feature\/second/);
380
+ } finally {
381
+ testSetup.renderer.destroy();
382
+ }
383
+ });
384
+ });
385
+
386
+ describe("AppSolid unsafe selection confirm", () => {
387
+ it("shows confirm and cancels selection on Cancel", async () => {
388
+ const getCleanupStatusMock = mock(async () => [
389
+ {
390
+ worktreePath: "/tmp/worktree",
391
+ branch: "feature/unsafe",
392
+ hasUncommittedChanges: false,
393
+ hasUnpushedCommits: true,
394
+ cleanupType: "worktree-and-branch",
395
+ hasRemoteBranch: true,
396
+ hasUniqueCommits: false,
397
+ hasUpstream: true,
398
+ upstream: "origin/feature/unsafe",
399
+ isAccessible: true,
400
+ reasons: ["no-diff-with-base"],
401
+ },
402
+ ]);
403
+
404
+ mock.module?.("../../../../worktree.js", () => ({
405
+ listAdditionalWorktrees: mock(async () => []),
406
+ repairWorktrees: mock(async () => ({
407
+ repairedCount: 0,
408
+ failedCount: 0,
409
+ failures: [],
410
+ })),
411
+ removeWorktree: mock(async () => {}),
412
+ getCleanupStatus: getCleanupStatusMock,
413
+ isProtectedBranchName: mock(() => false),
414
+ }));
415
+
416
+ mock.module?.("../../../../git.js", () => ({
417
+ getRepositoryRoot: mock(async () => "/repo"),
418
+ getAllBranches: mock(async () => []),
419
+ getLocalBranches: mock(async () => []),
420
+ getCurrentBranch: mock(async () => "main"),
421
+ deleteBranch: mock(async () => {}),
422
+ }));
423
+
424
+ mock.module?.("../../../../config/index.js", () => ({
425
+ getConfig: mock(async () => ({ defaultBaseBranch: "main" })),
426
+ getLastToolUsageMap: mock(async () => new Map()),
427
+ loadSession: mock(async () => null),
428
+ }));
429
+
430
+ mock.module?.("../../../../config/tools.js", () => ({
431
+ getAllCodingAgents: mock(async () => [
432
+ { id: "codex-cli", displayName: "Codex CLI" },
433
+ ]),
434
+ }));
435
+
436
+ mock.module?.("../../../../config/profiles.js", () => ({
437
+ loadProfiles: mock(async () => ({ profiles: {}, activeProfile: null })),
438
+ createProfile: mock(async () => {}),
439
+ updateProfile: mock(async () => {}),
440
+ deleteProfile: mock(async () => {}),
441
+ setActiveProfile: mock(async () => {}),
442
+ }));
443
+
444
+ const { AppSolid } = await import("../../App.solid.js");
445
+
446
+ const branch = createBranch({
447
+ name: "feature/unsafe",
448
+ label: "feature/unsafe",
449
+ value: "feature/unsafe",
450
+ });
451
+ const stats = makeStats({ localCount: 1, worktreeCount: 1 });
452
+
453
+ const testSetup = await testRender(
454
+ () => (
455
+ <AppSolid
456
+ branches={[branch]}
457
+ stats={stats}
458
+ version={null}
459
+ toolStatuses={[]}
460
+ />
461
+ ),
462
+ { width: 80, height: 24 },
463
+ );
464
+ await testSetup.renderOnce();
465
+ await new Promise((resolve) => setTimeout(resolve, 0));
466
+ await testSetup.renderOnce();
467
+
468
+ try {
469
+ await testSetup.mockInput.typeText(" ");
470
+ await testSetup.renderOnce();
471
+
472
+ let frame = testSetup.captureCharFrame();
473
+ expect(frame).toContain("Unsafe branch selected. Select anyway?");
474
+ expect(frame).toContain("OK");
475
+ expect(frame).toContain("Cancel");
476
+
477
+ await testSetup.mockInput.typeText("n");
478
+ await testSetup.renderOnce();
479
+
480
+ frame = testSetup.captureCharFrame();
481
+ expect(frame).toContain("[ ] w");
482
+ expect(frame).toContain("feature/unsafe");
483
+ expect(frame).not.toContain("Unsafe branch selected. Select anyway?");
484
+ } finally {
485
+ testSetup.renderer.destroy();
486
+ }
487
+ });
488
+
489
+ it("does not propagate Enter from confirm to branch selection", async () => {
490
+ const getCleanupStatusMock = mock(async () => [
491
+ {
492
+ worktreePath: "/tmp/worktree",
493
+ branch: "feature/unsafe-enter",
494
+ hasUncommittedChanges: false,
495
+ hasUnpushedCommits: true,
496
+ cleanupType: "worktree-and-branch",
497
+ hasRemoteBranch: true,
498
+ hasUniqueCommits: false,
499
+ hasUpstream: true,
500
+ upstream: "origin/feature/unsafe-enter",
501
+ isAccessible: true,
502
+ reasons: ["no-diff-with-base"],
503
+ },
504
+ ]);
505
+
506
+ mock.module?.("../../../../worktree.js", () => ({
507
+ listAdditionalWorktrees: mock(async () => []),
508
+ repairWorktrees: mock(async () => ({
509
+ repairedCount: 0,
510
+ failedCount: 0,
511
+ failures: [],
512
+ })),
513
+ removeWorktree: mock(async () => {}),
514
+ getCleanupStatus: getCleanupStatusMock,
515
+ isProtectedBranchName: mock(() => false),
516
+ }));
517
+
518
+ mock.module?.("../../../../git.js", () => ({
519
+ getRepositoryRoot: mock(async () => "/repo"),
520
+ getAllBranches: mock(async () => []),
521
+ getLocalBranches: mock(async () => []),
522
+ getCurrentBranch: mock(async () => "main"),
523
+ deleteBranch: mock(async () => {}),
524
+ }));
525
+
526
+ mock.module?.("../../../../config/index.js", () => ({
527
+ getConfig: mock(async () => ({ defaultBaseBranch: "main" })),
528
+ getLastToolUsageMap: mock(async () => new Map()),
529
+ loadSession: mock(async () => null),
530
+ }));
531
+
532
+ mock.module?.("../../../../config/tools.js", () => ({
533
+ getAllCodingAgents: mock(async () => [
534
+ { id: "codex-cli", displayName: "Codex CLI" },
535
+ ]),
536
+ }));
537
+
538
+ mock.module?.("../../../../config/profiles.js", () => ({
539
+ loadProfiles: mock(async () => ({ profiles: {}, activeProfile: null })),
540
+ createProfile: mock(async () => {}),
541
+ updateProfile: mock(async () => {}),
542
+ deleteProfile: mock(async () => {}),
543
+ setActiveProfile: mock(async () => {}),
544
+ }));
545
+
546
+ const { AppSolid } = await import("../../App.solid.js");
547
+
548
+ const branch = createBranch({
549
+ name: "feature/unsafe-enter",
550
+ label: "feature/unsafe-enter",
551
+ value: "feature/unsafe-enter",
552
+ });
553
+ const stats = makeStats({ localCount: 1, worktreeCount: 1 });
554
+
555
+ const testSetup = await testRender(
556
+ () => (
557
+ <AppSolid
558
+ branches={[branch]}
559
+ stats={stats}
560
+ version={null}
561
+ toolStatuses={[]}
562
+ />
563
+ ),
564
+ { width: 80, height: 24 },
565
+ );
566
+ await testSetup.renderOnce();
567
+ await new Promise((resolve) => setTimeout(resolve, 0));
568
+ await testSetup.renderOnce();
569
+
570
+ try {
571
+ await testSetup.mockInput.typeText(" ");
572
+ await testSetup.renderOnce();
573
+
574
+ testSetup.mockInput.pressEnter();
575
+ await testSetup.renderOnce();
576
+
577
+ const frame = testSetup.captureCharFrame();
578
+ expect(frame).not.toContain("Unsafe branch selected. Select anyway?");
579
+ expect(frame).toContain("[ ] w");
580
+ expect(frame).not.toContain("Open existing worktree");
581
+ } finally {
582
+ testSetup.renderer.destroy();
583
+ }
584
+ });
585
+
586
+ it("selects unsafe branch on OK", async () => {
587
+ const getCleanupStatusMock = mock(async () => [
588
+ {
589
+ worktreePath: "/tmp/worktree",
590
+ branch: "feature/unsafe-ok",
591
+ hasUncommittedChanges: false,
592
+ hasUnpushedCommits: true,
593
+ cleanupType: "worktree-and-branch",
594
+ hasRemoteBranch: true,
595
+ hasUniqueCommits: false,
596
+ hasUpstream: true,
597
+ upstream: "origin/feature/unsafe-ok",
598
+ isAccessible: true,
599
+ reasons: ["no-diff-with-base"],
600
+ },
601
+ ]);
602
+
603
+ mock.module?.("../../../../worktree.js", () => ({
604
+ listAdditionalWorktrees: mock(async () => []),
605
+ repairWorktrees: mock(async () => ({
606
+ repairedCount: 0,
607
+ failedCount: 0,
608
+ failures: [],
609
+ })),
610
+ removeWorktree: mock(async () => {}),
611
+ getCleanupStatus: getCleanupStatusMock,
612
+ isProtectedBranchName: mock(() => false),
613
+ }));
614
+
615
+ mock.module?.("../../../../git.js", () => ({
616
+ getRepositoryRoot: mock(async () => "/repo"),
617
+ getAllBranches: mock(async () => []),
618
+ getLocalBranches: mock(async () => []),
619
+ getCurrentBranch: mock(async () => "main"),
620
+ deleteBranch: mock(async () => {}),
621
+ }));
622
+
623
+ mock.module?.("../../../../config/index.js", () => ({
624
+ getConfig: mock(async () => ({ defaultBaseBranch: "main" })),
625
+ getLastToolUsageMap: mock(async () => new Map()),
626
+ loadSession: mock(async () => null),
627
+ }));
628
+
629
+ mock.module?.("../../../../config/tools.js", () => ({
630
+ getAllCodingAgents: mock(async () => [
631
+ { id: "codex-cli", displayName: "Codex CLI" },
632
+ ]),
633
+ }));
634
+
635
+ mock.module?.("../../../../config/profiles.js", () => ({
636
+ loadProfiles: mock(async () => ({ profiles: {}, activeProfile: null })),
637
+ createProfile: mock(async () => {}),
638
+ updateProfile: mock(async () => {}),
639
+ deleteProfile: mock(async () => {}),
640
+ setActiveProfile: mock(async () => {}),
641
+ }));
642
+
643
+ const { AppSolid } = await import("../../App.solid.js");
644
+
645
+ const branch = createBranch({
646
+ name: "feature/unsafe-ok",
647
+ label: "feature/unsafe-ok",
648
+ value: "feature/unsafe-ok",
649
+ });
650
+ const stats = makeStats({ localCount: 1, worktreeCount: 1 });
651
+
652
+ const testSetup = await testRender(
653
+ () => (
654
+ <AppSolid
655
+ branches={[branch]}
656
+ stats={stats}
657
+ version={null}
658
+ toolStatuses={[]}
659
+ />
660
+ ),
661
+ { width: 80, height: 24 },
662
+ );
663
+ await testSetup.renderOnce();
664
+ await new Promise((resolve) => setTimeout(resolve, 0));
665
+ await testSetup.renderOnce();
666
+
667
+ try {
668
+ await testSetup.mockInput.typeText(" ");
669
+ await testSetup.renderOnce();
670
+
671
+ await testSetup.mockInput.typeText("y");
672
+ await testSetup.renderOnce();
673
+
674
+ const frame = testSetup.captureCharFrame();
675
+ expect(frame).toContain("[*]");
676
+ expect(frame).toContain("feature/unsafe-ok");
677
+ expect(frame).not.toContain("Unsafe branch selected. Select anyway?");
678
+ } finally {
679
+ testSetup.renderer.destroy();
680
+ }
681
+ });
682
+ });
683
+
684
+ describe("AppSolid selected cleanup targets", () => {
685
+ it("cleans unsafe branch when confirmed and selected", async () => {
686
+ const deleteBranchMock = mock(async () => {});
687
+ const removeWorktreeMock = mock(async () => {});
688
+ const getCleanupStatusMock = mock(async () => [
689
+ {
690
+ worktreePath: "/tmp/worktree",
691
+ branch: "feature/unsafe-clean",
692
+ hasUncommittedChanges: false,
693
+ hasUnpushedCommits: true,
694
+ cleanupType: "worktree-and-branch",
695
+ hasRemoteBranch: true,
696
+ hasUniqueCommits: false,
697
+ hasUpstream: true,
698
+ upstream: "origin/feature/unsafe-clean",
699
+ isAccessible: true,
700
+ reasons: ["no-diff-with-base"],
701
+ },
702
+ ]);
703
+
704
+ mock.module?.("../../../../worktree.js", () => ({
705
+ listAdditionalWorktrees: mock(async () => []),
706
+ repairWorktrees: mock(async () => ({
707
+ repairedCount: 0,
708
+ failedCount: 0,
709
+ failures: [],
710
+ })),
711
+ removeWorktree: removeWorktreeMock,
712
+ getCleanupStatus: getCleanupStatusMock,
713
+ isProtectedBranchName: mock(() => false),
714
+ }));
715
+
716
+ mock.module?.("../../../../git.js", () => ({
717
+ getRepositoryRoot: mock(async () => "/repo"),
718
+ getAllBranches: mock(async () => []),
719
+ getLocalBranches: mock(async () => []),
720
+ getCurrentBranch: mock(async () => "main"),
721
+ deleteBranch: deleteBranchMock,
722
+ }));
723
+
724
+ mock.module?.("../../../../config/index.js", () => ({
725
+ getConfig: mock(async () => ({ defaultBaseBranch: "main" })),
726
+ getLastToolUsageMap: mock(async () => new Map()),
727
+ loadSession: mock(async () => null),
728
+ }));
729
+
730
+ mock.module?.("../../../../config/tools.js", () => ({
731
+ getAllCodingAgents: mock(async () => [
732
+ { id: "codex-cli", displayName: "Codex CLI" },
733
+ ]),
734
+ }));
735
+
736
+ mock.module?.("../../../../config/profiles.js", () => ({
737
+ loadProfiles: mock(async () => ({ profiles: {}, activeProfile: null })),
738
+ createProfile: mock(async () => {}),
739
+ updateProfile: mock(async () => {}),
740
+ deleteProfile: mock(async () => {}),
741
+ setActiveProfile: mock(async () => {}),
742
+ }));
743
+
744
+ const { AppSolid } = await import("../../App.solid.js");
745
+
746
+ const branch = createBranch({
747
+ name: "feature/unsafe-clean",
748
+ label: "feature/unsafe-clean",
749
+ value: "feature/unsafe-clean",
750
+ });
751
+ const stats = makeStats({ localCount: 1, worktreeCount: 1 });
752
+
753
+ const testSetup = await testRender(
754
+ () => (
755
+ <AppSolid
756
+ branches={[branch]}
757
+ stats={stats}
758
+ version={null}
759
+ toolStatuses={[]}
760
+ />
761
+ ),
762
+ { width: 80, height: 24 },
763
+ );
764
+ await testSetup.renderOnce();
765
+ await new Promise((resolve) => setTimeout(resolve, 0));
766
+ await testSetup.renderOnce();
767
+
768
+ try {
769
+ await testSetup.mockInput.typeText(" ");
770
+ await testSetup.renderOnce();
771
+
772
+ await testSetup.mockInput.typeText("y");
773
+ await testSetup.renderOnce();
774
+
775
+ await testSetup.mockInput.typeText("c");
776
+ await testSetup.renderOnce();
777
+ await new Promise((resolve) => setTimeout(resolve, 0));
778
+
779
+ expect(removeWorktreeMock).toHaveBeenCalledWith("/tmp/worktree", false);
780
+ expect(deleteBranchMock).toHaveBeenCalledWith(
781
+ "feature/unsafe-clean",
782
+ true,
783
+ );
784
+ } finally {
785
+ testSetup.renderer.destroy();
786
+ }
787
+ });
788
+
789
+ it("includes protected branch when selected", async () => {
790
+ const deleteBranchMock = mock(async () => {});
791
+ const removeWorktreeMock = mock(async () => {});
792
+ const getCleanupStatusMock = mock(async () => [
793
+ {
794
+ worktreePath: "/tmp/worktree",
795
+ branch: "develop",
796
+ hasUncommittedChanges: false,
797
+ hasUnpushedCommits: false,
798
+ cleanupType: "worktree-and-branch",
799
+ hasRemoteBranch: true,
800
+ hasUniqueCommits: false,
801
+ hasUpstream: true,
802
+ upstream: "origin/develop",
803
+ isAccessible: true,
804
+ reasons: ["no-diff-with-base"],
805
+ },
806
+ ]);
807
+
808
+ mock.module?.("../../../../worktree.js", () => ({
809
+ listAdditionalWorktrees: mock(async () => []),
810
+ repairWorktrees: mock(async () => ({
811
+ repairedCount: 0,
812
+ failedCount: 0,
813
+ failures: [],
814
+ })),
815
+ removeWorktree: removeWorktreeMock,
816
+ getCleanupStatus: getCleanupStatusMock,
817
+ isProtectedBranchName: mock(() => true),
818
+ }));
819
+
820
+ mock.module?.("../../../../git.js", () => ({
821
+ getRepositoryRoot: mock(async () => "/repo"),
822
+ getAllBranches: mock(async () => []),
823
+ getLocalBranches: mock(async () => []),
824
+ getCurrentBranch: mock(async () => "main"),
825
+ deleteBranch: deleteBranchMock,
826
+ }));
827
+
828
+ mock.module?.("../../../../config/index.js", () => ({
829
+ getConfig: mock(async () => ({ defaultBaseBranch: "main" })),
830
+ getLastToolUsageMap: mock(async () => new Map()),
831
+ loadSession: mock(async () => null),
832
+ }));
833
+
834
+ mock.module?.("../../../../config/tools.js", () => ({
835
+ getAllCodingAgents: mock(async () => [
836
+ { id: "codex-cli", displayName: "Codex CLI" },
837
+ ]),
838
+ }));
839
+
840
+ mock.module?.("../../../../config/profiles.js", () => ({
841
+ loadProfiles: mock(async () => ({ profiles: {}, activeProfile: null })),
842
+ createProfile: mock(async () => {}),
843
+ updateProfile: mock(async () => {}),
844
+ deleteProfile: mock(async () => {}),
845
+ setActiveProfile: mock(async () => {}),
846
+ }));
847
+
848
+ const { AppSolid } = await import("../../App.solid.js");
849
+
850
+ const branch = createBranch({
851
+ name: "develop",
852
+ label: "develop",
853
+ value: "develop",
854
+ });
855
+ const stats = makeStats({ localCount: 1, worktreeCount: 1 });
856
+
857
+ const testSetup = await testRender(
858
+ () => (
859
+ <AppSolid
860
+ branches={[branch]}
861
+ stats={stats}
862
+ version={null}
863
+ toolStatuses={[]}
864
+ />
865
+ ),
866
+ { width: 80, height: 24 },
867
+ );
868
+ await testSetup.renderOnce();
869
+ await new Promise((resolve) => setTimeout(resolve, 0));
870
+ await testSetup.renderOnce();
871
+
872
+ try {
873
+ await testSetup.mockInput.typeText(" ");
874
+ await testSetup.renderOnce();
875
+
876
+ await testSetup.mockInput.typeText("c");
877
+ await testSetup.renderOnce();
878
+ await new Promise((resolve) => setTimeout(resolve, 0));
879
+
880
+ expect(deleteBranchMock).toHaveBeenCalledWith("develop", true);
881
+ expect(removeWorktreeMock).toHaveBeenCalledWith("/tmp/worktree", false);
882
+ } finally {
883
+ testSetup.renderer.destroy();
884
+ }
885
+ });
886
+
887
+ it("excludes current branch even when selected", async () => {
888
+ const deleteBranchMock = mock(async () => {});
889
+ const removeWorktreeMock = mock(async () => {});
890
+ const getCleanupStatusMock = mock(async () => [
891
+ {
892
+ worktreePath: "/tmp/worktree",
893
+ branch: "main",
894
+ hasUncommittedChanges: false,
895
+ hasUnpushedCommits: false,
896
+ cleanupType: "worktree-and-branch",
897
+ hasRemoteBranch: true,
898
+ hasUniqueCommits: false,
899
+ hasUpstream: true,
900
+ upstream: "origin/main",
901
+ isAccessible: true,
902
+ reasons: ["no-diff-with-base"],
903
+ },
904
+ ]);
905
+
906
+ mock.module?.("../../../../worktree.js", () => ({
907
+ listAdditionalWorktrees: mock(async () => []),
908
+ repairWorktrees: mock(async () => ({
909
+ repairedCount: 0,
910
+ failedCount: 0,
911
+ failures: [],
912
+ })),
913
+ removeWorktree: removeWorktreeMock,
914
+ getCleanupStatus: getCleanupStatusMock,
915
+ isProtectedBranchName: mock(() => true),
916
+ }));
917
+
918
+ mock.module?.("../../../../git.js", () => ({
919
+ getRepositoryRoot: mock(async () => "/repo"),
920
+ getAllBranches: mock(async () => []),
921
+ getLocalBranches: mock(async () => []),
922
+ getCurrentBranch: mock(async () => "main"),
923
+ deleteBranch: deleteBranchMock,
924
+ }));
925
+
926
+ mock.module?.("../../../../config/index.js", () => ({
927
+ getConfig: mock(async () => ({ defaultBaseBranch: "main" })),
928
+ getLastToolUsageMap: mock(async () => new Map()),
929
+ loadSession: mock(async () => null),
930
+ }));
931
+
932
+ mock.module?.("../../../../config/tools.js", () => ({
933
+ getAllCodingAgents: mock(async () => [
934
+ { id: "codex-cli", displayName: "Codex CLI" },
935
+ ]),
936
+ }));
937
+
938
+ mock.module?.("../../../../config/profiles.js", () => ({
939
+ loadProfiles: mock(async () => ({ profiles: {}, activeProfile: null })),
940
+ createProfile: mock(async () => {}),
941
+ updateProfile: mock(async () => {}),
942
+ deleteProfile: mock(async () => {}),
943
+ setActiveProfile: mock(async () => {}),
944
+ }));
945
+
946
+ const { AppSolid } = await import("../../App.solid.js");
947
+
948
+ const branch = createBranch({
949
+ name: "main",
950
+ label: "main",
951
+ value: "main",
952
+ isCurrent: true,
953
+ });
954
+ const stats = makeStats({ localCount: 1, worktreeCount: 1 });
955
+
956
+ const testSetup = await testRender(
957
+ () => (
958
+ <AppSolid
959
+ branches={[branch]}
960
+ stats={stats}
961
+ version={null}
962
+ toolStatuses={[]}
963
+ />
964
+ ),
965
+ { width: 80, height: 24 },
966
+ );
967
+ await testSetup.renderOnce();
968
+ await new Promise((resolve) => setTimeout(resolve, 0));
969
+ await testSetup.renderOnce();
970
+
971
+ try {
972
+ await testSetup.mockInput.typeText(" ");
973
+ await testSetup.renderOnce();
974
+
975
+ await testSetup.mockInput.typeText("c");
976
+ await testSetup.renderOnce();
977
+ await new Promise((resolve) => setTimeout(resolve, 0));
978
+
979
+ expect(deleteBranchMock).not.toHaveBeenCalled();
980
+ expect(removeWorktreeMock).not.toHaveBeenCalled();
981
+ } finally {
982
+ testSetup.renderer.destroy();
983
+ }
984
+ });
985
+
986
+ it("repairs selected branch even when worktree is accessible", async () => {
987
+ const repairWorktreesMock = mock(async () => ({
988
+ repairedCount: 1,
989
+ failedCount: 0,
990
+ failures: [],
991
+ }));
992
+ const getCleanupStatusMock = mock(async () => [
993
+ {
994
+ worktreePath: "/tmp/worktree",
995
+ branch: "feature/repair-target",
996
+ hasUncommittedChanges: false,
997
+ hasUnpushedCommits: false,
998
+ cleanupType: "worktree-and-branch",
999
+ hasRemoteBranch: true,
1000
+ hasUniqueCommits: false,
1001
+ hasUpstream: true,
1002
+ upstream: "origin/feature/repair-target",
1003
+ isAccessible: true,
1004
+ reasons: ["no-diff-with-base"],
1005
+ },
1006
+ ]);
1007
+
1008
+ mock.module?.("../../../../worktree.js", () => ({
1009
+ listAdditionalWorktrees: mock(async () => []),
1010
+ repairWorktrees: repairWorktreesMock,
1011
+ removeWorktree: mock(async () => {}),
1012
+ getCleanupStatus: getCleanupStatusMock,
1013
+ isProtectedBranchName: mock(() => false),
1014
+ }));
1015
+
1016
+ mock.module?.("../../../../git.js", () => ({
1017
+ getRepositoryRoot: mock(async () => "/repo"),
1018
+ getAllBranches: mock(async () => []),
1019
+ getLocalBranches: mock(async () => []),
1020
+ getCurrentBranch: mock(async () => "main"),
1021
+ deleteBranch: mock(async () => {}),
1022
+ }));
1023
+
1024
+ mock.module?.("../../../../config/index.js", () => ({
1025
+ getConfig: mock(async () => ({ defaultBaseBranch: "main" })),
1026
+ getLastToolUsageMap: mock(async () => new Map()),
1027
+ loadSession: mock(async () => null),
1028
+ }));
1029
+
1030
+ mock.module?.("../../../../config/tools.js", () => ({
1031
+ getAllCodingAgents: mock(async () => [
1032
+ { id: "codex-cli", displayName: "Codex CLI" },
1033
+ ]),
1034
+ }));
1035
+
1036
+ mock.module?.("../../../../config/profiles.js", () => ({
1037
+ loadProfiles: mock(async () => ({ profiles: {}, activeProfile: null })),
1038
+ createProfile: mock(async () => {}),
1039
+ updateProfile: mock(async () => {}),
1040
+ deleteProfile: mock(async () => {}),
1041
+ setActiveProfile: mock(async () => {}),
1042
+ }));
1043
+
1044
+ const { AppSolid } = await import("../../App.solid.js");
1045
+
1046
+ const branch = createBranch({
1047
+ name: "feature/repair-target",
1048
+ label: "feature/repair-target",
1049
+ value: "feature/repair-target",
1050
+ worktreeStatus: "active",
1051
+ });
1052
+ const stats = makeStats({ localCount: 1, worktreeCount: 1 });
1053
+
1054
+ const testSetup = await testRender(
1055
+ () => (
1056
+ <AppSolid
1057
+ branches={[branch]}
1058
+ stats={stats}
1059
+ version={null}
1060
+ toolStatuses={[]}
1061
+ />
1062
+ ),
1063
+ { width: 80, height: 24 },
1064
+ );
1065
+ await testSetup.renderOnce();
1066
+ await new Promise((resolve) => setTimeout(resolve, 0));
1067
+ await testSetup.renderOnce();
1068
+
1069
+ try {
1070
+ await testSetup.mockInput.typeText(" ");
1071
+ await testSetup.renderOnce();
1072
+
1073
+ await testSetup.mockInput.typeText("x");
1074
+ await testSetup.renderOnce();
1075
+ await new Promise((resolve) => setTimeout(resolve, 0));
1076
+
1077
+ expect(repairWorktreesMock).toHaveBeenCalledWith([
1078
+ "feature/repair-target",
1079
+ ]);
1080
+ } finally {
1081
+ testSetup.renderer.destroy();
1082
+ }
1083
+ });
255
1084
  });