Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion chrome-extension/public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"storage",
"sidePanel",
"nativeMessaging",
"tabs"
"tabs",
"scripting"
],
"host_permissions": [
"<all_urls>"
Expand All @@ -35,5 +36,12 @@
},
"default_title": "reverse-api-engineer"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["content/codegen-recorder.js"],
"run_at": "document_start"
}
],
"default_locale": "en"
}
291 changes: 275 additions & 16 deletions chrome-extension/src/background/service-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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)
}
}

Expand Down Expand Up @@ -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 +188 to +189
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 27, 2026

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 stopCodegen persists to activeSessionId.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At chrome-extension/src/background/service-worker.ts, line 188:

<comment>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 `stopCodegen` persists to `activeSessionId`.</comment>

<file context>
@@ -83,26 +105,251 @@ async function handleMessage(message: { type: string; [key: string]: unknown }):
+
+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.')
+  }
</file context>
Suggested change
if (captureManager.isCapturing()) {
throw new Error('Cannot switch sessions while capturing. Stop capture first.')
if (captureManager.isCapturing() || codegenActive) {
throw new Error('Cannot switch sessions while capturing or recording. Stop first.')
Fix with Cubic

}
Comment on lines +186 to +190

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Block session switching while codegen is active

Session switching only guards against captureManager.isCapturing() and ignores codegenActive, so users can switch sessions mid‑recording. Because stopCodegen later persists the script to whatever activeSessionId is current, any mid‑recording session switch will save the generated script into the wrong session and orphan the original session’s recording. This is a data integrity issue for multi‑session codegen; consider preventing switching when codegen is active or tracking the session id that started codegen and persisting to that session regardless of UI switches.

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`
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Jan 27, 2026

Choose a reason for hiding this comment

The 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
Check if this issue is valid — if so, understand the root cause and fix it. At chrome-extension/src/background/service-worker.ts, line 329:

<comment>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.</comment>

<file context>
@@ -83,26 +105,251 @@ async function handleMessage(message: { type: string; [key: string]: unknown }):
+
+    switch (action) {
+      case 'click':
+        code = `        page.click("${selector}")\n`
+        break
+      case 'fill':
</file context>
Fix with Cubic

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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Prompt To Fix With AI
This 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')
Expand All @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Prompt To Fix With AI
This 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' })
Expand All @@ -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
Expand Down
Loading