@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,383 @@
1
+ import { useState, useEffect, useCallback, useRef } from "react";
2
+ import type {
3
+ CapturedRequest,
4
+ JsHookRecord,
5
+ StorageSnapshot,
6
+ AnalysisReport,
7
+ ChatMessage,
8
+ InteractionEvent,
9
+ } from "@shared/types";
10
+ import { IPC_CHANNELS } from "@shared/types";
11
+
12
+ interface UseCaptureState {
13
+ requests: CapturedRequest[];
14
+ hooks: JsHookRecord[];
15
+ snapshots: StorageSnapshot[];
16
+ reports: AnalysisReport[];
17
+ interactions: InteractionEvent[];
18
+ isAnalyzing: boolean;
19
+ analysisError: string | null;
20
+ streamingContent: string;
21
+ selectedRequest: CapturedRequest | null;
22
+ chatHistory: ChatMessage[];
23
+ isChatting: boolean;
24
+ chatError: string | null;
25
+ }
26
+
27
+ interface UseCaptureReturn extends UseCaptureState {
28
+ loadData: (sessionId: string) => Promise<void>;
29
+ clearData: () => void;
30
+ clearCaptureData: (sessionId: string) => Promise<void>;
31
+ selectRequest: (request: CapturedRequest | null) => void;
32
+ startAnalysis: (sessionId: string, purpose?: string, selectedSeqs?: number[]) => Promise<void>;
33
+ cancelAnalysis: (sessionId: string) => Promise<void>;
34
+ sendFollowUp: (sessionId: string, message: string) => Promise<void>;
35
+ }
36
+
37
+ const INITIAL_STATE: UseCaptureState = {
38
+ requests: [],
39
+ hooks: [],
40
+ snapshots: [],
41
+ reports: [],
42
+ interactions: [],
43
+ isAnalyzing: false,
44
+ analysisError: null,
45
+ streamingContent: "",
46
+ selectedRequest: null,
47
+ chatHistory: [],
48
+ isChatting: false,
49
+ chatError: null,
50
+ };
51
+
52
+ export function useCapture(sessionId: string | null): UseCaptureReturn {
53
+ const [state, setState] = useState<UseCaptureState>(INITIAL_STATE);
54
+ const sessionIdRef = useRef(sessionId);
55
+
56
+ // Keep ref in sync for use in callbacks
57
+ useEffect(() => {
58
+ sessionIdRef.current = sessionId;
59
+ }, [sessionId]);
60
+
61
+ // Clear all data
62
+ const clearData = useCallback(() => {
63
+ setState(INITIAL_STATE);
64
+ }, []);
65
+
66
+ // Clear all capture data from DB and reset local state
67
+ const clearCaptureData = useCallback(async (sid: string) => {
68
+ await window.electronAPI.clearCaptureData(sid);
69
+ setState(INITIAL_STATE);
70
+ }, []);
71
+
72
+ // Select a request for detail view
73
+ const selectRequest = useCallback((request: CapturedRequest | null) => {
74
+ setState((prev) => ({ ...prev, selectedRequest: request }));
75
+ }, []);
76
+
77
+ // Load all data for a session from main process
78
+ const loadData = useCallback(async (sid: string) => {
79
+ try {
80
+ const [requests, hooks, snapshots, reports, interactions] = await Promise.all([
81
+ window.electronAPI.getRequests(sid),
82
+ window.electronAPI.getHooks(sid),
83
+ window.electronAPI.getStorage(sid),
84
+ window.electronAPI.getReports(sid),
85
+ window.electronAPI.getInteractions(sid),
86
+ ]);
87
+
88
+ // Restore chat history for the latest report
89
+ let chatHistory: ChatMessage[] = [];
90
+ if (reports.length > 0) {
91
+ const latestReport = reports.sort((a, b) => b.created_at - a.created_at)[0];
92
+ const savedMessages = await window.electronAPI.getChatMessages(latestReport.id);
93
+ if (savedMessages.length > 0) {
94
+ chatHistory = savedMessages as ChatMessage[];
95
+ } else {
96
+ // Legacy report without persisted chat — reconstruct [system, assistant] prefix
97
+ // so that chatHistory.slice(2) renders follow-up messages correctly
98
+ const reqSummary = requests.slice(0, 50).map(r => {
99
+ let path = r.url;
100
+ try { path = new URL(r.url).pathname; } catch { /* keep full url */ }
101
+ return `#${r.sequence} ${r.method} ${path} → ${r.status_code ?? '?'}`;
102
+ }).join('\n');
103
+
104
+ const hookSummary = hooks.length > 0
105
+ ? '\n\nDetected hooks:\n' + hooks.slice(0, 20).map(h =>
106
+ `[${h.hook_type}] ${h.function_name}`
107
+ ).join('\n')
108
+ : '';
109
+
110
+ const contextBlock = reqSummary
111
+ ? `\n\n<captured_data_summary>\nCaptured ${requests.length} requests:\n${reqSummary}${requests.length > 50 ? `\n... and ${requests.length - 50} more` : ''}${hookSummary}\n</captured_data_summary>`
112
+ : '';
113
+
114
+ const systemContent = `你是一位网站协议分析专家。基于之前的分析报告和捕获数据,回答用户的追问。保持技术精确,用中文回复。\n\n你可以使用 get_request_detail 工具,通过传入请求序号(seq)来查看任意请求的完整详情(请求头、请求体、响应头、响应体)。当用户追问某个具体请求或需要更多细节时,请主动调用此工具获取数据。${contextBlock}`;
115
+
116
+ chatHistory = [
117
+ { role: 'system' as const, content: systemContent },
118
+ { role: 'assistant' as const, content: latestReport.report_content },
119
+ ];
120
+
121
+ // Persist for future loads
122
+ window.electronAPI.saveChatMessages(latestReport.id, chatHistory)
123
+ .catch(err => console.error("Failed to backfill chat messages:", err));
124
+ }
125
+ }
126
+
127
+ // Only update if session hasn't changed while loading
128
+ if (sessionIdRef.current === sid) {
129
+ setState((prev) => ({
130
+ ...prev,
131
+ requests: requests.sort((a, b) => a.sequence - b.sequence),
132
+ hooks: hooks.sort((a, b) => b.timestamp - a.timestamp),
133
+ snapshots,
134
+ reports: reports.sort((a, b) => b.created_at - a.created_at),
135
+ interactions: (interactions || []).sort((a, b) => a.sequence - b.sequence),
136
+ chatHistory,
137
+ }));
138
+ }
139
+ } catch (err) {
140
+ console.error("Failed to load capture data:", err);
141
+ }
142
+ }, []);
143
+
144
+ // Start AI analysis for a session
145
+ const startAnalysis = useCallback(async (sid: string, purpose?: string, selectedSeqs?: number[]) => {
146
+ setState((prev) => ({
147
+ ...prev,
148
+ isAnalyzing: true,
149
+ analysisError: null,
150
+ streamingContent: "",
151
+ }));
152
+
153
+ try {
154
+ const report = await window.electronAPI.startAnalysis(sid, purpose, selectedSeqs);
155
+
156
+ // Only update if session hasn't changed
157
+ if (sessionIdRef.current === sid) {
158
+ // Build context summary from captured data for follow-up chat
159
+ // (read current state synchronously via a mini-setState that returns prev unchanged)
160
+ let systemContent = '';
161
+ setState((prev) => {
162
+ const reqSummary = prev.requests.slice(0, 50).map(r => {
163
+ let path = r.url
164
+ try { path = new URL(r.url).pathname } catch { /* keep full url */ }
165
+ return `#${r.sequence} ${r.method} ${path} → ${r.status_code ?? '?'}`
166
+ }).join('\n')
167
+
168
+ const hookSummary = prev.hooks.length > 0
169
+ ? '\n\nDetected hooks:\n' + prev.hooks.slice(0, 20).map(h =>
170
+ `[${h.hook_type}] ${h.function_name}`
171
+ ).join('\n')
172
+ : ''
173
+
174
+ const contextBlock = reqSummary
175
+ ? `\n\n<captured_data_summary>\nCaptured ${prev.requests.length} requests:\n${reqSummary}${prev.requests.length > 50 ? `\n... and ${prev.requests.length - 50} more` : ''}${hookSummary}\n</captured_data_summary>`
176
+ : ''
177
+
178
+ systemContent = `你是一位网站协议分析专家。基于之前的分析报告和捕获数据,回答用户的追问。保持技术精确,用中文回复。
179
+
180
+ 你可以使用 get_request_detail 工具,通过传入请求序号(seq)来查看任意请求的完整详情(请求头、请求体、响应头、响应体)。当用户追问某个具体请求或需要更多细节时,请主动调用此工具获取数据。${contextBlock}`
181
+
182
+ const chatHistory: ChatMessage[] = [
183
+ { role: 'system' as const, content: systemContent },
184
+ { role: 'assistant' as const, content: report.report_content },
185
+ ];
186
+
187
+ return {
188
+ ...prev,
189
+ isAnalyzing: false,
190
+ streamingContent: "",
191
+ reports: [report, ...prev.reports],
192
+ chatHistory,
193
+ chatError: null,
194
+ }
195
+ });
196
+
197
+ // Persist initial chat messages (system prompt + report) to database
198
+ window.electronAPI.saveChatMessages(report.id, [
199
+ { role: 'system', content: systemContent },
200
+ { role: 'assistant', content: report.report_content },
201
+ ]).catch(err => console.error("Failed to save initial chat messages:", err));
202
+ }
203
+ } catch (err) {
204
+ console.error("Analysis failed:", err);
205
+ const errMsg = err instanceof Error ? err.message : String(err);
206
+ const isCancelled = errMsg.includes("Analysis cancelled") || errMsg.includes("aborted");
207
+ if (sessionIdRef.current === sid) {
208
+ setState((prev) => ({
209
+ ...prev,
210
+ isAnalyzing: false,
211
+ streamingContent: "",
212
+ analysisError: isCancelled ? null : errMsg,
213
+ }));
214
+ }
215
+ }
216
+ }, []);
217
+
218
+ // Cancel an in-progress analysis
219
+ const cancelAnalysis = useCallback(async (sid: string) => {
220
+ await window.electronAPI.cancelAnalysis(sid);
221
+ setState((prev) => ({
222
+ ...prev,
223
+ isAnalyzing: false,
224
+ streamingContent: "",
225
+ analysisError: null,
226
+ }));
227
+ }, []);
228
+
229
+ const chatHistoryRef = useRef<ChatMessage[]>([]);
230
+ useEffect(() => {
231
+ chatHistoryRef.current = state.chatHistory;
232
+ }, [state.chatHistory]);
233
+
234
+ const sendFollowUp = useCallback(async (sid: string, message: string) => {
235
+ // Get the latest report ID for persisting chat messages
236
+ let currentReportId = '';
237
+ setState((prev) => {
238
+ if (prev.reports.length > 0) {
239
+ currentReportId = prev.reports[0].id;
240
+ }
241
+ return {
242
+ ...prev,
243
+ isChatting: true,
244
+ chatError: null,
245
+ streamingContent: "",
246
+ chatHistory: [...prev.chatHistory, { role: 'user' as const, content: message }],
247
+ };
248
+ });
249
+
250
+ try {
251
+ const reply = await window.electronAPI.sendFollowUp(sid, currentReportId, chatHistoryRef.current, message);
252
+
253
+ if (sessionIdRef.current === sid) {
254
+ setState((prev) => ({
255
+ ...prev,
256
+ isChatting: false,
257
+ streamingContent: "",
258
+ chatHistory: [...prev.chatHistory, { role: 'assistant' as const, content: reply }],
259
+ }));
260
+ }
261
+ } catch (err) {
262
+ console.error("Follow-up chat failed:", err);
263
+ const errMsg = err instanceof Error ? err.message : String(err);
264
+ if (sessionIdRef.current === sid) {
265
+ setState((prev) => ({
266
+ ...prev,
267
+ isChatting: false,
268
+ streamingContent: "",
269
+ chatError: errMsg,
270
+ // Roll back the optimistically added user message on failure
271
+ chatHistory: prev.chatHistory.length > 0 && prev.chatHistory[prev.chatHistory.length - 1]?.role === 'user'
272
+ ? prev.chatHistory.slice(0, -1)
273
+ : prev.chatHistory,
274
+ }));
275
+ }
276
+ }
277
+ }, []);
278
+
279
+ // Set up IPC event listeners for real-time updates
280
+ useEffect(() => {
281
+ if (!sessionId) {
282
+ clearData();
283
+ return;
284
+ }
285
+
286
+ // Load initial data
287
+ loadData(sessionId);
288
+
289
+ // --- Batched request/hook/storage buffering for performance ---
290
+ const requestBuffer: CapturedRequest[] = [];
291
+ const hookBuffer: JsHookRecord[] = [];
292
+ const storageBuffer: StorageSnapshot[] = [];
293
+ let flushTimer: ReturnType<typeof setInterval> | null = null;
294
+
295
+ const flush = () => {
296
+ if (requestBuffer.length > 0 || hookBuffer.length > 0 || storageBuffer.length > 0) {
297
+ const reqBatch = requestBuffer.splice(0);
298
+ const hookBatch = hookBuffer.splice(0);
299
+ const storageBatch = storageBuffer.splice(0);
300
+ setState((prev) => ({
301
+ ...prev,
302
+ requests: reqBatch.length > 0 ? [...prev.requests, ...reqBatch] : prev.requests,
303
+ hooks: hookBatch.length > 0 ? [...hookBatch, ...prev.hooks] : prev.hooks,
304
+ snapshots: storageBatch.length > 0 ? [...prev.snapshots, ...storageBatch] : prev.snapshots,
305
+ }));
306
+ }
307
+ };
308
+
309
+ flushTimer = setInterval(flush, 300);
310
+
311
+ // Listen for new captured requests — buffer instead of immediate setState
312
+ const handleRequest = (data: CapturedRequest) => {
313
+ if (data.session_id !== sessionIdRef.current) return;
314
+ requestBuffer.push(data);
315
+ };
316
+
317
+ // Listen for new hook records — buffer instead of immediate setState
318
+ const handleHook = (data: JsHookRecord) => {
319
+ if (data.session_id !== sessionIdRef.current) return;
320
+ hookBuffer.push(data);
321
+ };
322
+
323
+ // Listen for new storage snapshots — buffer instead of immediate setState
324
+ const handleStorage = (data: StorageSnapshot) => {
325
+ if (data.session_id !== sessionIdRef.current) return;
326
+ storageBuffer.push(data);
327
+ };
328
+
329
+ // Listen for analysis progress (streaming chunks)
330
+ const handleAnalysisProgress = (chunk: string) => {
331
+ setState((prev) => ({
332
+ ...prev,
333
+ streamingContent: prev.streamingContent + chunk,
334
+ }));
335
+ };
336
+
337
+ window.electronAPI.onRequestCaptured(handleRequest);
338
+ window.electronAPI.onHookCaptured(handleHook);
339
+ window.electronAPI.onStorageCaptured(handleStorage);
340
+ window.electronAPI.onAnalysisProgress(handleAnalysisProgress);
341
+
342
+ // Listen for interaction recording events (debounced to avoid excessive DB queries)
343
+ let interactionDebounceTimer: ReturnType<typeof setTimeout> | null = null;
344
+ window.electronAPI.onInteractionRecorded(() => {
345
+ if (interactionDebounceTimer) clearTimeout(interactionDebounceTimer);
346
+ interactionDebounceTimer = setTimeout(() => {
347
+ if (sessionIdRef.current) {
348
+ window.electronAPI.getInteractions(sessionIdRef.current).then((interactions: InteractionEvent[]) => {
349
+ setState((prev) => ({
350
+ ...prev,
351
+ interactions: (interactions || []).sort((a, b) => a.sequence - b.sequence),
352
+ }));
353
+ }).catch(() => {});
354
+ }
355
+ }, 500);
356
+ });
357
+
358
+ // Cleanup listeners on unmount or session change
359
+ return () => {
360
+ if (flushTimer) clearInterval(flushTimer);
361
+ if (interactionDebounceTimer) clearTimeout(interactionDebounceTimer);
362
+ flush(); // flush remaining buffered items
363
+ window.electronAPI.removeAllListeners(IPC_CHANNELS.CAPTURE_REQUEST);
364
+ window.electronAPI.removeAllListeners(IPC_CHANNELS.CAPTURE_HOOK);
365
+ window.electronAPI.removeAllListeners(IPC_CHANNELS.CAPTURE_STORAGE);
366
+ window.electronAPI.removeAllListeners(IPC_CHANNELS.AI_PROGRESS);
367
+ window.electronAPI.removeAllListeners('interaction:recorded');
368
+ };
369
+ }, [sessionId, loadData, clearData]);
370
+
371
+ return {
372
+ ...state,
373
+ loadData,
374
+ clearData,
375
+ clearCaptureData,
376
+ selectRequest,
377
+ startAnalysis,
378
+ cancelAnalysis,
379
+ sendFollowUp,
380
+ };
381
+ }
382
+
383
+ export default useCapture;
@@ -0,0 +1,91 @@
1
+ import React, { useState, useCallback, useRef } from 'react'
2
+ import { createPortal } from 'react-dom'
3
+ import { Button } from '../ui'
4
+
5
+ /**
6
+ * useConfirm — lightweight confirmation dialog rendered via Portal.
7
+ * Cross-platform safe (no window.confirm dependency).
8
+ */
9
+ export function useConfirm() {
10
+ const [state, setState] = useState<{
11
+ message: string
12
+ okText?: string
13
+ cancelText?: string
14
+ } | null>(null)
15
+
16
+ const resolveRef = useRef<((ok: boolean) => void) | null>(null)
17
+
18
+ const confirm = useCallback(
19
+ (message: string, opts?: { okText?: string; cancelText?: string }): Promise<boolean> => {
20
+ return new Promise((resolve) => {
21
+ resolveRef.current = resolve
22
+ setState({ message, ...opts })
23
+ })
24
+ },
25
+ [],
26
+ )
27
+
28
+ const handleOk = useCallback(() => {
29
+ resolveRef.current?.(true)
30
+ resolveRef.current = null
31
+ setState(null)
32
+ }, [])
33
+
34
+ const handleCancel = useCallback(() => {
35
+ resolveRef.current?.(false)
36
+ resolveRef.current = null
37
+ setState(null)
38
+ }, [])
39
+
40
+ const ConfirmDialog = state
41
+ ? createPortal(
42
+ <div
43
+ style={{
44
+ position: 'fixed',
45
+ inset: 0,
46
+ zIndex: 9999,
47
+ display: 'flex',
48
+ alignItems: 'center',
49
+ justifyContent: 'center',
50
+ background: 'rgba(0,0,0,0.35)',
51
+ }}
52
+ onClick={handleCancel}
53
+ >
54
+ <div
55
+ style={{
56
+ background: 'var(--color-frame)',
57
+ border: '1px solid var(--color-border)',
58
+ borderRadius: 'var(--radius-lg)',
59
+ boxShadow: 'var(--shadow-lg)',
60
+ padding: '20px 24px',
61
+ minWidth: 280,
62
+ maxWidth: 400,
63
+ }}
64
+ onClick={(e) => e.stopPropagation()}
65
+ >
66
+ <div
67
+ style={{
68
+ fontSize: 'var(--font-size-base)',
69
+ color: 'var(--text-secondary)',
70
+ lineHeight: 'var(--line-height-normal)',
71
+ marginBottom: 16,
72
+ }}
73
+ >
74
+ {state.message}
75
+ </div>
76
+ <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8 }}>
77
+ <Button size="sm" onClick={handleCancel}>
78
+ {state.cancelText || 'Cancel'}
79
+ </Button>
80
+ <Button size="sm" variant="danger" onClick={handleOk}>
81
+ {state.okText || 'OK'}
82
+ </Button>
83
+ </div>
84
+ </div>
85
+ </div>,
86
+ document.body,
87
+ )
88
+ : null
89
+
90
+ return { confirm, ConfirmDialog }
91
+ }
@@ -0,0 +1,136 @@
1
+ import { useState, useCallback, useEffect, useMemo } from 'react'
2
+ import type { Session } from '../../shared/types'
3
+
4
+ export interface UseSessionReturn {
5
+ sessions: Session[]
6
+ currentSessionId: string | null
7
+ currentSession: Session | null
8
+ loading: boolean
9
+ loadSessions: () => Promise<void>
10
+ createSession: (name: string, url: string) => Promise<void>
11
+ selectSession: (id: string | null) => void
12
+ deleteSession: (id: string) => Promise<void>
13
+ startCapture: () => Promise<void>
14
+ resumeCapture: () => Promise<void>
15
+ pauseCapture: () => Promise<void>
16
+ stopCapture: () => Promise<void>
17
+ }
18
+
19
+ export function useSession(): UseSessionReturn {
20
+ const [sessions, setSessions] = useState<Session[]>([])
21
+ const [currentSessionId, setCurrentSessionId] = useState<string | null>(null)
22
+ const [loading, setLoading] = useState(false)
23
+
24
+ const currentSession = useMemo(
25
+ () => sessions.find((s) => s.id === currentSessionId) ?? null,
26
+ [sessions, currentSessionId]
27
+ )
28
+
29
+ const loadSessions = useCallback(async () => {
30
+ try {
31
+ setLoading(true)
32
+ const list = await window.electronAPI.listSessions()
33
+ setSessions(list)
34
+ } catch (err) {
35
+ console.error('Failed to load sessions:', err)
36
+ } finally {
37
+ setLoading(false)
38
+ }
39
+ }, [])
40
+
41
+ const createSession = useCallback(async (name: string, url: string) => {
42
+ try {
43
+ const session = await window.electronAPI.createSession(name, url)
44
+ setSessions((prev) => [...prev, session])
45
+ setCurrentSessionId(session.id)
46
+ } catch (err) {
47
+ console.error('Failed to create session:', err)
48
+ throw err
49
+ }
50
+ }, [])
51
+
52
+ const selectSession = useCallback((id: string | null) => {
53
+ setCurrentSessionId(id)
54
+ }, [])
55
+
56
+ const deleteSession = useCallback(
57
+ async (id: string) => {
58
+ try {
59
+ await window.electronAPI.deleteSession(id)
60
+ setSessions((prev) => prev.filter((s) => s.id !== id))
61
+ if (currentSessionId === id) {
62
+ setCurrentSessionId(null)
63
+ }
64
+ } catch (err) {
65
+ console.error('Failed to delete session:', err)
66
+ throw err
67
+ }
68
+ },
69
+ [currentSessionId]
70
+ )
71
+
72
+ const startCapture = useCallback(async () => {
73
+ if (!currentSessionId) return
74
+ try {
75
+ await window.electronAPI.startCapture(currentSessionId)
76
+ // Reload from DB to ensure UI stays in sync (avoids stale closure issues)
77
+ await loadSessions()
78
+ } catch (err) {
79
+ console.error('Failed to start capture:', err)
80
+ throw err
81
+ }
82
+ }, [currentSessionId, loadSessions])
83
+
84
+ const resumeCapture = useCallback(async () => {
85
+ if (!currentSessionId) return
86
+ try {
87
+ await window.electronAPI.resumeCapture(currentSessionId)
88
+ await loadSessions()
89
+ } catch (err) {
90
+ console.error('Failed to resume capture:', err)
91
+ throw err
92
+ }
93
+ }, [currentSessionId, loadSessions])
94
+
95
+ const pauseCapture = useCallback(async () => {
96
+ if (!currentSessionId) return
97
+ try {
98
+ await window.electronAPI.pauseCapture(currentSessionId)
99
+ await loadSessions()
100
+ } catch (err) {
101
+ console.error('Failed to pause capture:', err)
102
+ throw err
103
+ }
104
+ }, [currentSessionId, loadSessions])
105
+
106
+ const stopCapture = useCallback(async () => {
107
+ if (!currentSessionId) return
108
+ try {
109
+ await window.electronAPI.stopCapture(currentSessionId)
110
+ await loadSessions()
111
+ } catch (err) {
112
+ console.error('Failed to stop capture:', err)
113
+ throw err
114
+ }
115
+ }, [currentSessionId, loadSessions])
116
+
117
+ // Load sessions on mount
118
+ useEffect(() => {
119
+ loadSessions()
120
+ }, [loadSessions])
121
+
122
+ return {
123
+ sessions,
124
+ currentSessionId,
125
+ currentSession,
126
+ loading,
127
+ loadSessions,
128
+ createSession,
129
+ selectSession,
130
+ deleteSession,
131
+ startCapture,
132
+ resumeCapture,
133
+ pauseCapture,
134
+ stopCapture
135
+ }
136
+ }