@akiojin/gwt 4.6.1 → 4.8.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/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +6 -2
- package/dist/claude.js.map +1 -1
- package/dist/cli/ui/components/App.d.ts.map +1 -1
- package/dist/cli/ui/components/App.js +103 -2
- package/dist/cli/ui/components/App.js.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts +1 -0
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.js +11 -8
- package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
- package/dist/cli/ui/components/screens/LogDatePickerScreen.d.ts +10 -0
- package/dist/cli/ui/components/screens/LogDatePickerScreen.d.ts.map +1 -0
- package/dist/cli/ui/components/screens/LogDatePickerScreen.js +44 -0
- package/dist/cli/ui/components/screens/LogDatePickerScreen.js.map +1 -0
- package/dist/cli/ui/components/screens/LogDetailScreen.d.ts +14 -0
- package/dist/cli/ui/components/screens/LogDetailScreen.d.ts.map +1 -0
- package/dist/cli/ui/components/screens/LogDetailScreen.js +34 -0
- package/dist/cli/ui/components/screens/LogDetailScreen.js.map +1 -0
- package/dist/cli/ui/components/screens/LogListScreen.d.ts +19 -0
- package/dist/cli/ui/components/screens/LogListScreen.d.ts.map +1 -0
- package/dist/cli/ui/components/screens/LogListScreen.js +107 -0
- package/dist/cli/ui/components/screens/LogListScreen.js.map +1 -0
- package/dist/cli/ui/hooks/useGitData.d.ts.map +1 -1
- package/dist/cli/ui/hooks/useGitData.js +10 -3
- package/dist/cli/ui/hooks/useGitData.js.map +1 -1
- package/dist/cli/ui/types.d.ts +1 -1
- package/dist/cli/ui/types.d.ts.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/cli/ui/utils/clipboard.d.ts +7 -0
- package/dist/cli/ui/utils/clipboard.d.ts.map +1 -0
- package/dist/cli/ui/utils/clipboard.js +21 -0
- package/dist/cli/ui/utils/clipboard.js.map +1 -0
- package/dist/codex.d.ts.map +1 -1
- package/dist/codex.js +0 -1
- package/dist/codex.js.map +1 -1
- package/dist/gemini.d.ts.map +1 -1
- package/dist/gemini.js +6 -3
- package/dist/gemini.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +104 -81
- package/dist/index.js.map +1 -1
- package/dist/logging/formatter.d.ts +15 -0
- package/dist/logging/formatter.d.ts.map +1 -0
- package/dist/logging/formatter.js +81 -0
- package/dist/logging/formatter.js.map +1 -0
- package/dist/logging/reader.d.ts +12 -0
- package/dist/logging/reader.d.ts.map +1 -0
- package/dist/logging/reader.js +63 -0
- package/dist/logging/reader.js.map +1 -0
- package/dist/worktree.d.ts.map +1 -1
- package/dist/worktree.js +57 -0
- package/dist/worktree.js.map +1 -1
- package/package.json +2 -2
- package/src/claude.ts +7 -2
- package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +8 -4
- package/src/cli/ui/__tests__/components/App.test.tsx +65 -3
- package/src/cli/ui/__tests__/components/common/Select.test.tsx +17 -11
- package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +28 -2
- package/src/cli/ui/__tests__/components/screens/LogDetailScreen.test.tsx +57 -0
- package/src/cli/ui/__tests__/components/screens/LogListScreen.test.tsx +102 -0
- package/src/cli/ui/__tests__/hooks/useGitData.test.ts +197 -0
- package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +84 -13
- 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/__tests__/utils/clipboard.test.ts +65 -0
- package/src/cli/ui/components/App.tsx +178 -1
- package/src/cli/ui/components/screens/BranchListScreen.tsx +11 -6
- package/src/cli/ui/components/screens/LogDatePickerScreen.tsx +83 -0
- package/src/cli/ui/components/screens/LogDetailScreen.tsx +67 -0
- package/src/cli/ui/components/screens/LogListScreen.tsx +192 -0
- package/src/cli/ui/hooks/useGitData.ts +12 -3
- package/src/cli/ui/types.ts +3 -0
- package/src/cli/ui/utils/branchFormatter.ts +19 -5
- package/src/cli/ui/utils/clipboard.ts +31 -0
- package/src/codex.ts +0 -1
- package/src/gemini.ts +7 -3
- package/src/index.ts +147 -123
- package/src/logging/formatter.ts +106 -0
- package/src/logging/reader.ts +76 -0
- package/src/worktree.ts +77 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useState, useEffect, useCallback } from "react";
|
|
1
|
+
import { useState, useEffect, useCallback, useRef } from "react";
|
|
2
2
|
import {
|
|
3
3
|
getAllBranches,
|
|
4
4
|
hasUnpushedCommitsInRepo,
|
|
@@ -80,7 +80,15 @@ export function useGitData(options?: UseGitDataOptions): UseGitDataResult {
|
|
|
80
80
|
const [error, setError] = useState<Error | null>(null);
|
|
81
81
|
const [lastUpdated, setLastUpdated] = useState<Date | null>(null);
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
// キャッシュ機構: 初回ロード完了を追跡(useRef で再レンダリングを防ぐ)
|
|
84
|
+
const hasLoadedOnceRef = useRef(false);
|
|
85
|
+
|
|
86
|
+
const loadData = useCallback(async (forceRefresh = false) => {
|
|
87
|
+
// キャッシュがあり、強制リフレッシュでなければスキップ
|
|
88
|
+
if (hasLoadedOnceRef.current && !forceRefresh) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
84
92
|
setLoading(true);
|
|
85
93
|
setError(null);
|
|
86
94
|
|
|
@@ -295,6 +303,7 @@ export function useGitData(options?: UseGitDataOptions): UseGitDataResult {
|
|
|
295
303
|
|
|
296
304
|
setBranches(enrichedBranches);
|
|
297
305
|
setLastUpdated(new Date());
|
|
306
|
+
hasLoadedOnceRef.current = true; // 初回ロード完了をマーク
|
|
298
307
|
} catch (err) {
|
|
299
308
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|
300
309
|
setBranches([]);
|
|
@@ -305,7 +314,7 @@ export function useGitData(options?: UseGitDataOptions): UseGitDataResult {
|
|
|
305
314
|
}, []);
|
|
306
315
|
|
|
307
316
|
const refresh = useCallback(() => {
|
|
308
|
-
loadData();
|
|
317
|
+
loadData(true); // forceRefresh = true で強制的にデータを再取得
|
|
309
318
|
}, [loadData]);
|
|
310
319
|
|
|
311
320
|
useEffect(() => {
|
package/src/cli/ui/types.ts
CHANGED
|
@@ -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
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { execa } from "execa";
|
|
2
|
+
|
|
3
|
+
export interface ClipboardOptions {
|
|
4
|
+
platform?: NodeJS.Platform;
|
|
5
|
+
execa?: typeof execa;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export async function copyToClipboard(
|
|
9
|
+
text: string,
|
|
10
|
+
options: ClipboardOptions = {},
|
|
11
|
+
): Promise<void> {
|
|
12
|
+
const runner = options.execa ?? execa;
|
|
13
|
+
const platform = options.platform ?? process.platform;
|
|
14
|
+
|
|
15
|
+
if (platform === "win32") {
|
|
16
|
+
await runner("cmd", ["/c", "clip"], { input: text, windowsHide: true });
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (platform === "darwin") {
|
|
21
|
+
await runner("pbcopy", [], { input: text });
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
await runner("xclip", ["-selection", "clipboard"], { input: text });
|
|
27
|
+
return;
|
|
28
|
+
} catch {
|
|
29
|
+
await runner("xsel", ["--clipboard", "--input"], { input: text });
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/codex.ts
CHANGED
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,
|
|
@@ -201,7 +200,12 @@ export async function launchGeminiCLI(
|
|
|
201
200
|
);
|
|
202
201
|
console.log(chalk.yellow(" npm install -g @google/gemini-cli"));
|
|
203
202
|
console.log("");
|
|
204
|
-
|
|
203
|
+
const shouldSkipDelay =
|
|
204
|
+
typeof process !== "undefined" &&
|
|
205
|
+
(process.env?.NODE_ENV === "test" || Boolean(process.env?.VITEST));
|
|
206
|
+
if (!shouldSkipDelay) {
|
|
207
|
+
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
208
|
+
}
|
|
205
209
|
return await run("bunx", [GEMINI_CLI_PACKAGE, ...runArgs]);
|
|
206
210
|
};
|
|
207
211
|
|
|
@@ -323,7 +327,7 @@ export async function launchGeminiCLI(
|
|
|
323
327
|
*/
|
|
324
328
|
export async function isGeminiCLIAvailable(): Promise<boolean> {
|
|
325
329
|
try {
|
|
326
|
-
await execa("bunx", [GEMINI_CLI_PACKAGE, "--version"]
|
|
330
|
+
await execa("bunx", [GEMINI_CLI_PACKAGE, "--version"]);
|
|
327
331
|
return true;
|
|
328
332
|
} catch (error: unknown) {
|
|
329
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)
|
|
@@ -777,8 +805,12 @@ export async function handleAIToolWorkflow(
|
|
|
777
805
|
if (hasUncommitted) {
|
|
778
806
|
const uncommittedCount = await getUncommittedChangesCount(worktreePath);
|
|
779
807
|
const countLabel =
|
|
780
|
-
uncommittedCount > 0
|
|
781
|
-
|
|
808
|
+
uncommittedCount > 0
|
|
809
|
+
? ` (${uncommittedCount} ${
|
|
810
|
+
uncommittedCount === 1 ? "change" : "changes"
|
|
811
|
+
})`
|
|
812
|
+
: "";
|
|
813
|
+
printWarning(`Uncommitted changes detected${countLabel}.`);
|
|
782
814
|
}
|
|
783
815
|
|
|
784
816
|
if (hasUnpushed) {
|
|
@@ -786,27 +818,19 @@ export async function handleAIToolWorkflow(
|
|
|
786
818
|
worktreePath,
|
|
787
819
|
branch,
|
|
788
820
|
);
|
|
789
|
-
const countLabel =
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
try {
|
|
797
|
-
await pushBranchToRemote(worktreePath, branch);
|
|
798
|
-
printInfo(`Push completed for ${branch}.`);
|
|
799
|
-
} catch (error) {
|
|
800
|
-
const details =
|
|
801
|
-
error instanceof Error ? error.message : String(error);
|
|
802
|
-
printWarning(`Push failed for ${branch}: ${details}`);
|
|
803
|
-
}
|
|
804
|
-
}
|
|
821
|
+
const countLabel =
|
|
822
|
+
unpushedCount > 0
|
|
823
|
+
? ` (${unpushedCount} ${
|
|
824
|
+
unpushedCount === 1 ? "commit" : "commits"
|
|
825
|
+
})`
|
|
826
|
+
: "";
|
|
827
|
+
printWarning(`Unpushed commits detected${countLabel}.`);
|
|
805
828
|
}
|
|
806
829
|
} catch (error) {
|
|
807
830
|
const details = error instanceof Error ? error.message : String(error);
|
|
808
831
|
printWarning(`Failed to check git status after session: ${details}`);
|
|
809
832
|
}
|
|
833
|
+
|
|
810
834
|
// Small buffer before returning to branch list to avoid abrupt screen swap
|
|
811
835
|
await new Promise((resolve) => setTimeout(resolve, POST_SESSION_DELAY_MS));
|
|
812
836
|
printInfo("Session completed successfully. Returning to main menu...");
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
export interface FormattedLogEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
raw: Record<string, unknown>;
|
|
4
|
+
timestamp: number | null;
|
|
5
|
+
timeLabel: string;
|
|
6
|
+
levelLabel: string;
|
|
7
|
+
category: string;
|
|
8
|
+
message: string;
|
|
9
|
+
summary: string;
|
|
10
|
+
json: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const LEVEL_LABELS: Record<number, string> = {
|
|
14
|
+
10: "TRACE",
|
|
15
|
+
20: "DEBUG",
|
|
16
|
+
30: "INFO",
|
|
17
|
+
40: "WARN",
|
|
18
|
+
50: "ERROR",
|
|
19
|
+
60: "FATAL",
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const formatTimeLabel = (
|
|
23
|
+
value: unknown,
|
|
24
|
+
): { label: string; timestamp: number | null } => {
|
|
25
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
26
|
+
const date = new Date(value);
|
|
27
|
+
if (!Number.isNaN(date.getTime())) {
|
|
28
|
+
const hours = String(date.getHours()).padStart(2, "0");
|
|
29
|
+
const minutes = String(date.getMinutes()).padStart(2, "0");
|
|
30
|
+
const seconds = String(date.getSeconds()).padStart(2, "0");
|
|
31
|
+
return {
|
|
32
|
+
label: `${hours}:${minutes}:${seconds}`,
|
|
33
|
+
timestamp: date.getTime(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return { label: "--:--:--", timestamp: null };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const formatLevelLabel = (value: unknown): string => {
|
|
42
|
+
if (typeof value === "number") {
|
|
43
|
+
return LEVEL_LABELS[value] ?? `LEVEL-${value}`;
|
|
44
|
+
}
|
|
45
|
+
if (typeof value === "string") {
|
|
46
|
+
return value.toUpperCase();
|
|
47
|
+
}
|
|
48
|
+
return "UNKNOWN";
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const resolveMessage = (entry: Record<string, unknown>): string => {
|
|
52
|
+
if (typeof entry.msg === "string") {
|
|
53
|
+
return entry.msg;
|
|
54
|
+
}
|
|
55
|
+
if (typeof entry.message === "string") {
|
|
56
|
+
return entry.message;
|
|
57
|
+
}
|
|
58
|
+
if (entry.msg !== undefined) {
|
|
59
|
+
return String(entry.msg);
|
|
60
|
+
}
|
|
61
|
+
return "";
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export function parseLogLines(
|
|
65
|
+
lines: string[],
|
|
66
|
+
options: { limit?: number } = {},
|
|
67
|
+
): FormattedLogEntry[] {
|
|
68
|
+
const entries: FormattedLogEntry[] = [];
|
|
69
|
+
|
|
70
|
+
lines.forEach((line, index) => {
|
|
71
|
+
if (!line.trim()) return;
|
|
72
|
+
try {
|
|
73
|
+
const parsed = JSON.parse(line) as Record<string, unknown>;
|
|
74
|
+
const { label: timeLabel, timestamp } = formatTimeLabel(parsed.time);
|
|
75
|
+
const levelLabel = formatLevelLabel(parsed.level);
|
|
76
|
+
const category =
|
|
77
|
+
typeof parsed.category === "string" ? parsed.category : "unknown";
|
|
78
|
+
const message = resolveMessage(parsed);
|
|
79
|
+
const summary =
|
|
80
|
+
`[${timeLabel}] [${levelLabel}] [${category}] ${message}`.trim();
|
|
81
|
+
const json = JSON.stringify(parsed, null, 2);
|
|
82
|
+
const id = `${timestamp ?? "unknown"}-${index}`;
|
|
83
|
+
|
|
84
|
+
entries.push({
|
|
85
|
+
id,
|
|
86
|
+
raw: parsed,
|
|
87
|
+
timestamp,
|
|
88
|
+
timeLabel,
|
|
89
|
+
levelLabel,
|
|
90
|
+
category,
|
|
91
|
+
message,
|
|
92
|
+
summary,
|
|
93
|
+
json,
|
|
94
|
+
});
|
|
95
|
+
} catch {
|
|
96
|
+
// Skip malformed JSON lines
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
const limit = options.limit ?? 100;
|
|
101
|
+
if (entries.length <= limit) {
|
|
102
|
+
return [...entries].reverse();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return entries.slice(-limit).reverse();
|
|
106
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import { formatDate } from "./logger.js";
|
|
5
|
+
|
|
6
|
+
export interface LogFileInfo {
|
|
7
|
+
date: string;
|
|
8
|
+
path: string;
|
|
9
|
+
mtimeMs: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const LOG_FILENAME_PATTERN = /^\d{4}-\d{2}-\d{2}\.jsonl$/;
|
|
13
|
+
|
|
14
|
+
export function resolveLogDir(cwd: string = process.cwd()): string {
|
|
15
|
+
const cwdBase = path.basename(cwd) || "workspace";
|
|
16
|
+
return path.join(os.homedir(), ".gwt", "logs", cwdBase);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function buildLogFilePath(logDir: string, date: string): string {
|
|
20
|
+
return path.join(logDir, `${date}.jsonl`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getTodayLogDate(): string {
|
|
24
|
+
return formatDate(new Date());
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function readLogFileLines(filePath: string): Promise<string[]> {
|
|
28
|
+
try {
|
|
29
|
+
const content = await fs.readFile(filePath, "utf-8");
|
|
30
|
+
return content.split("\n").filter(Boolean);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
const err = error as NodeJS.ErrnoException;
|
|
33
|
+
if (err.code === "ENOENT") {
|
|
34
|
+
return [];
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function listLogFiles(logDir: string): Promise<LogFileInfo[]> {
|
|
41
|
+
try {
|
|
42
|
+
const entries = await fs.readdir(logDir, { withFileTypes: true });
|
|
43
|
+
const files: LogFileInfo[] = [];
|
|
44
|
+
|
|
45
|
+
for (const entry of entries) {
|
|
46
|
+
if (!entry.isFile()) continue;
|
|
47
|
+
if (!LOG_FILENAME_PATTERN.test(entry.name)) continue;
|
|
48
|
+
|
|
49
|
+
const date = entry.name.replace(/\.jsonl$/, "");
|
|
50
|
+
const fullPath = path.join(logDir, entry.name);
|
|
51
|
+
try {
|
|
52
|
+
const stat = await fs.stat(fullPath);
|
|
53
|
+
files.push({ date, path: fullPath, mtimeMs: stat.mtimeMs });
|
|
54
|
+
} catch {
|
|
55
|
+
// Ignore stat errors per-file
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return files.sort((a, b) => b.date.localeCompare(a.date));
|
|
60
|
+
} catch (error) {
|
|
61
|
+
const err = error as NodeJS.ErrnoException;
|
|
62
|
+
if (err.code === "ENOENT") {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export async function listRecentLogFiles(
|
|
70
|
+
logDir: string,
|
|
71
|
+
days = 7,
|
|
72
|
+
): Promise<LogFileInfo[]> {
|
|
73
|
+
const files = await listLogFiles(logDir);
|
|
74
|
+
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
|
|
75
|
+
return files.filter((file) => file.mtimeMs >= cutoff);
|
|
76
|
+
}
|