@akiojin/gwt 4.7.0 → 4.8.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 (44) hide show
  1. package/dist/claude.js +1 -1
  2. package/dist/claude.js.map +1 -1
  3. package/dist/cli/ui/components/App.js +1 -1
  4. package/dist/cli/ui/components/App.js.map +1 -1
  5. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  6. package/dist/cli/ui/components/screens/BranchListScreen.js +5 -6
  7. package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
  8. package/dist/cli/ui/components/screens/LogDatePickerScreen.js +1 -1
  9. package/dist/cli/ui/components/screens/LogDatePickerScreen.js.map +1 -1
  10. package/dist/cli/ui/components/screens/LogDetailScreen.js +1 -1
  11. package/dist/cli/ui/components/screens/LogDetailScreen.js.map +1 -1
  12. package/dist/cli/ui/components/screens/LogListScreen.js +1 -1
  13. package/dist/cli/ui/components/screens/LogListScreen.js.map +1 -1
  14. package/dist/cli/ui/utils/branchFormatter.d.ts +5 -0
  15. package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
  16. package/dist/cli/ui/utils/branchFormatter.js +18 -5
  17. package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
  18. package/dist/codex.d.ts.map +1 -1
  19. package/dist/codex.js +0 -1
  20. package/dist/codex.js.map +1 -1
  21. package/dist/gemini.d.ts.map +1 -1
  22. package/dist/gemini.js +1 -2
  23. package/dist/gemini.js.map +1 -1
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +106 -90
  26. package/dist/index.js.map +1 -1
  27. package/package.json +2 -2
  28. package/src/claude.ts +1 -1
  29. package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +1 -1
  30. package/src/cli/ui/__tests__/components/App.test.tsx +65 -3
  31. package/src/cli/ui/__tests__/components/screens/LogDetailScreen.test.tsx +1 -1
  32. package/src/cli/ui/__tests__/components/screens/LogListScreen.test.tsx +1 -1
  33. package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +83 -22
  34. package/src/cli/ui/__tests__/integration/navigation.test.tsx +57 -37
  35. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +105 -0
  36. package/src/cli/ui/components/App.tsx +1 -1
  37. package/src/cli/ui/components/screens/BranchListScreen.tsx +5 -5
  38. package/src/cli/ui/components/screens/LogDatePickerScreen.tsx +1 -1
  39. package/src/cli/ui/components/screens/LogDetailScreen.tsx +1 -1
  40. package/src/cli/ui/components/screens/LogListScreen.tsx +1 -1
  41. package/src/cli/ui/utils/branchFormatter.ts +19 -5
  42. package/src/codex.ts +0 -1
  43. package/src/gemini.ts +1 -2
  44. package/src/index.ts +148 -133
package/src/index.ts CHANGED
@@ -13,7 +13,6 @@ import {
13
13
  hasUnpushedCommits,
14
14
  getUncommittedChangesCount,
15
15
  getUnpushedCommitsCount,
16
- pushBranchToRemote,
17
16
  GitError,
18
17
  } from "./git.js";
19
18
  import { launchClaudeCode } from "./claude.js";
@@ -57,7 +56,7 @@ import {
57
56
  DependencyInstallError,
58
57
  type DependencyInstallResult,
59
58
  } from "./services/dependency-installer.js";
60
- import { confirmYesNo, waitForEnter } from "./utils/prompt.js";
59
+ import { waitForEnter } from "./utils/prompt.js";
61
60
 
62
61
  const ERROR_PROMPT = chalk.yellow(
63
62
  "Review the error details, then press Enter to continue.",
@@ -435,7 +434,7 @@ export async function handleAIToolWorkflow(
435
434
  switch (dependencyStatus.reason) {
436
435
  case "missing-lockfile":
437
436
  warningMessage =
438
- "Skipping automatic install because no lockfiles (bun.lock / pnpm-lock.yaml / package-lock.json) or package.json were found. Run the appropriate package-manager install command manually if needed.";
437
+ "Skipping automatic install because no lockfiles (bun.lock / pnpm-lock.yaml / package-lock.json) or package.json could be found. Run the appropriate package-manager install command manually if needed.";
439
438
  break;
440
439
  case "missing-binary":
441
440
  warningMessage = `Package manager '${dependencyStatus.manager ?? "unknown"}' is not available in this environment; skipping automatic install.`;
@@ -554,23 +553,21 @@ export async function handleAIToolWorkflow(
554
553
  throw new Error(`Tool not found: ${tool}`);
555
554
  }
556
555
 
557
- // Save selection immediately so "last tool" is reflected even if the tool
558
- // is interrupted or killed mid-run (e.g., Ctrl+C).
559
- await saveSession(
560
- {
561
- lastWorktreePath: worktreePath,
562
- lastBranch: branch,
563
- lastUsedTool: tool,
564
- toolLabel: toolConfig.displayName ?? tool,
565
- mode,
566
- model: normalizedModel ?? null,
567
- reasoningLevel: inferenceLevel ?? null,
568
- skipPermissions: skipPermissions ?? null,
569
- timestamp: Date.now(),
570
- repositoryRoot: repoRoot,
571
- },
572
- { skipHistory: true },
573
- );
556
+ // Save selection immediately (including history) so "last tool" is reflected
557
+ // even if the tool is interrupted or killed mid-run (e.g., Ctrl+C).
558
+ // FR-042: Record timestamp to session history immediately on tool start.
559
+ await saveSession({
560
+ lastWorktreePath: worktreePath,
561
+ lastBranch: branch,
562
+ lastUsedTool: tool,
563
+ toolLabel: toolConfig.displayName ?? tool,
564
+ mode,
565
+ model: normalizedModel ?? null,
566
+ reasoningLevel: inferenceLevel ?? null,
567
+ skipPermissions: skipPermissions ?? null,
568
+ timestamp: Date.now(),
569
+ repositoryRoot: repoRoot,
570
+ });
574
571
 
575
572
  // Lookup saved session ID for Continue (auto attach)
576
573
  let resumeSessionId: string | null =
@@ -600,96 +597,127 @@ export async function handleAIToolWorkflow(
600
597
 
601
598
  const launchStartedAt = Date.now();
602
599
 
600
+ // FR-043: Start periodic timestamp update timer (30 seconds interval)
601
+ // This ensures the latest activity time is updated even if the tool is force-killed
602
+ const SESSION_UPDATE_INTERVAL_MS = 30_000;
603
+ const updateTimer = setInterval(async () => {
604
+ try {
605
+ await saveSession(
606
+ {
607
+ lastWorktreePath: worktreePath,
608
+ lastBranch: branch,
609
+ lastUsedTool: tool,
610
+ toolLabel: toolConfig.displayName ?? tool,
611
+ mode,
612
+ model: normalizedModel ?? null,
613
+ reasoningLevel: inferenceLevel ?? null,
614
+ skipPermissions: skipPermissions ?? null,
615
+ timestamp: Date.now(),
616
+ repositoryRoot: repoRoot,
617
+ },
618
+ { skipHistory: true }, // Don't add to history, just update timestamp
619
+ );
620
+ } catch {
621
+ // Ignore errors during periodic update
622
+ }
623
+ }, SESSION_UPDATE_INTERVAL_MS);
624
+
603
625
  // Launch selected AI tool
604
626
  // Builtin tools use their dedicated launch functions
605
627
  // Custom tools use the generic launchCustomAITool function
606
628
  let launchResult: { sessionId?: string | null } | void;
607
- if (tool === "claude-code") {
608
- const launchOptions: {
609
- mode?: "normal" | "continue" | "resume";
610
- skipPermissions?: boolean;
611
- envOverrides?: Record<string, string>;
612
- model?: string;
613
- sessionId?: string | null;
614
- chrome?: boolean;
615
- } = {
616
- mode:
617
- mode === "resume"
618
- ? "resume"
619
- : mode === "continue"
620
- ? "continue"
621
- : "normal",
622
- skipPermissions,
623
- envOverrides: sharedEnv,
624
- sessionId: resumeSessionId,
625
- chrome: true,
626
- };
627
- if (normalizedModel) {
628
- launchOptions.model = normalizedModel;
629
- }
630
- launchResult = await launchClaudeCode(worktreePath, launchOptions);
631
- } else if (tool === "codex-cli") {
632
- const launchOptions: {
633
- mode?: "normal" | "continue" | "resume";
634
- bypassApprovals?: boolean;
635
- envOverrides?: Record<string, string>;
636
- model?: string;
637
- reasoningEffort?: CodexReasoningEffort;
638
- sessionId?: string | null;
639
- } = {
640
- mode:
641
- mode === "resume"
642
- ? "resume"
643
- : mode === "continue"
644
- ? "continue"
645
- : "normal",
646
- bypassApprovals: skipPermissions,
647
- envOverrides: sharedEnv,
648
- sessionId: resumeSessionId,
649
- };
650
- if (normalizedModel) {
651
- launchOptions.model = normalizedModel;
652
- }
653
- if (inferenceLevel) {
654
- launchOptions.reasoningEffort = inferenceLevel as CodexReasoningEffort;
655
- }
656
- launchResult = await launchCodexCLI(worktreePath, launchOptions);
657
- } else if (tool === "gemini-cli") {
658
- const launchOptions: {
659
- mode?: "normal" | "continue" | "resume";
660
- skipPermissions?: boolean;
661
- envOverrides?: Record<string, string>;
662
- model?: string;
663
- sessionId?: string | null;
664
- } = {
665
- mode:
666
- mode === "resume"
667
- ? "resume"
668
- : mode === "continue"
669
- ? "continue"
670
- : "normal",
671
- skipPermissions,
672
- envOverrides: sharedEnv,
673
- sessionId: resumeSessionId,
674
- };
675
- if (normalizedModel) {
676
- launchOptions.model = normalizedModel;
629
+ try {
630
+ if (tool === "claude-code") {
631
+ const launchOptions: {
632
+ mode?: "normal" | "continue" | "resume";
633
+ skipPermissions?: boolean;
634
+ envOverrides?: Record<string, string>;
635
+ model?: string;
636
+ sessionId?: string | null;
637
+ chrome?: boolean;
638
+ } = {
639
+ mode:
640
+ mode === "resume"
641
+ ? "resume"
642
+ : mode === "continue"
643
+ ? "continue"
644
+ : "normal",
645
+ skipPermissions,
646
+ envOverrides: sharedEnv,
647
+ sessionId: resumeSessionId,
648
+ chrome: true,
649
+ };
650
+ if (normalizedModel) {
651
+ launchOptions.model = normalizedModel;
652
+ }
653
+ launchResult = await launchClaudeCode(worktreePath, launchOptions);
654
+ } else if (tool === "codex-cli") {
655
+ const launchOptions: {
656
+ mode?: "normal" | "continue" | "resume";
657
+ bypassApprovals?: boolean;
658
+ envOverrides?: Record<string, string>;
659
+ model?: string;
660
+ reasoningEffort?: CodexReasoningEffort;
661
+ sessionId?: string | null;
662
+ } = {
663
+ mode:
664
+ mode === "resume"
665
+ ? "resume"
666
+ : mode === "continue"
667
+ ? "continue"
668
+ : "normal",
669
+ bypassApprovals: skipPermissions,
670
+ envOverrides: sharedEnv,
671
+ sessionId: resumeSessionId,
672
+ };
673
+ if (normalizedModel) {
674
+ launchOptions.model = normalizedModel;
675
+ }
676
+ if (inferenceLevel) {
677
+ launchOptions.reasoningEffort =
678
+ inferenceLevel as CodexReasoningEffort;
679
+ }
680
+ launchResult = await launchCodexCLI(worktreePath, launchOptions);
681
+ } else if (tool === "gemini-cli") {
682
+ const launchOptions: {
683
+ mode?: "normal" | "continue" | "resume";
684
+ skipPermissions?: boolean;
685
+ envOverrides?: Record<string, string>;
686
+ model?: string;
687
+ sessionId?: string | null;
688
+ } = {
689
+ mode:
690
+ mode === "resume"
691
+ ? "resume"
692
+ : mode === "continue"
693
+ ? "continue"
694
+ : "normal",
695
+ skipPermissions,
696
+ envOverrides: sharedEnv,
697
+ sessionId: resumeSessionId,
698
+ };
699
+ if (normalizedModel) {
700
+ launchOptions.model = normalizedModel;
701
+ }
702
+ launchResult = await launchGeminiCLI(worktreePath, launchOptions);
703
+ } else {
704
+ // Custom tool
705
+ printInfo(`Launching custom tool: ${toolConfig.displayName}`);
706
+ launchResult = await launchCustomAITool(toolConfig, {
707
+ mode:
708
+ mode === "resume"
709
+ ? "resume"
710
+ : mode === "continue"
711
+ ? "continue"
712
+ : "normal",
713
+ skipPermissions,
714
+ cwd: worktreePath,
715
+ sharedEnv,
716
+ });
677
717
  }
678
- launchResult = await launchGeminiCLI(worktreePath, launchOptions);
679
- } else {
680
- // Custom tool
681
- printInfo(`Launching custom tool: ${toolConfig.displayName}`);
682
- launchResult = await launchCustomAITool(toolConfig, {
683
- mode:
684
- mode === "resume"
685
- ? "resume"
686
- : mode === "continue"
687
- ? "continue"
688
- : "normal",
689
- skipPermissions,
690
- cwd: worktreePath,
691
- sharedEnv,
692
- });
718
+ } finally {
719
+ // FR-043: Clear the periodic timestamp update timer
720
+ clearInterval(updateTimer);
693
721
  }
694
722
 
695
723
  // Persist session with captured session ID (if any)
@@ -768,19 +796,21 @@ export async function handleAIToolWorkflow(
768
796
  lastSessionId: finalSessionId,
769
797
  });
770
798
 
771
- let uncommittedExists = false;
772
799
  try {
773
800
  const [hasUncommitted, hasUnpushed] = await Promise.all([
774
801
  hasUncommittedChanges(worktreePath),
775
802
  hasUnpushedCommits(worktreePath, branch),
776
803
  ]);
777
- uncommittedExists = hasUncommitted;
778
804
 
779
805
  if (hasUncommitted) {
780
806
  const uncommittedCount = await getUncommittedChangesCount(worktreePath);
781
807
  const countLabel =
782
- uncommittedCount > 0 ? ` (${uncommittedCount}件)` : "";
783
- printWarning(`未コミットの変更があります${countLabel}。`);
808
+ uncommittedCount > 0
809
+ ? ` (${uncommittedCount} ${
810
+ uncommittedCount === 1 ? "change" : "changes"
811
+ })`
812
+ : "";
813
+ printWarning(`Uncommitted changes detected${countLabel}.`);
784
814
  }
785
815
 
786
816
  if (hasUnpushed) {
@@ -788,36 +818,21 @@ export async function handleAIToolWorkflow(
788
818
  worktreePath,
789
819
  branch,
790
820
  );
791
- const countLabel = unpushedCount > 0 ? ` (${unpushedCount}件)` : "";
792
- const shouldPush = await confirmYesNo(
793
- `未プッシュのコミットがあります${countLabel}。プッシュしますか?`,
794
- { defaultValue: false },
795
- );
796
- if (shouldPush) {
797
- printInfo(`Pushing origin/${branch}...`);
798
- try {
799
- await pushBranchToRemote(worktreePath, branch);
800
- printInfo(`Push completed for ${branch}.`);
801
- } catch (error) {
802
- const details =
803
- error instanceof Error ? error.message : String(error);
804
- printWarning(`Push failed for ${branch}: ${details}`);
805
- }
806
- }
821
+ const countLabel =
822
+ unpushedCount > 0
823
+ ? ` (${unpushedCount} ${
824
+ unpushedCount === 1 ? "commit" : "commits"
825
+ })`
826
+ : "";
827
+ printWarning(`Unpushed commits detected${countLabel}.`);
807
828
  }
808
829
  } catch (error) {
809
830
  const details = error instanceof Error ? error.message : String(error);
810
831
  printWarning(`Failed to check git status after session: ${details}`);
811
832
  }
812
833
 
813
- if (uncommittedExists) {
814
- await waitForEnter("Press Enter to return to the main menu...");
815
- } else {
816
- // Small buffer before returning to branch list to avoid abrupt screen swap
817
- await new Promise((resolve) =>
818
- setTimeout(resolve, POST_SESSION_DELAY_MS),
819
- );
820
- }
834
+ // Small buffer before returning to branch list to avoid abrupt screen swap
835
+ await new Promise((resolve) => setTimeout(resolve, POST_SESSION_DELAY_MS));
821
836
  printInfo("Session completed successfully. Returning to main menu...");
822
837
  return;
823
838
  } catch (error) {