@akiojin/gwt 4.6.0 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/claude.d.ts.map +1 -1
  2. package/dist/claude.js +38 -17
  3. package/dist/claude.js.map +1 -1
  4. package/dist/cli/ui/components/App.d.ts.map +1 -1
  5. package/dist/cli/ui/components/App.js +102 -1
  6. package/dist/cli/ui/components/App.js.map +1 -1
  7. package/dist/cli/ui/components/screens/BranchListScreen.d.ts +1 -0
  8. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  9. package/dist/cli/ui/components/screens/BranchListScreen.js +6 -2
  10. package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
  11. package/dist/cli/ui/components/screens/LogDatePickerScreen.d.ts +10 -0
  12. package/dist/cli/ui/components/screens/LogDatePickerScreen.d.ts.map +1 -0
  13. package/dist/cli/ui/components/screens/LogDatePickerScreen.js +44 -0
  14. package/dist/cli/ui/components/screens/LogDatePickerScreen.js.map +1 -0
  15. package/dist/cli/ui/components/screens/LogDetailScreen.d.ts +14 -0
  16. package/dist/cli/ui/components/screens/LogDetailScreen.d.ts.map +1 -0
  17. package/dist/cli/ui/components/screens/LogDetailScreen.js +34 -0
  18. package/dist/cli/ui/components/screens/LogDetailScreen.js.map +1 -0
  19. package/dist/cli/ui/components/screens/LogListScreen.d.ts +19 -0
  20. package/dist/cli/ui/components/screens/LogListScreen.d.ts.map +1 -0
  21. package/dist/cli/ui/components/screens/LogListScreen.js +107 -0
  22. package/dist/cli/ui/components/screens/LogListScreen.js.map +1 -0
  23. package/dist/cli/ui/hooks/useGitData.d.ts.map +1 -1
  24. package/dist/cli/ui/hooks/useGitData.js +10 -3
  25. package/dist/cli/ui/hooks/useGitData.js.map +1 -1
  26. package/dist/cli/ui/types.d.ts +1 -1
  27. package/dist/cli/ui/types.d.ts.map +1 -1
  28. package/dist/cli/ui/utils/clipboard.d.ts +7 -0
  29. package/dist/cli/ui/utils/clipboard.d.ts.map +1 -0
  30. package/dist/cli/ui/utils/clipboard.js +21 -0
  31. package/dist/cli/ui/utils/clipboard.js.map +1 -0
  32. package/dist/gemini.d.ts.map +1 -1
  33. package/dist/gemini.js +12 -6
  34. package/dist/gemini.js.map +1 -1
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +9 -2
  37. package/dist/index.js.map +1 -1
  38. package/dist/logging/formatter.d.ts +15 -0
  39. package/dist/logging/formatter.d.ts.map +1 -0
  40. package/dist/logging/formatter.js +81 -0
  41. package/dist/logging/formatter.js.map +1 -0
  42. package/dist/logging/reader.d.ts +12 -0
  43. package/dist/logging/reader.d.ts.map +1 -0
  44. package/dist/logging/reader.js +63 -0
  45. package/dist/logging/reader.js.map +1 -0
  46. package/dist/worktree.d.ts.map +1 -1
  47. package/dist/worktree.js +57 -0
  48. package/dist/worktree.js.map +1 -1
  49. package/package.json +1 -1
  50. package/src/claude.ts +51 -22
  51. package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +7 -3
  52. package/src/cli/ui/__tests__/components/common/Select.test.tsx +17 -11
  53. package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +28 -2
  54. package/src/cli/ui/__tests__/components/screens/LogDetailScreen.test.tsx +57 -0
  55. package/src/cli/ui/__tests__/components/screens/LogListScreen.test.tsx +102 -0
  56. package/src/cli/ui/__tests__/hooks/useGitData.test.ts +197 -0
  57. package/src/cli/ui/__tests__/integration/edgeCases.test.tsx +19 -9
  58. package/src/cli/ui/__tests__/utils/clipboard.test.ts +65 -0
  59. package/src/cli/ui/components/App.tsx +177 -0
  60. package/src/cli/ui/components/screens/BranchListScreen.tsx +6 -1
  61. package/src/cli/ui/components/screens/LogDatePickerScreen.tsx +83 -0
  62. package/src/cli/ui/components/screens/LogDetailScreen.tsx +67 -0
  63. package/src/cli/ui/components/screens/LogListScreen.tsx +192 -0
  64. package/src/cli/ui/hooks/useGitData.ts +12 -3
  65. package/src/cli/ui/types.ts +3 -0
  66. package/src/cli/ui/utils/clipboard.ts +31 -0
  67. package/src/gemini.ts +14 -6
  68. package/src/index.ts +11 -2
  69. package/src/logging/formatter.ts +106 -0
  70. package/src/logging/reader.ts +76 -0
  71. package/src/worktree.ts +77 -0
@@ -13,7 +13,14 @@ import type { BranchInfo, BranchItem, Statistics } from "../../types.js";
13
13
  const mockRefresh = vi.fn();
14
14
  const branchListProps: BranchListScreenProps[] = [];
15
15
  const useGitDataMock = vi.fn();
16
- let App: typeof import("../../components/App.js").App;
16
+ let App: typeof import("../../components/App.js").App | null = null;
17
+
18
+ const loadApp = async () => {
19
+ if (!App) {
20
+ App = (await import("../../components/App.js")).App;
21
+ }
22
+ return App;
23
+ };
17
24
 
18
25
  vi.mock("../../hooks/useGitData.js", () => ({
19
26
  useGitData: (...args: unknown[]) => useGitDataMock(...args),
@@ -53,7 +60,6 @@ describe("Edge Cases Integration Tests", () => {
53
60
  vi.clearAllMocks();
54
61
  useGitDataMock.mockReset();
55
62
  branchListProps.length = 0;
56
- App = (await import("../../components/App.js")).App;
57
63
  });
58
64
 
59
65
  /**
@@ -237,13 +243,14 @@ describe("Edge Cases Integration Tests", () => {
237
243
  });
238
244
 
239
245
  const onExit = vi.fn();
240
- const { container } = render(<App onExit={onExit} />);
246
+ const AppComponent = await loadApp();
247
+ const { container } = render(<AppComponent onExit={onExit} />);
241
248
 
242
249
  // Initial render should work
243
250
  expect(container).toBeDefined();
244
251
  });
245
252
 
246
- it("[T093] should display error message when data loading fails", () => {
253
+ it("[T093] should display error message when data loading fails", async () => {
247
254
  const testError = new Error("Test error: Failed to load Git data");
248
255
  useGitDataMock.mockReturnValue({
249
256
  branches: [],
@@ -255,7 +262,8 @@ describe("Edge Cases Integration Tests", () => {
255
262
  });
256
263
 
257
264
  const onExit = vi.fn();
258
- const { getByText } = render(<App onExit={onExit} />);
265
+ const AppComponent = await loadApp();
266
+ const { getByText } = render(<AppComponent onExit={onExit} />);
259
267
 
260
268
  expect(branchListProps).not.toHaveLength(0);
261
269
  expect(branchListProps.at(-1)?.error).toBe(testError);
@@ -264,7 +272,7 @@ describe("Edge Cases Integration Tests", () => {
264
272
  expect(getByText(/Failed to load Git data/i)).toBeDefined();
265
273
  });
266
274
 
267
- it("[T093] should handle empty branches list gracefully", () => {
275
+ it("[T093] should handle empty branches list gracefully", async () => {
268
276
  useGitDataMock.mockReturnValue({
269
277
  branches: [],
270
278
  worktrees: [],
@@ -275,7 +283,8 @@ describe("Edge Cases Integration Tests", () => {
275
283
  });
276
284
 
277
285
  const onExit = vi.fn();
278
- const { container } = render(<App onExit={onExit} />);
286
+ const AppComponent = await loadApp();
287
+ const { container } = render(<AppComponent onExit={onExit} />);
279
288
 
280
289
  // Should render without error even with no branches
281
290
  expect(container).toBeDefined();
@@ -284,7 +293,7 @@ describe("Edge Cases Integration Tests", () => {
284
293
  /**
285
294
  * Additional edge cases
286
295
  */
287
- it("should handle large number of worktrees", () => {
296
+ it("should handle large number of worktrees", async () => {
288
297
  const mockBranches: BranchInfo[] = Array.from({ length: 50 }, (_, i) => ({
289
298
  name: `feature/branch-${i}`,
290
299
  type: "local" as const,
@@ -307,7 +316,8 @@ describe("Edge Cases Integration Tests", () => {
307
316
  });
308
317
 
309
318
  const onExit = vi.fn();
310
- const { container } = render(<App onExit={onExit} />);
319
+ const AppComponent = await loadApp();
320
+ const { container } = render(<AppComponent onExit={onExit} />);
311
321
 
312
322
  expect(container).toBeDefined();
313
323
  });
@@ -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) => {
@@ -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
@@ -93,6 +93,7 @@ export interface BranchListScreenProps {
93
93
  onCleanupCommand?: () => void;
94
94
  onRefresh?: () => void;
95
95
  onOpenProfiles?: () => void;
96
+ onOpenLogs?: () => void;
96
97
  loading?: boolean;
97
98
  error?: Error | null;
98
99
  lastUpdated?: Date | null;
@@ -128,6 +129,7 @@ export const BranchListScreen = React.memo(function BranchListScreen({
128
129
  onCleanupCommand,
129
130
  onRefresh,
130
131
  onOpenProfiles,
132
+ onOpenLogs,
131
133
  loading = false,
132
134
  error = null,
133
135
  lastUpdated = null,
@@ -273,6 +275,8 @@ export const BranchListScreen = React.memo(function BranchListScreen({
273
275
  onRefresh();
274
276
  } else if (input === "p" && onOpenProfiles) {
275
277
  onOpenProfiles();
278
+ } else if (input === "l" && onOpenLogs) {
279
+ onOpenLogs();
276
280
  }
277
281
  });
278
282
 
@@ -346,6 +350,7 @@ export const BranchListScreen = React.memo(function BranchListScreen({
346
350
  { key: "r", description: "Refresh" },
347
351
  { key: "c", description: "Cleanup" },
348
352
  { key: "p", description: "Profiles" },
353
+ { key: "l", description: "Logs" },
349
354
  ];
350
355
 
351
356
  const formatLatestCommit = useCallback((timestamp?: number) => {
@@ -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>ログがありません</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 ["ログがありません"]; // 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
+ }