@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.
- package/README.ja.md +1 -1
- package/README.md +1 -1
- package/dist/claude.js +1 -1
- package/dist/claude.js.map +1 -1
- package/dist/cli/ui/components/App.js +1 -1
- package/dist/cli/ui/components/App.js.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.js +8 -7
- package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/LogDatePickerScreen.js +1 -1
- package/dist/cli/ui/components/screens/LogDatePickerScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/LogDetailScreen.js +1 -1
- package/dist/cli/ui/components/screens/LogDetailScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/LogListScreen.js +1 -1
- package/dist/cli/ui/components/screens/LogListScreen.js.map +1 -1
- package/dist/cli/ui/utils/branchFormatter.d.ts +5 -0
- package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
- package/dist/cli/ui/utils/branchFormatter.js +18 -5
- package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
- package/dist/codex.d.ts.map +1 -1
- package/dist/codex.js +0 -1
- package/dist/codex.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +3 -7
- package/dist/config/index.js.map +1 -1
- package/dist/config/profiles.d.ts +2 -2
- package/dist/config/profiles.d.ts.map +1 -1
- package/dist/config/profiles.js +4 -7
- package/dist/config/profiles.js.map +1 -1
- package/dist/config/tools.d.ts +1 -1
- package/dist/config/tools.d.ts.map +1 -1
- package/dist/config/tools.js +3 -43
- package/dist/config/tools.js.map +1 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +1 -2
- package/dist/gemini.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +106 -90
- package/dist/index.js.map +1 -1
- package/dist/utils/command.d.ts +11 -0
- package/dist/utils/command.d.ts.map +1 -1
- package/dist/utils/command.js +33 -0
- package/dist/utils/command.js.map +1 -1
- package/dist/web/client/src/pages/ConfigPage.js +1 -1
- package/dist/web/client/src/pages/ConfigPage.js.map +1 -1
- package/package.json +2 -2
- package/src/claude.ts +1 -1
- package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +1 -1
- package/src/cli/ui/__tests__/components/App.test.tsx +65 -3
- package/src/cli/ui/__tests__/components/screens/LogDetailScreen.test.tsx +1 -1
- package/src/cli/ui/__tests__/components/screens/LogListScreen.test.tsx +1 -1
- package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +83 -22
- package/src/cli/ui/__tests__/integration/navigation.test.tsx +57 -37
- package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +105 -0
- package/src/cli/ui/components/App.tsx +1 -1
- package/src/cli/ui/components/screens/BranchListScreen.tsx +9 -7
- package/src/cli/ui/components/screens/LogDatePickerScreen.tsx +1 -1
- package/src/cli/ui/components/screens/LogDetailScreen.tsx +1 -1
- package/src/cli/ui/components/screens/LogListScreen.tsx +1 -1
- package/src/cli/ui/utils/branchFormatter.ts +19 -5
- package/src/codex.ts +0 -1
- package/src/config/index.ts +3 -7
- package/src/config/profiles.ts +4 -7
- package/src/config/tools.ts +3 -56
- package/src/gemini.ts +1 -2
- package/src/index.ts +148 -133
- package/src/utils/command.ts +37 -0
- package/src/web/client/src/pages/ConfigPage.tsx +2 -2
- 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
|
|
361
|
-
|
|
362
|
-
const
|
|
363
|
-
|
|
364
|
-
|
|
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
package/src/config/index.ts
CHANGED
|
@@ -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.
|
|
83
|
-
|
|
84
|
-
|
|
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
|
};
|
package/src/config/profiles.ts
CHANGED
|
@@ -29,16 +29,13 @@ import {
|
|
|
29
29
|
/**
|
|
30
30
|
* 設定ディレクトリのパスを取得
|
|
31
31
|
*
|
|
32
|
-
*
|
|
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
|
-
:
|
|
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
|
|
137
|
+
* 実行中に環境変数(GWT_HOME)を変更しても反映されません。
|
|
141
138
|
*/
|
|
142
139
|
export const PROFILES_CONFIG_PATH = getProfilesConfigPath();
|
|
143
140
|
|
package/src/config/tools.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
-
:
|
|
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"]
|
|
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 {
|
|
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
|
|
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
|
|
558
|
-
// is interrupted or killed mid-run (e.g., Ctrl+C).
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
mode
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
mode
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
? "
|
|
670
|
-
: "
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
|
783
|
-
|
|
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 =
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
814
|
-
|
|
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) {
|
package/src/utils/command.ts
CHANGED
|
@@ -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
|
-
|
|
278
|
+
CLI と Web UI は同じ設定を参照します。更新すると ~/.gwt/tools.json
|
|
279
|
+
に保存されます。
|
|
280
280
|
</p>
|
|
281
281
|
</CardHeader>
|
|
282
282
|
<CardContent>
|