-
Notifications
You must be signed in to change notification settings - Fork 28
Multi-Session & Codegen Support #36
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,24 +8,46 @@ import { | |
| getSettings, | ||
| saveSettings, | ||
| getCurrentSession, | ||
| saveCurrentSession, | ||
| clearCapturedRequests, | ||
| addCapturedRequest | ||
| addCapturedRequest, | ||
| getAllSessions, | ||
| getSession, | ||
| saveSession, | ||
| deleteSession as deleteSessionFromStorage, | ||
| getActiveSessionId, | ||
| setActiveSessionId, | ||
| getAppMode, | ||
| setAppMode | ||
| } from '../shared/storage' | ||
| import type { Session, AppMode } from '../shared/types' | ||
|
|
||
| let currentRunId: string | null = null | ||
| let activeSessionId: string | null = null | ||
| let nativeHostConnected = false | ||
| let currentMode: AppMode = 'capture' | ||
|
|
||
| // Codegen state | ||
| let codegenActive = false | ||
| let codegenScript = '' | ||
| let codegenTabId: number | null = null | ||
|
|
||
| async function initialize(): Promise<void> { | ||
| console.log('Reverse API Engineer: Initializing...') | ||
| chrome.sidePanel.setPanelBehavior({ openPanelOnActionClick: true }) | ||
|
|
||
| const session = await getCurrentSession() | ||
| if (session) { | ||
| currentRunId = session.runId | ||
| console.log('Restored session:', currentRunId) | ||
| // Restore active session | ||
| activeSessionId = await getActiveSessionId() | ||
| if (activeSessionId) { | ||
| const session = await getSession(activeSessionId) | ||
| if (session) { | ||
| currentRunId = session.runId | ||
| console.log('Restored session:', currentRunId) | ||
| } | ||
| } | ||
|
|
||
| // Restore mode | ||
| currentMode = await getAppMode() | ||
|
|
||
| await checkNativeHost() | ||
| captureManager.addListener(handleCaptureEvent) | ||
| console.log('Reverse API Engineer: Ready') | ||
|
|
@@ -45,7 +67,7 @@ async function checkNativeHost(): Promise<boolean> { | |
| function handleCaptureEvent(event: { type: string; request?: unknown }): void { | ||
| broadcastMessage({ type: 'captureEvent', event }) | ||
| if (event.type === 'complete' || event.type === 'failed') { | ||
| addCapturedRequest(event.request) | ||
| addCapturedRequest(event.request, activeSessionId || undefined) | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -83,26 +105,251 @@ async function handleMessage(message: { type: string; [key: string]: unknown }): | |
| return nativeHost.getStatus() | ||
| case 'chat': | ||
| return handleChat(message.message as string, message.model as string | undefined) | ||
| // Session management | ||
| case 'getSessions': | ||
| return getAllSessions() | ||
| case 'createSession': | ||
| return createSession(message.name as string | undefined) | ||
| case 'switchSession': | ||
| return switchSession(message.sessionId as string) | ||
| case 'deleteSession': | ||
| return deleteSession(message.sessionId as string) | ||
| case 'renameSession': | ||
| return renameSession(message.sessionId as string, message.name as string) | ||
| // Mode management | ||
| case 'setMode': | ||
| return setMode(message.mode as AppMode) | ||
| case 'startCodegen': | ||
| return startCodegen() | ||
| case 'stopCodegen': | ||
| return stopCodegen() | ||
| // Codegen state for content script | ||
| case 'getCodegenState': | ||
| return { codegenActive, codegenTabId } | ||
| default: | ||
| throw new Error(`Unknown message type: ${message.type}`) | ||
| } | ||
| } | ||
|
|
||
| async function getState(): Promise<Record<string, unknown>> { | ||
| const session = await getCurrentSession() | ||
| const sessions = await getAllSessions() | ||
| const settings = await getSettings() | ||
| const stats = captureManager.getStats() | ||
|
|
||
| return { | ||
| capturing: captureManager.isCapturing(), | ||
| runId: currentRunId, | ||
| session, | ||
| sessions, | ||
| settings, | ||
| stats, | ||
| nativeHostConnected | ||
| nativeHostConnected, | ||
| activeSessionId, | ||
| mode: currentMode, | ||
| codegenActive, | ||
| codegenScript | ||
| } | ||
| } | ||
|
|
||
| // Session management functions | ||
| async function createSession(name?: string): Promise<{ success: boolean; session: Session }> { | ||
| const runId = generateRunId() | ||
| const sessionId = `session_${Date.now()}` | ||
| const sessionName = name || `Session ${new Date().toLocaleString()}` | ||
|
|
||
| const newSession: Session = { | ||
| id: sessionId, | ||
| runId, | ||
| name: sessionName, | ||
| tabId: 0, | ||
| startTime: new Date().toISOString(), | ||
| requestCount: 0, | ||
| isActive: true, | ||
| messages: [] | ||
| } | ||
|
|
||
| await saveSession(newSession) | ||
|
|
||
| // Don't switch if currently capturing - just create the session | ||
| if (!captureManager.isCapturing()) { | ||
| await setActiveSessionId(sessionId) | ||
| activeSessionId = sessionId | ||
| currentRunId = runId | ||
| } | ||
|
|
||
| broadcastMessage({ type: 'sessionCreated', session: newSession }) | ||
|
|
||
| return { success: true, session: newSession } | ||
| } | ||
|
|
||
| async function switchSession(sessionId: string): Promise<{ success: boolean; session: Session | null }> { | ||
| // Don't allow switching if capturing on current session | ||
| if (captureManager.isCapturing()) { | ||
| throw new Error('Cannot switch sessions while capturing. Stop capture first.') | ||
| } | ||
|
Comment on lines
+186
to
+190
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Session switching only guards against Useful? React with 👍 / 👎. |
||
|
|
||
| const session = await getSession(sessionId) | ||
| if (!session) { | ||
| throw new Error('Session not found') | ||
| } | ||
|
|
||
| await setActiveSessionId(sessionId) | ||
| activeSessionId = sessionId | ||
| currentRunId = session.runId | ||
|
|
||
| broadcastMessage({ type: 'sessionSwitched', session }) | ||
|
|
||
| return { success: true, session } | ||
| } | ||
|
|
||
| async function deleteSession(sessionId: string): Promise<{ success: boolean }> { | ||
| // Don't allow deleting active session while capturing | ||
| if (sessionId === activeSessionId && captureManager.isCapturing()) { | ||
| throw new Error('Cannot delete active session while capturing.') | ||
| } | ||
|
|
||
| await deleteSessionFromStorage(sessionId) | ||
|
|
||
| // If we deleted the active session, clear the active state | ||
| if (sessionId === activeSessionId) { | ||
| await setActiveSessionId(null) | ||
| activeSessionId = null | ||
| currentRunId = null | ||
| } | ||
|
|
||
| broadcastMessage({ type: 'sessionDeleted', sessionId }) | ||
|
|
||
| return { success: true } | ||
| } | ||
|
|
||
| async function renameSession(sessionId: string, name: string): Promise<{ success: boolean; session: Session | null }> { | ||
| const session = await getSession(sessionId) | ||
| if (!session) { | ||
| throw new Error('Session not found') | ||
| } | ||
|
|
||
| session.name = name | ||
| await saveSession(session) | ||
|
|
||
| broadcastMessage({ type: 'sessionRenamed', session }) | ||
|
|
||
| return { success: true, session } | ||
| } | ||
|
|
||
| // Mode management | ||
| async function setMode(mode: AppMode): Promise<{ success: boolean; mode: AppMode }> { | ||
| currentMode = mode | ||
| await setAppMode(mode) | ||
| broadcastMessage({ type: 'modeChanged', mode }) | ||
| return { success: true, mode } | ||
| } | ||
|
|
||
| // Codegen functions | ||
| async function startCodegen(): Promise<{ success: boolean }> { | ||
| if (codegenActive) { | ||
| throw new Error('Codegen already active') | ||
| } | ||
|
|
||
| const [tab] = await chrome.tabs.query({ active: true, currentWindow: true }) | ||
| if (!tab?.id) throw new Error('No active tab') | ||
| codegenTabId = tab.id | ||
|
|
||
| codegenActive = true | ||
| codegenScript = `from playwright.sync_api import sync_playwright | ||
| def run(): | ||
| with sync_playwright() as p: | ||
| browser = p.chromium.launch(headless=False) | ||
| page = browser.new_page() | ||
| page.goto("${tab.url || 'about:blank'}") | ||
| ` | ||
|
|
||
| // Send message to content script to start recording | ||
| try { | ||
| await chrome.tabs.sendMessage(codegenTabId, { type: 'startCodegenRecording' }) | ||
| } catch (error) { | ||
| console.error('Failed to start content script recording:', error) | ||
| } | ||
|
|
||
| broadcastMessage({ type: 'codegenStarted', script: codegenScript }) | ||
|
|
||
| return { success: true } | ||
| } | ||
|
|
||
| async function stopCodegen(): Promise<{ success: boolean; script: string }> { | ||
| if (!codegenActive) { | ||
| throw new Error('Codegen not active') | ||
| } | ||
|
|
||
| // Close the script | ||
| codegenScript += ` browser.close() | ||
| if __name__ == "__main__": | ||
| run() | ||
| ` | ||
|
|
||
| // Send message to content script to stop recording | ||
| if (codegenTabId) { | ||
| try { | ||
| await chrome.tabs.sendMessage(codegenTabId, { type: 'stopCodegenRecording' }) | ||
| } catch (error) { | ||
| console.error('Failed to stop content script recording:', error) | ||
| } | ||
| } | ||
|
|
||
| codegenActive = false | ||
| const finalScript = codegenScript | ||
|
|
||
| // Save to active session if exists | ||
| if (activeSessionId) { | ||
| const session = await getSession(activeSessionId) | ||
| if (session) { | ||
| session.codegenScript = finalScript | ||
| await saveSession(session) | ||
| } | ||
| } | ||
|
|
||
| broadcastMessage({ type: 'codegenStopped', script: finalScript }) | ||
|
|
||
| codegenTabId = null | ||
|
|
||
| return { success: true, script: finalScript } | ||
| } | ||
|
|
||
| // Listen for codegen events from content script | ||
| chrome.runtime.onMessage.addListener((message, sender, sendResponse) => { | ||
| if (message.type === 'codegenAction' && codegenActive && sender.tab?.id === codegenTabId) { | ||
| const { action, selector, value, url } = message | ||
| let code = '' | ||
|
|
||
| switch (action) { | ||
| case 'click': | ||
| code = ` page.click("${selector}")\n` | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Escape selectors/values before embedding them into the generated Playwright script so that recorded interactions containing quotes or backslashes don’t produce invalid Python code. Prompt for AI agents |
||
| break | ||
| case 'fill': | ||
| code = ` page.fill("${selector}", "${value}")\n` | ||
| break | ||
| case 'navigate': | ||
| code = ` page.goto("${url}")\n` | ||
| break | ||
| case 'select': | ||
| code = ` page.select_option("${selector}", "${value}")\n` | ||
| break | ||
| } | ||
|
|
||
| if (code) { | ||
| codegenScript += code | ||
| broadcastMessage({ type: 'codegenUpdate', script: codegenScript, newCode: code }) | ||
| } | ||
|
|
||
| sendResponse({ success: true }) | ||
| return true | ||
| } | ||
| return false | ||
|
Comment on lines
+322
to
+350
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Code Python généré vulnérable aux injections: les valeurs Les chaînes de caractères provenant du content script sont insérées directement dans le code Python sans validation ni échappement. Un utilisateur malveillant pourrait injecter du code Python arbitraire via les interactions de la page. Prompt To Fix With AIThis is a comment left during a code review.
Path: chrome-extension/src/background/service-worker.ts
Line: 322:350
Comment:
Code Python généré vulnérable aux injections: les valeurs `selector`, `value` et `url` sont directement interpolées sans échappement
Les chaînes de caractères provenant du content script sont insérées directement dans le code Python sans validation ni échappement. Un utilisateur malveillant pourrait injecter du code Python arbitraire via les interactions de la page.
How can I resolve this? If you propose a fix, please make it concise. |
||
| }) | ||
|
|
||
| async function startCapture(tabId?: number): Promise<{ success: boolean; runId: string; tabId: number }> { | ||
| if (captureManager.isCapturing()) { | ||
| throw new Error('Already capturing') | ||
|
|
@@ -114,18 +361,29 @@ async function startCapture(tabId?: number): Promise<{ success: boolean; runId: | |
| tabId = tab.id | ||
| } | ||
|
|
||
| // Create a new session if none exists | ||
| if (!activeSessionId) { | ||
| const { session } = await createSession() | ||
| activeSessionId = session.id | ||
| currentRunId = session.runId | ||
| } | ||
|
|
||
| currentRunId = generateRunId() | ||
|
Comment on lines
+364
to
371
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Confusion logique: génération d'un nouveau La ligne 368 définit Prompt To Fix With AIThis is a comment left during a code review.
Path: chrome-extension/src/background/service-worker.ts
Line: 364:371
Comment:
Confusion logique: génération d'un nouveau `runId` juste après avoir créé une session avec son propre `runId`
La ligne 368 définit `currentRunId = session.runId`, puis la ligne 371 génère immédiatement un nouveau `runId` qui écrase le précédent. Cela rend le `runId` de la session nouvellement créée inutile.
How can I resolve this? If you propose a fix, please make it concise. |
||
| const settings = await getSettings() | ||
| await clearCapturedRequests() | ||
| await clearCapturedRequests(activeSessionId || undefined) | ||
|
|
||
| await captureManager.start(tabId, { captureTypes: settings.captureTypes }) | ||
|
|
||
| await saveCurrentSession({ | ||
| runId: currentRunId, | ||
| tabId, | ||
| startTime: new Date().toISOString(), | ||
| requestCount: 0 | ||
| }) | ||
| // Update the active session | ||
| const session = await getSession(activeSessionId) | ||
| if (session) { | ||
| session.runId = currentRunId | ||
| session.tabId = tabId | ||
| session.startTime = new Date().toISOString() | ||
| session.requestCount = 0 | ||
| session.isActive = true | ||
| await saveSession(session) | ||
| } | ||
|
|
||
| chrome.action.setBadgeText({ text: 'REC' }) | ||
| chrome.action.setBadgeBackgroundColor({ color: '#ff0000' }) | ||
|
|
@@ -145,7 +403,8 @@ async function stopCapture(): Promise<{ success: boolean; runId: string | null; | |
| if (session && har) { | ||
| session.endTime = new Date().toISOString() | ||
| session.requestCount = har.log.entries.length | ||
| await saveCurrentSession(session) | ||
| session.isActive = false | ||
| await saveSession(session) | ||
| } | ||
|
|
||
| let harPath: string | null = null | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P1: Session switching should also be blocked when codegen recording is active. Currently only capture mode is checked, but switching sessions during codegen will cause the recorded script to be saved to the wrong session when
stopCodegenpersists toactiveSessionId.Prompt for AI agents