@akiojin/gwt 4.7.0 → 4.9.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 (69) hide show
  1. package/README.ja.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/claude.js +1 -1
  4. package/dist/claude.js.map +1 -1
  5. package/dist/cli/ui/components/App.js +1 -1
  6. package/dist/cli/ui/components/App.js.map +1 -1
  7. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  8. package/dist/cli/ui/components/screens/BranchListScreen.js +8 -7
  9. package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
  10. package/dist/cli/ui/components/screens/LogDatePickerScreen.js +1 -1
  11. package/dist/cli/ui/components/screens/LogDatePickerScreen.js.map +1 -1
  12. package/dist/cli/ui/components/screens/LogDetailScreen.js +1 -1
  13. package/dist/cli/ui/components/screens/LogDetailScreen.js.map +1 -1
  14. package/dist/cli/ui/components/screens/LogListScreen.js +1 -1
  15. package/dist/cli/ui/components/screens/LogListScreen.js.map +1 -1
  16. package/dist/cli/ui/utils/branchFormatter.d.ts +5 -0
  17. package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
  18. package/dist/cli/ui/utils/branchFormatter.js +18 -5
  19. package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
  20. package/dist/codex.d.ts.map +1 -1
  21. package/dist/codex.js +0 -1
  22. package/dist/codex.js.map +1 -1
  23. package/dist/config/index.d.ts.map +1 -1
  24. package/dist/config/index.js +3 -7
  25. package/dist/config/index.js.map +1 -1
  26. package/dist/config/profiles.d.ts +2 -2
  27. package/dist/config/profiles.d.ts.map +1 -1
  28. package/dist/config/profiles.js +4 -7
  29. package/dist/config/profiles.js.map +1 -1
  30. package/dist/config/tools.d.ts +1 -1
  31. package/dist/config/tools.d.ts.map +1 -1
  32. package/dist/config/tools.js +3 -43
  33. package/dist/config/tools.js.map +1 -1
  34. package/dist/gemini.d.ts.map +1 -1
  35. package/dist/gemini.js +1 -2
  36. package/dist/gemini.js.map +1 -1
  37. package/dist/index.d.ts.map +1 -1
  38. package/dist/index.js +106 -90
  39. package/dist/index.js.map +1 -1
  40. package/dist/utils/command.d.ts +11 -0
  41. package/dist/utils/command.d.ts.map +1 -1
  42. package/dist/utils/command.js +33 -0
  43. package/dist/utils/command.js.map +1 -1
  44. package/dist/web/client/src/pages/ConfigPage.js +1 -1
  45. package/dist/web/client/src/pages/ConfigPage.js.map +1 -1
  46. package/package.json +2 -2
  47. package/src/claude.ts +1 -1
  48. package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +1 -1
  49. package/src/cli/ui/__tests__/components/App.test.tsx +65 -3
  50. package/src/cli/ui/__tests__/components/screens/LogDetailScreen.test.tsx +1 -1
  51. package/src/cli/ui/__tests__/components/screens/LogListScreen.test.tsx +1 -1
  52. package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +83 -22
  53. package/src/cli/ui/__tests__/integration/navigation.test.tsx +57 -37
  54. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +105 -0
  55. package/src/cli/ui/components/App.tsx +1 -1
  56. package/src/cli/ui/components/screens/BranchListScreen.tsx +9 -7
  57. package/src/cli/ui/components/screens/LogDatePickerScreen.tsx +1 -1
  58. package/src/cli/ui/components/screens/LogDetailScreen.tsx +1 -1
  59. package/src/cli/ui/components/screens/LogListScreen.tsx +1 -1
  60. package/src/cli/ui/utils/branchFormatter.ts +19 -5
  61. package/src/codex.ts +0 -1
  62. package/src/config/index.ts +3 -7
  63. package/src/config/profiles.ts +4 -7
  64. package/src/config/tools.ts +3 -56
  65. package/src/gemini.ts +1 -2
  66. package/src/index.ts +148 -133
  67. package/src/utils/command.ts +37 -0
  68. package/src/web/client/src/pages/ConfigPage.tsx +2 -2
  69. package/src/index.ts.backup +0 -1543
@@ -125,6 +125,19 @@ function buildLastToolUsageLabel(
125
125
  return parts.join(" | ");
126
126
  }
127
127
 
128
+ /**
129
+ * Calculate the latest activity timestamp for a branch.
130
+ * Returns the maximum of git commit timestamp and tool usage timestamp (in seconds).
131
+ */
132
+ export function getLatestActivityTimestamp(branch: BranchInfo): number {
133
+ const gitTimestampSec = branch.latestCommitTimestamp ?? 0;
134
+ // lastToolUsage.timestamp is in milliseconds, convert to seconds
135
+ const toolTimestampSec = branch.lastToolUsage?.timestamp
136
+ ? Math.floor(branch.lastToolUsage.timestamp / 1000)
137
+ : 0;
138
+ return Math.max(gitTimestampSec, toolTimestampSec);
139
+ }
140
+
128
141
  /**
129
142
  * Converts BranchInfo to BranchItem with display properties
130
143
  */
@@ -357,11 +370,12 @@ function sortBranches(
357
370
  if (aHasWorktree && !bHasWorktree) return -1;
358
371
  if (!aHasWorktree && bHasWorktree) return 1;
359
372
 
360
- // 5. Prioritize most recent commit within same worktree status
361
- const aCommit = a.latestCommitTimestamp ?? 0;
362
- const bCommit = b.latestCommitTimestamp ?? 0;
363
- if (aCommit !== bCommit) {
364
- return bCommit - aCommit;
373
+ // 5. Prioritize most recent activity within same worktree status
374
+ // (max of git commit timestamp and tool usage timestamp)
375
+ const aLatest = getLatestActivityTimestamp(a);
376
+ const bLatest = getLatestActivityTimestamp(b);
377
+ if (aLatest !== bLatest) {
378
+ return bLatest - aLatest;
365
379
  }
366
380
 
367
381
  // 6. Local branches are prioritized over remote-only
package/src/codex.ts CHANGED
@@ -196,7 +196,6 @@ export async function launchCodexCLI(
196
196
 
197
197
  const child = execa("bunx", [CODEX_CLI_PACKAGE, ...args], {
198
198
  cwd: worktreePath,
199
- shell: true,
200
199
  stdin: childStdio.stdin,
201
200
  stdout: childStdio.stdout,
202
201
  stderr: childStdio.stderr,
@@ -52,11 +52,8 @@ const DEFAULT_CONFIG: AppConfig = {
52
52
  export async function loadConfig(): Promise<AppConfig> {
53
53
  const configPaths = [
54
54
  path.join(process.cwd(), ".gwt.json"),
55
- path.join(process.cwd(), ".claude-worktree.json"), // 後方互換性
56
55
  path.join(homedir(), ".config", "gwt", "config.json"),
57
- path.join(homedir(), ".config", "claude-worktree", "config.json"), // 後方互換性
58
56
  path.join(homedir(), ".gwt.json"),
59
- path.join(homedir(), ".claude-worktree.json"), // 後方互換性
60
57
  ];
61
58
 
62
59
  for (const configPath of configPaths) {
@@ -79,10 +76,9 @@ export async function loadConfig(): Promise<AppConfig> {
79
76
  return {
80
77
  ...DEFAULT_CONFIG,
81
78
  defaultBaseBranch:
82
- process.env.CLAUDE_WORKTREE_BASE_BRANCH ||
83
- DEFAULT_CONFIG.defaultBaseBranch,
84
- skipPermissions: process.env.CLAUDE_WORKTREE_SKIP_PERMISSIONS === "true",
85
- enableGitHubIntegration: process.env.CLAUDE_WORKTREE_GITHUB !== "false",
79
+ process.env.GWT_BASE_BRANCH || DEFAULT_CONFIG.defaultBaseBranch,
80
+ skipPermissions: process.env.GWT_SKIP_PERMISSIONS === "true",
81
+ enableGitHubIntegration: process.env.GWT_GITHUB !== "false",
86
82
  enableDebugMode:
87
83
  process.env.DEBUG_CLEANUP === "true" || process.env.DEBUG === "true",
88
84
  };
@@ -29,16 +29,13 @@ import {
29
29
  /**
30
30
  * 設定ディレクトリのパスを取得
31
31
  *
32
- * 環境変数の優先順位: GWT_HOME > CLAUDE_WORKTREE_HOME (後方互換性) > ホームディレクトリ
32
+ * 環境変数 GWT_HOME が設定されている場合はそれを使用、それ以外はホームディレクトリ
33
33
  */
34
34
  function getConfigDir(): string {
35
35
  const worktreeHome =
36
36
  process.env.GWT_HOME && process.env.GWT_HOME.trim().length > 0
37
37
  ? process.env.GWT_HOME
38
- : process.env.CLAUDE_WORKTREE_HOME &&
39
- process.env.CLAUDE_WORKTREE_HOME.trim().length > 0
40
- ? process.env.CLAUDE_WORKTREE_HOME
41
- : homedir();
38
+ : homedir();
42
39
  return path.join(worktreeHome, ".gwt");
43
40
  }
44
41
 
@@ -133,11 +130,11 @@ async function mutateProfiles(
133
130
  }
134
131
 
135
132
  /**
136
- * プロファイル設定ファイルのパス(後方互換性のためエクスポート)
133
+ * プロファイル設定ファイルのパス
137
134
  *
138
135
  * @deprecated 内部では getProfilesConfigPath() を使用してください。
139
136
  * この定数はモジュールロード時に評価されるため、
140
- * 実行中に環境変数(GWT_HOME/CLAUDE_WORKTREE_HOME)を変更しても反映されません。
137
+ * 実行中に環境変数(GWT_HOME)を変更しても反映されません。
141
138
  */
142
139
  export const PROFILES_CONFIG_PATH = getProfilesConfigPath();
143
140
 
@@ -7,14 +7,7 @@
7
7
 
8
8
  import { homedir } from "node:os";
9
9
  import path from "node:path";
10
- import {
11
- readFile,
12
- writeFile,
13
- mkdir,
14
- rename,
15
- access,
16
- cp,
17
- } from "node:fs/promises";
10
+ import { readFile, writeFile, mkdir, rename } from "node:fs/promises";
18
11
  import type {
19
12
  ToolsConfig,
20
13
  CustomAITool,
@@ -28,60 +21,17 @@ const logger = createLogger({ category: "config" });
28
21
 
29
22
  /**
30
23
  * ツール設定ファイルのパス
31
- * 環境変数の優先順位: GWT_HOME > CLAUDE_WORKTREE_HOME (後方互換性) > ホームディレクトリ
24
+ * 環境変数 GWT_HOME が設定されている場合はそれを使用、それ以外はホームディレクトリ
32
25
  */
33
26
  export const WORKTREE_HOME =
34
27
  process.env.GWT_HOME && process.env.GWT_HOME.trim().length > 0
35
28
  ? process.env.GWT_HOME
36
- : process.env.CLAUDE_WORKTREE_HOME &&
37
- process.env.CLAUDE_WORKTREE_HOME.trim().length > 0
38
- ? process.env.CLAUDE_WORKTREE_HOME
39
- : homedir();
29
+ : homedir();
40
30
 
41
- const LEGACY_CONFIG_DIR = path.join(homedir(), ".claude-worktree");
42
31
  export const CONFIG_DIR = path.join(WORKTREE_HOME, ".gwt");
43
32
  export const TOOLS_CONFIG_PATH = path.join(CONFIG_DIR, "tools.json");
44
33
  const TEMP_CONFIG_PATH = `${TOOLS_CONFIG_PATH}.tmp`;
45
34
 
46
- /**
47
- * レガシー設定ディレクトリから新しいディレクトリへ移行
48
- */
49
- async function migrateLegacyConfig(): Promise<void> {
50
- try {
51
- // 新しいディレクトリが既に存在する場合は移行不要
52
- try {
53
- await access(CONFIG_DIR);
54
- return;
55
- } catch {
56
- // 新しいディレクトリが存在しない場合は続行
57
- }
58
-
59
- // レガシーディレクトリの存在を確認
60
- try {
61
- await access(LEGACY_CONFIG_DIR);
62
- } catch {
63
- // レガシーディレクトリも存在しない場合は移行不要
64
- return;
65
- }
66
-
67
- // レガシーディレクトリを新しいディレクトリにコピー
68
- await mkdir(path.dirname(CONFIG_DIR), { recursive: true });
69
- await cp(LEGACY_CONFIG_DIR, CONFIG_DIR, { recursive: true });
70
- logger.info(
71
- { from: LEGACY_CONFIG_DIR, to: CONFIG_DIR },
72
- "Legacy config migrated",
73
- );
74
- console.log(
75
- `✅ Migrated configuration from ${LEGACY_CONFIG_DIR} to ${CONFIG_DIR}`,
76
- );
77
- } catch (error) {
78
- // 移行に失敗しても継続(エラーログのみ)
79
- if (process.env.DEBUG) {
80
- console.error("Failed to migrate legacy config:", error);
81
- }
82
- }
83
- }
84
-
85
35
  const DEFAULT_CONFIG: ToolsConfig = {
86
36
  version: "1.0.0",
87
37
  env: {},
@@ -98,9 +48,6 @@ const DEFAULT_CONFIG: ToolsConfig = {
98
48
  * @throws JSON構文エラー時
99
49
  */
100
50
  export async function loadToolsConfig(): Promise<ToolsConfig> {
101
- // 最初の呼び出し時にレガシー設定の移行を試行
102
- await migrateLegacyConfig();
103
-
104
51
  try {
105
52
  const content = await readFile(TOOLS_CONFIG_PATH, "utf-8");
106
53
  const config = JSON.parse(content) as ToolsConfig;
package/src/gemini.ts CHANGED
@@ -175,7 +175,6 @@ export async function launchGeminiCLI(
175
175
  const run = async (cmd: string, args: string[]) => {
176
176
  const child = execa(cmd, args, {
177
177
  cwd: worktreePath,
178
- shell: true,
179
178
  stdin: childStdio.stdin,
180
179
  stdout: childStdio.stdout,
181
180
  stderr: childStdio.stderr,
@@ -328,7 +327,7 @@ export async function launchGeminiCLI(
328
327
  */
329
328
  export async function isGeminiCLIAvailable(): Promise<boolean> {
330
329
  try {
331
- await execa("bunx", [GEMINI_CLI_PACKAGE, "--version"], { shell: true });
330
+ await execa("bunx", [GEMINI_CLI_PACKAGE, "--version"]);
332
331
  return true;
333
332
  } catch (error: unknown) {
334
333
  const err = error as NodeJS.ErrnoException;
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) {
@@ -59,6 +59,7 @@ export interface CommandLookupResult {
59
59
  available: boolean;
60
60
  path: string | null;
61
61
  source: "installed" | "bunx";
62
+ version?: string | null;
62
63
  }
63
64
 
64
65
  /**
@@ -69,6 +70,7 @@ export interface ToolStatus {
69
70
  name: string;
70
71
  status: "installed" | "bunx";
71
72
  path: string | null;
73
+ version?: string | null;
72
74
  }
73
75
 
74
76
  /**
@@ -85,6 +87,33 @@ export function clearCommandLookupCache(): void {
85
87
  commandLookupCache.clear();
86
88
  }
87
89
 
90
+ /**
91
+ * Gets the version of a command by running it with --version.
92
+ * FR-022: Returns version in "v{version}" format, null on failure.
93
+ * FR-023: Times out after 3 seconds to minimize startup delay.
94
+ *
95
+ * @param commandPath - Full path to the command
96
+ * @returns Version string (e.g., "v1.0.3") or null on failure
97
+ */
98
+ export async function getCommandVersion(
99
+ commandPath: string,
100
+ ): Promise<string | null> {
101
+ try {
102
+ const result = await execa(commandPath, ["--version"], {
103
+ timeout: 3000,
104
+ stdin: "ignore",
105
+ stdout: "pipe",
106
+ stderr: "ignore",
107
+ });
108
+ // Extract version number from output
109
+ // Examples: "claude 1.0.3", "codex 0.77.0", "gemini 0.1.0"
110
+ const match = result.stdout.match(/(\d+\.\d+(?:\.\d+)?(?:-[\w.]+)?)/);
111
+ return match ? `v${match[1]}` : null;
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
116
+
88
117
  /**
89
118
  * Finds a command by checking PATH first, then fallback paths.
90
119
  * Results are cached for the lifetime of the process (FR-020).
@@ -141,6 +170,13 @@ export async function findCommand(
141
170
  lookupResult = { available: true, path: null, source: "bunx" };
142
171
  }
143
172
 
173
+ // Step 4: Get version for installed commands (FR-022)
174
+ if (lookupResult.source === "installed" && lookupResult.path) {
175
+ lookupResult.version = await getCommandVersion(lookupResult.path);
176
+ } else {
177
+ lookupResult.version = null;
178
+ }
179
+
144
180
  // Cache the result (FR-020)
145
181
  commandLookupCache.set(commandName, lookupResult);
146
182
 
@@ -181,6 +217,7 @@ export async function detectAllToolStatuses(): Promise<ToolStatus[]> {
181
217
  name: tool.displayName,
182
218
  status: result.source,
183
219
  path: result.path,
220
+ version: result.version ?? null,
184
221
  };
185
222
  }),
186
223
  );
@@ -275,8 +275,8 @@ export function ConfigPage() {
275
275
  </p>
276
276
  <h3 className="mt-1 text-lg font-semibold">登録済みツール</h3>
277
277
  <p className="mt-2 text-sm text-muted-foreground">
278
- CLI と Web UI は同じ設定を参照します。更新すると
279
- ~/.claude-worktree/tools.json に保存されます。
278
+ CLI と Web UI は同じ設定を参照します。更新すると ~/.gwt/tools.json
279
+ に保存されます。
280
280
  </p>
281
281
  </CardHeader>
282
282
  <CardContent>