@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
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from "vitest";
|
|
2
|
+
import { copyToClipboard } from "../../utils/clipboard.js";
|
|
3
|
+
|
|
4
|
+
const execaMock = vi.fn();
|
|
5
|
+
|
|
6
|
+
vi.mock("execa", () => ({
|
|
7
|
+
execa: (...args: unknown[]) => execaMock(...args),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
describe("copyToClipboard", () => {
|
|
11
|
+
it("uses pbcopy on darwin", async () => {
|
|
12
|
+
execaMock.mockResolvedValue({ stdout: "" });
|
|
13
|
+
|
|
14
|
+
await copyToClipboard("hello", {
|
|
15
|
+
platform: "darwin",
|
|
16
|
+
execa: execaMock as unknown as typeof import("execa").execa,
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
expect(execaMock).toHaveBeenCalledWith(
|
|
20
|
+
"pbcopy",
|
|
21
|
+
[],
|
|
22
|
+
expect.objectContaining({ input: "hello" }),
|
|
23
|
+
);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("falls back to xsel when xclip fails", async () => {
|
|
27
|
+
execaMock.mockImplementation((command: string) => {
|
|
28
|
+
if (command === "xclip") {
|
|
29
|
+
return Promise.reject(new Error("missing"));
|
|
30
|
+
}
|
|
31
|
+
return Promise.resolve({ stdout: "" });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
await copyToClipboard("hello", {
|
|
35
|
+
platform: "linux",
|
|
36
|
+
execa: execaMock as unknown as typeof import("execa").execa,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(execaMock).toHaveBeenCalledWith(
|
|
40
|
+
"xclip",
|
|
41
|
+
["-selection", "clipboard"],
|
|
42
|
+
expect.objectContaining({ input: "hello" }),
|
|
43
|
+
);
|
|
44
|
+
expect(execaMock).toHaveBeenCalledWith(
|
|
45
|
+
"xsel",
|
|
46
|
+
["--clipboard", "--input"],
|
|
47
|
+
expect.objectContaining({ input: "hello" }),
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("uses clip on windows", async () => {
|
|
52
|
+
execaMock.mockResolvedValue({ stdout: "" });
|
|
53
|
+
|
|
54
|
+
await copyToClipboard("hello", {
|
|
55
|
+
platform: "win32",
|
|
56
|
+
execa: execaMock as unknown as typeof import("execa").execa,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
expect(execaMock).toHaveBeenCalledWith(
|
|
60
|
+
"cmd",
|
|
61
|
+
["/c", "clip"],
|
|
62
|
+
expect.objectContaining({ input: "hello" }),
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
BranchListScreen,
|
|
12
12
|
type BranchListScreenProps,
|
|
13
13
|
} from "./screens/BranchListScreen.js";
|
|
14
|
+
import { LogListScreen } from "./screens/LogListScreen.js";
|
|
15
|
+
import { LogDetailScreen } from "./screens/LogDetailScreen.js";
|
|
16
|
+
import { LogDatePickerScreen } from "./screens/LogDatePickerScreen.js";
|
|
14
17
|
import { BranchCreatorScreen } from "./screens/BranchCreatorScreen.js";
|
|
15
18
|
import { BranchActionSelectorScreen } from "../screens/BranchActionSelectorScreen.js";
|
|
16
19
|
import { AIToolSelectorScreen } from "./screens/AIToolSelectorScreen.js";
|
|
@@ -29,6 +32,19 @@ import { useScreenState } from "../hooks/useScreenState.js";
|
|
|
29
32
|
import { useToolStatus } from "../hooks/useToolStatus.js";
|
|
30
33
|
import { formatBranchItems } from "../utils/branchFormatter.js";
|
|
31
34
|
import { calculateStatistics } from "../utils/statisticsCalculator.js";
|
|
35
|
+
import { copyToClipboard } from "../utils/clipboard.js";
|
|
36
|
+
import {
|
|
37
|
+
parseLogLines,
|
|
38
|
+
type FormattedLogEntry,
|
|
39
|
+
} from "../../../logging/formatter.js";
|
|
40
|
+
import {
|
|
41
|
+
buildLogFilePath,
|
|
42
|
+
getTodayLogDate,
|
|
43
|
+
listRecentLogFiles,
|
|
44
|
+
readLogFileLines,
|
|
45
|
+
resolveLogDir,
|
|
46
|
+
type LogFileInfo,
|
|
47
|
+
} from "../../../logging/reader.js";
|
|
32
48
|
import type {
|
|
33
49
|
AITool,
|
|
34
50
|
BranchInfo,
|
|
@@ -131,6 +147,26 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
|
|
|
131
147
|
>([]);
|
|
132
148
|
const [branchQuickStartLoading, setBranchQuickStartLoading] = useState(false);
|
|
133
149
|
|
|
150
|
+
// Log viewer state
|
|
151
|
+
const logDir = useMemo(
|
|
152
|
+
() => resolveLogDir(workingDirectory),
|
|
153
|
+
[workingDirectory],
|
|
154
|
+
);
|
|
155
|
+
const [logEntries, setLogEntries] = useState<FormattedLogEntry[]>([]);
|
|
156
|
+
const [logLoading, setLogLoading] = useState(false);
|
|
157
|
+
const [logError, setLogError] = useState<string | null>(null);
|
|
158
|
+
const [logSelectedDate, setLogSelectedDate] = useState<string | null>(
|
|
159
|
+
getTodayLogDate(),
|
|
160
|
+
);
|
|
161
|
+
const [logDates, setLogDates] = useState<LogFileInfo[]>([]);
|
|
162
|
+
const [logSelectedEntry, setLogSelectedEntry] =
|
|
163
|
+
useState<FormattedLogEntry | null>(null);
|
|
164
|
+
const [logNotification, setLogNotification] = useState<{
|
|
165
|
+
message: string;
|
|
166
|
+
tone: "success" | "error";
|
|
167
|
+
} | null>(null);
|
|
168
|
+
const logNotificationTimerRef = useRef<NodeJS.Timeout | null>(null);
|
|
169
|
+
|
|
134
170
|
// Selection state (for branch → tool → mode flow)
|
|
135
171
|
const [selectedBranch, setSelectedBranch] =
|
|
136
172
|
useState<SelectedBranchState | null>(null);
|
|
@@ -182,6 +218,70 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
|
|
|
182
218
|
.catch(() => setRepoRoot(null));
|
|
183
219
|
}, []);
|
|
184
220
|
|
|
221
|
+
const showLogNotification = useCallback(
|
|
222
|
+
(message: string, tone: "success" | "error" = "success") => {
|
|
223
|
+
setLogNotification({ message, tone });
|
|
224
|
+
if (logNotificationTimerRef.current) {
|
|
225
|
+
clearTimeout(logNotificationTimerRef.current);
|
|
226
|
+
}
|
|
227
|
+
logNotificationTimerRef.current = setTimeout(() => {
|
|
228
|
+
setLogNotification(null);
|
|
229
|
+
}, 2000);
|
|
230
|
+
},
|
|
231
|
+
[],
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
useEffect(() => {
|
|
235
|
+
return () => {
|
|
236
|
+
if (logNotificationTimerRef.current) {
|
|
237
|
+
clearTimeout(logNotificationTimerRef.current);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
}, []);
|
|
241
|
+
|
|
242
|
+
const loadLogEntries = useCallback(
|
|
243
|
+
async (date: string | null) => {
|
|
244
|
+
const targetDate = date ?? getTodayLogDate();
|
|
245
|
+
setLogLoading(true);
|
|
246
|
+
setLogError(null);
|
|
247
|
+
try {
|
|
248
|
+
const filePath = buildLogFilePath(logDir, targetDate);
|
|
249
|
+
const lines = await readLogFileLines(filePath);
|
|
250
|
+
const parsed = parseLogLines(lines, { limit: 100 });
|
|
251
|
+
setLogEntries(parsed);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
setLogEntries([]);
|
|
254
|
+
setLogError(
|
|
255
|
+
error instanceof Error ? error.message : "Failed to load logs",
|
|
256
|
+
);
|
|
257
|
+
} finally {
|
|
258
|
+
setLogLoading(false);
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
[logDir],
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
const loadLogDates = useCallback(async () => {
|
|
265
|
+
try {
|
|
266
|
+
const files = await listRecentLogFiles(logDir, 7);
|
|
267
|
+
setLogDates(files);
|
|
268
|
+
} catch {
|
|
269
|
+
setLogDates([]);
|
|
270
|
+
}
|
|
271
|
+
}, [logDir]);
|
|
272
|
+
|
|
273
|
+
useEffect(() => {
|
|
274
|
+
if (currentScreen === "log-list") {
|
|
275
|
+
void loadLogEntries(logSelectedDate);
|
|
276
|
+
}
|
|
277
|
+
}, [currentScreen, loadLogEntries, logSelectedDate]);
|
|
278
|
+
|
|
279
|
+
useEffect(() => {
|
|
280
|
+
if (currentScreen === "log-date-picker") {
|
|
281
|
+
void loadLogDates();
|
|
282
|
+
}
|
|
283
|
+
}, [currentScreen, loadLogDates]);
|
|
284
|
+
|
|
185
285
|
useEffect(() => {
|
|
186
286
|
if (!hiddenBranches.length) {
|
|
187
287
|
return;
|
|
@@ -720,6 +820,45 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
|
|
|
720
820
|
exit();
|
|
721
821
|
}, [onExit, exit]);
|
|
722
822
|
|
|
823
|
+
const handleOpenLogs = useCallback(() => {
|
|
824
|
+
setLogSelectedDate(getTodayLogDate());
|
|
825
|
+
setLogSelectedEntry(null);
|
|
826
|
+
navigateTo("log-list");
|
|
827
|
+
}, [navigateTo]);
|
|
828
|
+
|
|
829
|
+
const handleSelectLogEntry = useCallback(
|
|
830
|
+
(entry: FormattedLogEntry) => {
|
|
831
|
+
setLogSelectedEntry(entry);
|
|
832
|
+
navigateTo("log-detail");
|
|
833
|
+
},
|
|
834
|
+
[navigateTo],
|
|
835
|
+
);
|
|
836
|
+
|
|
837
|
+
const handleCopyLogEntry = useCallback(
|
|
838
|
+
async (entry: FormattedLogEntry) => {
|
|
839
|
+
try {
|
|
840
|
+
await copyToClipboard(entry.json);
|
|
841
|
+
showLogNotification("Copied to clipboard.", "success");
|
|
842
|
+
} catch {
|
|
843
|
+
showLogNotification("Failed to copy to clipboard.", "error");
|
|
844
|
+
}
|
|
845
|
+
},
|
|
846
|
+
[showLogNotification],
|
|
847
|
+
);
|
|
848
|
+
|
|
849
|
+
const handleOpenLogDates = useCallback(() => {
|
|
850
|
+
navigateTo("log-date-picker");
|
|
851
|
+
}, [navigateTo]);
|
|
852
|
+
|
|
853
|
+
const handleSelectLogDate = useCallback(
|
|
854
|
+
(date: string) => {
|
|
855
|
+
setLogSelectedDate(date);
|
|
856
|
+
setLogSelectedEntry(null);
|
|
857
|
+
navigateTo("log-list");
|
|
858
|
+
},
|
|
859
|
+
[navigateTo],
|
|
860
|
+
);
|
|
861
|
+
|
|
723
862
|
// Handle branch creation
|
|
724
863
|
const handleCreate = useCallback(
|
|
725
864
|
async (branchName: string) => {
|
|
@@ -787,7 +926,7 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
|
|
|
787
926
|
if (selectedBranches.length === 0) {
|
|
788
927
|
setCleanupIndicators({});
|
|
789
928
|
setCleanupFooterMessage({
|
|
790
|
-
text: "
|
|
929
|
+
text: "No cleanup targets selected.",
|
|
791
930
|
color: "yellow",
|
|
792
931
|
});
|
|
793
932
|
setCleanupInputLocked(false);
|
|
@@ -1150,6 +1289,7 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
|
|
|
1150
1289
|
workingDirectory={workingDirectory}
|
|
1151
1290
|
activeProfile={activeProfileName}
|
|
1152
1291
|
onOpenProfiles={() => navigateTo("environment-profile")}
|
|
1292
|
+
onOpenLogs={handleOpenLogs}
|
|
1153
1293
|
toolStatuses={toolStatuses}
|
|
1154
1294
|
{...additionalProps}
|
|
1155
1295
|
/>
|
|
@@ -1168,6 +1308,43 @@ export function App({ onExit, loadingIndicatorDelay = 300 }: AppProps) {
|
|
|
1168
1308
|
onToggleSelect: toggleBranchSelection,
|
|
1169
1309
|
});
|
|
1170
1310
|
|
|
1311
|
+
case "log-list":
|
|
1312
|
+
return (
|
|
1313
|
+
<LogListScreen
|
|
1314
|
+
entries={logEntries}
|
|
1315
|
+
loading={logLoading}
|
|
1316
|
+
error={logError}
|
|
1317
|
+
onBack={goBack}
|
|
1318
|
+
onSelect={handleSelectLogEntry}
|
|
1319
|
+
onCopy={handleCopyLogEntry}
|
|
1320
|
+
onPickDate={handleOpenLogDates}
|
|
1321
|
+
notification={logNotification}
|
|
1322
|
+
version={version}
|
|
1323
|
+
selectedDate={logSelectedDate}
|
|
1324
|
+
/>
|
|
1325
|
+
);
|
|
1326
|
+
|
|
1327
|
+
case "log-detail":
|
|
1328
|
+
return (
|
|
1329
|
+
<LogDetailScreen
|
|
1330
|
+
entry={logSelectedEntry}
|
|
1331
|
+
onBack={goBack}
|
|
1332
|
+
onCopy={handleCopyLogEntry}
|
|
1333
|
+
notification={logNotification}
|
|
1334
|
+
version={version}
|
|
1335
|
+
/>
|
|
1336
|
+
);
|
|
1337
|
+
|
|
1338
|
+
case "log-date-picker":
|
|
1339
|
+
return (
|
|
1340
|
+
<LogDatePickerScreen
|
|
1341
|
+
dates={logDates}
|
|
1342
|
+
onBack={goBack}
|
|
1343
|
+
onSelect={handleSelectLogDate}
|
|
1344
|
+
version={version}
|
|
1345
|
+
/>
|
|
1346
|
+
);
|
|
1347
|
+
|
|
1171
1348
|
case "branch-creator":
|
|
1172
1349
|
return (
|
|
1173
1350
|
<BranchCreatorScreen
|
|
@@ -11,6 +11,7 @@ import { useAppInput } from "../../hooks/useAppInput.js";
|
|
|
11
11
|
import { useTerminalSize } from "../../hooks/useTerminalSize.js";
|
|
12
12
|
import type { BranchItem, Statistics, BranchViewMode } from "../../types.js";
|
|
13
13
|
import type { ToolStatus } from "../../hooks/useToolStatus.js";
|
|
14
|
+
import { getLatestActivityTimestamp } from "../../utils/branchFormatter.js";
|
|
14
15
|
import stringWidth from "string-width";
|
|
15
16
|
import stripAnsi from "strip-ansi";
|
|
16
17
|
import chalk from "chalk";
|
|
@@ -93,6 +94,7 @@ export interface BranchListScreenProps {
|
|
|
93
94
|
onCleanupCommand?: () => void;
|
|
94
95
|
onRefresh?: () => void;
|
|
95
96
|
onOpenProfiles?: () => void;
|
|
97
|
+
onOpenLogs?: () => void;
|
|
96
98
|
loading?: boolean;
|
|
97
99
|
error?: Error | null;
|
|
98
100
|
lastUpdated?: Date | null;
|
|
@@ -128,6 +130,7 @@ export const BranchListScreen = React.memo(function BranchListScreen({
|
|
|
128
130
|
onCleanupCommand,
|
|
129
131
|
onRefresh,
|
|
130
132
|
onOpenProfiles,
|
|
133
|
+
onOpenLogs,
|
|
131
134
|
loading = false,
|
|
132
135
|
error = null,
|
|
133
136
|
lastUpdated = null,
|
|
@@ -273,6 +276,8 @@ export const BranchListScreen = React.memo(function BranchListScreen({
|
|
|
273
276
|
onRefresh();
|
|
274
277
|
} else if (input === "p" && onOpenProfiles) {
|
|
275
278
|
onOpenProfiles();
|
|
279
|
+
} else if (input === "l" && onOpenLogs) {
|
|
280
|
+
onOpenLogs();
|
|
276
281
|
}
|
|
277
282
|
});
|
|
278
283
|
|
|
@@ -346,6 +351,7 @@ export const BranchListScreen = React.memo(function BranchListScreen({
|
|
|
346
351
|
{ key: "r", description: "Refresh" },
|
|
347
352
|
{ key: "c", description: "Cleanup" },
|
|
348
353
|
{ key: "p", description: "Profiles" },
|
|
354
|
+
{ key: "l", description: "Logs" },
|
|
349
355
|
];
|
|
350
356
|
|
|
351
357
|
const formatLatestCommit = useCallback((timestamp?: number) => {
|
|
@@ -420,12 +426,11 @@ export const BranchListScreen = React.memo(function BranchListScreen({
|
|
|
420
426
|
const columns = Math.max(20, context.columns - 1);
|
|
421
427
|
const visibleWidth = (value: string) =>
|
|
422
428
|
measureDisplayWidth(stripAnsi(value));
|
|
429
|
+
// FR-041: Display latest activity time (max of git commit and tool usage)
|
|
423
430
|
let commitText = "---";
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
const seconds = Math.floor(item.lastToolUsage.timestamp / 1000);
|
|
428
|
-
commitText = formatLatestCommit(seconds);
|
|
431
|
+
const latestActivitySec = getLatestActivityTimestamp(item);
|
|
432
|
+
if (latestActivitySec > 0) {
|
|
433
|
+
commitText = formatLatestCommit(latestActivitySec);
|
|
429
434
|
}
|
|
430
435
|
const toolLabelRaw =
|
|
431
436
|
item.lastToolUsageLabel?.split("|")?.[0]?.trim() ??
|
|
@@ -622,7 +627,7 @@ export const BranchListScreen = React.memo(function BranchListScreen({
|
|
|
622
627
|
onChange={setFilterQuery}
|
|
623
628
|
onSubmit={() => {}} // No-op: filter is applied in real-time
|
|
624
629
|
placeholder="Type to search..."
|
|
625
|
-
blockKeys={["c", "r", "f"]} // Block shortcuts while typing
|
|
630
|
+
blockKeys={["c", "r", "f", "l"]} // Block shortcuts while typing
|
|
626
631
|
/>
|
|
627
632
|
) : (
|
|
628
633
|
<Text dimColor>{filterQuery || "(press f to filter)"}</Text>
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { Header } from "../parts/Header.js";
|
|
4
|
+
import { Footer } from "../parts/Footer.js";
|
|
5
|
+
import { Select } from "../common/Select.js";
|
|
6
|
+
import { useAppInput } from "../../hooks/useAppInput.js";
|
|
7
|
+
import { useTerminalSize } from "../../hooks/useTerminalSize.js";
|
|
8
|
+
import type { LogFileInfo } from "../../../../logging/reader.js";
|
|
9
|
+
|
|
10
|
+
interface DateItem {
|
|
11
|
+
label: string;
|
|
12
|
+
value: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface LogDatePickerScreenProps {
|
|
16
|
+
dates: LogFileInfo[];
|
|
17
|
+
onBack: () => void;
|
|
18
|
+
onSelect: (date: string) => void;
|
|
19
|
+
version?: string | null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function LogDatePickerScreen({
|
|
23
|
+
dates,
|
|
24
|
+
onBack,
|
|
25
|
+
onSelect,
|
|
26
|
+
version,
|
|
27
|
+
}: LogDatePickerScreenProps) {
|
|
28
|
+
const { rows } = useTerminalSize();
|
|
29
|
+
|
|
30
|
+
useAppInput((input, key) => {
|
|
31
|
+
if (key.escape || input === "q") {
|
|
32
|
+
onBack();
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const items: DateItem[] = dates.map((date) => ({
|
|
37
|
+
label: date.date,
|
|
38
|
+
value: date.date,
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
const handleSelect = (item: DateItem) => {
|
|
42
|
+
onSelect(item.value);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const headerLines = 2;
|
|
46
|
+
const statsLines = 1;
|
|
47
|
+
const emptyLine = 1;
|
|
48
|
+
const footerLines = 1;
|
|
49
|
+
const fixedLines = headerLines + statsLines + emptyLine + footerLines;
|
|
50
|
+
const contentHeight = rows - fixedLines;
|
|
51
|
+
const limit = Math.max(5, contentHeight);
|
|
52
|
+
|
|
53
|
+
const footerActions = [
|
|
54
|
+
{ key: "enter", description: "Select" },
|
|
55
|
+
{ key: "esc", description: "Back" },
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<Box flexDirection="column" height={rows}>
|
|
60
|
+
<Header title="gwt - Log Date" titleColor="cyan" version={version} />
|
|
61
|
+
|
|
62
|
+
<Box marginTop={1}>
|
|
63
|
+
<Text>
|
|
64
|
+
Total: <Text bold>{dates.length}</Text>
|
|
65
|
+
</Text>
|
|
66
|
+
</Box>
|
|
67
|
+
|
|
68
|
+
<Box height={1} />
|
|
69
|
+
|
|
70
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
71
|
+
{dates.length === 0 ? (
|
|
72
|
+
<Box>
|
|
73
|
+
<Text dimColor>No logs available.</Text>
|
|
74
|
+
</Box>
|
|
75
|
+
) : (
|
|
76
|
+
<Select items={items} onSelect={handleSelect} limit={limit} />
|
|
77
|
+
)}
|
|
78
|
+
</Box>
|
|
79
|
+
|
|
80
|
+
<Footer actions={footerActions} />
|
|
81
|
+
</Box>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import React, { useMemo } from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import { Header } from "../parts/Header.js";
|
|
4
|
+
import { Footer } from "../parts/Footer.js";
|
|
5
|
+
import { useAppInput } from "../../hooks/useAppInput.js";
|
|
6
|
+
import { useTerminalSize } from "../../hooks/useTerminalSize.js";
|
|
7
|
+
import type { FormattedLogEntry } from "../../../../logging/formatter.js";
|
|
8
|
+
|
|
9
|
+
export interface LogDetailScreenProps {
|
|
10
|
+
entry: FormattedLogEntry | null;
|
|
11
|
+
onBack: () => void;
|
|
12
|
+
onCopy: (entry: FormattedLogEntry) => void;
|
|
13
|
+
notification?: { message: string; tone: "success" | "error" } | null;
|
|
14
|
+
version?: string | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function LogDetailScreen({
|
|
18
|
+
entry,
|
|
19
|
+
onBack,
|
|
20
|
+
onCopy,
|
|
21
|
+
notification,
|
|
22
|
+
version,
|
|
23
|
+
}: LogDetailScreenProps) {
|
|
24
|
+
const { rows } = useTerminalSize();
|
|
25
|
+
|
|
26
|
+
useAppInput((input, key) => {
|
|
27
|
+
if (key.escape || input === "q") {
|
|
28
|
+
onBack();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (input === "c" && entry) {
|
|
33
|
+
onCopy(entry);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const jsonLines = useMemo<string[]>(() => {
|
|
38
|
+
if (!entry) return ["No logs available."]; // fallback
|
|
39
|
+
return entry.json.split("\n");
|
|
40
|
+
}, [entry]);
|
|
41
|
+
|
|
42
|
+
const footerActions = [
|
|
43
|
+
{ key: "c", description: "Copy" },
|
|
44
|
+
{ key: "esc", description: "Back" },
|
|
45
|
+
];
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<Box flexDirection="column" height={rows}>
|
|
49
|
+
<Header title="gwt - Log Detail" titleColor="cyan" version={version} />
|
|
50
|
+
|
|
51
|
+
{notification ? (
|
|
52
|
+
<Box marginTop={1}>
|
|
53
|
+
<Text color={notification.tone === "error" ? "red" : "green"}>
|
|
54
|
+
{notification.message}
|
|
55
|
+
</Text>
|
|
56
|
+
</Box>
|
|
57
|
+
) : null}
|
|
58
|
+
<Box flexDirection="column" flexGrow={1} marginTop={1}>
|
|
59
|
+
{jsonLines.map((line, index) => (
|
|
60
|
+
<Text key={`${line}-${index}`}>{line}</Text>
|
|
61
|
+
))}
|
|
62
|
+
</Box>
|
|
63
|
+
|
|
64
|
+
<Footer actions={footerActions} />
|
|
65
|
+
</Box>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import React, { useCallback, useMemo, useState } from "react";
|
|
2
|
+
import { Box, Text } from "ink";
|
|
3
|
+
import stringWidth from "string-width";
|
|
4
|
+
import { Header } from "../parts/Header.js";
|
|
5
|
+
import { Footer } from "../parts/Footer.js";
|
|
6
|
+
import { Select } from "../common/Select.js";
|
|
7
|
+
import { useAppInput } from "../../hooks/useAppInput.js";
|
|
8
|
+
import { useTerminalSize } from "../../hooks/useTerminalSize.js";
|
|
9
|
+
import type { FormattedLogEntry } from "../../../../logging/formatter.js";
|
|
10
|
+
|
|
11
|
+
interface LogListItem {
|
|
12
|
+
label: string;
|
|
13
|
+
value: string;
|
|
14
|
+
entry: FormattedLogEntry;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface LogListScreenProps {
|
|
18
|
+
entries: FormattedLogEntry[];
|
|
19
|
+
loading?: boolean;
|
|
20
|
+
error?: string | null;
|
|
21
|
+
onBack: () => void;
|
|
22
|
+
onSelect: (entry: FormattedLogEntry) => void;
|
|
23
|
+
onCopy: (entry: FormattedLogEntry) => void;
|
|
24
|
+
onPickDate?: () => void;
|
|
25
|
+
notification?: { message: string; tone: "success" | "error" } | null;
|
|
26
|
+
version?: string | null;
|
|
27
|
+
selectedDate?: string | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const truncateToWidth = (value: string, maxWidth: number): string => {
|
|
31
|
+
if (maxWidth <= 0) return "";
|
|
32
|
+
if (stringWidth(value) <= maxWidth) return value;
|
|
33
|
+
const ellipsis = "…";
|
|
34
|
+
const ellipsisWidth = stringWidth(ellipsis);
|
|
35
|
+
if (ellipsisWidth >= maxWidth) return ellipsis;
|
|
36
|
+
|
|
37
|
+
let result = "";
|
|
38
|
+
for (const char of Array.from(value)) {
|
|
39
|
+
if (stringWidth(result + char) + ellipsisWidth > maxWidth) {
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
result += char;
|
|
43
|
+
}
|
|
44
|
+
return result + ellipsis;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const padToWidth = (value: string, width: number): string => {
|
|
48
|
+
if (width <= 0) return "";
|
|
49
|
+
if (stringWidth(value) >= width) return value;
|
|
50
|
+
return value + " ".repeat(width - stringWidth(value));
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export function LogListScreen({
|
|
54
|
+
entries,
|
|
55
|
+
loading = false,
|
|
56
|
+
error = null,
|
|
57
|
+
onBack,
|
|
58
|
+
onSelect,
|
|
59
|
+
onCopy,
|
|
60
|
+
onPickDate,
|
|
61
|
+
notification,
|
|
62
|
+
version,
|
|
63
|
+
selectedDate,
|
|
64
|
+
}: LogListScreenProps) {
|
|
65
|
+
const { rows, columns } = useTerminalSize();
|
|
66
|
+
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
67
|
+
|
|
68
|
+
const maxLabelWidth = Math.max(10, columns - 2);
|
|
69
|
+
|
|
70
|
+
const items = useMemo<LogListItem[]>(
|
|
71
|
+
() =>
|
|
72
|
+
entries.map((entry) => ({
|
|
73
|
+
label: truncateToWidth(entry.summary, maxLabelWidth),
|
|
74
|
+
value: entry.id,
|
|
75
|
+
entry,
|
|
76
|
+
})),
|
|
77
|
+
[entries, maxLabelWidth],
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
const handleSelect = useCallback(
|
|
81
|
+
(item: LogListItem) => {
|
|
82
|
+
onSelect(item.entry);
|
|
83
|
+
},
|
|
84
|
+
[onSelect],
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const handleCopy = useCallback(() => {
|
|
88
|
+
const entry = entries[selectedIndex];
|
|
89
|
+
if (entry) {
|
|
90
|
+
onCopy(entry);
|
|
91
|
+
}
|
|
92
|
+
}, [entries, selectedIndex, onCopy]);
|
|
93
|
+
|
|
94
|
+
useAppInput((input, key) => {
|
|
95
|
+
if (key.escape || input === "q") {
|
|
96
|
+
onBack();
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (input === "c") {
|
|
101
|
+
handleCopy();
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (input === "d" && onPickDate) {
|
|
106
|
+
onPickDate();
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const renderItem = useCallback(
|
|
111
|
+
(item: LogListItem, isSelected: boolean) => {
|
|
112
|
+
if (!isSelected) {
|
|
113
|
+
return <Text>{item.label}</Text>;
|
|
114
|
+
}
|
|
115
|
+
const padded = padToWidth(item.label, maxLabelWidth);
|
|
116
|
+
const output = `\u001b[46m\u001b[30m${padded}\u001b[0m`;
|
|
117
|
+
return <Text>{output}</Text>;
|
|
118
|
+
},
|
|
119
|
+
[maxLabelWidth],
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const headerLines = 2;
|
|
123
|
+
const statsLines = 1;
|
|
124
|
+
const emptyLine = 1;
|
|
125
|
+
const footerLines = 1;
|
|
126
|
+
const fixedLines = headerLines + statsLines + emptyLine + footerLines;
|
|
127
|
+
const contentHeight = rows - fixedLines;
|
|
128
|
+
const limit = Math.max(5, contentHeight);
|
|
129
|
+
|
|
130
|
+
const footerActions = [
|
|
131
|
+
{ key: "enter", description: "Detail" },
|
|
132
|
+
{ key: "c", description: "Copy" },
|
|
133
|
+
{ key: "d", description: "Date" },
|
|
134
|
+
{ key: "esc", description: "Back" },
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
return (
|
|
138
|
+
<Box flexDirection="column" height={rows}>
|
|
139
|
+
<Header title="gwt - Log Viewer" titleColor="cyan" version={version} />
|
|
140
|
+
|
|
141
|
+
{notification ? (
|
|
142
|
+
<Box marginTop={1}>
|
|
143
|
+
<Text color={notification.tone === "error" ? "red" : "green"}>
|
|
144
|
+
{notification.message}
|
|
145
|
+
</Text>
|
|
146
|
+
</Box>
|
|
147
|
+
) : null}
|
|
148
|
+
|
|
149
|
+
<Box marginTop={1}>
|
|
150
|
+
<Box marginRight={2}>
|
|
151
|
+
<Text>
|
|
152
|
+
Date: <Text bold>{selectedDate ?? "---"}</Text>
|
|
153
|
+
</Text>
|
|
154
|
+
</Box>
|
|
155
|
+
<Text>
|
|
156
|
+
Total: <Text bold>{entries.length}</Text>
|
|
157
|
+
</Text>
|
|
158
|
+
</Box>
|
|
159
|
+
|
|
160
|
+
<Box height={1} />
|
|
161
|
+
|
|
162
|
+
<Box flexDirection="column" flexGrow={1}>
|
|
163
|
+
{loading ? (
|
|
164
|
+
<Box>
|
|
165
|
+
<Text dimColor>Loading logs...</Text>
|
|
166
|
+
</Box>
|
|
167
|
+
) : entries.length === 0 ? (
|
|
168
|
+
<Box>
|
|
169
|
+
<Text dimColor>No logs available.</Text>
|
|
170
|
+
</Box>
|
|
171
|
+
) : (
|
|
172
|
+
<Select
|
|
173
|
+
items={items}
|
|
174
|
+
onSelect={handleSelect}
|
|
175
|
+
limit={limit}
|
|
176
|
+
selectedIndex={selectedIndex}
|
|
177
|
+
onSelectedIndexChange={setSelectedIndex}
|
|
178
|
+
renderItem={renderItem}
|
|
179
|
+
/>
|
|
180
|
+
)}
|
|
181
|
+
|
|
182
|
+
{error ? (
|
|
183
|
+
<Box marginTop={1}>
|
|
184
|
+
<Text color="red">{error}</Text>
|
|
185
|
+
</Box>
|
|
186
|
+
) : null}
|
|
187
|
+
</Box>
|
|
188
|
+
|
|
189
|
+
<Footer actions={footerActions} />
|
|
190
|
+
</Box>
|
|
191
|
+
);
|
|
192
|
+
}
|