@gxp-dev/tools 2.0.63 → 2.0.65

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 (182) hide show
  1. package/README.md +32 -31
  2. package/bin/gx-devtools.js +74 -54
  3. package/bin/lib/cli.js +23 -21
  4. package/bin/lib/commands/add-dependency.js +366 -325
  5. package/bin/lib/commands/assets.js +137 -139
  6. package/bin/lib/commands/build.js +169 -174
  7. package/bin/lib/commands/datastore.js +181 -183
  8. package/bin/lib/commands/dev.js +127 -131
  9. package/bin/lib/commands/extensions.js +147 -149
  10. package/bin/lib/commands/extract-config.js +73 -67
  11. package/bin/lib/commands/index.js +12 -12
  12. package/bin/lib/commands/init.js +342 -240
  13. package/bin/lib/commands/publish.js +69 -75
  14. package/bin/lib/commands/socket.js +69 -69
  15. package/bin/lib/commands/ssl.js +14 -14
  16. package/bin/lib/constants.js +10 -24
  17. package/bin/lib/tui/App.tsx +761 -705
  18. package/bin/lib/tui/components/AIPanel.tsx +191 -171
  19. package/bin/lib/tui/components/CommandInput.tsx +394 -343
  20. package/bin/lib/tui/components/GeminiPanel.tsx +175 -151
  21. package/bin/lib/tui/components/Header.tsx +23 -21
  22. package/bin/lib/tui/components/LogPanel.tsx +244 -220
  23. package/bin/lib/tui/components/TabBar.tsx +50 -48
  24. package/bin/lib/tui/components/WelcomeScreen.tsx +126 -71
  25. package/bin/lib/tui/index.tsx +37 -39
  26. package/bin/lib/tui/services/AIService.ts +518 -462
  27. package/bin/lib/tui/services/ExtensionService.ts +140 -129
  28. package/bin/lib/tui/services/GeminiService.ts +367 -337
  29. package/bin/lib/tui/services/ServiceManager.ts +344 -322
  30. package/bin/lib/tui/services/SocketService.ts +168 -168
  31. package/bin/lib/tui/services/ViteService.ts +88 -88
  32. package/bin/lib/tui/services/index.ts +47 -22
  33. package/bin/lib/utils/ai-scaffold.js +291 -280
  34. package/bin/lib/utils/extract-config.js +157 -140
  35. package/bin/lib/utils/files.js +82 -86
  36. package/bin/lib/utils/index.js +7 -7
  37. package/bin/lib/utils/paths.js +34 -34
  38. package/bin/lib/utils/prompts.js +194 -169
  39. package/bin/lib/utils/ssl.js +79 -81
  40. package/browser-extensions/README.md +0 -1
  41. package/browser-extensions/chrome/background.js +244 -237
  42. package/browser-extensions/chrome/content.js +32 -29
  43. package/browser-extensions/chrome/devtools.html +7 -7
  44. package/browser-extensions/chrome/devtools.js +19 -19
  45. package/browser-extensions/chrome/inspector.js +802 -767
  46. package/browser-extensions/chrome/manifest.json +71 -63
  47. package/browser-extensions/chrome/panel.html +674 -636
  48. package/browser-extensions/chrome/panel.js +722 -712
  49. package/browser-extensions/chrome/popup.html +586 -543
  50. package/browser-extensions/chrome/popup.js +282 -244
  51. package/browser-extensions/chrome/rules.json +1 -1
  52. package/browser-extensions/chrome/test-chrome.html +216 -136
  53. package/browser-extensions/chrome/test-mixed-content.html +284 -189
  54. package/browser-extensions/chrome/test-uri-pattern.html +221 -198
  55. package/browser-extensions/firefox/README.md +9 -6
  56. package/browser-extensions/firefox/background.js +221 -218
  57. package/browser-extensions/firefox/content.js +55 -52
  58. package/browser-extensions/firefox/debug-errors.html +386 -228
  59. package/browser-extensions/firefox/debug-https.html +153 -105
  60. package/browser-extensions/firefox/devtools.html +7 -7
  61. package/browser-extensions/firefox/devtools.js +23 -20
  62. package/browser-extensions/firefox/inspector.js +802 -767
  63. package/browser-extensions/firefox/manifest.json +68 -68
  64. package/browser-extensions/firefox/panel.html +674 -636
  65. package/browser-extensions/firefox/panel.js +722 -712
  66. package/browser-extensions/firefox/popup.html +572 -535
  67. package/browser-extensions/firefox/popup.js +281 -236
  68. package/browser-extensions/firefox/test-gramercy.html +170 -125
  69. package/browser-extensions/firefox/test-imports.html +59 -55
  70. package/browser-extensions/firefox/test-masking.html +231 -140
  71. package/browser-extensions/firefox/test-uri-pattern.html +221 -198
  72. package/dist/tui/App.d.ts +1 -1
  73. package/dist/tui/App.d.ts.map +1 -1
  74. package/dist/tui/App.js +154 -150
  75. package/dist/tui/App.js.map +1 -1
  76. package/dist/tui/components/AIPanel.d.ts.map +1 -1
  77. package/dist/tui/components/AIPanel.js +42 -35
  78. package/dist/tui/components/AIPanel.js.map +1 -1
  79. package/dist/tui/components/CommandInput.d.ts +1 -1
  80. package/dist/tui/components/CommandInput.d.ts.map +1 -1
  81. package/dist/tui/components/CommandInput.js +92 -62
  82. package/dist/tui/components/CommandInput.js.map +1 -1
  83. package/dist/tui/components/GeminiPanel.d.ts.map +1 -1
  84. package/dist/tui/components/GeminiPanel.js +37 -30
  85. package/dist/tui/components/GeminiPanel.js.map +1 -1
  86. package/dist/tui/components/Header.d.ts.map +1 -1
  87. package/dist/tui/components/Header.js +1 -1
  88. package/dist/tui/components/Header.js.map +1 -1
  89. package/dist/tui/components/LogPanel.d.ts +1 -1
  90. package/dist/tui/components/LogPanel.d.ts.map +1 -1
  91. package/dist/tui/components/LogPanel.js +26 -24
  92. package/dist/tui/components/LogPanel.js.map +1 -1
  93. package/dist/tui/components/TabBar.d.ts +2 -2
  94. package/dist/tui/components/TabBar.d.ts.map +1 -1
  95. package/dist/tui/components/TabBar.js +11 -11
  96. package/dist/tui/components/TabBar.js.map +1 -1
  97. package/dist/tui/components/WelcomeScreen.d.ts.map +1 -1
  98. package/dist/tui/components/WelcomeScreen.js +6 -6
  99. package/dist/tui/components/WelcomeScreen.js.map +1 -1
  100. package/dist/tui/index.d.ts.map +1 -1
  101. package/dist/tui/index.js +8 -8
  102. package/dist/tui/index.js.map +1 -1
  103. package/dist/tui/services/AIService.d.ts +2 -2
  104. package/dist/tui/services/AIService.d.ts.map +1 -1
  105. package/dist/tui/services/AIService.js +165 -125
  106. package/dist/tui/services/AIService.js.map +1 -1
  107. package/dist/tui/services/ExtensionService.d.ts +1 -1
  108. package/dist/tui/services/ExtensionService.d.ts.map +1 -1
  109. package/dist/tui/services/ExtensionService.js +33 -26
  110. package/dist/tui/services/ExtensionService.js.map +1 -1
  111. package/dist/tui/services/GeminiService.d.ts +1 -1
  112. package/dist/tui/services/GeminiService.d.ts.map +1 -1
  113. package/dist/tui/services/GeminiService.js +87 -76
  114. package/dist/tui/services/GeminiService.js.map +1 -1
  115. package/dist/tui/services/ServiceManager.d.ts +3 -3
  116. package/dist/tui/services/ServiceManager.d.ts.map +1 -1
  117. package/dist/tui/services/ServiceManager.js +72 -58
  118. package/dist/tui/services/ServiceManager.js.map +1 -1
  119. package/dist/tui/services/SocketService.d.ts.map +1 -1
  120. package/dist/tui/services/SocketService.js +32 -32
  121. package/dist/tui/services/SocketService.js.map +1 -1
  122. package/dist/tui/services/ViteService.d.ts.map +1 -1
  123. package/dist/tui/services/ViteService.js +26 -28
  124. package/dist/tui/services/ViteService.js.map +1 -1
  125. package/dist/tui/services/index.d.ts +6 -6
  126. package/dist/tui/services/index.d.ts.map +1 -1
  127. package/dist/tui/services/index.js +6 -6
  128. package/dist/tui/services/index.js.map +1 -1
  129. package/mcp/gxp-api-server.js +83 -81
  130. package/package.json +109 -93
  131. package/runtime/PortalContainer.vue +258 -234
  132. package/runtime/dev-tools/DevToolsModal.vue +153 -155
  133. package/runtime/dev-tools/LayoutSwitcher.vue +144 -140
  134. package/runtime/dev-tools/MockDataEditor.vue +456 -433
  135. package/runtime/dev-tools/SocketSimulator.vue +379 -371
  136. package/runtime/dev-tools/StoreInspector.vue +517 -455
  137. package/runtime/dev-tools/index.js +5 -5
  138. package/runtime/fallback-layouts/PrivateLayout.vue +2 -2
  139. package/runtime/fallback-layouts/PublicLayout.vue +2 -2
  140. package/runtime/fallback-layouts/SystemLayout.vue +2 -2
  141. package/runtime/gxpStringsPlugin.js +159 -134
  142. package/runtime/index.html +17 -19
  143. package/runtime/main.js +24 -22
  144. package/runtime/mock-api/auth-middleware.js +15 -15
  145. package/runtime/mock-api/image-generator.js +46 -46
  146. package/runtime/mock-api/index.js +55 -55
  147. package/runtime/mock-api/response-generator.js +116 -105
  148. package/runtime/mock-api/route-generator.js +107 -84
  149. package/runtime/mock-api/socket-triggers.js +94 -93
  150. package/runtime/mock-api/spec-loader.js +79 -80
  151. package/runtime/package.json +3 -0
  152. package/runtime/server.js +68 -68
  153. package/runtime/stores/gxpPortalConfigStore.js +204 -186
  154. package/runtime/stores/index.js +2 -2
  155. package/runtime/vite-inspector-plugin.js +858 -707
  156. package/runtime/vite-source-tracker-plugin.js +132 -113
  157. package/runtime/vite.config.js +191 -139
  158. package/scripts/launch-chrome.js +41 -41
  159. package/scripts/pack-chrome.js +38 -39
  160. package/socket-events/AiSessionMessageCreated.json +17 -17
  161. package/socket-events/SocialStreamPostCreated.json +23 -23
  162. package/socket-events/SocialStreamPostVariantCompleted.json +22 -22
  163. package/template/.claude/agents/gxp-developer.md +100 -99
  164. package/template/.claude/settings.json +7 -7
  165. package/template/AGENTS.md +30 -23
  166. package/template/GEMINI.md +20 -20
  167. package/template/README.md +70 -53
  168. package/template/app-manifest.json +2 -4
  169. package/template/configuration.json +10 -10
  170. package/template/default-styling.css +1 -1
  171. package/template/index.html +18 -20
  172. package/template/main.js +24 -22
  173. package/template/src/DemoPage.vue +415 -362
  174. package/template/src/Plugin.vue +76 -85
  175. package/template/src/stores/index.js +3 -3
  176. package/template/src/stores/test-data.json +164 -172
  177. package/template/theme-layouts/AdditionalStyling.css +50 -50
  178. package/template/theme-layouts/PrivateLayout.vue +8 -12
  179. package/template/theme-layouts/PublicLayout.vue +8 -12
  180. package/template/theme-layouts/SystemLayout.vue +8 -12
  181. package/template/vite.extend.js +45 -0
  182. package/template/vite.config.js +0 -409
@@ -1,233 +1,257 @@
1
- import React, { useState, useEffect, useMemo, useRef, memo } from 'react';
2
- import { Box, Text, useStdout, useInput } from 'ink';
1
+ import React, { useState, useEffect, useMemo, useRef, memo } from "react"
2
+ import { Box, Text, useStdout, useInput } from "ink"
3
3
 
4
4
  interface LogPanelProps {
5
- logs: string[];
6
- isActive?: boolean;
7
- maxHeight?: number; // Maximum height in rows (from parent container)
5
+ logs: string[]
6
+ isActive?: boolean
7
+ maxHeight?: number // Maximum height in rows (from parent container)
8
8
  }
9
9
 
10
10
  // Memoized log line component to prevent unnecessary re-renders
11
11
  const LogLine = memo(({ log, index }: { log: string; index: number }) => (
12
- <Text key={index} wrap="wrap">
13
- {formatLog(log)}
14
- </Text>
15
- ));
16
- LogLine.displayName = 'LogLine';
12
+ <Text key={index} wrap="wrap">
13
+ {formatLog(log)}
14
+ </Text>
15
+ ))
16
+ LogLine.displayName = "LogLine"
17
17
 
18
18
  function LogPanel({ logs, isActive = true, maxHeight }: LogPanelProps) {
19
- const { stdout } = useStdout();
20
- const [scrollOffset, setScrollOffset] = useState(0);
21
- const [autoScroll, setAutoScroll] = useState(true);
22
- const mouseEnabledRef = useRef(false);
23
- const logsLengthRef = useRef(logs.length);
24
- const maxLinesRef = useRef(0);
25
-
26
- // Keep refs in sync
27
- logsLengthRef.current = logs.length;
28
-
29
- // Calculate visible lines based on provided maxHeight or terminal height
30
- // Subtract 2 for padding, 2 for border, 1 for hint bar
31
- const maxLines = useMemo(() => {
32
- const defaultMaxLines = stdout ? Math.max(5, stdout.rows - 10) : 15;
33
- return maxHeight ? Math.max(3, maxHeight - 5) : defaultMaxLines;
34
- }, [stdout?.rows, maxHeight]);
35
-
36
- maxLinesRef.current = maxLines;
37
-
38
- // Reset scroll when logs are cleared
39
- useEffect(() => {
40
- if (logs.length === 0) {
41
- setScrollOffset(0);
42
- setAutoScroll(true);
43
- }
44
- }, [logs.length]);
45
-
46
- // Auto-scroll to bottom when new logs arrive (if autoScroll is enabled)
47
- useEffect(() => {
48
- if (autoScroll) {
49
- setScrollOffset(0);
50
- }
51
- }, [logs.length, autoScroll]);
52
-
53
- // Scroll helper functions
54
- const scrollUp = (lines: number) => {
55
- const maxOffset = Math.max(0, logs.length - maxLines);
56
- setScrollOffset(prev => Math.min(prev + lines, maxOffset));
57
- setAutoScroll(false);
58
- };
59
-
60
- const scrollDown = (lines: number) => {
61
- setScrollOffset(prev => {
62
- const newOffset = Math.max(prev - lines, 0);
63
- if (newOffset === 0) setAutoScroll(true);
64
- return newOffset;
65
- });
66
- };
67
-
68
- // Enable mouse wheel scrolling via terminal mouse reporting
69
- // We patch stdin.emit to intercept mouse sequences BEFORE Ink processes them,
70
- // preventing the escape codes from appearing in the command input
71
- useEffect(() => {
72
- if (!isActive || !process.stdin.isTTY) return;
73
-
74
- const mouseRegex = /\x1b\[<(\d+);\d+;\d+[Mm]/g;
75
- const originalEmit = process.stdin.emit;
76
-
77
- // Patch emit to intercept mouse data before Ink sees it
78
- const stdin = process.stdin;
79
- stdin.emit = function (this: typeof stdin, event: string, ...args: any[]): boolean {
80
- if (event === 'data') {
81
- const data = args[0];
82
- const str = typeof data === 'string' ? data : data.toString();
83
-
84
- // Check for mouse sequences
85
- let hasMouseEvent = false;
86
- let match;
87
- while ((match = mouseRegex.exec(str)) !== null) {
88
- hasMouseEvent = true;
89
- const button = parseInt(match[1], 10);
90
- if (button === 64) {
91
- // Scroll up
92
- const maxOffset = Math.max(0, logsLengthRef.current - maxLinesRef.current);
93
- setScrollOffset(prev => Math.min(prev + 3, maxOffset));
94
- setAutoScroll(false);
95
- } else if (button === 65) {
96
- // Scroll down
97
- setScrollOffset(prev => {
98
- const newOffset = Math.max(prev - 3, 0);
99
- if (newOffset === 0) setAutoScroll(true);
100
- return newOffset;
101
- });
102
- }
103
- }
104
- mouseRegex.lastIndex = 0;
105
-
106
- if (hasMouseEvent) {
107
- // Strip mouse sequences, forward any remaining non-mouse data to Ink
108
- const remaining = str.replace(mouseRegex, '');
109
- if (remaining.length > 0) {
110
- return originalEmit.call(stdin, event, remaining);
111
- }
112
- return true; // consumed entirely
113
- }
114
- }
115
- return originalEmit.apply(stdin, [event, ...args]);
116
- } as typeof stdin.emit;
117
-
118
- // Disable mouse mode helper - used on unmount and process exit
119
- const disableMouse = () => {
120
- if (mouseEnabledRef.current) {
121
- process.stdout.write('\x1b[?1000l\x1b[?1006l');
122
- mouseEnabledRef.current = false;
123
- }
124
- };
125
-
126
- // Ensure mouse mode is disabled on process exit/signals
127
- const onExit = () => disableMouse();
128
- process.on('exit', onExit);
129
- process.on('SIGINT', onExit);
130
- process.on('SIGTERM', onExit);
131
-
132
- // Enable SGR mouse mode for wheel events
133
- process.stdout.write('\x1b[?1000h\x1b[?1006h');
134
- mouseEnabledRef.current = true;
135
-
136
- return () => {
137
- // Restore original emit and disable mouse mode
138
- process.stdin.emit = originalEmit;
139
- disableMouse();
140
- process.off('exit', onExit);
141
- process.off('SIGINT', onExit);
142
- process.off('SIGTERM', onExit);
143
- };
144
- }, [isActive]);
145
-
146
- // Handle keyboard input for scrolling
147
- useInput((input, key) => {
148
- if (!isActive) return;
149
-
150
- // Page Up - scroll up by half a page
151
- if (key.pageUp || (key.shift && key.upArrow)) {
152
- scrollUp(Math.floor(maxLines / 2));
153
- return;
154
- }
155
-
156
- // Page Down - scroll down by half a page
157
- if (key.pageDown || (key.shift && key.downArrow)) {
158
- scrollDown(Math.floor(maxLines / 2));
159
- return;
160
- }
161
-
162
- // Home - scroll to top
163
- if (key.meta && key.upArrow) {
164
- const maxOffset = Math.max(0, logs.length - maxLines);
165
- setScrollOffset(maxOffset);
166
- setAutoScroll(false);
167
- return;
168
- }
169
-
170
- // End - scroll to bottom
171
- if (key.meta && key.downArrow) {
172
- setScrollOffset(0);
173
- setAutoScroll(true);
174
- return;
175
- }
176
- });
177
-
178
- // Calculate visible logs with scroll offset - memoized
179
- const { visibleLogs, startIndex, canScrollUp, canScrollDown } = useMemo(() => {
180
- const totalLogs = logs.length;
181
- const start = Math.max(0, totalLogs - maxLines - scrollOffset);
182
- const end = Math.max(0, totalLogs - scrollOffset);
183
- return {
184
- visibleLogs: logs.slice(start, end),
185
- startIndex: start,
186
- canScrollUp: start > 0,
187
- canScrollDown: scrollOffset > 0,
188
- };
189
- }, [logs, maxLines, scrollOffset]);
190
-
191
- return (
192
- <Box flexDirection="column" padding={1} flexGrow={1} overflow="hidden">
193
- <Box justifyContent="flex-end">
194
- <Text color="gray" dimColor>Shift+↑↓ scroll Ctrl+↑↓ jump mouse wheel</Text>
195
- </Box>
196
- {canScrollUp && (
197
- <Text color="gray" dimColor>↑ {startIndex} more lines</Text>
198
- )}
199
- {visibleLogs.length === 0 ? (
200
- <Text color="gray" dimColor>No logs yet...</Text>
201
- ) : (
202
- visibleLogs.map((log, index) => (
203
- <LogLine key={startIndex + index} log={log} index={startIndex + index} />
204
- ))
205
- )}
206
- {canScrollDown && (
207
- <Text color="gray" dimColor>↓ {scrollOffset} more lines</Text>
208
- )}
209
- </Box>
210
- );
19
+ const { stdout } = useStdout()
20
+ const [scrollOffset, setScrollOffset] = useState(0)
21
+ const [autoScroll, setAutoScroll] = useState(true)
22
+ const mouseEnabledRef = useRef(false)
23
+ const logsLengthRef = useRef(logs.length)
24
+ const maxLinesRef = useRef(0)
25
+
26
+ // Keep refs in sync
27
+ logsLengthRef.current = logs.length
28
+
29
+ // Calculate visible lines based on provided maxHeight or terminal height
30
+ // Subtract 2 for padding, 2 for border, 1 for hint bar
31
+ const maxLines = useMemo(() => {
32
+ const defaultMaxLines = stdout ? Math.max(5, stdout.rows - 10) : 15
33
+ return maxHeight ? Math.max(3, maxHeight - 5) : defaultMaxLines
34
+ }, [stdout?.rows, maxHeight])
35
+
36
+ maxLinesRef.current = maxLines
37
+
38
+ // Reset scroll when logs are cleared
39
+ useEffect(() => {
40
+ if (logs.length === 0) {
41
+ setScrollOffset(0)
42
+ setAutoScroll(true)
43
+ }
44
+ }, [logs.length])
45
+
46
+ // Auto-scroll to bottom when new logs arrive (if autoScroll is enabled)
47
+ useEffect(() => {
48
+ if (autoScroll) {
49
+ setScrollOffset(0)
50
+ }
51
+ }, [logs.length, autoScroll])
52
+
53
+ // Scroll helper functions
54
+ const scrollUp = (lines: number) => {
55
+ const maxOffset = Math.max(0, logs.length - maxLines)
56
+ setScrollOffset((prev) => Math.min(prev + lines, maxOffset))
57
+ setAutoScroll(false)
58
+ }
59
+
60
+ const scrollDown = (lines: number) => {
61
+ setScrollOffset((prev) => {
62
+ const newOffset = Math.max(prev - lines, 0)
63
+ if (newOffset === 0) setAutoScroll(true)
64
+ return newOffset
65
+ })
66
+ }
67
+
68
+ // Enable mouse wheel scrolling via terminal mouse reporting
69
+ // We patch stdin.emit to intercept mouse sequences BEFORE Ink processes them,
70
+ // preventing the escape codes from appearing in the command input
71
+ useEffect(() => {
72
+ if (!isActive || !process.stdin.isTTY) return
73
+
74
+ const mouseRegex = /\x1b\[<(\d+);\d+;\d+[Mm]/g
75
+ const originalEmit = process.stdin.emit
76
+
77
+ // Patch emit to intercept mouse data before Ink sees it
78
+ const stdin = process.stdin
79
+ stdin.emit = function (
80
+ this: typeof stdin,
81
+ event: string,
82
+ ...args: any[]
83
+ ): boolean {
84
+ if (event === "data") {
85
+ const data = args[0]
86
+ const str = typeof data === "string" ? data : data.toString()
87
+
88
+ // Check for mouse sequences
89
+ let hasMouseEvent = false
90
+ let match
91
+ while ((match = mouseRegex.exec(str)) !== null) {
92
+ hasMouseEvent = true
93
+ const button = parseInt(match[1], 10)
94
+ if (button === 64) {
95
+ // Scroll up
96
+ const maxOffset = Math.max(
97
+ 0,
98
+ logsLengthRef.current - maxLinesRef.current,
99
+ )
100
+ setScrollOffset((prev) => Math.min(prev + 3, maxOffset))
101
+ setAutoScroll(false)
102
+ } else if (button === 65) {
103
+ // Scroll down
104
+ setScrollOffset((prev) => {
105
+ const newOffset = Math.max(prev - 3, 0)
106
+ if (newOffset === 0) setAutoScroll(true)
107
+ return newOffset
108
+ })
109
+ }
110
+ }
111
+ mouseRegex.lastIndex = 0
112
+
113
+ if (hasMouseEvent) {
114
+ // Strip mouse sequences, forward any remaining non-mouse data to Ink
115
+ const remaining = str.replace(mouseRegex, "")
116
+ if (remaining.length > 0) {
117
+ return originalEmit.call(stdin, event, remaining)
118
+ }
119
+ return true // consumed entirely
120
+ }
121
+ }
122
+ return originalEmit.apply(stdin, [event, ...args])
123
+ } as typeof stdin.emit
124
+
125
+ // Disable mouse mode helper - used on unmount and process exit
126
+ const disableMouse = () => {
127
+ if (mouseEnabledRef.current) {
128
+ process.stdout.write("\x1b[?1000l\x1b[?1006l")
129
+ mouseEnabledRef.current = false
130
+ }
131
+ }
132
+
133
+ // Ensure mouse mode is disabled on process exit/signals
134
+ const onExit = () => disableMouse()
135
+ process.on("exit", onExit)
136
+ process.on("SIGINT", onExit)
137
+ process.on("SIGTERM", onExit)
138
+
139
+ // Enable SGR mouse mode for wheel events
140
+ process.stdout.write("\x1b[?1000h\x1b[?1006h")
141
+ mouseEnabledRef.current = true
142
+
143
+ return () => {
144
+ // Restore original emit and disable mouse mode
145
+ process.stdin.emit = originalEmit
146
+ disableMouse()
147
+ process.off("exit", onExit)
148
+ process.off("SIGINT", onExit)
149
+ process.off("SIGTERM", onExit)
150
+ }
151
+ }, [isActive])
152
+
153
+ // Handle keyboard input for scrolling
154
+ useInput((input, key) => {
155
+ if (!isActive) return
156
+
157
+ // Page Up - scroll up by half a page
158
+ if (key.pageUp || (key.shift && key.upArrow)) {
159
+ scrollUp(Math.floor(maxLines / 2))
160
+ return
161
+ }
162
+
163
+ // Page Down - scroll down by half a page
164
+ if (key.pageDown || (key.shift && key.downArrow)) {
165
+ scrollDown(Math.floor(maxLines / 2))
166
+ return
167
+ }
168
+
169
+ // Home - scroll to top
170
+ if (key.meta && key.upArrow) {
171
+ const maxOffset = Math.max(0, logs.length - maxLines)
172
+ setScrollOffset(maxOffset)
173
+ setAutoScroll(false)
174
+ return
175
+ }
176
+
177
+ // End - scroll to bottom
178
+ if (key.meta && key.downArrow) {
179
+ setScrollOffset(0)
180
+ setAutoScroll(true)
181
+ return
182
+ }
183
+ })
184
+
185
+ // Calculate visible logs with scroll offset - memoized
186
+ const { visibleLogs, startIndex, canScrollUp, canScrollDown } =
187
+ useMemo(() => {
188
+ const totalLogs = logs.length
189
+ const start = Math.max(0, totalLogs - maxLines - scrollOffset)
190
+ const end = Math.max(0, totalLogs - scrollOffset)
191
+ return {
192
+ visibleLogs: logs.slice(start, end),
193
+ startIndex: start,
194
+ canScrollUp: start > 0,
195
+ canScrollDown: scrollOffset > 0,
196
+ }
197
+ }, [logs, maxLines, scrollOffset])
198
+
199
+ return (
200
+ <Box flexDirection="column" padding={1} flexGrow={1} overflow="hidden">
201
+ <Box justifyContent="flex-end">
202
+ <Text color="gray" dimColor>
203
+ Shift+↑↓ scroll Ctrl+↑↓ jump mouse wheel
204
+ </Text>
205
+ </Box>
206
+ {canScrollUp && (
207
+ <Text color="gray" dimColor>
208
+ ↑ {startIndex} more lines
209
+ </Text>
210
+ )}
211
+ {visibleLogs.length === 0 ? (
212
+ <Text color="gray" dimColor>
213
+ No logs yet...
214
+ </Text>
215
+ ) : (
216
+ visibleLogs.map((log, index) => (
217
+ <LogLine
218
+ key={startIndex + index}
219
+ log={log}
220
+ index={startIndex + index}
221
+ />
222
+ ))
223
+ )}
224
+ {canScrollDown && (
225
+ <Text color="gray" dimColor>
226
+ ↓ {scrollOffset} more lines
227
+ </Text>
228
+ )}
229
+ </Box>
230
+ )
211
231
  }
212
232
 
213
233
  function formatLog(log: string): React.ReactNode {
214
- // Color code different log types
215
- if (log.startsWith('[VITE]') || log.includes('VITE')) {
216
- return <Text color="cyan">{log}</Text>;
217
- }
218
- if (log.startsWith('[SOCKET]') || log.includes('Socket')) {
219
- return <Text color="green">{log}</Text>;
220
- }
221
- if (log.includes('error') || log.includes('Error') || log.includes('ERROR')) {
222
- return <Text color="red">{log}</Text>;
223
- }
224
- if (log.includes('warning') || log.includes('Warning') || log.includes('WARN')) {
225
- return <Text color="yellow">{log}</Text>;
226
- }
227
- if (log.includes('Starting') || log.includes('started')) {
228
- return <Text color="blue">{log}</Text>;
229
- }
230
- return <Text>{log}</Text>;
234
+ // Color code different log types
235
+ if (log.startsWith("[VITE]") || log.includes("VITE")) {
236
+ return <Text color="cyan">{log}</Text>
237
+ }
238
+ if (log.startsWith("[SOCKET]") || log.includes("Socket")) {
239
+ return <Text color="green">{log}</Text>
240
+ }
241
+ if (log.includes("error") || log.includes("Error") || log.includes("ERROR")) {
242
+ return <Text color="red">{log}</Text>
243
+ }
244
+ if (
245
+ log.includes("warning") ||
246
+ log.includes("Warning") ||
247
+ log.includes("WARN")
248
+ ) {
249
+ return <Text color="yellow">{log}</Text>
250
+ }
251
+ if (log.includes("Starting") || log.includes("started")) {
252
+ return <Text color="blue">{log}</Text>
253
+ }
254
+ return <Text>{log}</Text>
231
255
  }
232
256
 
233
- export default memo(LogPanel);
257
+ export default memo(LogPanel)
@@ -1,56 +1,58 @@
1
- import React from 'react';
2
- import { Box, Text } from 'ink';
3
- import type { Service } from '../App.js';
1
+ import React from "react"
2
+ import { Box, Text } from "ink"
3
+ import type { Service } from "../App.js"
4
4
 
5
5
  interface TabBarProps {
6
- services: Service[];
7
- activeTab: number;
8
- onTabChange: (index: number) => void;
6
+ services: Service[]
7
+ activeTab: number
8
+ onTabChange: (index: number) => void
9
9
  }
10
10
 
11
- const STATUS_COLORS: Record<Service['status'], string> = {
12
- stopped: 'gray',
13
- starting: 'yellow',
14
- running: 'green',
15
- error: 'red',
16
- };
11
+ const STATUS_COLORS: Record<Service["status"], string> = {
12
+ stopped: "gray",
13
+ starting: "yellow",
14
+ running: "green",
15
+ error: "red",
16
+ }
17
17
 
18
- const STATUS_ICONS: Record<Service['status'], string> = {
19
- stopped: '',
20
- starting: '',
21
- running: '',
22
- error: '',
23
- };
18
+ const STATUS_ICONS: Record<Service["status"], string> = {
19
+ stopped: "",
20
+ starting: "",
21
+ running: "",
22
+ error: "",
23
+ }
24
24
 
25
- export default function TabBar({ services, activeTab, onTabChange }: TabBarProps) {
26
- return (
27
- <Box paddingX={1} gap={2} justifyContent="space-between">
28
- <Box gap={2}>
29
- {services.map((service, index) => {
30
- const isActive = index === activeTab;
31
- const statusColor = STATUS_COLORS[service.status];
32
- const statusIcon = STATUS_ICONS[service.status];
25
+ export default function TabBar({
26
+ services,
27
+ activeTab,
28
+ onTabChange,
29
+ }: TabBarProps) {
30
+ return (
31
+ <Box paddingX={1} gap={2} justifyContent="space-between">
32
+ <Box gap={2}>
33
+ {services.map((service, index) => {
34
+ const isActive = index === activeTab
35
+ const statusColor = STATUS_COLORS[service.status]
36
+ const statusIcon = STATUS_ICONS[service.status]
33
37
 
34
- return (
35
- <Box key={service.id}>
36
- <Text
37
- color={isActive ? 'white' : 'gray'}
38
- backgroundColor={isActive ? 'blue' : undefined}
39
- bold={isActive}
40
- >
41
- {' '}
42
- <Text color={statusColor}>{statusIcon}</Text>
43
- {' '}
44
- {service.name}
45
- {' '}
46
- </Text>
47
- </Box>
48
- );
49
- })}
50
- </Box>
51
- <Box>
52
- <Text color="gray">←/→ switch tabs</Text>
53
- </Box>
54
- </Box>
55
- );
38
+ return (
39
+ <Box key={service.id}>
40
+ <Text
41
+ color={isActive ? "white" : "gray"}
42
+ backgroundColor={isActive ? "blue" : undefined}
43
+ bold={isActive}
44
+ >
45
+ {" "}
46
+ <Text color={statusColor}>{statusIcon}</Text>{" "}
47
+ {service.name}{" "}
48
+ </Text>
49
+ </Box>
50
+ )
51
+ })}
52
+ </Box>
53
+ <Box>
54
+ <Text color="gray">←/→ switch tabs</Text>
55
+ </Box>
56
+ </Box>
57
+ )
56
58
  }