@mseep/anything-analyzer 3.6.50

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 (172) hide show
  1. package/.codeartsdoer/.codebaseignore +0 -0
  2. package/.codeartsdoer/AGENTS.md +12 -0
  3. package/.github/workflows/build.yml +146 -0
  4. package/README.en.md +264 -0
  5. package/README.md +276 -0
  6. package/RELEASE_NOTES.md +16 -0
  7. package/USAGE.md +490 -0
  8. package/color-preview-r3.html +414 -0
  9. package/color-preview.html +414 -0
  10. package/dev-app-update.yml +3 -0
  11. package/electron-builder.yml +36 -0
  12. package/electron.vite.config.ts +40 -0
  13. package/package.json +53 -0
  14. package/report-2026-04-13-copilot-claude-sonnet-4.6.md +955 -0
  15. package/resources/doloffer-logo.png +0 -0
  16. package/resources/entitlements.mac.plist +12 -0
  17. package/resources/icon.ico +0 -0
  18. package/resources/icon.png +0 -0
  19. package/src/main/ai/ai-analyzer.ts +517 -0
  20. package/src/main/ai/crypto-script-extractor.ts +206 -0
  21. package/src/main/ai/data-assembler.ts +205 -0
  22. package/src/main/ai/llm-router.ts +1120 -0
  23. package/src/main/ai/prompt-builder.ts +349 -0
  24. package/src/main/ai/scene-detector.ts +302 -0
  25. package/src/main/capture/capture-engine.ts +130 -0
  26. package/src/main/capture/interaction-recorder.ts +171 -0
  27. package/src/main/capture/js-injector.ts +57 -0
  28. package/src/main/capture/replay-engine.ts +256 -0
  29. package/src/main/capture/storage-collector.ts +76 -0
  30. package/src/main/cdp/cdp-manager.ts +233 -0
  31. package/src/main/db/database.ts +41 -0
  32. package/src/main/db/migrations.ts +235 -0
  33. package/src/main/db/repositories.ts +574 -0
  34. package/src/main/fingerprint/http-spoofing.ts +48 -0
  35. package/src/main/fingerprint/presets.ts +173 -0
  36. package/src/main/fingerprint/profile-generator.ts +115 -0
  37. package/src/main/fingerprint/profile-store.ts +52 -0
  38. package/src/main/index.ts +260 -0
  39. package/src/main/ipc.ts +856 -0
  40. package/src/main/logger.ts +42 -0
  41. package/src/main/mcp/mcp-config.ts +66 -0
  42. package/src/main/mcp/mcp-manager.ts +155 -0
  43. package/src/main/mcp/mcp-server.ts +1038 -0
  44. package/src/main/prompt-templates.ts +170 -0
  45. package/src/main/proxy/ca-manager.ts +204 -0
  46. package/src/main/proxy/cert-download-page.ts +171 -0
  47. package/src/main/proxy/cert-installer.ts +242 -0
  48. package/src/main/proxy/mitm-proxy-config.ts +37 -0
  49. package/src/main/proxy/mitm-proxy-server.ts +1085 -0
  50. package/src/main/proxy/system-proxy.ts +248 -0
  51. package/src/main/session/session-manager.ts +724 -0
  52. package/src/main/tab-manager.ts +582 -0
  53. package/src/main/updater.ts +111 -0
  54. package/src/main/window.ts +235 -0
  55. package/src/preload/hook-script.ts +270 -0
  56. package/src/preload/index.ts +211 -0
  57. package/src/preload/interaction-hook.ts +286 -0
  58. package/src/preload/stealth-script.ts +302 -0
  59. package/src/preload/target-preload.ts +15 -0
  60. package/src/renderer/App.tsx +656 -0
  61. package/src/renderer/components/AiLogDetail.tsx +173 -0
  62. package/src/renderer/components/AiLogList.tsx +101 -0
  63. package/src/renderer/components/AiLogView.module.css +364 -0
  64. package/src/renderer/components/AiLogView.tsx +86 -0
  65. package/src/renderer/components/AnalyzeBar.module.css +79 -0
  66. package/src/renderer/components/AnalyzeBar.tsx +104 -0
  67. package/src/renderer/components/BrowserPanel.module.css +67 -0
  68. package/src/renderer/components/BrowserPanel.tsx +90 -0
  69. package/src/renderer/components/ControlBar.module.css +47 -0
  70. package/src/renderer/components/ControlBar.tsx +205 -0
  71. package/src/renderer/components/HookLog.tsx +132 -0
  72. package/src/renderer/components/InteractionLog.tsx +183 -0
  73. package/src/renderer/components/MCPServerModal.tsx +427 -0
  74. package/src/renderer/components/PromptTemplateModal.tsx +254 -0
  75. package/src/renderer/components/ReportView.module.css +413 -0
  76. package/src/renderer/components/ReportView.tsx +429 -0
  77. package/src/renderer/components/RequestDetail.module.css +191 -0
  78. package/src/renderer/components/RequestDetail.tsx +202 -0
  79. package/src/renderer/components/RequestLog.module.css +69 -0
  80. package/src/renderer/components/RequestLog.tsx +208 -0
  81. package/src/renderer/components/SessionList.module.css +245 -0
  82. package/src/renderer/components/SessionList.tsx +247 -0
  83. package/src/renderer/components/SettingsModal.tsx +100 -0
  84. package/src/renderer/components/StatusBar.module.css +44 -0
  85. package/src/renderer/components/StatusBar.tsx +102 -0
  86. package/src/renderer/components/StorageView.module.css +41 -0
  87. package/src/renderer/components/StorageView.tsx +178 -0
  88. package/src/renderer/components/TabBar.module.css +88 -0
  89. package/src/renderer/components/TabBar.tsx +70 -0
  90. package/src/renderer/components/Titlebar.module.css +254 -0
  91. package/src/renderer/components/Titlebar.tsx +169 -0
  92. package/src/renderer/components/settings/FingerprintSection.tsx +198 -0
  93. package/src/renderer/components/settings/GeneralSection.tsx +164 -0
  94. package/src/renderer/components/settings/LLMSection.tsx +148 -0
  95. package/src/renderer/components/settings/MCPServerSection.tsx +136 -0
  96. package/src/renderer/components/settings/MitmProxySection.tsx +320 -0
  97. package/src/renderer/components/settings/ProxySection.tsx +110 -0
  98. package/src/renderer/css-modules.d.ts +4 -0
  99. package/src/renderer/hooks/useCapture.ts +383 -0
  100. package/src/renderer/hooks/useConfirm.tsx +91 -0
  101. package/src/renderer/hooks/useSession.ts +136 -0
  102. package/src/renderer/hooks/useTabs.ts +103 -0
  103. package/src/renderer/i18n/en.ts +167 -0
  104. package/src/renderer/i18n/index.ts +47 -0
  105. package/src/renderer/i18n/zh.ts +170 -0
  106. package/src/renderer/index.html +12 -0
  107. package/src/renderer/main.tsx +15 -0
  108. package/src/renderer/styles/global.css +144 -0
  109. package/src/renderer/styles/themes/ayu-dark.css +59 -0
  110. package/src/renderer/styles/themes/catppuccin.css +59 -0
  111. package/src/renderer/styles/themes/discord.css +59 -0
  112. package/src/renderer/styles/themes/dracula.css +59 -0
  113. package/src/renderer/styles/themes/github-dark.css +59 -0
  114. package/src/renderer/styles/themes/gruvbox.css +59 -0
  115. package/src/renderer/styles/themes/index.css +11 -0
  116. package/src/renderer/styles/themes/light.css +59 -0
  117. package/src/renderer/styles/themes/nord.css +59 -0
  118. package/src/renderer/styles/themes/one-dark.css +59 -0
  119. package/src/renderer/styles/themes/tokyo-night.css +59 -0
  120. package/src/renderer/styles/tokens.css +137 -0
  121. package/src/renderer/theme.ts +31 -0
  122. package/src/renderer/ui/Badge.module.css +38 -0
  123. package/src/renderer/ui/Badge.tsx +36 -0
  124. package/src/renderer/ui/Button.module.css +142 -0
  125. package/src/renderer/ui/Button.tsx +46 -0
  126. package/src/renderer/ui/Collapse.module.css +49 -0
  127. package/src/renderer/ui/Collapse.tsx +57 -0
  128. package/src/renderer/ui/CopyableBlock.module.css +56 -0
  129. package/src/renderer/ui/CopyableBlock.tsx +42 -0
  130. package/src/renderer/ui/Empty.module.css +19 -0
  131. package/src/renderer/ui/Empty.tsx +34 -0
  132. package/src/renderer/ui/Icons.tsx +346 -0
  133. package/src/renderer/ui/Input.module.css +103 -0
  134. package/src/renderer/ui/Input.tsx +94 -0
  135. package/src/renderer/ui/InputNumber.module.css +68 -0
  136. package/src/renderer/ui/InputNumber.tsx +104 -0
  137. package/src/renderer/ui/Modal.module.css +83 -0
  138. package/src/renderer/ui/Modal.tsx +67 -0
  139. package/src/renderer/ui/Popconfirm.module.css +73 -0
  140. package/src/renderer/ui/Popconfirm.tsx +74 -0
  141. package/src/renderer/ui/Progress.module.css +35 -0
  142. package/src/renderer/ui/Progress.tsx +30 -0
  143. package/src/renderer/ui/Select.module.css +91 -0
  144. package/src/renderer/ui/Select.tsx +100 -0
  145. package/src/renderer/ui/Spinner.module.css +44 -0
  146. package/src/renderer/ui/Spinner.tsx +27 -0
  147. package/src/renderer/ui/Switch.module.css +39 -0
  148. package/src/renderer/ui/Switch.tsx +43 -0
  149. package/src/renderer/ui/Tabs.module.css +76 -0
  150. package/src/renderer/ui/Tabs.tsx +53 -0
  151. package/src/renderer/ui/Tag.module.css +66 -0
  152. package/src/renderer/ui/Tag.tsx +47 -0
  153. package/src/renderer/ui/Timeline.module.css +42 -0
  154. package/src/renderer/ui/Timeline.tsx +29 -0
  155. package/src/renderer/ui/Toast.module.css +99 -0
  156. package/src/renderer/ui/Toast.tsx +90 -0
  157. package/src/renderer/ui/Tooltip.module.css +26 -0
  158. package/src/renderer/ui/Tooltip.tsx +23 -0
  159. package/src/renderer/ui/VirtualTable.module.css +230 -0
  160. package/src/renderer/ui/VirtualTable.tsx +416 -0
  161. package/src/renderer/ui/index.ts +55 -0
  162. package/src/shared/types.ts +695 -0
  163. package/tests/main/ai/crypto-script-extractor.test.ts +281 -0
  164. package/tests/main/ai/llm-router.test.ts +1537 -0
  165. package/tests/main/ai/prompt-builder.test.ts +178 -0
  166. package/tests/main/ai/scene-detector.test.ts +212 -0
  167. package/tests/main/db/migrations.test.ts +134 -0
  168. package/tests/main/release-workflow.test.ts +59 -0
  169. package/tsconfig.json +7 -0
  170. package/tsconfig.node.json +23 -0
  171. package/tsconfig.web.json +24 -0
  172. package/vitest.config.ts +13 -0
@@ -0,0 +1,656 @@
1
+ import React, { useState, useCallback, useRef, useEffect } from 'react'
2
+
3
+ import Titlebar from './components/Titlebar'
4
+ import type { AppView } from './components/Titlebar'
5
+ import StatusBar from './components/StatusBar'
6
+ import SessionList from './components/SessionList'
7
+ import BrowserPanel from './components/BrowserPanel'
8
+ import TabBar from './components/TabBar'
9
+ import AnalyzeBar from './components/AnalyzeBar'
10
+ import SettingsModal from './components/SettingsModal'
11
+ import { THEMES, DEFAULT_THEME } from './theme'
12
+ import RequestLog from './components/RequestLog'
13
+ import RequestDetail from './components/RequestDetail'
14
+ import HookLog from './components/HookLog'
15
+ import StorageView from './components/StorageView'
16
+ import ReportView from './components/ReportView'
17
+ import InteractionLog from './components/InteractionLog'
18
+ import { useSession } from './hooks/useSession'
19
+ import { useCapture } from './hooks/useCapture'
20
+ import { useTabs } from './hooks/useTabs'
21
+ import { useConfirm } from './hooks/useConfirm'
22
+ import { useToast } from './ui/Toast'
23
+
24
+ import { LocaleProvider } from './i18n'
25
+ import { zh } from './i18n/zh'
26
+ import { en } from './i18n/en'
27
+ import type { LocaleKey } from './i18n'
28
+
29
+ function App(): React.ReactElement {
30
+ const toast = useToast()
31
+ const { confirm, ConfirmDialog } = useConfirm()
32
+
33
+ const {
34
+ sessions,
35
+ currentSessionId,
36
+ currentSession,
37
+ loadSessions,
38
+ createSession,
39
+ selectSession,
40
+ deleteSession,
41
+ startCapture,
42
+ resumeCapture,
43
+ pauseCapture,
44
+ stopCapture
45
+ } = useSession()
46
+
47
+ const { tabs, activeTabId, activeTabUrl, isActiveTabLoading, activateTab, closeTab, createTab } = useTabs()
48
+
49
+ const [settingsOpen, setSettingsOpen] = useState(false)
50
+ const [activeView, setActiveView] = useState<AppView>('browser')
51
+ const [createTrigger, setCreateTrigger] = useState(0)
52
+
53
+ // Theme & locale state (persisted to localStorage)
54
+ const [appTheme, setAppTheme] = useState<string>(() => {
55
+ const saved = localStorage.getItem('app-theme')
56
+ return saved && THEMES.some(t => t.id === saved) ? saved : DEFAULT_THEME
57
+ })
58
+ const [appLocale, setAppLocale] = useState<'en' | 'zh'>(() => {
59
+ return (localStorage.getItem('app-locale') as 'en' | 'zh') || 'zh'
60
+ })
61
+
62
+ // Simple t() for App-level strings (outside LocaleProvider context)
63
+ const localeMaps: Record<string, Record<string, string>> = { zh, en }
64
+ const t = useCallback((key: LocaleKey, vars?: Record<string, string | number>) => {
65
+ let text = localeMaps[appLocale]?.[key] ?? zh[key] ?? key
66
+ if (vars) {
67
+ Object.entries(vars).forEach(([k, v]) => {
68
+ text = text.replace(`{${k}}`, String(v))
69
+ })
70
+ }
71
+ return text
72
+ }, [appLocale]) // eslint-disable-line react-hooks/exhaustive-deps
73
+
74
+ const handleThemeChange = useCallback((themeId: string) => {
75
+ setAppTheme(themeId)
76
+ localStorage.setItem('app-theme', themeId)
77
+ if (themeId === 'dark') {
78
+ document.documentElement.removeAttribute('data-theme')
79
+ } else {
80
+ document.documentElement.setAttribute('data-theme', themeId)
81
+ }
82
+ }, [])
83
+
84
+ const handleLocaleToggle = useCallback(() => {
85
+ setAppLocale(prev => {
86
+ const next = prev === 'zh' ? 'en' : 'zh'
87
+ localStorage.setItem('app-locale', next)
88
+ return next
89
+ })
90
+ }, [])
91
+
92
+ // Apply theme attribute on mount
93
+ useEffect(() => {
94
+ if (appTheme === 'dark') {
95
+ document.documentElement.removeAttribute('data-theme')
96
+ } else {
97
+ document.documentElement.setAttribute('data-theme', appTheme)
98
+ }
99
+ }, []) // eslint-disable-line react-hooks/exhaustive-deps
100
+
101
+ const openSettings = useCallback(() => {
102
+ setSettingsOpen(true)
103
+ window.electronAPI.setTargetViewVisible(false)
104
+ }, [])
105
+
106
+ const closeSettings = useCallback(() => {
107
+ setSettingsOpen(false)
108
+ if (activeView === 'browser' && currentSession) {
109
+ window.electronAPI.setTargetViewVisible(true)
110
+ }
111
+ }, [activeView, currentSession])
112
+
113
+ const [selectedRequestId, setSelectedRequestId] = useState<string | null>(null)
114
+ const [selectedSeqs, setSelectedSeqs] = useState<number[]>([])
115
+ const [activeTab, setActiveTab] = useState('requests')
116
+
117
+ /** Ref to browser placeholder for reporting exact bounds to main process */
118
+ const placeholderRef = useRef<HTMLDivElement>(null)
119
+
120
+ const { requests, hooks, snapshots, reports, interactions, isAnalyzing, analysisError, streamingContent, startAnalysis, cancelAnalysis, chatHistory, isChatting, chatError, sendFollowUp, clearCaptureData } = useCapture(currentSessionId)
121
+
122
+ const selectedRequest = requests.find(r => r.id === selectedRequestId) || null
123
+
124
+ // Navigate browser to session URL when session changes
125
+ // Also enable standalone fingerprint protection
126
+ useEffect(() => {
127
+ setSelectedSeqs([])
128
+ setSelectedRequestId(null)
129
+ const setup = async (): Promise<void> => {
130
+ if (currentSessionId) {
131
+ // Switch to session's isolated partition (hides old tabs, restores/creates new)
132
+ // Navigation to target_url is handled in main process when a blank tab is created
133
+ await window.electronAPI.enableFingerprint(currentSessionId)
134
+ } else {
135
+ await window.electronAPI.disableFingerprint()
136
+ }
137
+ }
138
+ setup().catch((err) => {
139
+ console.error('Session setup failed:', err)
140
+ })
141
+ }, [currentSessionId]) // eslint-disable-line react-hooks/exhaustive-deps
142
+
143
+ // Report exact browser placeholder bounds to main process via ResizeObserver
144
+ useEffect(() => {
145
+ const el = placeholderRef.current
146
+ if (!el) return
147
+
148
+ const reportBounds = () => {
149
+ const rect = el.getBoundingClientRect()
150
+ window.electronAPI.syncBrowserBounds({
151
+ x: Math.round(rect.left),
152
+ y: Math.round(rect.top),
153
+ width: Math.round(rect.width),
154
+ height: Math.round(rect.height)
155
+ })
156
+ }
157
+
158
+ const observer = new ResizeObserver(reportBounds)
159
+ observer.observe(el)
160
+ reportBounds()
161
+
162
+ return () => observer.disconnect()
163
+ }, [])
164
+
165
+ // Hide/show browser view based on active view and session
166
+ useEffect(() => {
167
+ if (activeView === 'browser' && currentSession) {
168
+ window.electronAPI.setTargetViewVisible(true)
169
+ } else {
170
+ window.electronAPI.setTargetViewVisible(false)
171
+ }
172
+ }, [activeView, currentSession])
173
+
174
+ // Browser navigation handlers
175
+ const handleNavigate = useCallback(async (url: string) => {
176
+ try { await window.electronAPI.navigate(url) } catch (err) { console.error('Navigation failed:', err) }
177
+ }, [])
178
+
179
+ const handleBack = useCallback(async () => {
180
+ try { await window.electronAPI.goBack() } catch (err) { console.error('Go back failed:', err) }
181
+ }, [])
182
+
183
+ const handleForward = useCallback(async () => {
184
+ try { await window.electronAPI.goForward() } catch (err) { console.error('Go forward failed:', err) }
185
+ }, [])
186
+
187
+ const handleReload = useCallback(async () => {
188
+ try { await window.electronAPI.reload() } catch (err) { console.error('Reload failed:', err) }
189
+ }, [])
190
+
191
+ // Analyze handler
192
+ const handleAnalyze = useCallback(async (purpose?: string) => {
193
+ if (!currentSessionId) return
194
+ setActiveView('report')
195
+ await startAnalysis(currentSessionId, purpose, selectedSeqs.length > 0 ? selectedSeqs : undefined)
196
+ }, [currentSessionId, startAnalysis, selectedSeqs])
197
+
198
+ // Cancel analysis handler
199
+ const handleCancelAnalysis = useCallback(async () => {
200
+ if (!currentSessionId) return
201
+ await cancelAnalysis(currentSessionId)
202
+ }, [currentSessionId, cancelAnalysis])
203
+
204
+ // Export requests handler
205
+ const handleExport = useCallback(async () => {
206
+ if (!currentSessionId) return
207
+ try {
208
+ await window.electronAPI.exportRequests(currentSessionId)
209
+ } catch (err) {
210
+ console.error('Export failed:', err)
211
+ }
212
+ }, [currentSessionId])
213
+
214
+ // Clear browser environment (with confirmation)
215
+ // Hide native WebContentsView so the confirm dialog is not obscured
216
+ const handleClearEnv = useCallback(async () => {
217
+ window.electronAPI.setTargetViewVisible(false)
218
+ const ok = await confirm(t('data.clearEnvConfirm'), { okText: t('data.clear') })
219
+ if (!ok) {
220
+ if (activeView === 'browser' && currentSession) {
221
+ window.electronAPI.setTargetViewVisible(true)
222
+ }
223
+ return
224
+ }
225
+ try {
226
+ await window.electronAPI.clearBrowserEnv()
227
+ toast.success(t('toast.envCleared'))
228
+ } catch (err) {
229
+ console.error('Clear env failed:', err)
230
+ toast.error(t('toast.envClearFailed'))
231
+ }
232
+ if (activeView === 'browser' && currentSession) {
233
+ window.electronAPI.setTargetViewVisible(true)
234
+ }
235
+ }, [toast, confirm, t, activeView, currentSession])
236
+
237
+ // Clear capture data for re-analysis
238
+ const handleClearData = useCallback(async () => {
239
+ if (!currentSessionId) return
240
+ try {
241
+ await clearCaptureData(currentSessionId)
242
+ setSelectedRequestId(null)
243
+ setSelectedSeqs([])
244
+ toast.success(t('toast.dataCleared'))
245
+ } catch (err) {
246
+ console.error('Clear data failed:', err)
247
+ toast.error(t('toast.dataClearFailed'))
248
+ }
249
+ }, [currentSessionId, clearCaptureData, toast])
250
+
251
+ const handleFollowUp = useCallback(async (msg: string) => {
252
+ if (!currentSessionId) return
253
+ await sendFollowUp(currentSessionId, msg)
254
+ }, [currentSessionId, sendFollowUp])
255
+
256
+ // Pill button style for capture controls in browser address bar
257
+ const pillStyle: React.CSSProperties = {
258
+ padding: '5px 14px',
259
+ borderRadius: 6,
260
+ fontSize: 'var(--font-size-2xs)',
261
+ cursor: 'pointer',
262
+ border: 'none',
263
+ whiteSpace: 'nowrap',
264
+ lineHeight: 1.2,
265
+ fontWeight: 600,
266
+ fontFamily: 'var(--font-sans)',
267
+ }
268
+ const pillActive: React.CSSProperties = { ...pillStyle, background: 'var(--color-success-bg)', color: 'var(--color-success)', border: '1px solid var(--color-success-border)' }
269
+ const pillDisabled: React.CSSProperties = { ...pillStyle, background: 'var(--color-active)', color: 'var(--text-disabled)', cursor: 'not-allowed' }
270
+ const pillStart: React.CSSProperties = { ...pillStyle, background: 'var(--color-success)', color: '#000', padding: '5px 18px' }
271
+ const pillPause: React.CSSProperties = { ...pillStyle, background: 'var(--color-warning-bg)', color: 'var(--color-warning)', border: '1px solid var(--color-warning-border)' }
272
+ const pillStop: React.CSSProperties = { ...pillStyle, background: 'var(--color-error-bg)', color: 'var(--color-error)', border: '1px solid var(--color-error-border)' }
273
+ const pillPauseActive: React.CSSProperties = { ...pillStyle, background: 'var(--color-warning-bg)', color: 'var(--color-warning)', border: '1px solid var(--color-warning)' }
274
+
275
+ // Build capture slot for BrowserPanel
276
+ const buildCaptureSlot = () => {
277
+ if (!currentSessionId) return null
278
+ if (!currentSession?.status || currentSession.status === 'stopped') {
279
+ return (
280
+ <>
281
+ <button style={pillStart} onClick={startCapture}>● {t('browser.start')}</button>
282
+ <button style={pillDisabled}>⏸ {t('browser.pause')}</button>
283
+ <button style={pillDisabled}>■ {t('browser.stop')}</button>
284
+ </>
285
+ )
286
+ }
287
+ if (currentSession.status === 'running') {
288
+ return (
289
+ <>
290
+ <button style={pillActive}>● {t('browser.start')}</button>
291
+ <button style={pillPause} onClick={pauseCapture}>⏸ {t('browser.pause')}</button>
292
+ <button style={pillStop} onClick={stopCapture}>■ {t('browser.stop')}</button>
293
+ </>
294
+ )
295
+ }
296
+ if (currentSession.status === 'paused') {
297
+ return (
298
+ <>
299
+ <button style={pillPauseActive}>⏸ {t('browser.pause')}</button>
300
+ <button style={pillStart} onClick={resumeCapture}>▶ {t('browser.resume')}</button>
301
+ <button style={pillStop} onClick={stopCapture}>■ {t('browser.stop')}</button>
302
+ </>
303
+ )
304
+ }
305
+ return null
306
+ }
307
+
308
+ // Empty state guide for when no session is selected
309
+ const renderEmptyGuide = () => (
310
+ <div style={{
311
+ flex: 1,
312
+ display: 'flex',
313
+ flexDirection: 'column',
314
+ alignItems: 'center',
315
+ justifyContent: 'center',
316
+ gap: 24,
317
+ padding: 40,
318
+ color: 'var(--text-muted)',
319
+ }}>
320
+ <div style={{ fontSize: 32, fontWeight: 700, color: 'var(--text-secondary)', letterSpacing: '-0.5px' }}>
321
+ {t('session.emptyTitle')}
322
+ </div>
323
+ <div style={{
324
+ display: 'flex',
325
+ flexDirection: 'column',
326
+ gap: 14,
327
+ fontSize: 'var(--font-size-base)',
328
+ lineHeight: 1.6,
329
+ }}>
330
+ {[
331
+ { step: '1', icon: '+', text: t('session.emptyStep1') },
332
+ { step: '2', icon: '🌐', text: t('session.emptyStep2') },
333
+ { step: '3', icon: '●', text: t('session.emptyStep3') },
334
+ { step: '4', icon: '⚡', text: t('session.emptyStep4') },
335
+ ].map(({ step, icon, text }) => (
336
+ <div key={step} style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
337
+ <span style={{
338
+ width: 28,
339
+ height: 28,
340
+ borderRadius: '50%',
341
+ background: 'var(--color-active)',
342
+ color: 'var(--text-secondary)',
343
+ display: 'flex',
344
+ alignItems: 'center',
345
+ justifyContent: 'center',
346
+ fontSize: 'var(--font-size-sm)',
347
+ fontWeight: 600,
348
+ flexShrink: 0,
349
+ }}>
350
+ {step}
351
+ </span>
352
+ <span style={{ color: 'var(--text-secondary)' }}>{text}</span>
353
+ </div>
354
+ ))}
355
+ </div>
356
+ <button
357
+ style={{
358
+ marginTop: 8,
359
+ padding: '8px 24px',
360
+ borderRadius: 6,
361
+ background: 'var(--text-primary)',
362
+ color: 'var(--color-base)',
363
+ border: 'none',
364
+ fontSize: 'var(--font-size-base)',
365
+ fontWeight: 600,
366
+ cursor: 'pointer',
367
+ fontFamily: 'var(--font-sans)',
368
+ }}
369
+ onClick={() => setCreateTrigger(n => n + 1)}
370
+ >
371
+ {t('session.newSession')}
372
+ </button>
373
+ </div>
374
+ )
375
+
376
+ // Render the Browser view — ONLY browser, no data panel
377
+ const renderBrowserView = () => (
378
+ <div style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden', flex: 1 }}>
379
+ {currentSession ? (
380
+ <>
381
+ {/* Browser tab bar */}
382
+ <TabBar
383
+ tabs={tabs}
384
+ activeTabId={activeTabId}
385
+ onActivate={activateTab}
386
+ onClose={closeTab}
387
+ onCreate={() => createTab()}
388
+ />
389
+
390
+ {/* Browser panel - address bar + nav buttons + capture pills */}
391
+ <BrowserPanel
392
+ currentUrl={activeTabUrl}
393
+ isLoading={isActiveTabLoading}
394
+ onNavigate={handleNavigate}
395
+ onBack={handleBack}
396
+ onForward={handleForward}
397
+ onReload={handleReload}
398
+ captureSlot={buildCaptureSlot()}
399
+ onClearEnv={handleClearEnv}
400
+ onToggleDevTools={() => window.electronAPI.toggleDevTools()}
401
+ />
402
+
403
+ {/* Browser view placeholder — native WebContentsView overlays this area */}
404
+ <div
405
+ ref={placeholderRef}
406
+ style={{
407
+ flex: 1,
408
+ position: 'relative',
409
+ minHeight: 80
410
+ }}
411
+ />
412
+ </>
413
+ ) : (
414
+ renderEmptyGuide()
415
+ )}
416
+ </div>
417
+ )
418
+
419
+ // Inspector sub-tab styles
420
+ const inspectorTabStyle: React.CSSProperties = {
421
+ fontSize: 'var(--font-size-2xs)',
422
+ color: 'var(--text-muted)',
423
+ display: 'flex',
424
+ alignItems: 'center',
425
+ cursor: 'pointer',
426
+ letterSpacing: '0.3px',
427
+ padding: '0',
428
+ background: 'none',
429
+ border: 'none',
430
+ fontFamily: 'var(--font-sans)',
431
+ height: '100%',
432
+ boxShadow: 'none',
433
+ transition: 'color 0.15s',
434
+ }
435
+ const inspectorTabActiveStyle: React.CSSProperties = {
436
+ ...inspectorTabStyle,
437
+ color: 'var(--text-primary)',
438
+ boxShadow: 'inset 0 -2px 0 var(--text-primary)',
439
+ }
440
+ const inspectorTabCountStyle: React.CSSProperties = {
441
+ fontSize: 'var(--font-size-3xs)',
442
+ color: 'var(--text-muted)',
443
+ marginLeft: 5,
444
+ }
445
+
446
+ // Render the Inspector view — sub-tabs + left/right split + bottom AnalyzeBar
447
+ const renderInspectorView = () => (
448
+ <div style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden', flex: 1 }}>
449
+ {currentSession ? (
450
+ <>
451
+ {/* Sub-tabs: Requests / Hooks / Storage + Capture controls */}
452
+ <div style={{
453
+ height: 36,
454
+ background: 'var(--color-bar)',
455
+ borderBottom: '1px solid var(--color-border)',
456
+ display: 'flex',
457
+ alignItems: 'stretch',
458
+ padding: '0 16px',
459
+ gap: 24,
460
+ flexShrink: 0,
461
+ }}>
462
+ <button
463
+ style={activeTab === 'requests' ? inspectorTabActiveStyle : inspectorTabStyle}
464
+ onClick={() => setActiveTab('requests')}
465
+ >
466
+ {t('data.requests')} <span style={inspectorTabCountStyle}>{requests.length}</span>
467
+ </button>
468
+ <button
469
+ style={activeTab === 'hooks' ? inspectorTabActiveStyle : inspectorTabStyle}
470
+ onClick={() => setActiveTab('hooks')}
471
+ >
472
+ {t('data.hooks')} <span style={inspectorTabCountStyle}>{hooks.length}</span>
473
+ </button>
474
+ <button
475
+ style={activeTab === 'storage' ? inspectorTabActiveStyle : inspectorTabStyle}
476
+ onClick={() => setActiveTab('storage')}
477
+ >
478
+ {t('data.storage')} <span style={inspectorTabCountStyle}>{snapshots.length}</span>
479
+ </button>
480
+ <button
481
+ style={activeTab === 'interactions' ? inspectorTabActiveStyle : inspectorTabStyle}
482
+ onClick={() => setActiveTab('interactions')}
483
+ >
484
+ {t('data.interactions')} <span style={inspectorTabCountStyle}>{interactions.length}</span>
485
+ </button>
486
+
487
+ {/* Spacer */}
488
+ <div style={{ flex: 1 }} />
489
+
490
+ {/* Clear data button */}
491
+ {currentSessionId && requests.length > 0 && (
492
+ <button
493
+ style={{
494
+ fontSize: 'var(--font-size-2xs)',
495
+ color: 'var(--text-muted)',
496
+ background: 'none',
497
+ border: 'none',
498
+ cursor: 'pointer',
499
+ padding: '0 4px',
500
+ fontFamily: 'var(--font-sans)',
501
+ whiteSpace: 'nowrap',
502
+ }}
503
+ onClick={async () => {
504
+ const ok = await confirm(t('data.clearDataConfirm'), {
505
+ okText: t('data.clear'),
506
+ })
507
+ if (ok) handleClearData()
508
+ }}
509
+ title={t('data.clearDataConfirm')}
510
+ >{t('data.clearData')}</button>
511
+ )}
512
+
513
+ {/* Capture controls in Inspector */}
514
+ {currentSessionId && (
515
+ <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
516
+ {buildCaptureSlot()}
517
+ </div>
518
+ )}
519
+ </div>
520
+
521
+ {/* Tab content */}
522
+ {activeTab === 'requests' ? (
523
+ /* Left-right split: request list (420px) + detail panel */
524
+ <div style={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
525
+ <div style={{ flex: 1, minWidth: 400, borderRight: '1px solid var(--color-border)', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
526
+ <RequestLog requests={requests} selectedId={selectedRequestId} onSelect={(r) => setSelectedRequestId(r.id)} selectedSeqs={selectedSeqs} onSelectedSeqsChange={setSelectedSeqs} />
527
+ </div>
528
+ <div style={{ width: 400, minWidth: 320, flexShrink: 0, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
529
+ <RequestDetail request={selectedRequest} hooks={hooks} />
530
+ </div>
531
+ </div>
532
+ ) : activeTab === 'hooks' ? (
533
+ <div style={{ flex: 1, overflow: 'auto', padding: '0 12px' }}>
534
+ <HookLog hooks={hooks} />
535
+ </div>
536
+ ) : activeTab === 'storage' ? (
537
+ <div style={{ flex: 1, overflow: 'auto', padding: '0 12px' }}>
538
+ <StorageView snapshots={snapshots} />
539
+ </div>
540
+ ) : activeTab === 'interactions' ? (
541
+ <div style={{ flex: 1, overflow: 'hidden', padding: '0 12px' }}>
542
+ <InteractionLog interactions={interactions} />
543
+ </div>
544
+ ) : null}
545
+
546
+ {/* Bottom AnalyzeBar */}
547
+ <AnalyzeBar
548
+ onAnalyze={handleAnalyze}
549
+ onExport={handleExport}
550
+ hasRequests={requests.length > 0}
551
+ isAnalyzing={isAnalyzing}
552
+ isStopped={currentSession.status !== 'running'}
553
+ selectedSeqCount={selectedSeqs.length}
554
+ totalCount={requests.length}
555
+ />
556
+ </>
557
+ ) : (
558
+ renderEmptyGuide()
559
+ )}
560
+ </div>
561
+ )
562
+
563
+ // Render the Report view
564
+ const renderReportView = () => (
565
+ <div style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden', flex: 1 }}>
566
+ {currentSession ? (
567
+ <ReportView
568
+ report={reports[0] || null}
569
+ isAnalyzing={isAnalyzing}
570
+ analysisError={analysisError}
571
+ streamingContent={streamingContent}
572
+ onReAnalyze={handleAnalyze}
573
+ onCancelAnalysis={handleCancelAnalysis}
574
+ chatHistory={chatHistory}
575
+ isChatting={isChatting}
576
+ chatError={chatError}
577
+ onSendFollowUp={handleFollowUp}
578
+ sessionName={currentSession?.name}
579
+ requests={requests}
580
+ hooks={hooks}
581
+ />
582
+ ) : (
583
+ renderEmptyGuide()
584
+ )}
585
+ </div>
586
+ )
587
+
588
+ return (
589
+ <LocaleProvider locale={appLocale}>
590
+ <div style={{ width: '100vw', height: '100vh', display: 'flex', flexDirection: 'column', background: 'var(--color-base)' }}>
591
+ {/* Custom Titlebar */}
592
+ <Titlebar
593
+ theme={appTheme}
594
+ onThemeChange={handleThemeChange}
595
+ locale={appLocale}
596
+ onLocaleToggle={handleLocaleToggle}
597
+ activeView={activeView}
598
+ onViewChange={setActiveView}
599
+ requestCount={requests.length}
600
+ />
601
+
602
+ {/* Main content area: Sidebar + View */}
603
+ <div style={{ flex: 1, display: 'flex', overflow: 'hidden' }}>
604
+ {/* Left sidebar */}
605
+ <div style={{
606
+ width: 'var(--sidebar-width)',
607
+ minWidth: 220,
608
+ maxWidth: 220,
609
+ borderRight: '1px solid var(--color-border)',
610
+ background: 'var(--color-sidebar)',
611
+ overflow: 'hidden',
612
+ display: 'flex',
613
+ flexDirection: 'column'
614
+ }}>
615
+ <SessionList
616
+ sessions={sessions}
617
+ currentSessionId={currentSessionId}
618
+ onSelect={selectSession}
619
+ onCreate={createSession}
620
+ onDelete={deleteSession}
621
+ onOpenSettings={openSettings}
622
+ activeRequestCount={requests.length}
623
+ createTrigger={createTrigger}
624
+ />
625
+ </div>
626
+
627
+ {/* Main view area */}
628
+ <div style={{ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', background: 'var(--color-content)' }}>
629
+ {activeView === 'browser' && renderBrowserView()}
630
+ {activeView === 'inspector' && renderInspectorView()}
631
+ {activeView === 'report' && renderReportView()}
632
+ </div>
633
+ </div>
634
+
635
+ {/* Status bar */}
636
+ <StatusBar
637
+ status={currentSession?.status ?? null}
638
+ requestCount={requests.length}
639
+ hookCount={hooks.length}
640
+ interactionCount={interactions.length}
641
+ sessionName={currentSession?.name}
642
+ activeView={activeView}
643
+ llmModel={reports[0]?.llm_model}
644
+ tokenCount={reports[0] ? (reports[0].prompt_tokens ?? 0) + (reports[0].completion_tokens ?? 0) : undefined}
645
+ />
646
+
647
+ {/* Settings modal */}
648
+ <SettingsModal open={settingsOpen} onClose={closeSettings} currentSessionId={currentSession?.id ?? null} />
649
+ {/* Confirm dialog (portal) */}
650
+ {ConfirmDialog}
651
+ </div>
652
+ </LocaleProvider>
653
+ )
654
+ }
655
+
656
+ export default App