Skip to content

Commit 81c30df

Browse files
committed
fix: pause/reset + newtab connector
1 parent 413df3d commit 81c30df

File tree

10 files changed

+172
-63
lines changed

10 files changed

+172
-63
lines changed

src/background/handlers/ExecutionHandler.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,25 @@ export class ExecutionHandler {
3232
// Use executionId from port or generate default
3333
const execId = executionId || 'default'
3434

35-
// If source is newtab, open sidepanel for the tab
35+
// If source is newtab, notify sidepanel to switch context and open it
3636
if (metadata?.source === 'newtab') {
3737
const tabId = tabIds?.[0]
3838
if (tabId) {
3939
try {
40+
// Send context switch message via chrome.runtime.sendMessage
41+
// The sidepanel will listen for this and reconnect with new executionId
42+
chrome.runtime.sendMessage({
43+
type: MessageType.SWITCH_EXECUTION_CONTEXT,
44+
payload: {
45+
executionId: execId,
46+
tabId: tabId,
47+
cancelExisting: true
48+
}
49+
}).catch(() => {
50+
// No listeners yet, that's fine - sidepanel might not be open
51+
})
52+
53+
// Open sidepanel
4054
await chrome.sidePanel.open({ tabId })
4155
// Give sidepanel time to initialize
4256
await new Promise(resolve => setTimeout(resolve, 300))

src/lib/types/messaging.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,9 @@ export enum MessageType {
3636
GET_PLAN_HISTORY = 'GET_PLAN_HISTORY',
3737
// Logging
3838
LOG_MESSAGE = 'LOG_MESSAGE',
39-
LOG_METRIC = 'LOG_METRIC'
39+
LOG_METRIC = 'LOG_METRIC',
40+
// Execution context management
41+
SWITCH_EXECUTION_CONTEXT = 'SWITCH_EXECUTION_CONTEXT'
4042
}
4143

4244
// Create a zod enum for MessageType
@@ -185,6 +187,21 @@ export const CancelTaskMessageSchema = MessageSchema.extend({
185187

186188
export type CancelTaskMessage = z.infer<typeof CancelTaskMessageSchema>
187189

190+
/**
191+
* Switch execution context message schema
192+
* Used to tell sidepanel to reconnect with a new executionId
193+
*/
194+
export const SwitchExecutionContextMessageSchema = MessageSchema.extend({
195+
type: z.literal(MessageType.SWITCH_EXECUTION_CONTEXT),
196+
payload: z.object({
197+
executionId: z.string(), // New execution ID to switch to
198+
tabId: z.number(), // Tab ID associated with this execution
199+
cancelExisting: z.boolean().default(true) // Whether to cancel existing execution
200+
})
201+
})
202+
203+
export type SwitchExecutionContextMessage = z.infer<typeof SwitchExecutionContextMessageSchema>
204+
188205
/**
189206
* Close panel message schema
190207
*/

src/lib/utils/executionUtils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,4 +51,4 @@ export function isValidExecutionId(executionId: string): boolean {
5151

5252
const tabId = getTabIdFromExecutionId(executionId)
5353
return tabId !== null && tabId > 0
54-
}
54+
}

src/newtab/stores/providerStore.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -203,8 +203,9 @@ export const useProviderStore = create<ProviderState & ProviderActions>()(
203203
}
204204
})
205205

206-
// Close port after sending message
207-
setTimeout(() => port.disconnect(), 100)
206+
// Keep port open longer to receive response
207+
// It will auto-disconnect when page unloads or after timeout
208+
setTimeout(() => port.disconnect(), 5000)
208209
} catch (error) {
209210
console.error('Failed to open sidepanel with query:', error)
210211
}
@@ -258,8 +259,9 @@ export const useProviderStore = create<ProviderState & ProviderActions>()(
258259
}
259260
})
260261

261-
// Close port after sending message
262-
setTimeout(() => port.disconnect(), 100)
262+
// Keep port open longer to receive response
263+
// It will auto-disconnect when page unloads or after timeout
264+
setTimeout(() => port.disconnect(), 5000)
263265
} catch (error) {
264266
console.error('Failed to execute agent:', error)
265267
}

src/sidepanel/App.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,16 @@ import './styles.css'
1414
* Uses Tailwind CSS for styling
1515
*/
1616
export function App() {
17-
// Initialize message handling
18-
const { humanInputRequest, clearHumanInputRequest } = useMessageHandler()
17+
// Get connection status and reconnect function from port messaging
18+
const { connected, reconnect } = useSidePanelPortMessaging()
1919

20-
// Get connection status from port messaging
21-
const { connected } = useSidePanelPortMessaging()
20+
// Initialize message handling and set up reconnect callback
21+
const { humanInputRequest, clearHumanInputRequest, setReconnectCallback } = useMessageHandler()
22+
23+
// Wire the reconnect callback
24+
useEffect(() => {
25+
setReconnectCallback(reconnect)
26+
}, [setReconnectCallback, reconnect])
2227

2328
// Initialize settings
2429
const { fontSize, theme } = useSettingsStore()

src/sidepanel/components/ChatInput.tsx

Lines changed: 5 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -116,20 +116,6 @@ export function ChatInput({ isConnected, isProcessing }: ChatInputProps) {
116116
const submitTask = (query: string) => {
117117
if (!query.trim()) return
118118

119-
if (!uiConnected) {
120-
// Show error message in chat
121-
const msg = !connectionOk
122-
? 'Cannot send message: Extension is disconnected'
123-
: 'Cannot send message: Provider not configured'
124-
upsertMessage({
125-
msgId: `error_${Date.now()}`,
126-
role: 'error',
127-
content: msg,
128-
ts: Date.now()
129-
})
130-
return
131-
}
132-
133119
// Add user message via upsert
134120
upsertMessage({
135121
msgId: `user_${Date.now()}`,
@@ -287,15 +273,11 @@ export function ChatInput({ isConnected, isProcessing }: ChatInputProps) {
287273
)
288274

289275
const getPlaceholder = () => {
290-
if (!connectionOk) return 'Disconnected'
291-
if (!providerOk) return 'Provider error'
292276
if (isProcessing) return 'Task running…'
293277
return chatMode ? 'Ask about this page...' : 'Ask me anything... (/ to pick an agent)'
294278
}
295279

296280
const getHintText = () => {
297-
if (!connectionOk) return 'Waiting for connection'
298-
if (!providerOk) return 'Provider not configured'
299281
if (isProcessing) return 'Task running… Press Esc to cancel'
300282
return chatMode
301283
? 'Chat mode is for simple Q&A • @ to select tabs • Press Enter to send'
@@ -382,7 +364,7 @@ export function ChatInput({ isConnected, isProcessing }: ChatInputProps) {
382364
onChange={handleInputChange}
383365
onKeyDown={handleKeyDown}
384366
placeholder={getPlaceholder()}
385-
disabled={!uiConnected}
367+
disabled={isProcessing}
386368
className={cn(
387369
'max-h-[200px] resize-none pr-16 text-sm w-full',
388370
'bg-background/80 backdrop-blur-sm border-2 border-brand/30',
@@ -392,13 +374,13 @@ export function ChatInput({ isConnected, isProcessing }: ChatInputProps) {
392374
'rounded-2xl shadow-lg',
393375
'px-3 py-2',
394376
'transition-all duration-300 ease-out',
395-
!uiConnected && 'opacity-50 cursor-not-allowed bg-muted'
377+
isProcessing && 'opacity-50 cursor-not-allowed bg-muted'
396378
)}
397379
rows={1}
398380
aria-label="Chat message input"
399381
aria-describedby="input-hint"
400-
aria-invalid={!uiConnected}
401-
aria-disabled={!uiConnected}
382+
aria-invalid={isProcessing}
383+
aria-disabled={isProcessing}
402384
/>
403385

404386
{/* Slash command palette overlay */}
@@ -454,7 +436,7 @@ export function ChatInput({ isConnected, isProcessing }: ChatInputProps) {
454436

455437
<Button
456438
type="submit"
457-
disabled={!uiConnected || isProcessing || !input.trim()}
439+
disabled={isProcessing || !input.trim()}
458440
size="sm"
459441
className="absolute right-3 bottom-3 h-8 w-8 p-0 rounded-full bg-[hsl(var(--brand))] hover:bg-[hsl(var(--brand))]/90 text-white shadow-lg flex items-center justify-center"
460442
variant={'default'}

src/sidepanel/components/Header.tsx

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ const MCP_FEATURE_ENABLED = true
1919

2020
interface HeaderProps {
2121
onReset: () => void
22-
showReset: boolean
22+
showReset: boolean // This now means "has messages to reset"
2323
isProcessing: boolean
2424
}
2525

@@ -307,42 +307,44 @@ export const Header = memo(function Header({ onReset, showReset, isProcessing }:
307307
</div>
308308
)}
309309

310-
{/* Settings button - Third position */}
311-
<Button
312-
onClick={handleSettingsClick}
313-
variant="ghost"
314-
size="sm"
315-
className="h-9 w-9 p-0 rounded-xl hover:bg-brand/10 hover:text-brand transition-all duration-300"
316-
aria-label="Open settings"
317-
>
318-
<Settings className="w-4 h-4" />
319-
</Button>
320-
310+
{/* Show Pause button if processing */}
321311
{isProcessing && (
322312
<Button
323313
onClick={handleCancel}
324314
variant="ghost"
325315
size="sm"
326-
className="text-xs hover:bg-brand/5 hover:text-brand transition-all duration-300 flex items-center gap-1"
316+
className="h-9 w-9 p-0 rounded-xl hover:bg-red-100 dark:hover:bg-red-900/20 hover:text-red-600 dark:hover:text-red-400 transition-all duration-300"
327317
aria-label="Pause current task"
318+
title="Pause"
328319
>
329320
<Pause className="w-4 h-4" />
330-
Pause
331321
</Button>
332322
)}
333323

334-
{showReset && !isProcessing && (
324+
{/* Show Reset button if has messages */}
325+
{showReset && (
335326
<Button
336327
onClick={handleReset}
337328
variant="ghost"
338329
size="sm"
339-
className="text-xs hover:bg-brand/5 hover:text-brand transition-all duration-300 flex items-center gap-1"
330+
className="h-9 w-9 p-0 rounded-xl hover:bg-orange-100 dark:hover:bg-orange-900/20 hover:text-orange-600 dark:hover:text-orange-400 transition-all duration-300"
340331
aria-label="Reset conversation"
332+
title="Reset"
341333
>
342334
<RotateCcw className="w-4 h-4" />
343-
Reset
344335
</Button>
345336
)}
337+
338+
{/* Settings button - Last position (rightmost) */}
339+
<Button
340+
onClick={handleSettingsClick}
341+
variant="ghost"
342+
size="sm"
343+
className="h-9 w-9 p-0 rounded-xl hover:bg-brand/10 hover:text-brand transition-all duration-300"
344+
aria-label="Open settings"
345+
>
346+
<Settings className="w-4 h-4" />
347+
</Button>
346348
</nav>
347349

348350
{/* Settings Modal */}

src/sidepanel/hooks/useMessageHandler.ts

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { useEffect, useCallback, useState } from 'react'
1+
import { useEffect, useCallback, useState, useRef } from 'react'
22
import { MessageType } from '@/lib/types/messaging'
33
import { useSidePanelPortMessaging } from '@/sidepanel/hooks'
44
import { useChatStore, type PubSubMessage } from '../stores/chatStore'
@@ -9,9 +9,16 @@ interface HumanInputRequest {
99
}
1010

1111
export function useMessageHandler() {
12-
const { upsertMessage, setProcessing } = useChatStore()
13-
const { addMessageListener, removeMessageListener, executionId } = useSidePanelPortMessaging()
12+
const { upsertMessage, setProcessing, reset } = useChatStore()
13+
const { addMessageListener, removeMessageListener, executionId, sendMessage } = useSidePanelPortMessaging()
1414
const [humanInputRequest, setHumanInputRequest] = useState<HumanInputRequest | null>(null)
15+
const reconnectCallbackRef = useRef<((executionId: string) => void) | null>(null)
16+
17+
// Keep executionId in a ref to always have the current value
18+
const executionIdRef = useRef<string | null>(executionId)
19+
useEffect(() => {
20+
executionIdRef.current = executionId
21+
}, [executionId])
1522

1623
const clearHumanInputRequest = useCallback(() => {
1724
setHumanInputRequest(null)
@@ -70,8 +77,8 @@ export function useMessageHandler() {
7077

7178
// Handle workflow status for processing state
7279
const handleWorkflowStatus = useCallback((payload: any) => {
73-
// Check if this is for our execution
74-
if (payload?.executionId && payload.executionId !== executionId) {
80+
// Check if this is for our execution using ref for current value
81+
if (payload?.executionId && payload.executionId !== executionIdRef.current) {
7582
return // Ignore messages for other executions
7683
}
7784

@@ -81,22 +88,62 @@ export function useMessageHandler() {
8188
}
8289
// Note: We still let ChatInput set processing(true) when sending query
8390
// This avoids race conditions and provides immediate UI feedback
84-
}, [executionId, setProcessing])
91+
}, [setProcessing])
8592

8693
useEffect(() => {
8794
// Register listeners
8895
addMessageListener(MessageType.AGENT_STREAM_UPDATE, handleStreamUpdate)
8996
addMessageListener(MessageType.WORKFLOW_STATUS, handleWorkflowStatus)
9097

98+
// Listen for context switch messages from background
99+
const handleRuntimeMessage = (message: any) => {
100+
if (message?.type === MessageType.SWITCH_EXECUTION_CONTEXT) {
101+
const { executionId: newExecutionId, tabId, cancelExisting } = message.payload
102+
103+
console.log(`[SidePanel] Received SWITCH_EXECUTION_CONTEXT: ${newExecutionId}, current: ${executionIdRef.current}`)
104+
105+
// Only switch if it's a different execution (use ref for current value)
106+
if (newExecutionId !== executionIdRef.current) {
107+
// If we should cancel and reset existing
108+
if (cancelExisting) {
109+
// Cancel any existing task
110+
sendMessage(MessageType.RESET_CONVERSATION, {
111+
reason: 'New task started from NewTab',
112+
source: 'sidepanel'
113+
})
114+
115+
setProcessing(false)
116+
reset()
117+
}
118+
119+
// Trigger reconnection with new executionId
120+
if (reconnectCallbackRef.current) {
121+
reconnectCallbackRef.current(newExecutionId)
122+
}
123+
} else {
124+
console.log(`[SidePanel] Same executionId, no need to switch`)
125+
}
126+
}
127+
}
128+
129+
chrome.runtime.onMessage.addListener(handleRuntimeMessage)
130+
91131
// Cleanup
92132
return () => {
93133
removeMessageListener(MessageType.AGENT_STREAM_UPDATE, handleStreamUpdate)
94134
removeMessageListener(MessageType.WORKFLOW_STATUS, handleWorkflowStatus)
135+
chrome.runtime.onMessage.removeListener(handleRuntimeMessage)
95136
}
96-
}, [addMessageListener, removeMessageListener, handleStreamUpdate, handleWorkflowStatus])
137+
}, [addMessageListener, removeMessageListener, handleStreamUpdate, handleWorkflowStatus, sendMessage, reset, setProcessing])
138+
139+
// Set the reconnect callback that will be triggered on context switch
140+
const setReconnectCallback = useCallback((callback: (executionId: string) => void) => {
141+
reconnectCallbackRef.current = callback
142+
}, [])
97143

98144
return {
99145
humanInputRequest,
100-
clearHumanInputRequest
146+
clearHumanInputRequest,
147+
setReconnectCallback
101148
}
102149
}

0 commit comments

Comments
 (0)