@akiojin/gwt 2.14.0 → 3.0.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.
- package/README.ja.md +10 -2
- package/README.md +9 -2
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.js +2 -14
- package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
- package/dist/client/assets/{index-DPWWHorC.js → index-f5D2XwDh.js} +12 -12
- package/dist/client/index.html +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -44
- package/dist/index.js.map +1 -1
- package/dist/web/client/src/pages/BranchDetailPage.d.ts.map +1 -1
- package/dist/web/client/src/pages/BranchDetailPage.js +49 -52
- package/dist/web/client/src/pages/BranchDetailPage.js.map +1 -1
- package/dist/web/server/tray.d.ts +1 -0
- package/dist/web/server/tray.d.ts.map +1 -1
- package/dist/web/server/tray.js +27 -8
- package/dist/web/server/tray.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +1 -1
- package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +0 -14
- package/src/cli/ui/components/screens/BranchListScreen.tsx +2 -15
- package/src/index.ts +7 -49
- package/src/web/client/src/pages/BranchDetailPage.tsx +60 -64
- package/src/web/server/tray.ts +43 -16
package/src/index.ts
CHANGED
|
@@ -33,7 +33,6 @@ import {
|
|
|
33
33
|
getTerminalStreams,
|
|
34
34
|
waitForUserAcknowledgement,
|
|
35
35
|
} from "./utils/terminal.js";
|
|
36
|
-
import { resolveWebUiPort, isPortInUse } from "./utils/webui.js";
|
|
37
36
|
import { createLogger } from "./logging/logger.js";
|
|
38
37
|
import { getToolById, getSharedEnvironment } from "./config/tools.js";
|
|
39
38
|
import { launchCustomAITool } from "./launcher.js";
|
|
@@ -204,7 +203,10 @@ function showHelp(): void {
|
|
|
204
203
|
console.log(`
|
|
205
204
|
Worktree Manager
|
|
206
205
|
|
|
207
|
-
Usage: gwt [options]
|
|
206
|
+
Usage: gwt [command] [options]
|
|
207
|
+
|
|
208
|
+
Commands:
|
|
209
|
+
serve Start Web UI server (http://localhost:3000)
|
|
208
210
|
|
|
209
211
|
Options:
|
|
210
212
|
-h, --help Show this help message
|
|
@@ -212,7 +214,8 @@ Options:
|
|
|
212
214
|
|
|
213
215
|
Description:
|
|
214
216
|
Interactive Git worktree manager with AI tool selection (Claude Code / Codex CLI) and graphical branch selection.
|
|
215
|
-
Launch without additional options to open the interactive menu.
|
|
217
|
+
Launch without additional options to open the interactive CLI menu.
|
|
218
|
+
Use 'gwt serve' to start the Web UI server for browser-based management.
|
|
216
219
|
`);
|
|
217
220
|
}
|
|
218
221
|
|
|
@@ -876,52 +879,7 @@ export async function main(): Promise<void> {
|
|
|
876
879
|
process.exit(1);
|
|
877
880
|
}
|
|
878
881
|
|
|
879
|
-
|
|
880
|
-
const port = resolveWebUiPort();
|
|
881
|
-
const portInUse = await isPortInUse(port);
|
|
882
|
-
let webServerHandlePromise: Promise<{
|
|
883
|
-
close: () => Promise<void>;
|
|
884
|
-
} | null> | null = null;
|
|
885
|
-
if (portInUse) {
|
|
886
|
-
printWarning(`Port ${port} is already in use. Skipping Web UI server.`);
|
|
887
|
-
} else {
|
|
888
|
-
const { startWebServer } = await import("./web/server/index.js");
|
|
889
|
-
webServerHandlePromise = startWebServer({ background: true }).catch(
|
|
890
|
-
(err) => {
|
|
891
|
-
appLogger.warn({ err }, "Web UI server failed to start");
|
|
892
|
-
return null;
|
|
893
|
-
},
|
|
894
|
-
);
|
|
895
|
-
printInfo(`Web UI available at http://localhost:${port}`);
|
|
896
|
-
}
|
|
897
|
-
|
|
898
|
-
try {
|
|
899
|
-
await runInteractiveLoop();
|
|
900
|
-
} finally {
|
|
901
|
-
if (webServerHandlePromise) {
|
|
902
|
-
const shutdownTimeoutMs = 2000;
|
|
903
|
-
const handleOrTimeout = await Promise.race([
|
|
904
|
-
webServerHandlePromise,
|
|
905
|
-
new Promise<"timeout">((resolve) => {
|
|
906
|
-
const timer = setTimeout(() => resolve("timeout"), shutdownTimeoutMs);
|
|
907
|
-
timer.unref?.();
|
|
908
|
-
}),
|
|
909
|
-
]);
|
|
910
|
-
|
|
911
|
-
if (handleOrTimeout === "timeout") {
|
|
912
|
-
appLogger.warn(
|
|
913
|
-
{ timeoutMs: shutdownTimeoutMs },
|
|
914
|
-
"Web UI server startup did not finish before shutdown timeout; skipping stop",
|
|
915
|
-
);
|
|
916
|
-
} else if (handleOrTimeout) {
|
|
917
|
-
try {
|
|
918
|
-
await handleOrTimeout.close();
|
|
919
|
-
} catch (err) {
|
|
920
|
-
appLogger.warn({ err }, "Web UI server failed to stop");
|
|
921
|
-
}
|
|
922
|
-
}
|
|
923
|
-
}
|
|
924
|
-
}
|
|
882
|
+
await runInteractiveLoop();
|
|
925
883
|
}
|
|
926
884
|
|
|
927
885
|
// Run the application if this module is executed directly
|
|
@@ -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 {
|
package/src/web/server/tray.ts
CHANGED
|
@@ -11,15 +11,18 @@ const TRAY_ICON_BASE64 =
|
|
|
11
11
|
"iVBORw0KGgoAAAANSUhEUgAAABAAAAAQBAMAAADt3eJSAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAJ1BMVEUAAAAAvNQAvNQAvNQAvNQAvNQAvNQAvNQAvNQAvNQAvNQAvNT////J1ubyAAAAC3RSTlMAJYTcgyQJnJ3U3WXfUogAAAABYktHRAyBs1FjAAAAB3RJTUUH6QwMCRccbOpRBQAAAFdJREFUCNdjYEAARuXNriCarXr37t1tQEYmkN69M4GBoRvE2F3AwAqmd29kYIEwNjEwQxibGbghjN0MXDARJpgaqK6tDAzVUHMQJoPtKgPZyuq8S5WBAQBeRj51tvdhawAAACV0RVh0ZGF0ZTpjcmVhdGUAMjAyNS0xMi0xMlQwOToyMzoyOCswMDowMBPEA5UAAAAldEVYdGRhdGU6bW9kaWZ5ADIwMjUtMTItMTJUMDk6MjM6MjgrMDA6MDBimbspAAAAAElFTkSuQmCC";
|
|
12
12
|
|
|
13
13
|
let trayInitAttempted = false;
|
|
14
|
-
|
|
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(
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
}
|