@akiojin/gwt 4.1.1 → 4.3.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 (135) hide show
  1. package/README.md +28 -3
  2. package/dist/claude.d.ts +4 -0
  3. package/dist/claude.d.ts.map +1 -1
  4. package/dist/claude.js +13 -1
  5. package/dist/claude.js.map +1 -1
  6. package/dist/cli/ui/components/App.d.ts.map +1 -1
  7. package/dist/cli/ui/components/App.js +68 -68
  8. package/dist/cli/ui/components/App.js.map +1 -1
  9. package/dist/cli/ui/components/common/Select.d.ts +3 -1
  10. package/dist/cli/ui/components/common/Select.d.ts.map +1 -1
  11. package/dist/cli/ui/components/common/Select.js +13 -2
  12. package/dist/cli/ui/components/common/Select.js.map +1 -1
  13. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  14. package/dist/cli/ui/components/screens/BranchListScreen.js +6 -1
  15. package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
  16. package/dist/client/assets/index-ChHC-Puh.css +1 -0
  17. package/dist/client/assets/index-PqK9jkug.js +78 -0
  18. package/dist/client/index.html +2 -2
  19. package/dist/config/builtin-tools.d.ts.map +1 -1
  20. package/dist/config/builtin-tools.js +3 -0
  21. package/dist/config/builtin-tools.js.map +1 -1
  22. package/dist/config/tools.d.ts.map +1 -1
  23. package/dist/config/tools.js +10 -1
  24. package/dist/config/tools.js.map +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +37 -4
  27. package/dist/index.js.map +1 -1
  28. package/dist/launcher.d.ts.map +1 -1
  29. package/dist/launcher.js +15 -0
  30. package/dist/launcher.js.map +1 -1
  31. package/dist/services/aiToolResolver.d.ts.map +1 -1
  32. package/dist/services/aiToolResolver.js +55 -8
  33. package/dist/services/aiToolResolver.js.map +1 -1
  34. package/dist/services/customToolResolver.d.ts.map +1 -1
  35. package/dist/services/customToolResolver.js +22 -17
  36. package/dist/services/customToolResolver.js.map +1 -1
  37. package/dist/utils/prompt.d.ts +12 -0
  38. package/dist/utils/prompt.d.ts.map +1 -1
  39. package/dist/utils/prompt.js +60 -10
  40. package/dist/utils/prompt.js.map +1 -1
  41. package/dist/utils/webui.js +1 -1
  42. package/dist/web/client/src/components/BranchGraph.d.ts +5 -0
  43. package/dist/web/client/src/components/BranchGraph.d.ts.map +1 -1
  44. package/dist/web/client/src/components/BranchGraph.js +35 -108
  45. package/dist/web/client/src/components/BranchGraph.js.map +1 -1
  46. package/dist/web/client/src/components/graph/BranchDetailPanel.d.ts +15 -0
  47. package/dist/web/client/src/components/graph/BranchDetailPanel.d.ts.map +1 -0
  48. package/dist/web/client/src/components/graph/BranchDetailPanel.js +57 -0
  49. package/dist/web/client/src/components/graph/BranchDetailPanel.js.map +1 -0
  50. package/dist/web/client/src/components/graph/BranchNode.d.ts +13 -0
  51. package/dist/web/client/src/components/graph/BranchNode.d.ts.map +1 -0
  52. package/dist/web/client/src/components/graph/BranchNode.js +103 -0
  53. package/dist/web/client/src/components/graph/BranchNode.js.map +1 -0
  54. package/dist/web/client/src/components/graph/ClusterNode.d.ts +13 -0
  55. package/dist/web/client/src/components/graph/ClusterNode.d.ts.map +1 -0
  56. package/dist/web/client/src/components/graph/ClusterNode.js +109 -0
  57. package/dist/web/client/src/components/graph/ClusterNode.js.map +1 -0
  58. package/dist/web/client/src/components/graph/SynapticCanvas.d.ts +17 -0
  59. package/dist/web/client/src/components/graph/SynapticCanvas.d.ts.map +1 -0
  60. package/dist/web/client/src/components/graph/SynapticCanvas.js +94 -0
  61. package/dist/web/client/src/components/graph/SynapticCanvas.js.map +1 -0
  62. package/dist/web/client/src/components/graph/SynapticEdge.d.ts +13 -0
  63. package/dist/web/client/src/components/graph/SynapticEdge.d.ts.map +1 -0
  64. package/dist/web/client/src/components/graph/SynapticEdge.js +113 -0
  65. package/dist/web/client/src/components/graph/SynapticEdge.js.map +1 -0
  66. package/dist/web/client/src/components/graph/graphUtils.d.ts +67 -0
  67. package/dist/web/client/src/components/graph/graphUtils.d.ts.map +1 -0
  68. package/dist/web/client/src/components/graph/graphUtils.js +175 -0
  69. package/dist/web/client/src/components/graph/graphUtils.js.map +1 -0
  70. package/dist/web/client/src/components/graph/index.d.ts +10 -0
  71. package/dist/web/client/src/components/graph/index.d.ts.map +1 -0
  72. package/dist/web/client/src/components/graph/index.js +10 -0
  73. package/dist/web/client/src/components/graph/index.js.map +1 -0
  74. package/dist/web/client/src/lib/websocket.d.ts.map +1 -1
  75. package/dist/web/client/src/lib/websocket.js +2 -1
  76. package/dist/web/client/src/lib/websocket.js.map +1 -1
  77. package/dist/web/client/vite.config.js +1 -1
  78. package/dist/web/server/env/importer.d.ts.map +1 -1
  79. package/dist/web/server/env/importer.js +4 -0
  80. package/dist/web/server/env/importer.js.map +1 -1
  81. package/dist/web/server/index.d.ts.map +1 -1
  82. package/dist/web/server/index.js +9 -0
  83. package/dist/web/server/index.js.map +1 -1
  84. package/dist/web/server/pty/manager.d.ts.map +1 -1
  85. package/dist/web/server/pty/manager.js +24 -1
  86. package/dist/web/server/pty/manager.js.map +1 -1
  87. package/dist/web/server/routes/sessions.d.ts.map +1 -1
  88. package/dist/web/server/routes/sessions.js +7 -0
  89. package/dist/web/server/routes/sessions.js.map +1 -1
  90. package/dist/web/server/tray.d.ts +1 -1
  91. package/dist/web/server/tray.d.ts.map +1 -1
  92. package/dist/web/server/tray.js +52 -34
  93. package/dist/web/server/tray.js.map +1 -1
  94. package/dist/web/server/websocket/handler.d.ts.map +1 -1
  95. package/dist/web/server/websocket/handler.js +4 -0
  96. package/dist/web/server/websocket/handler.js.map +1 -1
  97. package/package.json +6 -3
  98. package/src/claude.ts +15 -1
  99. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +2 -1
  100. package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +142 -8
  101. package/src/cli/ui/__tests__/components/App.test.tsx +4 -3
  102. package/src/cli/ui/__tests__/components/ModelSelectorScreen.initial.test.tsx +1 -0
  103. package/src/cli/ui/__tests__/components/common/Select.test.tsx +45 -0
  104. package/src/cli/ui/components/App.tsx +91 -81
  105. package/src/cli/ui/components/common/Select.tsx +14 -1
  106. package/src/cli/ui/components/screens/BranchListScreen.tsx +6 -1
  107. package/src/cli/ui/types.ts +1 -1
  108. package/src/config/builtin-tools.ts +3 -0
  109. package/src/config/tools.ts +24 -1
  110. package/src/index.ts +50 -3
  111. package/src/launcher.ts +26 -0
  112. package/src/services/aiToolResolver.ts +75 -9
  113. package/src/services/customToolResolver.ts +32 -17
  114. package/src/utils/__tests__/prompt.test.ts +72 -35
  115. package/src/utils/prompt.ts +79 -10
  116. package/src/utils/webui.ts +1 -1
  117. package/src/web/client/src/components/BranchGraph.tsx +51 -208
  118. package/src/web/client/src/components/graph/BranchDetailPanel.tsx +152 -0
  119. package/src/web/client/src/components/graph/BranchNode.tsx +200 -0
  120. package/src/web/client/src/components/graph/ClusterNode.tsx +211 -0
  121. package/src/web/client/src/components/graph/SynapticCanvas.tsx +171 -0
  122. package/src/web/client/src/components/graph/SynapticEdge.tsx +311 -0
  123. package/src/web/client/src/components/graph/graphUtils.ts +265 -0
  124. package/src/web/client/src/components/graph/index.ts +10 -0
  125. package/src/web/client/src/index.css +314 -29
  126. package/src/web/client/src/lib/websocket.ts +2 -1
  127. package/src/web/client/vite.config.ts +1 -1
  128. package/src/web/server/env/importer.ts +5 -0
  129. package/src/web/server/index.ts +10 -0
  130. package/src/web/server/pty/manager.ts +43 -1
  131. package/src/web/server/routes/sessions.ts +15 -0
  132. package/src/web/server/tray.ts +62 -46
  133. package/src/web/server/websocket/handler.ts +13 -0
  134. package/dist/client/assets/index-DsDNCy5f.css +0 -1
  135. package/dist/client/assets/index-v8smkNOL.js +0 -72
@@ -32,6 +32,7 @@ import type {
32
32
  AITool,
33
33
  BranchInfo,
34
34
  BranchItem,
35
+ CleanupTarget,
35
36
  InferenceLevel,
36
37
  SelectedBranchState,
37
38
  } from "../types.js";
@@ -40,7 +41,6 @@ import { loadSession } from "../../../config/index.js";
40
41
  import {
41
42
  createWorktree,
42
43
  generateWorktreePath,
43
- getMergedPRWorktrees,
44
44
  isProtectedBranchName,
45
45
  removeWorktree,
46
46
  switchToProtectedBranch,
@@ -166,7 +166,6 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
166
166
  } | null>(null);
167
167
  const [hiddenBranches, setHiddenBranches] = useState<string[]>([]);
168
168
  const [selectedBranches, setSelectedBranches] = useState<string[]>([]);
169
- const [safeBranches, setSafeBranches] = useState<Set<string>>(new Set());
170
169
  const spinnerFrameIndexRef = useRef(0);
171
170
  const [spinnerFrameIndex, setSpinnerFrameIndex] = useState(0);
172
171
  const completionTimerRef = useRef<NodeJS.Timeout | null>(null);
@@ -258,29 +257,6 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
258
257
  );
259
258
  }, [branches, hiddenBranches]);
260
259
 
261
- // Precompute safe-to-clean branches using cleanup candidate logic
262
- useEffect(() => {
263
- let cancelled = false;
264
- (async () => {
265
- try {
266
- const targets = await getMergedPRWorktrees();
267
- if (cancelled) return;
268
- const safe = new Set(
269
- targets
270
- .filter((t) => !t.hasUncommittedChanges && !t.hasUnpushedCommits)
271
- .map((t) => t.branch),
272
- );
273
- setSafeBranches(safe);
274
- } catch {
275
- if (cancelled) return;
276
- setSafeBranches(new Set());
277
- }
278
- })();
279
- return () => {
280
- cancelled = true;
281
- };
282
- }, [branches, worktrees]);
283
-
284
260
  // Load quick start options for selected branch (latest per tool)
285
261
  useEffect(() => {
286
262
  if (!selectedBranch) {
@@ -520,16 +496,15 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
520
496
  });
521
497
  }
522
498
  const baseItems = formatBranchItems(visibleBranches, worktreeMap);
523
- return baseItems.map((item) => ({
524
- ...item,
525
- safeToCleanup: safeBranches.has(item.name),
526
- }));
527
- }, [branchHash, worktreeHash, visibleBranches, worktrees, safeBranches]);
528
-
529
- const selectedBranchSet = useMemo(
530
- () => new Set(selectedBranches),
531
- [selectedBranches],
532
- );
499
+ return baseItems.map((item) => {
500
+ const hasUncommitted = item.worktree?.hasUncommittedChanges ?? false;
501
+ const hasUnpushed = Boolean(item.hasUnpushedCommits);
502
+ const isMerged = Boolean(item.mergedPR);
503
+ const safeToCleanup =
504
+ item.type === "local" && isMerged && !hasUncommitted && !hasUnpushed;
505
+ return { ...item, safeToCleanup };
506
+ });
507
+ }, [branchHash, worktreeHash, visibleBranches, worktrees]);
533
508
 
534
509
  // Calculate statistics (memoized for performance)
535
510
  const stats = useMemo(
@@ -622,17 +597,31 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
622
597
  [isProtectedBranchName],
623
598
  );
624
599
 
625
- const toggleBranchSelection = useCallback((branchName: string) => {
626
- setSelectedBranches((prev) => {
627
- const set = new Set(prev);
628
- if (set.has(branchName)) {
629
- set.delete(branchName);
630
- } else {
631
- set.add(branchName);
600
+ const toggleBranchSelection = useCallback(
601
+ (branchName: string) => {
602
+ const branch = branches.find((b) => b.name === branchName);
603
+ if (!branch || branch.type === "remote") {
604
+ return;
632
605
  }
633
- return Array.from(set);
634
- });
635
- }, []);
606
+ if (
607
+ isProtectedBranchName(branch.name) ||
608
+ branch.branchType === "main" ||
609
+ branch.branchType === "develop"
610
+ ) {
611
+ return;
612
+ }
613
+ setSelectedBranches((prev) => {
614
+ const set = new Set(prev);
615
+ if (set.has(branchName)) {
616
+ set.delete(branchName);
617
+ } else {
618
+ set.add(branchName);
619
+ }
620
+ return Array.from(set);
621
+ });
622
+ },
623
+ [branches, isProtectedBranchName],
624
+ );
636
625
 
637
626
  const protectedBranchInfo = useMemo(() => {
638
627
  if (!selectedBranch) {
@@ -846,6 +835,20 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
846
835
  completionTimerRef.current = null;
847
836
  }
848
837
 
838
+ if (selectedBranches.length === 0) {
839
+ setCleanupIndicators({});
840
+ setCleanupFooterMessage({
841
+ text: "クリーンアップ対象が選択されていません",
842
+ color: "yellow",
843
+ });
844
+ setCleanupInputLocked(false);
845
+ completionTimerRef.current = setTimeout(() => {
846
+ setCleanupFooterMessage(null);
847
+ completionTimerRef.current = null;
848
+ }, COMPLETION_HOLD_DURATION_MS);
849
+ return;
850
+ }
851
+
849
852
  const succeededBranches: string[] = [];
850
853
 
851
854
  const resetAfterWait = () => {
@@ -878,26 +881,50 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
878
881
  spinnerFrameIndexRef.current = 0;
879
882
  setSpinnerFrameIndex(0);
880
883
 
881
- let targets;
882
- try {
883
- targets = await getMergedPRWorktrees();
884
- } catch (error) {
885
- const message = error instanceof Error ? error.message : String(error);
886
- setCleanupIndicators({});
887
- setCleanupFooterMessage({ text: `❌ ${message}`, color: "red" });
888
- setCleanupInputLocked(false);
889
- completionTimerRef.current = setTimeout(() => {
890
- setCleanupFooterMessage(null);
891
- completionTimerRef.current = null;
892
- }, COMPLETION_HOLD_DURATION_MS);
893
- return;
894
- }
884
+ const branchMap = new Map(branches.map((branch) => [branch.name, branch]));
885
+ const worktreeMap = new Map(
886
+ worktrees.map((worktree) => [worktree.branch, worktree]),
887
+ );
888
+ const targets = selectedBranches.reduce<CleanupTarget[]>((acc, name) => {
889
+ const branch = branchMap.get(name);
890
+ if (!branch || branch.type === "remote") {
891
+ return acc;
892
+ }
893
+ if (
894
+ isProtectedBranchName(branch.name) ||
895
+ branch.branchType === "main" ||
896
+ branch.branchType === "develop"
897
+ ) {
898
+ return acc;
899
+ }
900
+
901
+ const worktree = worktreeMap.get(branch.name);
902
+ const hasRemoteBranch =
903
+ typeof branch.hasRemoteCounterpart === "boolean"
904
+ ? branch.hasRemoteCounterpart
905
+ : undefined;
906
+ const isAccessible =
907
+ typeof worktree?.isAccessible === "boolean"
908
+ ? worktree.isAccessible
909
+ : undefined;
910
+ acc.push({
911
+ branch: branch.name,
912
+ pullRequest: null,
913
+ worktreePath: worktree?.path ?? null,
914
+ cleanupType: worktree ? "worktree-and-branch" : "branch-only",
915
+ hasUncommittedChanges: worktree?.hasUncommittedChanges ?? false,
916
+ hasUnpushedCommits: Boolean(branch.hasUnpushedCommits),
917
+ ...(hasRemoteBranch !== undefined ? { hasRemoteBranch } : {}),
918
+ ...(isAccessible !== undefined ? { isAccessible } : {}),
919
+ });
920
+ return acc;
921
+ }, []);
895
922
 
896
923
  if (targets.length === 0) {
897
924
  setCleanupIndicators({});
898
925
  setCleanupFooterMessage({
899
- text: " Nothing to clean up.",
900
- color: "green",
926
+ text: "⚠️ No cleanup candidates among selected branches.",
927
+ color: "yellow",
901
928
  });
902
929
  setCleanupInputLocked(false);
903
930
  completionTimerRef.current = setTimeout(() => {
@@ -907,24 +934,6 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
907
934
  return;
908
935
  }
909
936
 
910
- // Manual selection: restrict targets when選択がある
911
- if (selectedBranchSet.size > 0) {
912
- targets = targets.filter((t) => selectedBranchSet.has(t.branch));
913
- if (targets.length === 0) {
914
- setCleanupIndicators({});
915
- setCleanupFooterMessage({
916
- text: "⚠️ No cleanup candidates among selected branches.",
917
- color: "yellow",
918
- });
919
- setCleanupInputLocked(false);
920
- completionTimerRef.current = setTimeout(() => {
921
- setCleanupFooterMessage(null);
922
- completionTimerRef.current = null;
923
- }, COMPLETION_HOLD_DURATION_MS);
924
- return;
925
- }
926
- }
927
-
928
937
  // Reset hidden branches that may already be gone
929
938
  setHiddenBranches((prev) =>
930
939
  prev.filter(
@@ -1037,11 +1046,12 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
1037
1046
  completionTimerRef.current = setTimeout(resetAfterWait, holdDuration);
1038
1047
  }, [
1039
1048
  cleanupInputLocked,
1049
+ branches,
1040
1050
  deleteBranch,
1041
- getMergedPRWorktrees,
1042
1051
  refresh,
1043
1052
  removeWorktree,
1044
- selectedBranchSet,
1053
+ selectedBranches,
1054
+ worktrees,
1045
1055
  ]);
1046
1056
 
1047
1057
  // Handle AI tool selection
@@ -28,6 +28,8 @@ export interface SelectProps<T extends SelectItem = SelectItem> {
28
28
  // Optional controlled component props for cursor position
29
29
  selectedIndex?: number;
30
30
  onSelectedIndexChange?: (index: number) => void;
31
+ onSpace?: (item: T) => void;
32
+ onEscape?: () => void;
31
33
  }
32
34
 
33
35
  /**
@@ -47,7 +49,9 @@ function arePropsEqual<T extends SelectItem = SelectItem>(
47
49
  prevProps.onSelect !== nextProps.onSelect ||
48
50
  prevProps.onSelectedIndexChange !== nextProps.onSelectedIndexChange ||
49
51
  prevProps.renderIndicator !== nextProps.renderIndicator ||
50
- prevProps.renderItem !== nextProps.renderItem
52
+ prevProps.renderItem !== nextProps.renderItem ||
53
+ prevProps.onSpace !== nextProps.onSpace ||
54
+ prevProps.onEscape !== nextProps.onEscape
51
55
  ) {
52
56
  return false;
53
57
  }
@@ -93,6 +97,8 @@ const SelectComponent = <T extends SelectItem = SelectItem>({
93
97
  renderItem,
94
98
  selectedIndex: externalSelectedIndex,
95
99
  onSelectedIndexChange,
100
+ onSpace,
101
+ onEscape,
96
102
  }: SelectProps<T>) => {
97
103
  // Support both controlled and uncontrolled modes
98
104
  const [internalSelectedIndex, setInternalSelectedIndex] =
@@ -177,6 +183,13 @@ const SelectComponent = <T extends SelectItem = SelectItem>({
177
183
  if (selectedItem && !disabled) {
178
184
  onSelect(selectedItem);
179
185
  }
186
+ } else if (input === ' ' && onSpace) {
187
+ const selectedItem = items[selectedIndex];
188
+ if (selectedItem && !disabled) {
189
+ onSpace(selectedItem);
190
+ }
191
+ } else if (key.escape && onEscape) {
192
+ onEscape();
180
193
  }
181
194
  // All other keys are ignored and will propagate to parent components
182
195
  });
@@ -407,7 +407,12 @@ export function BranchListScreen({
407
407
  const indicatorPrefix = indicatorIcon ? `${indicatorIcon} ` : "";
408
408
 
409
409
  const isChecked = selectedSet.has(item.name);
410
- const selectionIcon = isChecked ? "[*]" : "[ ]";
410
+ const isWarning = Boolean(item.hasUnpushedCommits) || !item.mergedPR;
411
+ const selectionIcon = isChecked
412
+ ? isWarning
413
+ ? chalk.red("[*]")
414
+ : "[*]"
415
+ : "[ ]";
411
416
  let worktreeIcon = chalk.gray("⚪");
412
417
  if (item.worktreeStatus === "active") {
413
418
  worktreeIcon = chalk.green("🟢");
@@ -231,7 +231,7 @@ export interface BranchItem extends BranchInfo {
231
231
  syncStatus?: SyncStatus;
232
232
  syncInfo?: string | undefined;
233
233
  remoteName?: string | undefined;
234
- // クリーンアップ判定で「未コミット/未プッシュなし」と評価された場合に true
234
+ // クリーンアップ判定で「未コミット/未プッシュなし かつマージ済み」と評価された場合に true
235
235
  safeToCleanup?: boolean;
236
236
  }
237
237
 
@@ -24,6 +24,9 @@ export const CLAUDE_CODE_TOOL: CustomAITool = {
24
24
  resume: ["-r"],
25
25
  },
26
26
  permissionSkipArgs: Array.from(CLAUDE_PERMISSION_SKIP_ARGS),
27
+ env: {
28
+ ENABLE_LSP_TOOL: "true",
29
+ },
27
30
  };
28
31
 
29
32
  /**
@@ -21,8 +21,11 @@ import type {
21
21
  AIToolConfig,
22
22
  } from "../types/tools.js";
23
23
  import { BUILTIN_TOOLS } from "./builtin-tools.js";
24
+ import { createLogger } from "../logging/logger.js";
24
25
  import { resolveProfileEnv } from "./profiles.js";
25
26
 
27
+ const logger = createLogger({ category: "config" });
28
+
26
29
  /**
27
30
  * ツール設定ファイルのパス
28
31
  * 環境変数の優先順位: GWT_HOME > CLAUDE_WORKTREE_HOME (後方互換性) > ホームディレクトリ
@@ -64,6 +67,10 @@ async function migrateLegacyConfig(): Promise<void> {
64
67
  // レガシーディレクトリを新しいディレクトリにコピー
65
68
  await mkdir(path.dirname(CONFIG_DIR), { recursive: true });
66
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
+ );
67
74
  console.log(
68
75
  `✅ Migrated configuration from ${LEGACY_CONFIG_DIR} to ${CONFIG_DIR}`,
69
76
  );
@@ -101,6 +108,11 @@ export async function loadToolsConfig(): Promise<ToolsConfig> {
101
108
  // 検証
102
109
  validateToolsConfig(config);
103
110
 
111
+ logger.debug(
112
+ { path: TOOLS_CONFIG_PATH, toolCount: config.customTools.length },
113
+ "Tools config loaded",
114
+ );
115
+
104
116
  return {
105
117
  ...config,
106
118
  env: config.env ?? {},
@@ -108,11 +120,19 @@ export async function loadToolsConfig(): Promise<ToolsConfig> {
108
120
  } catch (error) {
109
121
  // ファイルが存在しない場合は空配列を返す
110
122
  if (error instanceof Error && "code" in error && error.code === "ENOENT") {
123
+ logger.debug(
124
+ { path: TOOLS_CONFIG_PATH },
125
+ "Tools config not found, using defaults",
126
+ );
111
127
  return { ...DEFAULT_CONFIG };
112
128
  }
113
129
 
114
130
  // JSON構文エラーの場合
115
131
  if (error instanceof SyntaxError) {
132
+ logger.error(
133
+ { path: TOOLS_CONFIG_PATH, error: error.message },
134
+ "Tools config parse error",
135
+ );
116
136
  throw new Error(
117
137
  `Failed to parse tools.json: ${error.message}\n` +
118
138
  `Please check the JSON syntax in ${TOOLS_CONFIG_PATH}`,
@@ -283,12 +303,15 @@ export async function getToolById(
283
303
  // ビルトインツールから検索
284
304
  const builtinTool = BUILTIN_TOOLS.find((t) => t.id === id);
285
305
  if (builtinTool) {
306
+ logger.debug({ id, found: true, isBuiltin: true }, "Tool lookup");
286
307
  return builtinTool;
287
308
  }
288
309
 
289
310
  // カスタムツールから検索
290
311
  const config = await loadToolsConfig();
291
- return config.customTools.find((t) => t.id === id);
312
+ const customTool = config.customTools.find((t) => t.id === id);
313
+ logger.debug({ id, found: !!customTool, isBuiltin: false }, "Tool lookup");
314
+ return customTool;
292
315
  }
293
316
 
294
317
  /**
package/src/index.ts CHANGED
@@ -7,6 +7,11 @@ import {
7
7
  fetchAllRemotes,
8
8
  pullFastForward,
9
9
  getBranchDivergenceStatuses,
10
+ hasUncommittedChanges,
11
+ hasUnpushedCommits,
12
+ getUncommittedChangesCount,
13
+ getUnpushedCommitsCount,
14
+ pushBranchToRemote,
10
15
  GitError,
11
16
  } from "./git.js";
12
17
  import { launchClaudeCode } from "./claude.js";
@@ -50,11 +55,12 @@ import {
50
55
  DependencyInstallError,
51
56
  type DependencyInstallResult,
52
57
  } from "./services/dependency-installer.js";
53
- import { waitForEnter } from "./utils/prompt.js";
58
+ import { confirmYesNo, waitForEnter } from "./utils/prompt.js";
54
59
 
55
60
  const ERROR_PROMPT = chalk.yellow(
56
61
  "Review the error details, then press Enter to continue.",
57
62
  );
63
+ const POST_SESSION_DELAY_MS = 3000;
58
64
 
59
65
  // Category: cli
60
66
  const appLogger = createLogger({ category: "cli" });
@@ -203,7 +209,7 @@ Worktree Manager
203
209
  Usage: gwt [command] [options]
204
210
 
205
211
  Commands:
206
- serve Start Web UI server (http://localhost:3000)
212
+ serve Start Web UI server (http://localhost:3001)
207
213
 
208
214
  Options:
209
215
  -h, --help Show this help message
@@ -602,6 +608,7 @@ export async function handleAIToolWorkflow(
602
608
  envOverrides?: Record<string, string>;
603
609
  model?: string;
604
610
  sessionId?: string | null;
611
+ chrome?: boolean;
605
612
  } = {
606
613
  mode:
607
614
  mode === "resume"
@@ -612,6 +619,7 @@ export async function handleAIToolWorkflow(
612
619
  skipPermissions,
613
620
  envOverrides: sharedEnv,
614
621
  sessionId: resumeSessionId,
622
+ chrome: true,
615
623
  };
616
624
  if (normalizedModel) {
617
625
  launchOptions.model = normalizedModel;
@@ -757,8 +765,47 @@ export async function handleAIToolWorkflow(
757
765
  lastSessionId: finalSessionId,
758
766
  });
759
767
 
768
+ try {
769
+ const [hasUncommitted, hasUnpushed] = await Promise.all([
770
+ hasUncommittedChanges(worktreePath),
771
+ hasUnpushedCommits(worktreePath, branch),
772
+ ]);
773
+
774
+ if (hasUncommitted) {
775
+ const uncommittedCount = await getUncommittedChangesCount(worktreePath);
776
+ const countLabel =
777
+ uncommittedCount > 0 ? ` (${uncommittedCount}件)` : "";
778
+ printWarning(`未コミットの変更があります${countLabel}。`);
779
+ }
780
+
781
+ if (hasUnpushed) {
782
+ const unpushedCount = await getUnpushedCommitsCount(
783
+ worktreePath,
784
+ branch,
785
+ );
786
+ const countLabel = unpushedCount > 0 ? ` (${unpushedCount}件)` : "";
787
+ const shouldPush = await confirmYesNo(
788
+ `未プッシュのコミットがあります${countLabel}。プッシュしますか?`,
789
+ { defaultValue: false },
790
+ );
791
+ if (shouldPush) {
792
+ printInfo(`Pushing origin/${branch}...`);
793
+ try {
794
+ await pushBranchToRemote(worktreePath, branch);
795
+ printInfo(`Push completed for ${branch}.`);
796
+ } catch (error) {
797
+ const details =
798
+ error instanceof Error ? error.message : String(error);
799
+ printWarning(`Push failed for ${branch}: ${details}`);
800
+ }
801
+ }
802
+ }
803
+ } catch (error) {
804
+ const details = error instanceof Error ? error.message : String(error);
805
+ printWarning(`Failed to check git status after session: ${details}`);
806
+ }
760
807
  // Small buffer before returning to branch list to avoid abrupt screen swap
761
- await new Promise((resolve) => setTimeout(resolve, 3000));
808
+ await new Promise((resolve) => setTimeout(resolve, POST_SESSION_DELAY_MS));
762
809
  printInfo("Session completed successfully. Returning to main menu...");
763
810
  return;
764
811
  } catch (error) {
package/src/launcher.ts CHANGED
@@ -7,6 +7,9 @@
7
7
 
8
8
  import { execa } from "execa";
9
9
  import type { CustomAITool, LaunchOptions } from "./types/tools.js";
10
+ import { createLogger } from "./logging/logger.js";
11
+
12
+ const logger = createLogger({ category: "launcher" });
10
13
 
11
14
  /**
12
15
  * コマンド名をPATH環境変数から解決
@@ -28,16 +31,22 @@ export async function resolveCommand(commandName: string): Promise<string> {
28
31
  const resolvedPath = (result.stdout.split("\n")[0] ?? "").trim();
29
32
 
30
33
  if (!resolvedPath) {
34
+ logger.error({ commandName }, "Command not found in PATH");
31
35
  throw new Error(
32
36
  `Command "${commandName}" not found in PATH.\n` +
33
37
  `Please ensure the command is installed and available in your PATH environment variable.`,
34
38
  );
35
39
  }
36
40
 
41
+ logger.debug({ commandName, resolvedPath }, "Command resolved");
37
42
  return resolvedPath;
38
43
  } catch (error) {
39
44
  // which/whereコマンド自体が失敗した場合
40
45
  if (error instanceof Error) {
46
+ logger.error(
47
+ { commandName, error: error.message },
48
+ "Command resolution failed",
49
+ );
41
50
  throw new Error(
42
51
  `Failed to resolve command "${commandName}".\n` +
43
52
  `Error: ${error.message}\n` +
@@ -78,6 +87,10 @@ function buildArgs(tool: CustomAITool, options: LaunchOptions): string[] {
78
87
  args.push(...options.extraArgs);
79
88
  }
80
89
 
90
+ logger.debug(
91
+ { toolId: tool.id, mode: options.mode ?? "normal", argsCount: args.length },
92
+ "Args built",
93
+ );
81
94
  return args;
82
95
  }
83
96
 
@@ -110,10 +123,21 @@ export async function launchCustomAITool(
110
123
  env,
111
124
  };
112
125
 
126
+ logger.info(
127
+ {
128
+ toolId: tool.id,
129
+ toolType: tool.type,
130
+ command: tool.command,
131
+ mode: options.mode ?? "normal",
132
+ },
133
+ "Launching custom AI tool",
134
+ );
135
+
113
136
  switch (tool.type) {
114
137
  case "path": {
115
138
  // 絶対パスで直接実行
116
139
  await execa(tool.command, args, execaOptions);
140
+ logger.info({ toolId: tool.id }, "Custom AI tool completed (path)");
117
141
  break;
118
142
  }
119
143
 
@@ -121,6 +145,7 @@ export async function launchCustomAITool(
121
145
  // bunx経由でパッケージ実行
122
146
  // bunx [package] [args...]
123
147
  await execa("bunx", [tool.command, ...args], execaOptions);
148
+ logger.info({ toolId: tool.id }, "Custom AI tool completed (bunx)");
124
149
  break;
125
150
  }
126
151
 
@@ -128,6 +153,7 @@ export async function launchCustomAITool(
128
153
  // PATH解決 → 実行
129
154
  const resolvedPath = await resolveCommand(tool.command);
130
155
  await execa(resolvedPath, args, execaOptions);
156
+ logger.info({ toolId: tool.id }, "Custom AI tool completed (command)");
131
157
  break;
132
158
  }
133
159