Skip to content

Commit c7b6258

Browse files
committed
fixes to custom agent -> sidepanel
1 parent acf7897 commit c7b6258

File tree

10 files changed

+155
-82
lines changed

10 files changed

+155
-82
lines changed

src/background/handlers/ExecutionHandler.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@ import { PortMessage } from '@/lib/runtime/PortMessaging'
33
import { ExecutionManager } from '@/lib/execution/ExecutionManager'
44
import { Logging } from '@/lib/utils/Logging'
55
import { PubSub } from '@/lib/pubsub'
6+
import { getExecutionId } from '@/lib/utils/executionUtils'
67

78
/**
89
* Handles execution-related messages:
9-
* - EXECUTE_QUERY: Start a new query execution
10+
* - EXECUTE_QUERY: Start a new query execution (opens sidepanel if source is 'newtab')
1011
* - CANCEL_TASK: Cancel running execution
1112
* - RESET_CONVERSATION: Reset execution state
1213
*/
@@ -26,18 +27,33 @@ export class ExecutionHandler {
2627
executionId?: string
2728
): Promise<void> {
2829
const payload = message.payload as ExecuteQueryMessage['payload']
29-
const { query, tabIds, source, chatMode, metadata } = payload
30+
const { query, tabIds, chatMode, metadata } = payload
3031

3132
// Use executionId from port or generate default
3233
const execId = executionId || 'default'
3334

35+
// If source is newtab, open sidepanel for the tab
36+
if (metadata?.source === 'newtab') {
37+
const tabId = tabIds?.[0]
38+
if (tabId) {
39+
try {
40+
await chrome.sidePanel.open({ tabId })
41+
// Give sidepanel time to initialize
42+
await new Promise(resolve => setTimeout(resolve, 300))
43+
} catch (sidepanelError) {
44+
Logging.log('ExecutionHandler',
45+
`Could not open sidepanel for tab ${tabId}: ${sidepanelError}`, 'warning')
46+
}
47+
}
48+
}
49+
3450
Logging.log('ExecutionHandler',
3551
`Starting execution ${execId}: "${query}" (mode: ${chatMode ? 'chat' : 'browse'})`)
3652

3753
// Log metrics
3854
Logging.logMetric('query_initiated', {
3955
query,
40-
source: source || metadata?.source || 'unknown',
56+
source: metadata?.source || 'unknown',
4157
mode: chatMode ? 'chat' : 'browse',
4258
executionMode: metadata?.executionMode || 'dynamic',
4359
})
@@ -207,6 +223,26 @@ export class ExecutionHandler {
207223
}
208224
}
209225

226+
/**
227+
* Clean up execution for a closed tab
228+
*/
229+
async cleanupTabExecution(tabId: number): Promise<void> {
230+
const execId = await getExecutionId(tabId)
231+
232+
const execution = this.executionManager.get(execId)
233+
if (execution) {
234+
Logging.log('ExecutionHandler', `Cleaning up execution ${execId} for closed tab ${tabId}`)
235+
236+
// Cancel if running
237+
if (execution.isRunning()) {
238+
execution.cancel()
239+
}
240+
241+
// Delete the execution
242+
await this.executionManager.delete(execId)
243+
}
244+
}
245+
210246
/**
211247
* Get execution statistics
212248
*/

src/background/index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { MessageType } from '@/lib/types/messaging'
22
import { PortMessage } from '@/lib/runtime/PortMessaging'
33
import { Logging } from '@/lib/utils/Logging'
44
import { isDevelopmentMode } from '@/config'
5-
import { parsePortName } from './utils/portUtils'
5+
import { parsePortName } from '@/lib/utils/portUtils'
66

77
// Import router and managers
88
import { MessageRouter } from './router/MessageRouter'
@@ -339,11 +339,14 @@ function initialize(): void {
339339
})
340340

341341
// Clean up on tab removal
342-
chrome.tabs.onRemoved.addListener((tabId) => {
342+
chrome.tabs.onRemoved.addListener(async (tabId) => {
343343
// Clean up tab-specific panel state
344344
tabPanelState.delete(tabId)
345-
// Handlers can clean up tab-specific resources
346-
Logging.log('Background', `Tab ${tabId} removed, cleaned up panel state`)
345+
346+
// Clean up tab execution
347+
await executionHandler.cleanupTabExecution(tabId)
348+
349+
Logging.log('Background', `Tab ${tabId} removed, cleaned up panel state and execution`)
347350
})
348351

349352
Logging.log('Background', 'Nxtscape extension initialized successfully')

src/background/router/MessageRouter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { MessageType } from '@/lib/types/messaging'
22
import { PortMessage } from '@/lib/runtime/PortMessaging'
33
import { Logging } from '@/lib/utils/Logging'
4-
import { parsePortName } from '../utils/portUtils'
4+
import { parsePortName } from '@/lib/utils/portUtils'
55

66
// Handler function type
77
export type MessageHandler = (

src/background/router/PortManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { PortMessage } from '@/lib/runtime/PortMessaging'
44
import { PubSub } from '@/lib/pubsub'
55
import { PubSubChannel } from '@/lib/pubsub/PubSubChannel'
66
import { Subscription } from '@/lib/pubsub/types'
7-
import { parsePortName } from '../utils/portUtils'
7+
import { parsePortName } from '@/lib/utils/portUtils'
88

99
// Port info stored for each connection
1010
interface PortInfo {

src/lib/types/messaging.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,9 @@ export const ExecuteQueryMessageSchema = MessageSchema.extend({
114114
type: z.literal(MessageType.EXECUTE_QUERY),
115115
payload: z.object({
116116
query: z.string(),
117-
tabIds: z.array(z.number()).optional(), // Selected tab IDs for context
118-
source: z.string().optional(), // Source of the query (e.g., 'sidepanel')
119-
chatMode: z.boolean().optional(), // Whether to use ChatAgent (Q&A mode) instead of BrowserAgent
120-
metadata: ExecutionMetadataSchema.optional() // Execution metadata
117+
tabIds: z.array(z.number()).optional(),
118+
chatMode: z.boolean().optional(),
119+
metadata: ExecutionMetadataSchema.optional()
121120
})
122121
})
123122

src/lib/utils/Logging.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { MessageType } from '@/lib/types/messaging'
2-
import { parsePortName } from '@/background/utils/portUtils'
2+
import { parsePortName } from '@/lib/utils/portUtils'
33
import { isDevelopmentMode } from '@/config'
44
import { getBrowserOSAdapter } from '@/lib/browser/BrowserOSAdapter'
55
import { z } from 'zod'

src/lib/utils/executionUtils.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Get execution ID for the current context or a specific tab
3+
*/
4+
export async function getExecutionId(tabId?: number): Promise<string> {
5+
if (tabId !== undefined) {
6+
return `tab_${tabId}`
7+
}
8+
9+
try {
10+
const [activeTab] = await chrome.tabs.query({ active: true, currentWindow: true })
11+
12+
if (!activeTab?.id) {
13+
throw new Error('No active tab found to generate execution ID')
14+
}
15+
16+
return `tab_${activeTab.id}`
17+
} catch (error) {
18+
console.error('Failed to get tab for execution ID:', error)
19+
throw new Error('Unable to generate execution ID: ' + (error instanceof Error ? error.message : String(error)))
20+
}
21+
}
22+
23+
/**
24+
* Extract tab ID from an execution ID
25+
*/
26+
export function getTabIdFromExecutionId(executionId: string): number | null {
27+
if (!executionId.startsWith('tab_')) {
28+
return null
29+
}
30+
31+
const tabIdStr = executionId.slice(4)
32+
const tabId = parseInt(tabIdStr, 10)
33+
34+
return isNaN(tabId) ? null : tabId
35+
}
36+
37+
/**
38+
* Check if an execution ID is tab-scoped
39+
*/
40+
export function isTabExecution(executionId: string): boolean {
41+
return executionId.startsWith('tab_')
42+
}
43+
44+
/**
45+
* Validate an execution ID format
46+
*/
47+
export function isValidExecutionId(executionId: string): boolean {
48+
if (!executionId.startsWith('tab_')) {
49+
return false
50+
}
51+
52+
const tabId = getTabIdFromExecutionId(executionId)
53+
return tabId !== null && tabId > 0
54+
}

src/background/utils/portUtils.ts renamed to src/lib/utils/portUtils.ts

Lines changed: 25 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ export interface PortInfo {
1212
/**
1313
* Parse port name to extract structured information
1414
*
15-
* Port name formats:
16-
* - sidepanel:<tabId>:<executionId>
15+
* Port name formats (executionId is always tab_<tabId>):
16+
* - sidepanel:<executionId>
1717
* - newtab:<executionId>
1818
* - options:<executionId>
1919
*
@@ -26,33 +26,38 @@ export function parsePortName(portName: string): PortInfo {
2626
raw: portName
2727
}
2828

29-
// Check for sidepanel with dynamic format
29+
// Check for sidepanel
3030
if (portName.startsWith('sidepanel:')) {
3131
const parts = portName.split(':')
3232
result.type = 'sidepanel'
3333

34-
// Format: sidepanel:tabId:executionId
35-
if (parts.length >= 3) {
36-
const tabId = parseInt(parts[1])
37-
if (!isNaN(tabId)) {
38-
result.tabId = tabId
39-
}
40-
result.executionId = parts[2]
41-
}
42-
// Format without tabId: sidepanel:executionId (shouldn't happen but handle gracefully)
43-
else if (parts.length === 2) {
34+
if (parts.length >= 2) {
4435
result.executionId = parts[1]
36+
// Extract tabId from executionId if it's in tab_<id> format
37+
if (parts[1].startsWith('tab_')) {
38+
const tabId = parseInt(parts[1].slice(4))
39+
if (!isNaN(tabId)) {
40+
result.tabId = tabId
41+
}
42+
}
4543
}
4644
}
47-
// Check for newtab with dynamic format
45+
// Check for newtab with executionId (tab_<tabId>)
4846
else if (portName.startsWith('newtab:')) {
4947
const parts = portName.split(':')
5048
result.type = 'newtab'
5149
if (parts.length >= 2) {
5250
result.executionId = parts[1]
51+
// Extract tabId from executionId if it's in tab_<id> format
52+
if (parts[1].startsWith('tab_')) {
53+
const tabId = parseInt(parts[1].slice(4))
54+
if (!isNaN(tabId)) {
55+
result.tabId = tabId
56+
}
57+
}
5358
}
5459
}
55-
// Check for options with dynamic format
60+
// Check for options
5661
else if (portName.startsWith('options:')) {
5762
const parts = portName.split(':')
5863
result.type = 'options'
@@ -66,26 +71,17 @@ export function parsePortName(portName: string): PortInfo {
6671

6772
/**
6873
* Create a port name with the given parameters
74+
* Note: executionId should be in format tab_<tabId> for tab-scoped executions
6975
*
7076
* @param type - The type of port
71-
* @param executionId - Execution ID (required)
72-
* @param tabId - Optional tab ID (for sidepanel only)
77+
* @param executionId - Execution ID (should be tab_<tabId> for tab contexts)
7378
* @returns Formatted port name
7479
*/
7580
export function createPortName(
7681
type: 'sidepanel' | 'newtab' | 'options',
77-
executionId: string,
78-
tabId?: number
82+
executionId: string
7983
): string {
80-
if (type === 'sidepanel' && tabId) {
81-
return `sidepanel:${tabId}:${executionId}`
82-
}
83-
if (type === 'sidepanel') {
84-
// This shouldn't happen - sidepanel should always have tabId
85-
console.warn('Creating sidepanel port without tab ID')
86-
return `sidepanel:${executionId}`
87-
}
88-
89-
// For newtab and options
84+
// Simple format: type:executionId
85+
// Since executionId already contains tab info (tab_123), no need for redundancy
9086
return `${type}:${executionId}`
9187
}

src/newtab/stores/providerStore.ts

Lines changed: 11 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { MessageType } from '@/lib/types/messaging'
55
import { PortPrefix } from '@/lib/runtime/PortMessaging'
66
import { Agent } from '../stores/agentsStore'
77
import { Logging } from '@/lib/utils/Logging'
8+
import { getExecutionId } from '@/lib/utils/executionUtils'
89

910
// Provider schema
1011
export const ProviderSchema = z.object({
@@ -182,17 +183,14 @@ export const useProviderStore = create<ProviderState & ProviderActions>()(
182183
return
183184
}
184185

185-
// Open the sidepanel for the current tab
186-
await chrome.sidePanel.open({ tabId: activeTab.id })
186+
// Generate execution ID from tab ID
187+
const executionId = await getExecutionId(activeTab.id)
187188

188-
// Wait a bit for sidepanel to initialize
189-
await new Promise(resolve => setTimeout(resolve, 500))
190-
191-
// Connect to background script and send query
192-
const executionId = `exec_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
189+
// Connect to background and send EXECUTE_QUERY
190+
// This will open sidepanel automatically since source is 'newtab'
193191
const port = chrome.runtime.connect({ name: `${PortPrefix.NEWTAB}:${executionId}` })
194192

195-
// Send the query through port messaging
193+
// Send the query with tab context
196194
port.postMessage({
197195
type: MessageType.EXECUTE_QUERY,
198196
payload: {
@@ -234,17 +232,14 @@ export const useProviderStore = create<ProviderState & ProviderActions>()(
234232
? ['Create new tab', ...agent.steps]
235233
: agent.steps
236234

237-
// Open the sidepanel for the current tab
238-
await chrome.sidePanel.open({ tabId: activeTab.id })
239-
240-
// Wait a bit for sidepanel to initialize
241-
await new Promise(resolve => setTimeout(resolve, 500))
235+
// Generate execution ID from tab ID
236+
const executionId = await getExecutionId(activeTab.id)
242237

243-
// Connect to background script and send query with agent metadata
244-
const executionId = `exec_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
238+
// Connect to background and send EXECUTE_QUERY
239+
// This will open sidepanel automatically since source is 'newtab'
245240
const port = chrome.runtime.connect({ name: `${PortPrefix.NEWTAB}:${executionId}` })
246241

247-
// Send the query through port messaging with predefined plan
242+
// Send the query with tab context and predefined plan
248243
port.postMessage({
249244
type: MessageType.EXECUTE_QUERY,
250245
payload: {

0 commit comments

Comments
 (0)