@akiojin/gwt 4.11.6 → 4.12.1

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 (199) hide show
  1. package/bin/gwt.js +36 -10
  2. package/dist/claude.d.ts +1 -0
  3. package/dist/claude.d.ts.map +1 -1
  4. package/dist/claude.js +81 -24
  5. package/dist/claude.js.map +1 -1
  6. package/dist/cli/ui/App.solid.d.ts.map +1 -1
  7. package/dist/cli/ui/App.solid.js +280 -50
  8. package/dist/cli/ui/App.solid.js.map +1 -1
  9. package/dist/cli/ui/components/solid/QuickStartStep.d.ts.map +1 -1
  10. package/dist/cli/ui/components/solid/QuickStartStep.js +35 -22
  11. package/dist/cli/ui/components/solid/QuickStartStep.js.map +1 -1
  12. package/dist/cli/ui/components/solid/SelectInput.d.ts.map +1 -1
  13. package/dist/cli/ui/components/solid/SelectInput.js +2 -1
  14. package/dist/cli/ui/components/solid/SelectInput.js.map +1 -1
  15. package/dist/cli/ui/components/solid/WizardController.d.ts.map +1 -1
  16. package/dist/cli/ui/components/solid/WizardController.js +67 -13
  17. package/dist/cli/ui/components/solid/WizardController.js.map +1 -1
  18. package/dist/cli/ui/components/solid/WizardSteps.d.ts +5 -0
  19. package/dist/cli/ui/components/solid/WizardSteps.d.ts.map +1 -1
  20. package/dist/cli/ui/components/solid/WizardSteps.js +50 -70
  21. package/dist/cli/ui/components/solid/WizardSteps.js.map +1 -1
  22. package/dist/cli/ui/core/theme.d.ts +9 -0
  23. package/dist/cli/ui/core/theme.d.ts.map +1 -1
  24. package/dist/cli/ui/core/theme.js +21 -0
  25. package/dist/cli/ui/core/theme.js.map +1 -1
  26. package/dist/cli/ui/screens/solid/BranchListScreen.d.ts +9 -2
  27. package/dist/cli/ui/screens/solid/BranchListScreen.d.ts.map +1 -1
  28. package/dist/cli/ui/screens/solid/BranchListScreen.js +101 -28
  29. package/dist/cli/ui/screens/solid/BranchListScreen.js.map +1 -1
  30. package/dist/cli/ui/screens/solid/ConfirmScreen.d.ts +2 -1
  31. package/dist/cli/ui/screens/solid/ConfirmScreen.d.ts.map +1 -1
  32. package/dist/cli/ui/screens/solid/ConfirmScreen.js +11 -3
  33. package/dist/cli/ui/screens/solid/ConfirmScreen.js.map +1 -1
  34. package/dist/cli/ui/screens/solid/EnvironmentScreen.d.ts.map +1 -1
  35. package/dist/cli/ui/screens/solid/EnvironmentScreen.js +9 -10
  36. package/dist/cli/ui/screens/solid/EnvironmentScreen.js.map +1 -1
  37. package/dist/cli/ui/screens/solid/LogScreen.d.ts +7 -1
  38. package/dist/cli/ui/screens/solid/LogScreen.d.ts.map +1 -1
  39. package/dist/cli/ui/screens/solid/LogScreen.js +254 -16
  40. package/dist/cli/ui/screens/solid/LogScreen.js.map +1 -1
  41. package/dist/cli/ui/screens/solid/ProfileEnvScreen.d.ts.map +1 -1
  42. package/dist/cli/ui/screens/solid/ProfileEnvScreen.js +8 -5
  43. package/dist/cli/ui/screens/solid/ProfileEnvScreen.js.map +1 -1
  44. package/dist/cli/ui/screens/solid/SelectorScreen.d.ts.map +1 -1
  45. package/dist/cli/ui/screens/solid/SelectorScreen.js +12 -4
  46. package/dist/cli/ui/screens/solid/SelectorScreen.js.map +1 -1
  47. package/dist/cli/ui/types.d.ts +1 -0
  48. package/dist/cli/ui/types.d.ts.map +1 -1
  49. package/dist/cli/ui/utils/branchFormatter.d.ts +1 -0
  50. package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
  51. package/dist/cli/ui/utils/branchFormatter.js +29 -7
  52. package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
  53. package/dist/cli/ui/utils/continueSession.d.ts +14 -0
  54. package/dist/cli/ui/utils/continueSession.d.ts.map +1 -1
  55. package/dist/cli/ui/utils/continueSession.js +61 -3
  56. package/dist/cli/ui/utils/continueSession.js.map +1 -1
  57. package/dist/cli/ui/utils/installedVersionCache.d.ts +33 -0
  58. package/dist/cli/ui/utils/installedVersionCache.d.ts.map +1 -0
  59. package/dist/cli/ui/utils/installedVersionCache.js +59 -0
  60. package/dist/cli/ui/utils/installedVersionCache.js.map +1 -0
  61. package/dist/cli/ui/utils/modelOptions.d.ts.map +1 -1
  62. package/dist/cli/ui/utils/modelOptions.js +16 -0
  63. package/dist/cli/ui/utils/modelOptions.js.map +1 -1
  64. package/dist/cli/ui/utils/versionCache.d.ts +37 -0
  65. package/dist/cli/ui/utils/versionCache.d.ts.map +1 -0
  66. package/dist/cli/ui/utils/versionCache.js +70 -0
  67. package/dist/cli/ui/utils/versionCache.js.map +1 -0
  68. package/dist/cli/ui/utils/versionFetcher.d.ts +41 -0
  69. package/dist/cli/ui/utils/versionFetcher.d.ts.map +1 -0
  70. package/dist/cli/ui/utils/versionFetcher.js +89 -0
  71. package/dist/cli/ui/utils/versionFetcher.js.map +1 -0
  72. package/dist/codex.d.ts +1 -0
  73. package/dist/codex.d.ts.map +1 -1
  74. package/dist/codex.js +95 -25
  75. package/dist/codex.js.map +1 -1
  76. package/dist/config/index.d.ts.map +1 -1
  77. package/dist/config/index.js +10 -1
  78. package/dist/config/index.js.map +1 -1
  79. package/dist/gemini.d.ts +1 -0
  80. package/dist/gemini.d.ts.map +1 -1
  81. package/dist/gemini.js +36 -3
  82. package/dist/gemini.js.map +1 -1
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +35 -2
  85. package/dist/index.js.map +1 -1
  86. package/dist/launcher.d.ts.map +1 -1
  87. package/dist/launcher.js +43 -8
  88. package/dist/launcher.js.map +1 -1
  89. package/dist/logging/agentOutput.d.ts +21 -0
  90. package/dist/logging/agentOutput.d.ts.map +1 -0
  91. package/dist/logging/agentOutput.js +164 -0
  92. package/dist/logging/agentOutput.js.map +1 -0
  93. package/dist/logging/formatter.d.ts.map +1 -1
  94. package/dist/logging/formatter.js +18 -4
  95. package/dist/logging/formatter.js.map +1 -1
  96. package/dist/logging/logger.d.ts.map +1 -1
  97. package/dist/logging/logger.js +2 -0
  98. package/dist/logging/logger.js.map +1 -1
  99. package/dist/logging/reader.d.ts +22 -0
  100. package/dist/logging/reader.d.ts.map +1 -1
  101. package/dist/logging/reader.js +116 -1
  102. package/dist/logging/reader.js.map +1 -1
  103. package/dist/opentui/index.solid.js +2575 -888
  104. package/dist/services/codingAgentResolver.d.ts.map +1 -1
  105. package/dist/services/codingAgentResolver.js +8 -6
  106. package/dist/services/codingAgentResolver.js.map +1 -1
  107. package/dist/services/dependency-installer.js +2 -2
  108. package/dist/services/dependency-installer.js.map +1 -1
  109. package/dist/shared/codingAgentConstants.d.ts +3 -0
  110. package/dist/shared/codingAgentConstants.d.ts.map +1 -1
  111. package/dist/shared/codingAgentConstants.js +66 -0
  112. package/dist/shared/codingAgentConstants.js.map +1 -1
  113. package/dist/utils/bun-runtime.d.ts +12 -0
  114. package/dist/utils/bun-runtime.d.ts.map +1 -0
  115. package/dist/utils/bun-runtime.js +13 -0
  116. package/dist/utils/bun-runtime.js.map +1 -0
  117. package/dist/utils/session/common.d.ts +8 -0
  118. package/dist/utils/session/common.d.ts.map +1 -1
  119. package/dist/utils/session/common.js +22 -0
  120. package/dist/utils/session/common.js.map +1 -1
  121. package/dist/utils/session/parsers/claude.d.ts +10 -4
  122. package/dist/utils/session/parsers/claude.d.ts.map +1 -1
  123. package/dist/utils/session/parsers/claude.js +64 -18
  124. package/dist/utils/session/parsers/claude.js.map +1 -1
  125. package/dist/utils/session/parsers/codex.d.ts.map +1 -1
  126. package/dist/utils/session/parsers/codex.js +47 -28
  127. package/dist/utils/session/parsers/codex.js.map +1 -1
  128. package/dist/utils/session/parsers/gemini.d.ts.map +1 -1
  129. package/dist/utils/session/parsers/gemini.js +43 -6
  130. package/dist/utils/session/parsers/gemini.js.map +1 -1
  131. package/dist/utils/session/parsers/opencode.d.ts.map +1 -1
  132. package/dist/utils/session/parsers/opencode.js +43 -6
  133. package/dist/utils/session/parsers/opencode.js.map +1 -1
  134. package/dist/utils/session/types.d.ts +7 -0
  135. package/dist/utils/session/types.d.ts.map +1 -1
  136. package/dist/web/client/src/components/ui/alert.d.ts +1 -1
  137. package/dist/worktree.d.ts +4 -1
  138. package/dist/worktree.d.ts.map +1 -1
  139. package/dist/worktree.js +21 -15
  140. package/dist/worktree.js.map +1 -1
  141. package/package.json +2 -1
  142. package/src/claude.ts +99 -28
  143. package/src/cli/ui/App.solid.tsx +373 -51
  144. package/src/cli/ui/__tests__/solid/AppSolid.cleanup.test.tsx +921 -1
  145. package/src/cli/ui/__tests__/solid/BranchListScreen.test.tsx +105 -5
  146. package/src/cli/ui/__tests__/solid/ConfirmScreen.test.tsx +77 -0
  147. package/src/cli/ui/__tests__/solid/LogScreen.test.tsx +351 -0
  148. package/src/cli/ui/__tests__/solid/components/QuickStartStep.test.tsx +73 -2
  149. package/src/cli/ui/__tests__/solid/components/WizardController.test.tsx +71 -0
  150. package/src/cli/ui/__tests__/solid/components/WizardSteps.test.tsx +95 -2
  151. package/src/cli/ui/__tests__/utils/branchFormatter.test.ts +72 -45
  152. package/src/cli/ui/components/solid/QuickStartStep.tsx +35 -23
  153. package/src/cli/ui/components/solid/SearchInput.tsx +1 -1
  154. package/src/cli/ui/components/solid/SelectInput.tsx +4 -0
  155. package/src/cli/ui/components/solid/WizardController.tsx +85 -12
  156. package/src/cli/ui/components/solid/WizardSteps.tsx +78 -90
  157. package/src/cli/ui/core/theme.ts +32 -0
  158. package/src/cli/ui/hooks/solid/useAsyncOperation.ts +8 -6
  159. package/src/cli/ui/hooks/solid/useGitOperations.ts +6 -5
  160. package/src/cli/ui/screens/solid/BranchListScreen.tsx +135 -32
  161. package/src/cli/ui/screens/solid/ConfirmScreen.tsx +20 -8
  162. package/src/cli/ui/screens/solid/EnvironmentScreen.tsx +22 -20
  163. package/src/cli/ui/screens/solid/LogScreen.tsx +364 -35
  164. package/src/cli/ui/screens/solid/ProfileEnvScreen.tsx +19 -15
  165. package/src/cli/ui/screens/solid/SelectorScreen.tsx +25 -14
  166. package/src/cli/ui/screens/solid/SettingsScreen.tsx +5 -3
  167. package/src/cli/ui/types.ts +1 -0
  168. package/src/cli/ui/utils/__tests__/branchFormatter.test.ts +53 -6
  169. package/src/cli/ui/utils/__tests__/installedVersionCache.test.ts +46 -0
  170. package/src/cli/ui/utils/branchFormatter.ts +35 -7
  171. package/src/cli/ui/utils/continueSession.ts +90 -3
  172. package/src/cli/ui/utils/installedVersionCache.ts +84 -0
  173. package/src/cli/ui/utils/modelOptions.test.ts +6 -0
  174. package/src/cli/ui/utils/modelOptions.ts +16 -0
  175. package/src/cli/ui/utils/versionCache.ts +93 -0
  176. package/src/cli/ui/utils/versionFetcher.ts +120 -0
  177. package/src/codex.ts +124 -26
  178. package/src/config/__tests__/saveSession.test.ts +2 -2
  179. package/src/config/index.ts +11 -1
  180. package/src/gemini.ts +50 -4
  181. package/src/index.test.ts +16 -10
  182. package/src/index.ts +41 -1
  183. package/src/launcher.ts +49 -8
  184. package/src/logging/agentOutput.ts +216 -0
  185. package/src/logging/formatter.ts +23 -4
  186. package/src/logging/logger.ts +2 -0
  187. package/src/logging/reader.ts +165 -1
  188. package/src/services/__tests__/BatchMergeService.test.ts +34 -14
  189. package/src/services/codingAgentResolver.ts +12 -5
  190. package/src/services/dependency-installer.ts +2 -2
  191. package/src/shared/codingAgentConstants.ts +73 -0
  192. package/src/utils/bun-runtime.ts +29 -0
  193. package/src/utils/session/common.ts +28 -0
  194. package/src/utils/session/parsers/claude.ts +79 -29
  195. package/src/utils/session/parsers/codex.ts +49 -26
  196. package/src/utils/session/parsers/gemini.ts +46 -5
  197. package/src/utils/session/parsers/opencode.ts +46 -5
  198. package/src/utils/session/types.ts +4 -0
  199. package/src/worktree.ts +28 -15
@@ -1,12 +1,14 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
2
  import { useKeyboard } from "@opentui/solid";
3
- import { createMemo } from "solid-js";
3
+ import { createEffect, createMemo, createSignal, mergeProps } from "solid-js";
4
4
  import { TextAttributes } from "@opentui/core";
5
5
  import { Header } from "../../components/solid/Header.js";
6
6
  import { Footer } from "../../components/solid/Footer.js";
7
7
  import { useTerminalSize } from "../../hooks/solid/useTerminalSize.js";
8
8
  import type { FormattedLogEntry } from "../../../../logging/formatter.js";
9
9
  import { useScrollableList } from "../../hooks/solid/useScrollableList.js";
10
+ import stringWidth from "string-width";
11
+ import { getLogLevelColor, selectionStyle } from "../../core/theme.js";
10
12
 
11
13
  export interface LogScreenProps {
12
14
  entries: FormattedLogEntry[];
@@ -16,65 +18,312 @@ export interface LogScreenProps {
16
18
  onSelect: (entry: FormattedLogEntry) => void;
17
19
  onCopy: (entry: FormattedLogEntry) => void;
18
20
  onPickDate?: () => void;
21
+ onReload?: () => void;
22
+ onToggleTail?: () => void;
23
+ onReset?: () => void;
19
24
  notification?: { message: string; tone: "success" | "error" } | null;
20
25
  version?: string | null;
21
26
  selectedDate?: string | null;
27
+ branchLabel?: string | null;
28
+ sourceLabel?: string | null;
29
+ tailing?: boolean;
22
30
  helpVisible?: boolean;
23
31
  }
24
32
 
25
- export function LogScreen({
26
- entries,
27
- loading = false,
28
- error = null,
29
- onBack,
30
- onSelect,
31
- onCopy,
32
- onPickDate,
33
- notification,
34
- version,
35
- selectedDate,
36
- helpVisible = false,
37
- }: LogScreenProps) {
33
+ type LevelMode = "ALL" | "INFO+" | "WARN+" | "ERROR+";
34
+
35
+ const LEVEL_ORDER: LevelMode[] = ["ALL", "INFO+", "WARN+", "ERROR+"];
36
+
37
+ const LEVEL_THRESHOLDS: Record<LevelMode, number> = {
38
+ ALL: 0,
39
+ "INFO+": 30,
40
+ "WARN+": 40,
41
+ "ERROR+": 50,
42
+ };
43
+
44
+ const LEVEL_LABEL_VALUES: Record<string, number> = {
45
+ TRACE: 10,
46
+ DEBUG: 20,
47
+ INFO: 30,
48
+ WARN: 40,
49
+ ERROR: 50,
50
+ FATAL: 60,
51
+ };
52
+
53
+ const measureWidth = (value: string): number => stringWidth(value);
54
+
55
+ const truncateToWidth = (value: string, maxWidth: number): string => {
56
+ if (maxWidth <= 0) {
57
+ return "";
58
+ }
59
+ if (measureWidth(value) <= maxWidth) {
60
+ return value;
61
+ }
62
+ const ellipsis = "...";
63
+ const ellipsisWidth = measureWidth(ellipsis);
64
+ if (ellipsisWidth >= maxWidth) {
65
+ return ellipsis.slice(0, maxWidth);
66
+ }
67
+
68
+ let currentWidth = 0;
69
+ let result = "";
70
+ for (const char of Array.from(value)) {
71
+ const charWidth = measureWidth(char);
72
+ if (currentWidth + charWidth + ellipsisWidth > maxWidth) {
73
+ break;
74
+ }
75
+ result += char;
76
+ currentWidth += charWidth;
77
+ }
78
+ return `${result}${ellipsis}`;
79
+ };
80
+
81
+ const padToWidth = (value: string, width: number): string => {
82
+ if (width <= 0) {
83
+ return "";
84
+ }
85
+ const truncated = truncateToWidth(value, width);
86
+ const padding = Math.max(0, width - measureWidth(truncated));
87
+ return `${truncated}${" ".repeat(padding)}`;
88
+ };
89
+
90
+ interface TextSegment {
91
+ text: string;
92
+ fg?: string;
93
+ }
94
+
95
+ const appendSegment = (segments: TextSegment[], segment: TextSegment) => {
96
+ if (!segment.text) {
97
+ return;
98
+ }
99
+ segments.push(segment);
100
+ };
101
+
102
+ const measureSegmentsWidth = (segments: TextSegment[]): number =>
103
+ segments.reduce((total, segment) => total + measureWidth(segment.text), 0);
104
+
105
+ const truncateSegmentsToWidth = (
106
+ segments: TextSegment[],
107
+ maxWidth: number,
108
+ ): TextSegment[] => {
109
+ if (maxWidth <= 0) {
110
+ return [];
111
+ }
112
+ const result: TextSegment[] = [];
113
+ let currentWidth = 0;
114
+ for (const segment of segments) {
115
+ if (currentWidth >= maxWidth) {
116
+ break;
117
+ }
118
+ const segmentWidth = measureWidth(segment.text);
119
+ if (currentWidth + segmentWidth <= maxWidth) {
120
+ result.push(segment);
121
+ currentWidth += segmentWidth;
122
+ continue;
123
+ }
124
+ const remaining = maxWidth - currentWidth;
125
+ const truncated = truncateToWidth(segment.text, remaining);
126
+ if (truncated) {
127
+ result.push({ ...segment, text: truncated });
128
+ currentWidth += measureWidth(truncated);
129
+ }
130
+ break;
131
+ }
132
+ return result;
133
+ };
134
+
135
+ const segmentsToText = (segments: TextSegment[]): string =>
136
+ segments.map((segment) => segment.text).join("");
137
+
138
+ const padLine = (value: string, width: number): string => {
139
+ const padding = Math.max(0, width - measureWidth(value));
140
+ return padding > 0 ? `${value}${" ".repeat(padding)}` : value;
141
+ };
142
+
143
+ const resolveEntryLevel = (entry: FormattedLogEntry): number => {
144
+ const rawLevel = entry.raw?.level;
145
+ if (typeof rawLevel === "number") {
146
+ return rawLevel;
147
+ }
148
+ const label = entry.levelLabel?.toUpperCase() ?? "";
149
+ return LEVEL_LABEL_VALUES[label] ?? 0;
150
+ };
151
+
152
+ export function LogScreen(props: LogScreenProps) {
153
+ const merged = mergeProps(
154
+ {
155
+ loading: false,
156
+ error: null,
157
+ branchLabel: null,
158
+ sourceLabel: null,
159
+ tailing: false,
160
+ helpVisible: false,
161
+ },
162
+ props,
163
+ );
38
164
  const terminal = useTerminalSize();
165
+ const [filterQuery, setFilterQuery] = createSignal("");
166
+ const [filterMode, setFilterMode] = createSignal(false);
167
+ const [levelMode, setLevelMode] = createSignal<LevelMode>("ALL");
168
+
169
+ const filteredEntries = createMemo(() => {
170
+ let result = merged.entries;
171
+ const threshold = LEVEL_THRESHOLDS[levelMode()];
172
+ if (threshold > 0) {
173
+ result = result.filter((entry) => resolveEntryLevel(entry) >= threshold);
174
+ }
175
+
176
+ const query = filterQuery().trim().toLowerCase();
177
+ if (!query) {
178
+ return result;
179
+ }
180
+ return result.filter((entry) => {
181
+ const target =
182
+ `${entry.category} ${entry.levelLabel} ${entry.message}`.toLowerCase();
183
+ return target.includes(query);
184
+ });
185
+ });
186
+
187
+ const totalCount = createMemo(() => merged.entries.length);
188
+ const filteredCount = createMemo(() => filteredEntries().length);
189
+ const showFilterCount = createMemo(
190
+ () => filterMode() || filterQuery().trim().length > 0,
191
+ );
192
+
193
+ const levelWidth = createMemo(() => {
194
+ const MIN = 5;
195
+ return Math.max(
196
+ MIN,
197
+ ...filteredEntries().map((entry) => measureWidth(entry.levelLabel)),
198
+ );
199
+ });
200
+
201
+ const categoryWidth = createMemo(() => {
202
+ const MIN = 4;
203
+ const MAX = 20;
204
+ const maxWidth = Math.max(
205
+ MIN,
206
+ ...filteredEntries().map((entry) => measureWidth(entry.category)),
207
+ );
208
+ return Math.min(MAX, maxWidth);
209
+ });
210
+
39
211
  const listHeight = createMemo(() => {
40
212
  const headerRows = 2;
41
- const infoRows = 1;
213
+ const infoRows = 3;
42
214
  const footerRows = 1;
43
- const notificationRows = notification ? 1 : 0;
215
+ const notificationRows = merged.notification ? 1 : 0;
44
216
  const reserved = headerRows + infoRows + footerRows + notificationRows;
45
217
  return Math.max(1, terminal().rows - reserved);
46
218
  });
47
219
 
48
220
  const list = useScrollableList({
49
- items: () => entries,
221
+ items: filteredEntries,
50
222
  visibleCount: listHeight,
51
223
  });
52
224
 
53
- const currentEntry = createMemo(() => entries[list.selectedIndex()]);
225
+ const currentEntry = createMemo(
226
+ () => filteredEntries()[list.selectedIndex()],
227
+ );
228
+
229
+ createEffect(() => {
230
+ filterQuery();
231
+ levelMode();
232
+ list.setSelectedIndex(0);
233
+ list.setScrollOffset(0);
234
+ });
54
235
 
55
236
  const updateSelectedIndex = (value: number | ((prev: number) => number)) => {
56
237
  list.setSelectedIndex(value);
57
238
  };
58
239
 
59
240
  useKeyboard((key) => {
60
- if (helpVisible) {
241
+ if (merged.helpVisible) {
61
242
  return;
62
243
  }
244
+
245
+ if (filterMode()) {
246
+ if (key.name === "down") {
247
+ updateSelectedIndex((prev) => prev + 1);
248
+ return;
249
+ }
250
+ if (key.name === "up") {
251
+ updateSelectedIndex((prev) => prev - 1);
252
+ return;
253
+ }
254
+ if (key.name === "escape") {
255
+ if (filterQuery()) {
256
+ setFilterQuery("");
257
+ } else {
258
+ setFilterMode(false);
259
+ }
260
+ return;
261
+ }
262
+ if (key.name === "backspace") {
263
+ setFilterQuery((prev) => prev.slice(0, -1));
264
+ return;
265
+ }
266
+ if (key.name === "return" || key.name === "linefeed") {
267
+ setFilterMode(false);
268
+ return;
269
+ }
270
+ if (
271
+ key.sequence &&
272
+ key.sequence.length === 1 &&
273
+ !key.ctrl &&
274
+ !key.meta &&
275
+ !key.super &&
276
+ !key.hyper
277
+ ) {
278
+ setFilterQuery((prev) => prev + key.sequence);
279
+ }
280
+ return;
281
+ }
282
+
63
283
  if (key.name === "escape" || key.name === "q") {
64
- onBack();
284
+ merged.onBack();
65
285
  return;
66
286
  }
67
287
 
68
288
  if (key.name === "c") {
69
289
  const entry = currentEntry();
70
290
  if (entry) {
71
- onCopy(entry);
291
+ merged.onCopy(entry);
72
292
  }
73
293
  return;
74
294
  }
75
295
 
76
296
  if (key.name === "d") {
77
- onPickDate?.();
297
+ merged.onPickDate?.();
298
+ return;
299
+ }
300
+
301
+ if (key.name === "f") {
302
+ setFilterMode(true);
303
+ return;
304
+ }
305
+
306
+ if (key.name === "v") {
307
+ setLevelMode((prev) => {
308
+ const index = LEVEL_ORDER.indexOf(prev);
309
+ const nextIndex = (index + 1) % LEVEL_ORDER.length;
310
+ return LEVEL_ORDER[nextIndex] ?? "ALL";
311
+ });
312
+ return;
313
+ }
314
+
315
+ if (key.name === "r") {
316
+ merged.onReload?.();
317
+ return;
318
+ }
319
+
320
+ if (key.name === "t") {
321
+ merged.onToggleTail?.();
322
+ return;
323
+ }
324
+
325
+ if (key.name === "x") {
326
+ merged.onReset?.();
78
327
  return;
79
328
  }
80
329
 
@@ -104,14 +353,14 @@ export function LogScreen({
104
353
  }
105
354
 
106
355
  if (key.name === "end") {
107
- updateSelectedIndex(entries.length - 1);
356
+ updateSelectedIndex(filteredEntries().length - 1);
108
357
  return;
109
358
  }
110
359
 
111
360
  if (key.name === "return" || key.name === "linefeed") {
112
361
  const entry = currentEntry();
113
362
  if (entry) {
114
- onSelect(entry);
363
+ merged.onSelect(entry);
115
364
  }
116
365
  }
117
366
  });
@@ -121,52 +370,132 @@ export function LogScreen({
121
370
  { key: "enter", description: "Detail" },
122
371
  { key: "c", description: "Copy" },
123
372
  { key: "d", description: "Date" },
373
+ { key: "f", description: "Filter" },
374
+ { key: "v", description: "Level" },
375
+ { key: "r", description: "Reload" },
376
+ { key: "t", description: "Tail" },
377
+ { key: "x", description: "Reset" },
124
378
  { key: "esc", description: "Back" },
125
379
  ];
126
380
  return actions;
127
381
  });
128
382
 
383
+ const formatEntrySegments = (entry: FormattedLogEntry): TextSegment[] => {
384
+ const levelText = padToWidth(entry.levelLabel, levelWidth());
385
+ const categoryText = padToWidth(entry.category, categoryWidth());
386
+ const segments: TextSegment[] = [];
387
+ appendSegment(segments, { text: `[${entry.timeLabel}] ` });
388
+ appendSegment(segments, { text: "[" });
389
+ appendSegment(segments, {
390
+ text: levelText,
391
+ fg: getLogLevelColor(entry.levelLabel),
392
+ });
393
+ appendSegment(segments, { text: "] " });
394
+ appendSegment(segments, { text: "[" });
395
+ appendSegment(segments, { text: categoryText });
396
+ appendSegment(segments, { text: "] " });
397
+ appendSegment(segments, { text: entry.message });
398
+ const maxWidth = terminal().columns;
399
+ if (measureSegmentsWidth(segments) <= maxWidth) {
400
+ return segments;
401
+ }
402
+ return truncateSegmentsToWidth(segments, maxWidth);
403
+ };
404
+
405
+ const filterLabel = createMemo(() => {
406
+ if (filterQuery()) {
407
+ return filterQuery();
408
+ }
409
+ return filterMode() ? "(type to filter)" : "(press f to filter)";
410
+ });
411
+
129
412
  return (
130
413
  <box flexDirection="column" height={terminal().rows}>
131
- <Header title="gwt - Log Viewer" titleColor="cyan" version={version} />
414
+ <Header
415
+ title="gwt - Log Viewer"
416
+ titleColor="cyan"
417
+ version={merged.version}
418
+ />
132
419
 
133
- {notification ? (
134
- <text fg={notification.tone === "error" ? "red" : "green"}>
135
- {notification.message}
420
+ {merged.notification ? (
421
+ <text fg={merged.notification.tone === "error" ? "red" : "green"}>
422
+ {merged.notification.message}
136
423
  </text>
137
424
  ) : null}
138
425
 
426
+ <box flexDirection="row">
427
+ <text attributes={TextAttributes.DIM}>Branch: </text>
428
+ <text attributes={TextAttributes.BOLD}>
429
+ {merged.branchLabel ?? "(none)"}
430
+ </text>
431
+ <text attributes={TextAttributes.DIM}> Source: </text>
432
+ <text attributes={TextAttributes.BOLD}>
433
+ {merged.sourceLabel ?? "(none)"}
434
+ </text>
435
+ </box>
436
+
139
437
  <box flexDirection="row">
140
438
  <text attributes={TextAttributes.DIM}>Date: </text>
141
- <text attributes={TextAttributes.BOLD}>{selectedDate ?? "---"}</text>
439
+ <text attributes={TextAttributes.BOLD}>
440
+ {merged.selectedDate ?? "---"}
441
+ </text>
142
442
  <text attributes={TextAttributes.DIM}> Total: </text>
143
- <text attributes={TextAttributes.BOLD}>{entries.length}</text>
443
+ <text attributes={TextAttributes.BOLD}>{totalCount()}</text>
444
+ <text attributes={TextAttributes.DIM}> Level: </text>
445
+ <text attributes={TextAttributes.BOLD}>{levelMode()}</text>
446
+ <text attributes={TextAttributes.DIM}> Tail: </text>
447
+ <text attributes={TextAttributes.BOLD}>
448
+ {merged.tailing ? "ON" : "OFF"}
449
+ </text>
450
+ </box>
451
+
452
+ <box flexDirection="row">
453
+ <text attributes={TextAttributes.DIM}>Filter: </text>
454
+ <text attributes={TextAttributes.BOLD}>{filterLabel()}</text>
455
+ {showFilterCount() ? (
456
+ <text attributes={TextAttributes.DIM}>
457
+ {` (Showing ${filteredCount()} of ${totalCount()})`}
458
+ </text>
459
+ ) : null}
144
460
  </box>
145
461
 
146
462
  <box flexDirection="column" flexGrow={1}>
147
- {loading ? (
463
+ {merged.loading ? (
148
464
  <text fg="gray">Loading logs...</text>
149
- ) : entries.length === 0 ? (
465
+ ) : filteredEntries().length === 0 ? (
150
466
  <text fg="gray">No logs available.</text>
151
467
  ) : (
152
468
  <box flexDirection="column">
153
469
  {list.visibleItems().map((entry, index) => {
154
470
  const absoluteIndex = list.scrollOffset() + index;
155
471
  const isSelected = absoluteIndex === list.selectedIndex();
472
+ const segments = formatEntrySegments(entry);
473
+ const lineText = segmentsToText(segments);
474
+ const selectedContent = padLine(lineText, terminal().columns);
156
475
  return (
157
476
  <text
158
477
  {...(isSelected
159
- ? { fg: "cyan", attributes: TextAttributes.BOLD }
478
+ ? { fg: selectionStyle.fg, bg: selectionStyle.bg }
160
479
  : {})}
480
+ wrapMode="none"
481
+ width={terminal().columns}
161
482
  >
162
- {entry.summary}
483
+ {isSelected
484
+ ? selectedContent
485
+ : segments.map((segment) =>
486
+ segment.fg ? (
487
+ <span style={{ fg: segment.fg }}>{segment.text}</span>
488
+ ) : (
489
+ segment.text
490
+ ),
491
+ )}
163
492
  </text>
164
493
  );
165
494
  })}
166
495
  </box>
167
496
  )}
168
497
 
169
- {error ? <text fg="red">{error}</text> : null}
498
+ {merged.error ? <text fg="red">{merged.error}</text> : null}
170
499
  </box>
171
500
 
172
501
  <Footer actions={footerActions()} />
@@ -1,11 +1,12 @@
1
1
  /** @jsxImportSource @opentui/solid */
2
- import { TextAttributes } from "@opentui/core";
3
2
  import { useKeyboard } from "@opentui/solid";
4
3
  import { createMemo } from "solid-js";
5
4
  import { Header } from "../../components/solid/Header.js";
6
5
  import { Footer } from "../../components/solid/Footer.js";
7
6
  import { useTerminalSize } from "../../hooks/solid/useTerminalSize.js";
8
7
  import { useScrollableList } from "../../hooks/solid/useScrollableList.js";
8
+ import stringWidth from "string-width";
9
+ import { selectionStyle } from "../../core/theme.js";
9
10
 
10
11
  export interface ProfileEnvVariable {
11
12
  key: string;
@@ -39,6 +40,10 @@ export function ProfileEnvScreen({
39
40
  helpVisible = false,
40
41
  }: ProfileEnvScreenProps) {
41
42
  const terminal = useTerminalSize();
43
+ const padLine = (value: string, width: number) => {
44
+ const padding = Math.max(0, width - stringWidth(value));
45
+ return padding > 0 ? `${value}${" ".repeat(padding)}` : value;
46
+ };
42
47
  const listHeight = createMemo(() => {
43
48
  const headerRows = 2;
44
49
  const footerRows = 1;
@@ -163,22 +168,21 @@ export function ProfileEnvScreen({
163
168
  {list.visibleItems().map((variable, index) => {
164
169
  const absoluteIndex = list.scrollOffset() + index;
165
170
  const isSelected = absoluteIndex === list.selectedIndex();
166
- const attributes = isSelected ? TextAttributes.BOLD : undefined;
167
- const color = isSelected ? "cyan" : undefined;
168
171
  return (
169
172
  <box flexDirection="row">
170
- <text
171
- {...(color ? { fg: color } : {})}
172
- {...(attributes !== undefined ? { attributes } : {})}
173
- >
174
- {variable.key}
175
- </text>
176
- <text
177
- {...(color ? { fg: color } : {})}
178
- {...(attributes !== undefined ? { attributes } : {})}
179
- >
180
- ={variable.value}
181
- </text>
173
+ {isSelected ? (
174
+ <text fg={selectionStyle.fg} bg={selectionStyle.bg}>
175
+ {padLine(
176
+ `${variable.key}=${variable.value}`,
177
+ terminal().columns,
178
+ )}
179
+ </text>
180
+ ) : (
181
+ <>
182
+ <text>{variable.key}</text>
183
+ <text>={variable.value}</text>
184
+ </>
185
+ )}
182
186
  </box>
183
187
  );
184
188
  })}
@@ -6,6 +6,8 @@ import { Header } from "../../components/solid/Header.js";
6
6
  import { Footer } from "../../components/solid/Footer.js";
7
7
  import { useTerminalSize } from "../../hooks/solid/useTerminalSize.js";
8
8
  import { useScrollableList } from "../../hooks/solid/useScrollableList.js";
9
+ import stringWidth from "string-width";
10
+ import { selectionStyle } from "../../core/theme.js";
9
11
 
10
12
  export interface SelectorItem {
11
13
  label: string;
@@ -30,6 +32,11 @@ export interface SelectorScreenProps {
30
32
  const clamp = (value: number, min: number, max: number) =>
31
33
  Math.min(Math.max(value, min), max);
32
34
 
35
+ const padLine = (value: string, width: number): string => {
36
+ const padding = Math.max(0, width - stringWidth(value));
37
+ return padding > 0 ? `${value}${" ".repeat(padding)}` : value;
38
+ };
39
+
33
40
  export function SelectorScreen({
34
41
  title,
35
42
  description,
@@ -140,23 +147,27 @@ export function SelectorScreen({
140
147
  {list.visibleItems().map((item, index) => {
141
148
  const absoluteIndex = list.scrollOffset() + index;
142
149
  const isSelected = absoluteIndex === selectedIndex();
150
+ const descriptionText =
151
+ showDescription && item.description
152
+ ? ` - ${item.description}`
153
+ : "";
154
+ const fullLine = `${item.label}${descriptionText}`;
143
155
  return (
144
156
  <box flexDirection="row">
145
- <text
146
- {...(isSelected
147
- ? { fg: "cyan", attributes: TextAttributes.BOLD }
148
- : {})}
149
- >
150
- {item.label}
151
- </text>
152
- {showDescription && item.description ? (
153
- <text
154
- attributes={TextAttributes.DIM}
155
- {...(isSelected ? { fg: "cyan" } : {})}
156
- >
157
- {` - ${item.description}`}
157
+ {isSelected ? (
158
+ <text fg={selectionStyle.fg} bg={selectionStyle.bg}>
159
+ {padLine(fullLine, terminal().columns)}
158
160
  </text>
159
- ) : null}
161
+ ) : (
162
+ <>
163
+ <text>{item.label}</text>
164
+ {showDescription && item.description ? (
165
+ <text attributes={TextAttributes.DIM}>
166
+ {` - ${item.description}`}
167
+ </text>
168
+ ) : null}
169
+ </>
170
+ )}
160
171
  </box>
161
172
  );
162
173
  })}
@@ -25,7 +25,9 @@ export function SettingsScreen({
25
25
  const items = settings.map((setting) => ({
26
26
  label: setting.label,
27
27
  value: setting.value,
28
- description: setting.description,
28
+ ...(setting.description !== undefined
29
+ ? { description: setting.description }
30
+ : {}),
29
31
  }));
30
32
 
31
33
  const handleSelect = (item: { label: string; value: string }) => {
@@ -40,8 +42,8 @@ export function SettingsScreen({
40
42
  title="gwt - Settings"
41
43
  items={items}
42
44
  onSelect={handleSelect}
43
- onBack={onBack}
44
- version={version}
45
+ {...(onBack ? { onBack } : {})}
46
+ {...(version !== undefined ? { version } : {})}
45
47
  emptyMessage="No settings available."
46
48
  showDescription
47
49
  helpVisible={helpVisible}
@@ -113,6 +113,7 @@ export interface SelectedBranchState {
113
113
  branchType: "local" | "remote";
114
114
  branchCategory: BranchInfo["branchType"];
115
115
  remoteBranch?: string;
116
+ worktreePath?: string | null;
116
117
  }
117
118
 
118
119
  export interface UIFilter {