@akiojin/gwt 4.2.0 → 4.3.1

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 (119) hide show
  1. package/dist/claude.d.ts +2 -0
  2. package/dist/claude.d.ts.map +1 -1
  3. package/dist/claude.js +49 -2
  4. package/dist/claude.js.map +1 -1
  5. package/dist/cli/ui/components/App.d.ts.map +1 -1
  6. package/dist/cli/ui/components/App.js +68 -68
  7. package/dist/cli/ui/components/App.js.map +1 -1
  8. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  9. package/dist/cli/ui/components/screens/BranchListScreen.js +6 -1
  10. package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
  11. package/dist/client/assets/index-ChHC-Puh.css +1 -0
  12. package/dist/client/assets/index-PqK9jkug.js +78 -0
  13. package/dist/client/index.html +2 -2
  14. package/dist/config/builtin-tools.d.ts.map +1 -1
  15. package/dist/config/builtin-tools.js +3 -0
  16. package/dist/config/builtin-tools.js.map +1 -1
  17. package/dist/config/tools.d.ts.map +1 -1
  18. package/dist/config/tools.js +10 -1
  19. package/dist/config/tools.js.map +1 -1
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +2 -1
  22. package/dist/index.js.map +1 -1
  23. package/dist/launcher.d.ts.map +1 -1
  24. package/dist/launcher.js +15 -0
  25. package/dist/launcher.js.map +1 -1
  26. package/dist/services/aiToolResolver.d.ts.map +1 -1
  27. package/dist/services/aiToolResolver.js +55 -8
  28. package/dist/services/aiToolResolver.js.map +1 -1
  29. package/dist/services/customToolResolver.d.ts.map +1 -1
  30. package/dist/services/customToolResolver.js +22 -17
  31. package/dist/services/customToolResolver.js.map +1 -1
  32. package/dist/utils/webui.js +1 -1
  33. package/dist/web/client/src/components/BranchGraph.d.ts +5 -0
  34. package/dist/web/client/src/components/BranchGraph.d.ts.map +1 -1
  35. package/dist/web/client/src/components/BranchGraph.js +35 -108
  36. package/dist/web/client/src/components/BranchGraph.js.map +1 -1
  37. package/dist/web/client/src/components/graph/BranchDetailPanel.d.ts +15 -0
  38. package/dist/web/client/src/components/graph/BranchDetailPanel.d.ts.map +1 -0
  39. package/dist/web/client/src/components/graph/BranchDetailPanel.js +57 -0
  40. package/dist/web/client/src/components/graph/BranchDetailPanel.js.map +1 -0
  41. package/dist/web/client/src/components/graph/BranchNode.d.ts +13 -0
  42. package/dist/web/client/src/components/graph/BranchNode.d.ts.map +1 -0
  43. package/dist/web/client/src/components/graph/BranchNode.js +103 -0
  44. package/dist/web/client/src/components/graph/BranchNode.js.map +1 -0
  45. package/dist/web/client/src/components/graph/ClusterNode.d.ts +13 -0
  46. package/dist/web/client/src/components/graph/ClusterNode.d.ts.map +1 -0
  47. package/dist/web/client/src/components/graph/ClusterNode.js +109 -0
  48. package/dist/web/client/src/components/graph/ClusterNode.js.map +1 -0
  49. package/dist/web/client/src/components/graph/SynapticCanvas.d.ts +17 -0
  50. package/dist/web/client/src/components/graph/SynapticCanvas.d.ts.map +1 -0
  51. package/dist/web/client/src/components/graph/SynapticCanvas.js +94 -0
  52. package/dist/web/client/src/components/graph/SynapticCanvas.js.map +1 -0
  53. package/dist/web/client/src/components/graph/SynapticEdge.d.ts +13 -0
  54. package/dist/web/client/src/components/graph/SynapticEdge.d.ts.map +1 -0
  55. package/dist/web/client/src/components/graph/SynapticEdge.js +113 -0
  56. package/dist/web/client/src/components/graph/SynapticEdge.js.map +1 -0
  57. package/dist/web/client/src/components/graph/graphUtils.d.ts +67 -0
  58. package/dist/web/client/src/components/graph/graphUtils.d.ts.map +1 -0
  59. package/dist/web/client/src/components/graph/graphUtils.js +175 -0
  60. package/dist/web/client/src/components/graph/graphUtils.js.map +1 -0
  61. package/dist/web/client/src/components/graph/index.d.ts +10 -0
  62. package/dist/web/client/src/components/graph/index.d.ts.map +1 -0
  63. package/dist/web/client/src/components/graph/index.js +10 -0
  64. package/dist/web/client/src/components/graph/index.js.map +1 -0
  65. package/dist/web/client/src/lib/websocket.d.ts.map +1 -1
  66. package/dist/web/client/src/lib/websocket.js +2 -1
  67. package/dist/web/client/src/lib/websocket.js.map +1 -1
  68. package/dist/web/client/vite.config.js +1 -1
  69. package/dist/web/server/env/importer.d.ts.map +1 -1
  70. package/dist/web/server/env/importer.js +4 -0
  71. package/dist/web/server/env/importer.js.map +1 -1
  72. package/dist/web/server/index.d.ts.map +1 -1
  73. package/dist/web/server/index.js +9 -0
  74. package/dist/web/server/index.js.map +1 -1
  75. package/dist/web/server/pty/manager.d.ts.map +1 -1
  76. package/dist/web/server/pty/manager.js +24 -1
  77. package/dist/web/server/pty/manager.js.map +1 -1
  78. package/dist/web/server/routes/sessions.d.ts.map +1 -1
  79. package/dist/web/server/routes/sessions.js +7 -0
  80. package/dist/web/server/routes/sessions.js.map +1 -1
  81. package/dist/web/server/tray.d.ts +1 -1
  82. package/dist/web/server/tray.d.ts.map +1 -1
  83. package/dist/web/server/tray.js +52 -34
  84. package/dist/web/server/tray.js.map +1 -1
  85. package/dist/web/server/websocket/handler.d.ts.map +1 -1
  86. package/dist/web/server/websocket/handler.js +4 -0
  87. package/dist/web/server/websocket/handler.js.map +1 -1
  88. package/package.json +5 -2
  89. package/src/claude.ts +57 -2
  90. package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +104 -0
  91. package/src/cli/ui/components/App.tsx +91 -81
  92. package/src/cli/ui/components/screens/BranchListScreen.tsx +6 -1
  93. package/src/cli/ui/types.ts +1 -1
  94. package/src/config/builtin-tools.ts +3 -0
  95. package/src/config/tools.ts +24 -1
  96. package/src/index.ts +3 -1
  97. package/src/launcher.ts +26 -0
  98. package/src/services/aiToolResolver.ts +75 -9
  99. package/src/services/customToolResolver.ts +32 -17
  100. package/src/utils/webui.ts +1 -1
  101. package/src/web/client/src/components/BranchGraph.tsx +51 -208
  102. package/src/web/client/src/components/graph/BranchDetailPanel.tsx +152 -0
  103. package/src/web/client/src/components/graph/BranchNode.tsx +200 -0
  104. package/src/web/client/src/components/graph/ClusterNode.tsx +211 -0
  105. package/src/web/client/src/components/graph/SynapticCanvas.tsx +171 -0
  106. package/src/web/client/src/components/graph/SynapticEdge.tsx +311 -0
  107. package/src/web/client/src/components/graph/graphUtils.ts +265 -0
  108. package/src/web/client/src/components/graph/index.ts +10 -0
  109. package/src/web/client/src/index.css +314 -29
  110. package/src/web/client/src/lib/websocket.ts +2 -1
  111. package/src/web/client/vite.config.ts +1 -1
  112. package/src/web/server/env/importer.ts +5 -0
  113. package/src/web/server/index.ts +10 -0
  114. package/src/web/server/pty/manager.ts +43 -1
  115. package/src/web/server/routes/sessions.ts +15 -0
  116. package/src/web/server/tray.ts +62 -46
  117. package/src/web/server/websocket/handler.ts +13 -0
  118. package/dist/client/assets/index-DsDNCy5f.css +0 -1
  119. package/dist/client/assets/index-v8smkNOL.js +0 -72
package/src/claude.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { execa, type Options as ExecaOptions } from "execa";
2
2
  import chalk from "chalk";
3
- import { existsSync } from "fs";
3
+ import { existsSync, readFileSync } from "fs";
4
4
  import {
5
5
  createChildStdio,
6
6
  getTerminalStreams,
@@ -37,6 +37,7 @@ export class ClaudeError extends Error {
37
37
  *
38
38
  * @param worktreePath - Worktree directory to run Claude Code in
39
39
  * @param options - Launch options (mode/session/model/permissions/env)
40
+ * @param options.chrome - Enable Chrome extension integration (adds --chrome flag)
40
41
  * @returns Captured session id when available
41
42
  */
42
43
  export async function launchClaudeCode(
@@ -48,6 +49,7 @@ export async function launchClaudeCode(
48
49
  envOverrides?: Record<string, string>;
49
50
  model?: string;
50
51
  sessionId?: string | null;
52
+ chrome?: boolean;
51
53
  } = {},
52
54
  ): Promise<{ sessionId?: string | null }> {
53
55
  const terminal = getTerminalStreams();
@@ -169,6 +171,18 @@ export async function launchClaudeCode(
169
171
  break;
170
172
  }
171
173
 
174
+ // Handle Chrome extension integration
175
+ if (options.chrome && isChromeIntegrationSupported()) {
176
+ args.push("--chrome");
177
+ console.log(chalk.green(" 🌐 Chrome integration enabled"));
178
+ } else if (options.chrome) {
179
+ console.log(
180
+ chalk.yellow(
181
+ " ⚠️ Chrome integration is not supported on this platform. Skipping --chrome.",
182
+ ),
183
+ );
184
+ }
185
+
172
186
  // Detect root user for Docker/sandbox environments
173
187
  let isRoot = false;
174
188
  try {
@@ -201,7 +215,11 @@ export async function launchClaudeCode(
201
215
  terminal.exitRawMode();
202
216
  resetTerminalModes(terminal.stdout);
203
217
 
204
- const baseEnv = { ...process.env, ...(options.envOverrides ?? {}) };
218
+ const baseEnv: Record<string, string | undefined> = {
219
+ ...process.env,
220
+ ...(options.envOverrides ?? {}),
221
+ ENABLE_LSP_TOOL: "true", // Enable TypeScript LSP support in Claude Code
222
+ };
205
223
  const launchEnvSource =
206
224
  options.skipPermissions && !baseEnv.IS_SANDBOX
207
225
  ? { ...baseEnv, IS_SANDBOX: "1" }
@@ -427,3 +445,40 @@ export async function isClaudeCodeAvailable(): Promise<boolean> {
427
445
  return false;
428
446
  }
429
447
  }
448
+
449
+ /**
450
+ * Checks whether Chrome integration is supported on the current platform.
451
+ *
452
+ * Supported platforms:
453
+ * - Windows (win32)
454
+ * - macOS (darwin)
455
+ * - Linux (non-WSL)
456
+ */
457
+ function isChromeIntegrationSupported(): boolean {
458
+ switch (process.platform) {
459
+ case "win32":
460
+ case "darwin":
461
+ return true;
462
+ case "linux":
463
+ return !isWslEnvironment();
464
+ default:
465
+ return false;
466
+ }
467
+ }
468
+
469
+ function isWslEnvironment(): boolean {
470
+ if (process.platform !== "linux") {
471
+ return false;
472
+ }
473
+
474
+ if (process.env.WSL_DISTRO_NAME || process.env.WSL_INTEROP) {
475
+ return true;
476
+ }
477
+
478
+ try {
479
+ const procVersion = readFileSync("/proc/version", "utf8");
480
+ return /microsoft|wsl/i.test(procVersion);
481
+ } catch {
482
+ return false;
483
+ }
484
+ }
@@ -107,6 +107,18 @@ describe("App shortcuts integration", () => {
107
107
  },
108
108
  ],
109
109
  worktrees: [
110
+ {
111
+ branch: "feature/add-new-feature",
112
+ path: "/worktrees/feature-add-new-feature",
113
+ isAccessible: true,
114
+ hasUncommittedChanges: false,
115
+ },
116
+ {
117
+ branch: "hotfix/urgent-fix",
118
+ path: "/worktrees/hotfix-urgent-fix",
119
+ isAccessible: true,
120
+ hasUncommittedChanges: true,
121
+ },
110
122
  {
111
123
  branch: "feature/existing",
112
124
  path: "/worktrees/feature-existing",
@@ -208,6 +220,98 @@ describe("App shortcuts integration", () => {
208
220
  expect(navigateToMock).toHaveBeenCalledWith("ai-tool-selector");
209
221
  });
210
222
 
223
+ it("shows warning when cleanup runs without selection", async () => {
224
+ const onExit = vi.fn();
225
+
226
+ render(<App onExit={onExit} />);
227
+
228
+ const initialProps = branchListProps.at(-1);
229
+ expect(initialProps).toBeDefined();
230
+
231
+ act(() => {
232
+ initialProps?.onCleanupCommand?.();
233
+ });
234
+
235
+ await act(async () => {
236
+ await Promise.resolve();
237
+ });
238
+
239
+ const latestProps = branchListProps.at(-1);
240
+ expect(latestProps?.cleanupUI?.footerMessage?.text).toBe(
241
+ "クリーンアップ対象が選択されていません",
242
+ );
243
+ expect(removeWorktreeMock).not.toHaveBeenCalled();
244
+ expect(deleteBranchMock).not.toHaveBeenCalled();
245
+ });
246
+
247
+ it("marks branches safe only when merged and clean", () => {
248
+ const onExit = vi.fn();
249
+ const refresh = vi.fn();
250
+ useGitDataMock.mockReturnValue({
251
+ branches: [
252
+ {
253
+ name: "feature/merged-clean",
254
+ type: "local",
255
+ branchType: "feature",
256
+ isCurrent: false,
257
+ mergedPR: { number: 101, mergedAt: "2025-01-02T10:00:00Z" },
258
+ },
259
+ {
260
+ name: "feature/merged-unpushed",
261
+ type: "local",
262
+ branchType: "feature",
263
+ isCurrent: false,
264
+ hasUnpushedCommits: true,
265
+ mergedPR: { number: 102, mergedAt: "2025-01-03T10:00:00Z" },
266
+ },
267
+ {
268
+ name: "feature/unmerged",
269
+ type: "local",
270
+ branchType: "feature",
271
+ isCurrent: false,
272
+ },
273
+ ],
274
+ worktrees: [
275
+ {
276
+ branch: "feature/merged-clean",
277
+ path: "/worktrees/merged-clean",
278
+ isAccessible: true,
279
+ hasUncommittedChanges: false,
280
+ },
281
+ {
282
+ branch: "feature/merged-unpushed",
283
+ path: "/worktrees/merged-unpushed",
284
+ isAccessible: true,
285
+ hasUncommittedChanges: false,
286
+ },
287
+ {
288
+ branch: "feature/unmerged",
289
+ path: "/worktrees/unmerged",
290
+ isAccessible: true,
291
+ hasUncommittedChanges: false,
292
+ },
293
+ ],
294
+ loading: false,
295
+ error: null,
296
+ refresh,
297
+ lastUpdated: null,
298
+ });
299
+
300
+ render(<App onExit={onExit} />);
301
+
302
+ const latestProps = branchListProps.at(-1);
303
+ const safeMap = new Map(
304
+ latestProps?.branches?.map((branch: BranchItem) => [
305
+ branch.name,
306
+ branch.safeToCleanup,
307
+ ]),
308
+ );
309
+
310
+ expect(safeMap.get("feature/merged-clean")).toBe(true);
311
+ expect(safeMap.get("feature/merged-unpushed")).toBe(false);
312
+ expect(safeMap.get("feature/unmerged")).toBe(false);
313
+ });
314
+
211
315
  it("displays per-branch cleanup indicators and waits before clearing results", async () => {
212
316
  vi.useFakeTimers();
213
317
  const originalNodeEnv = process.env.NODE_ENV;
@@ -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
@@ -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
@@ -209,7 +209,7 @@ Worktree Manager
209
209
  Usage: gwt [command] [options]
210
210
 
211
211
  Commands:
212
- serve Start Web UI server (http://localhost:3000)
212
+ serve Start Web UI server (http://localhost:3001)
213
213
 
214
214
  Options:
215
215
  -h, --help Show this help message
@@ -608,6 +608,7 @@ export async function handleAIToolWorkflow(
608
608
  envOverrides?: Record<string, string>;
609
609
  model?: string;
610
610
  sessionId?: string | null;
611
+ chrome?: boolean;
611
612
  } = {
612
613
  mode:
613
614
  mode === "resume"
@@ -618,6 +619,7 @@ export async function handleAIToolWorkflow(
618
619
  skipPermissions,
619
620
  envOverrides: sharedEnv,
620
621
  sessionId: resumeSessionId,
622
+ chrome: true,
621
623
  };
622
624
  if (normalizedModel) {
623
625
  launchOptions.model = normalizedModel;
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