@akiojin/gwt 2.14.0 → 3.1.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 (64) hide show
  1. package/README.ja.md +10 -2
  2. package/README.md +9 -2
  3. package/dist/cli/ui/components/App.d.ts.map +1 -1
  4. package/dist/cli/ui/components/App.js +19 -4
  5. package/dist/cli/ui/components/App.js.map +1 -1
  6. package/dist/cli/ui/components/parts/Header.d.ts +8 -0
  7. package/dist/cli/ui/components/parts/Header.d.ts.map +1 -1
  8. package/dist/cli/ui/components/parts/Header.js +7 -2
  9. package/dist/cli/ui/components/parts/Header.js.map +1 -1
  10. package/dist/cli/ui/components/screens/BranchListScreen.d.ts +3 -1
  11. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  12. package/dist/cli/ui/components/screens/BranchListScreen.js +9 -17
  13. package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
  14. package/dist/cli/ui/components/screens/EnvironmentProfileScreen.d.ts +16 -0
  15. package/dist/cli/ui/components/screens/EnvironmentProfileScreen.d.ts.map +1 -0
  16. package/dist/cli/ui/components/screens/EnvironmentProfileScreen.js +576 -0
  17. package/dist/cli/ui/components/screens/EnvironmentProfileScreen.js.map +1 -0
  18. package/dist/cli/ui/hooks/useProfiles.d.ts +41 -0
  19. package/dist/cli/ui/hooks/useProfiles.d.ts.map +1 -0
  20. package/dist/cli/ui/hooks/useProfiles.js +136 -0
  21. package/dist/cli/ui/hooks/useProfiles.js.map +1 -0
  22. package/dist/cli/ui/types.d.ts +1 -1
  23. package/dist/cli/ui/types.d.ts.map +1 -1
  24. package/dist/client/assets/{index-DPWWHorC.js → index-f5D2XwDh.js} +12 -12
  25. package/dist/client/index.html +1 -1
  26. package/dist/config/profiles.d.ts +94 -0
  27. package/dist/config/profiles.d.ts.map +1 -0
  28. package/dist/config/profiles.js +287 -0
  29. package/dist/config/profiles.js.map +1 -0
  30. package/dist/config/tools.d.ts +10 -0
  31. package/dist/config/tools.d.ts.map +1 -1
  32. package/dist/config/tools.js +19 -2
  33. package/dist/config/tools.js.map +1 -1
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +7 -44
  36. package/dist/index.js.map +1 -1
  37. package/dist/types/profiles.d.ts +54 -0
  38. package/dist/types/profiles.d.ts.map +1 -0
  39. package/dist/types/profiles.js +33 -0
  40. package/dist/types/profiles.js.map +1 -0
  41. package/dist/web/client/src/components/ui/alert.d.ts +1 -1
  42. package/dist/web/client/src/components/ui/badge.d.ts +1 -1
  43. package/dist/web/client/src/pages/BranchDetailPage.d.ts.map +1 -1
  44. package/dist/web/client/src/pages/BranchDetailPage.js +49 -52
  45. package/dist/web/client/src/pages/BranchDetailPage.js.map +1 -1
  46. package/dist/web/server/tray.d.ts +1 -0
  47. package/dist/web/server/tray.d.ts.map +1 -1
  48. package/dist/web/server/tray.js +27 -8
  49. package/dist/web/server/tray.js.map +1 -1
  50. package/package.json +4 -3
  51. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +1 -1
  52. package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +0 -14
  53. package/src/cli/ui/components/App.tsx +49 -36
  54. package/src/cli/ui/components/parts/Header.tsx +16 -1
  55. package/src/cli/ui/components/screens/BranchListScreen.tsx +11 -16
  56. package/src/cli/ui/components/screens/EnvironmentProfileScreen.tsx +924 -0
  57. package/src/cli/ui/hooks/useProfiles.ts +211 -0
  58. package/src/cli/ui/types.ts +2 -1
  59. package/src/config/profiles.ts +362 -0
  60. package/src/config/tools.ts +20 -2
  61. package/src/index.ts +7 -49
  62. package/src/types/profiles.ts +64 -0
  63. package/src/web/client/src/pages/BranchDetailPage.tsx +60 -64
  64. package/src/web/server/tray.ts +43 -16
@@ -0,0 +1,64 @@
1
+ /**
2
+ * 環境変数プロファイル型定義
3
+ *
4
+ * プロファイル機能で使用する型を定義します。
5
+ * @see specs/SPEC-dafff079/spec.md
6
+ */
7
+
8
+ /**
9
+ * 環境変数プロファイル
10
+ *
11
+ * 環境変数のセットを表します。
12
+ */
13
+ export interface EnvironmentProfile {
14
+ /** プロファイルの表示名 */
15
+ displayName: string;
16
+ /** プロファイルの説明(オプション) */
17
+ description?: string;
18
+ /** 環境変数のキーバリューペア */
19
+ env: Record<string, string>;
20
+ }
21
+
22
+ /**
23
+ * プロファイル設定
24
+ *
25
+ * 全プロファイルを管理する設定です。
26
+ * ~/.gwt/profiles.yaml に保存されます。
27
+ */
28
+ export interface ProfilesConfig {
29
+ /** 設定ファイルのバージョン */
30
+ version: string;
31
+ /** 現在アクティブなプロファイル名(nullの場合はプロファイルなし) */
32
+ activeProfile: string | null;
33
+ /** プロファイルの辞書(キー: プロファイル名、値: プロファイル設定) */
34
+ profiles: Record<string, EnvironmentProfile>;
35
+ }
36
+
37
+ /**
38
+ * デフォルトのプロファイル設定
39
+ *
40
+ * profiles.yamlが存在しない場合に使用されます。
41
+ * 不変オブジェクトとして扱われるため、変更は禁止されています。
42
+ */
43
+ export const DEFAULT_PROFILES_CONFIG: Readonly<ProfilesConfig> = Object.freeze({
44
+ version: "1.0",
45
+ activeProfile: null,
46
+ profiles: Object.freeze({}),
47
+ });
48
+
49
+ /**
50
+ * プロファイル名のバリデーションパターン
51
+ *
52
+ * 小文字英数字とハイフンのみを許可し、先頭と末尾は英数字でなければなりません。
53
+ */
54
+ export const PROFILE_NAME_PATTERN = /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/;
55
+
56
+ /**
57
+ * プロファイル名をバリデート
58
+ *
59
+ * @param name - 検証するプロファイル名
60
+ * @returns バリデーション結果
61
+ */
62
+ export function isValidProfileName(name: string): boolean {
63
+ return name.length > 0 && PROFILE_NAME_PATTERN.test(name);
64
+ }
@@ -88,6 +88,66 @@ export function BranchDetailPage() {
88
88
  };
89
89
  }, [isTerminalFullscreen]);
90
90
 
91
+ // Available tools - must be before conditional returns
92
+ const customTools: CustomAITool[] = config?.tools ?? [];
93
+ const availableTools: SelectableTool[] = useMemo(
94
+ () => [
95
+ { id: "claude-code", label: "Claude Code", target: "claude" },
96
+ { id: "codex-cli", label: "Codex CLI", target: "codex" },
97
+ ...customTools.map(
98
+ (tool): SelectableTool => ({
99
+ id: tool.id,
100
+ label: tool.displayName,
101
+ target: "custom" as const,
102
+ definition: tool,
103
+ }),
104
+ ),
105
+ ],
106
+ [customTools],
107
+ );
108
+
109
+ // Ensure selected tool is valid - must be before conditional returns
110
+ useEffect(() => {
111
+ if (!availableTools.length) {
112
+ setSelectedToolId("claude-code");
113
+ return;
114
+ }
115
+ if (!availableTools.find((tool) => tool.id === selectedToolId)) {
116
+ const first = availableTools[0];
117
+ if (first) setSelectedToolId(first.id);
118
+ }
119
+ }, [availableTools, selectedToolId]);
120
+
121
+ // Branch sessions - must be before conditional returns
122
+ const branchSessions = useMemo(() => {
123
+ return (sessionsData ?? [])
124
+ .filter((session) => session.worktreePath === branch?.worktreePath)
125
+ .sort((a, b) => (b.startedAt ?? "").localeCompare(a.startedAt ?? ""));
126
+ }, [sessionsData, branch?.worktreePath]);
127
+
128
+ // Latest tool usage - must be before conditional returns
129
+ const latestToolUsage: LastToolUsage | null = useMemo(() => {
130
+ if (!branch) return null;
131
+ if (branch.lastToolUsage) return branch.lastToolUsage;
132
+ const first = branchSessions[0];
133
+ if (!first) return null;
134
+ return {
135
+ branch: branch.name,
136
+ worktreePath: branch.worktreePath ?? null,
137
+ toolId:
138
+ first.toolType === "custom"
139
+ ? (first.toolName ?? "custom")
140
+ : (first.toolType as LastToolUsage["toolId"]),
141
+ toolLabel:
142
+ first.toolType === "custom"
143
+ ? (first.toolName ?? "Custom")
144
+ : toolLabel(first.toolType),
145
+ mode: first.mode ?? "normal",
146
+ model: null,
147
+ timestamp: first.startedAt ? Date.parse(first.startedAt) : Date.now(),
148
+ };
149
+ }, [branch, branchSessions]);
150
+
91
151
  // Loading state
92
152
  if (isLoading) {
93
153
  return (
@@ -162,74 +222,10 @@ export function BranchDetailPage() {
162
222
  );
163
223
  const isSyncingBranch = syncBranch.isPending;
164
224
 
165
- // Available tools
166
- const customTools: CustomAITool[] = config?.tools ?? [];
167
- const availableTools: SelectableTool[] = useMemo(
168
- () => [
169
- { id: "claude-code", label: "Claude Code", target: "claude" },
170
- { id: "codex-cli", label: "Codex CLI", target: "codex" },
171
- ...customTools.map(
172
- (tool): SelectableTool => ({
173
- id: tool.id,
174
- label: tool.displayName,
175
- target: "custom" as const,
176
- definition: tool,
177
- }),
178
- ),
179
- ],
180
- [customTools],
181
- );
182
-
183
- // Ensure selected tool is valid
184
- useEffect(() => {
185
- if (!availableTools.length) {
186
- setSelectedToolId("claude-code");
187
- return;
188
- }
189
- if (!availableTools.find((tool) => tool.id === selectedToolId)) {
190
- const first = availableTools[0];
191
- if (first) setSelectedToolId(first.id);
192
- }
193
- }, [availableTools, selectedToolId]);
194
-
195
225
  const selectedTool = availableTools.find(
196
226
  (tool) => tool.id === selectedToolId,
197
227
  );
198
228
 
199
- // Branch sessions
200
- const branchSessions = useMemo(() => {
201
- return (sessionsData ?? [])
202
- .filter((session) => session.worktreePath === branch?.worktreePath)
203
- .sort((a, b) => (b.startedAt ?? "").localeCompare(a.startedAt ?? ""));
204
- }, [sessionsData, branch?.worktreePath]);
205
-
206
- // Latest tool usage
207
- const latestToolUsage: LastToolUsage | null = useMemo(() => {
208
- if (branch?.lastToolUsage) return branch.lastToolUsage;
209
- const first = branchSessions[0];
210
- if (!first) return null;
211
- return {
212
- branch: branch.name,
213
- worktreePath: branch.worktreePath ?? null,
214
- toolId:
215
- first.toolType === "custom"
216
- ? (first.toolName ?? "custom")
217
- : (first.toolType as LastToolUsage["toolId"]),
218
- toolLabel:
219
- first.toolType === "custom"
220
- ? (first.toolName ?? "Custom")
221
- : toolLabel(first.toolType),
222
- mode: first.mode ?? "normal",
223
- model: null,
224
- timestamp: first.startedAt ? Date.parse(first.startedAt) : Date.now(),
225
- };
226
- }, [
227
- branch?.lastToolUsage,
228
- branch?.name,
229
- branch?.worktreePath,
230
- branchSessions,
231
- ]);
232
-
233
229
  // Handlers
234
230
  const handleCreateWorktree = async () => {
235
231
  try {
@@ -11,15 +11,18 @@ const TRAY_ICON_BASE64 =
11
11
  "iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAJ1BMVEUAAAAAvNQAvNQAvNQAvNQAvNQAvNQAvNQAvNQAvNQAvNQAvNT////J1ubyAAAAC3RSTlMAJYTcgyQJnJ3U3WXfUogAAAABYktHRAyBs1FjAAAAB3RJTUUH6QwMCRccbOpRBQAAAFdJREFUCNdjYEAARuXNriCarXr37t1tQEYmkN69M4GBoRvE2F3AwAqmd29kYIEwNjEwQxibGbghjN0MXDARJpgaqK6tDAzVUHMQJoPtKgPZyuq8S5WBAQBeRj51tvdhawAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyNS0xMi0xMlQwOToyMzoyOCswMDowMBPEA5UAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjUtMTItMTJUMDk6MjM6MjgrMDA6MDBimbspAAAAAElFTkSuQmCC";
12
12
 
13
13
  let trayInitAttempted = false;
14
- let trayInstance: { dispose?: () => void } | null = null;
14
+ type TrayHandle = { dispose?: () => void; kill?: () => void };
15
+ let trayInstance: TrayHandle | null = null;
16
+ let trayInitPromise: Promise<TrayHandle> | null = null;
15
17
 
16
- function shouldEnableTray(): boolean {
18
+ function shouldEnableTray(
19
+ platform: NodeJS.Platform = process.platform,
20
+ ): boolean {
21
+ // NOTE: `trayicon` is a win32-only dependency.
22
+ if (platform !== "win32") return false;
17
23
  if (process.env.GWT_DISABLE_TRAY?.toLowerCase() === "true") return false;
18
24
  if (process.env.GWT_DISABLE_TRAY === "1") return false;
19
25
  if (process.env.CI) return false;
20
- if (process.platform === "linux") {
21
- if (!process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return false;
22
- }
23
26
  return true;
24
27
  }
25
28
 
@@ -52,9 +55,9 @@ export async function openUrl(url: string): Promise<void> {
52
55
  */
53
56
  export async function startSystemTray(
54
57
  url: string,
55
- opts?: { openUrl?: OpenUrlFn },
58
+ opts?: { openUrl?: OpenUrlFn; platform?: NodeJS.Platform },
56
59
  ): Promise<void> {
57
- if (trayInitAttempted || !shouldEnableTray()) return;
60
+ if (trayInitAttempted || !shouldEnableTray(opts?.platform)) return;
58
61
  trayInitAttempted = true;
59
62
 
60
63
  const logger = createLogger({ category: "tray" });
@@ -71,14 +74,31 @@ export async function startSystemTray(
71
74
  const icon = Buffer.from(TRAY_ICON_BASE64, "base64");
72
75
  const open = opts?.openUrl ?? openUrl;
73
76
 
74
- trayInstance = create({
75
- icon,
76
- title: "gwt Web UI",
77
- tooltip: "Double-click to open Web UI",
78
- action: async () => {
79
- await open(url);
80
- },
81
- });
77
+ const initPromise = Promise.resolve(
78
+ create({
79
+ icon,
80
+ title: "gwt Web UI",
81
+ tooltip: "Double-click to open Web UI",
82
+ action: async () => {
83
+ await open(url);
84
+ },
85
+ }) as TrayHandle,
86
+ );
87
+ trayInitPromise = initPromise;
88
+
89
+ void initPromise
90
+ .then((tray) => {
91
+ if (trayInitPromise !== initPromise) {
92
+ tray.dispose?.();
93
+ tray.kill?.();
94
+ return;
95
+ }
96
+ trayInstance = tray;
97
+ })
98
+ .catch((err) => {
99
+ if (trayInitPromise !== initPromise) return;
100
+ logger.warn({ err }, "System tray failed to initialize");
101
+ });
82
102
  } catch (err) {
83
103
  logger.warn({ err }, "System tray failed to initialize");
84
104
  }
@@ -88,6 +108,13 @@ export async function startSystemTray(
88
108
  * システムトレイアイコンを破棄
89
109
  */
90
110
  export function disposeSystemTray(): void {
91
- trayInstance?.dispose?.();
111
+ trayInitPromise = null;
112
+
113
+ const instance = trayInstance;
92
114
  trayInstance = null;
115
+
116
+ instance?.dispose?.();
117
+ instance?.kill?.();
118
+
119
+ trayInitAttempted = false;
93
120
  }