Skip to content

Commit b837d5a

Browse files
committed
update
1 parent 714506d commit b837d5a

File tree

9 files changed

+196
-45
lines changed

9 files changed

+196
-45
lines changed

platform/backend/src/features/browser-stream/services/browser-stream.feature.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,13 @@ class BrowserStreamFeature {
6161
agentId: string,
6262
conversationId: string,
6363
userContext: BrowserUserContext,
64+
initialUrl?: string,
6465
) {
6566
return this.getService().selectOrCreateTab(
6667
agentId,
6768
conversationId,
6869
userContext,
70+
initialUrl,
6971
);
7072
}
7173

platform/backend/src/features/browser-stream/services/browser-stream.service.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -452,6 +452,7 @@ export class BrowserStreamService {
452452
agentId: string,
453453
conversationId: string,
454454
userContext: BrowserUserContext,
455+
initialUrl?: string,
455456
): Promise<TabResult> {
456457
const lockKey = toConversationStateKey(
457458
agentId,
@@ -478,6 +479,7 @@ export class BrowserStreamService {
478479
agentId,
479480
conversationId,
480481
userContext,
482+
initialUrl,
481483
);
482484
this.tabSelectionLocks.set(lockKey, task);
483485

@@ -494,6 +496,7 @@ export class BrowserStreamService {
494496
agentId: string,
495497
conversationId: string,
496498
userContext: BrowserUserContext,
499+
initialUrl?: string,
497500
): Promise<TabResult> {
498501
const tabsTool = await this.findTabsTool(agentId, userContext.userId);
499502
if (!tabsTool) {
@@ -914,27 +917,25 @@ export class BrowserStreamService {
914917
});
915918
this.invalidateTabsListCache({ agentId, userContext, tabsTool });
916919

917-
// Navigate to default URL for new conversations
920+
// Navigate to initial URL (or default) for new conversations
921+
const targetUrl = initialUrl || DEFAULT_BROWSER_PREVIEW_URL;
918922
const navigateTool = await this.findNavigateTool(
919923
agentId,
920924
userContext.userId,
921925
);
922926
if (navigateTool) {
923927
logTabSyncInfo(
924-
{ agentId, conversationId, url: DEFAULT_BROWSER_PREVIEW_URL },
925-
"[BrowserTabs] Navigating to default URL for new conversation",
928+
{ agentId, conversationId, url: targetUrl },
929+
"[BrowserTabs] Navigating to initial URL for new conversation",
926930
);
927931
await client.callTool({
928932
name: navigateTool,
929-
arguments: { url: DEFAULT_BROWSER_PREVIEW_URL },
933+
arguments: { url: targetUrl },
930934
});
931935
}
932936

933937
const tabId = generateTabId();
934-
const initialState = createInitialState(
935-
tabId,
936-
DEFAULT_BROWSER_PREVIEW_URL,
937-
);
938+
const initialState = createInitialState(tabId, targetUrl);
938939
const stateWithIndex: BrowserState = {
939940
...initialState,
940941
tabs: initialState.tabs.map((t) =>
@@ -970,7 +971,7 @@ export class BrowserStreamService {
970971
userContext,
971972
client,
972973
tabsTool,
973-
null, // No URL to restore
974+
initialUrl || null, // Use initial URL if provided
974975
);
975976
} catch (error) {
976977
const errorMessage =

platform/backend/src/features/browser-stream/websocket/browser-stream.websocket.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ export class BrowserStreamSocketClientContext {
101101
ws,
102102
conversationId,
103103
clientContext,
104+
typeof payload?.initialUrl === "string"
105+
? payload.initialUrl
106+
: undefined,
104107
);
105108
return true;
106109

@@ -218,6 +221,7 @@ export class BrowserStreamSocketClientContext {
218221
organizationId: string;
219222
userIsProfileAdmin: boolean;
220223
},
224+
initialUrl?: string,
221225
): Promise<void> {
222226
// Unsubscribe from any existing stream first (for this WebSocket)
223227
this.unsubscribeBrowserStream(ws);
@@ -268,6 +272,7 @@ export class BrowserStreamSocketClientContext {
268272
agentId,
269273
conversationId,
270274
userContext,
275+
initialUrl,
271276
);
272277
if (!tabResult.success) {
273278
logger.warn(

platform/frontend/src/app/chat/page.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ export default function ChatPage() {
117117
const userMessageJustEdited = useRef(false);
118118
const textareaRef = useRef<HTMLTextAreaElement | null>(null);
119119
const autoSendTriggeredRef = useRef(false);
120+
// Store pending URL for browser navigation after conversation is created
121+
const [pendingBrowserUrl, setPendingBrowserUrl] = useState<
122+
string | undefined
123+
>(undefined);
120124

121125
// Dialog management for MCP installation
122126
const { isDialogOpened, openDialog, closeDialog } = useDialogs<
@@ -791,6 +795,56 @@ export default function ChatPage() {
791795
localStorage.setItem(LocalStorageKeys.browserOpen, "false");
792796
}, []);
793797

798+
// Handle creating conversation from browser URL input (when no conversation exists)
799+
const handleCreateConversationWithUrl = useCallback(
800+
(url: string) => {
801+
if (!initialAgentId || createConversationMutation.isPending) {
802+
return;
803+
}
804+
805+
// Store the URL to navigate to after conversation is created
806+
setPendingBrowserUrl(url);
807+
808+
// Find the provider for the initial model
809+
const modelInfo = chatModels.find((m) => m.id === initialModel);
810+
const selectedProvider = modelInfo?.provider as
811+
| SupportedChatProvider
812+
| undefined;
813+
814+
// Create conversation with the selected agent
815+
createConversationMutation.mutate(
816+
{
817+
agentId: initialAgentId,
818+
selectedModel: initialModel,
819+
selectedProvider,
820+
chatApiKeyId: initialApiKeyId,
821+
},
822+
{
823+
onSuccess: (newConversation) => {
824+
if (newConversation) {
825+
newlyCreatedConversationRef.current = newConversation.id;
826+
selectConversation(newConversation.id);
827+
// URL navigation will happen via useBrowserStream after conversation connects
828+
}
829+
},
830+
},
831+
);
832+
},
833+
[
834+
initialAgentId,
835+
initialModel,
836+
initialApiKeyId,
837+
chatModels,
838+
createConversationMutation,
839+
selectConversation,
840+
],
841+
);
842+
843+
// Callback to clear pending browser URL after navigation completes
844+
const handleInitialNavigateComplete = useCallback(() => {
845+
setPendingBrowserUrl(undefined);
846+
}, []);
847+
794848
// Handle initial agent change (when no conversation exists)
795849
const handleInitialAgentChange = useCallback((agentId: string) => {
796850
setInitialAgentId(agentId);
@@ -1296,6 +1350,10 @@ export default function ChatPage() {
12961350
onBrowserClose={closeBrowserPanel}
12971351
conversationId={conversationId}
12981352
isInstallingBrowser={isInstallingBrowser}
1353+
onCreateConversationWithUrl={handleCreateConversationWithUrl}
1354+
isCreatingConversation={createConversationMutation.isPending}
1355+
initialNavigateUrl={pendingBrowserUrl}
1356+
onInitialNavigateComplete={handleInitialNavigateComplete}
12991357
/>
13001358

13011359
<PromptVersionHistoryDialog

platform/frontend/src/components/chat/browser-panel.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,25 @@ interface BrowserPanelProps {
1616
conversationId: string | undefined;
1717
/** When true, shows "Installing browser" message instead of normal content */
1818
isInstalling?: boolean;
19+
/** Called when user enters a URL without a conversation - should create conversation and navigate */
20+
onCreateConversationWithUrl?: (url: string) => void;
21+
/** Whether conversation creation is in progress */
22+
isCreatingConversation?: boolean;
23+
/** URL to navigate to once connected (after conversation creation) */
24+
initialNavigateUrl?: string;
25+
/** Called after initial navigation is triggered */
26+
onInitialNavigateComplete?: () => void;
1927
}
2028

2129
export function BrowserPanel({
2230
isOpen,
2331
onClose,
2432
conversationId,
2533
isInstalling = false,
34+
onCreateConversationWithUrl,
35+
isCreatingConversation = false,
36+
initialNavigateUrl,
37+
onInitialNavigateComplete,
2638
}: BrowserPanelProps) {
2739
const handleOpenInNewWindow = useCallback(() => {
2840
if (!conversationId) return;
@@ -50,6 +62,10 @@ export function BrowserPanel({
5062
conversationId={conversationId}
5163
isActive={isOpen}
5264
isInstalling={isInstalling}
65+
onCreateConversationWithUrl={onCreateConversationWithUrl}
66+
isCreatingConversation={isCreatingConversation}
67+
initialNavigateUrl={initialNavigateUrl}
68+
onInitialNavigateComplete={onInitialNavigateComplete}
5369
className="border-t"
5470
headerActions={
5571
<>

platform/frontend/src/components/chat/browser-preview-content.tsx

Lines changed: 71 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
type FormEvent,
1919
type MouseEvent as ReactMouseEvent,
2020
useCallback,
21+
useEffect,
2122
useRef,
2223
useState,
2324
} from "react";
@@ -44,6 +45,14 @@ interface BrowserPreviewContentProps {
4445
isInstalling?: boolean;
4546
/** When true, this is a popup that follows the active conversation */
4647
isPopup?: boolean;
48+
/** Called when user enters a URL without a conversation - should create conversation and navigate */
49+
onCreateConversationWithUrl?: (url: string) => void;
50+
/** Whether conversation creation is in progress */
51+
isCreatingConversation?: boolean;
52+
/** URL to navigate to once connected (after conversation creation) */
53+
initialNavigateUrl?: string;
54+
/** Called after initial navigation is triggered */
55+
onInitialNavigateComplete?: () => void;
4756
}
4857

4958
export function BrowserPreviewContent({
@@ -53,10 +62,15 @@ export function BrowserPreviewContent({
5362
className,
5463
isInstalling = false,
5564
isPopup = false,
65+
onCreateConversationWithUrl,
66+
isCreatingConversation = false,
67+
initialNavigateUrl,
68+
onInitialNavigateComplete,
5669
}: BrowserPreviewContentProps) {
5770
const [typeText, setTypeText] = useState("");
5871
const imageRef = useRef<HTMLImageElement>(null);
5972
const containerRef = useRef<HTMLDivElement>(null);
73+
const initialNavigateTriggeredRef = useRef(false);
6074

6175
const {
6276
screenshot,
@@ -80,14 +94,32 @@ export function BrowserPreviewContent({
8094
conversationId,
8195
isActive,
8296
isPopup,
97+
initialUrl: initialNavigateUrl,
8398
});
8499

85100
const handleNavigate = useCallback(
86101
(e: FormEvent) => {
87102
e.preventDefault();
88-
navigate(urlInput);
103+
if (!urlInput.trim()) return;
104+
105+
// Normalize URL
106+
let normalizedUrl = urlInput.trim();
107+
if (
108+
!normalizedUrl.startsWith("http://") &&
109+
!normalizedUrl.startsWith("https://")
110+
) {
111+
normalizedUrl = `https://${normalizedUrl}`;
112+
}
113+
114+
if (conversationId) {
115+
// Has conversation - navigate directly
116+
navigate(normalizedUrl);
117+
} else if (onCreateConversationWithUrl) {
118+
// No conversation - create one and navigate
119+
onCreateConversationWithUrl(normalizedUrl);
120+
}
89121
},
90-
[urlInput, navigate],
122+
[urlInput, conversationId, navigate, onCreateConversationWithUrl],
91123
);
92124

93125
const handleType = useCallback(
@@ -155,6 +187,30 @@ export function BrowserPreviewContent({
155187
[isConnected, isInteracting, click],
156188
);
157189

190+
// Clear initial URL state once connected (backend handles initial navigation via subscription)
191+
useEffect(() => {
192+
if (
193+
initialNavigateUrl &&
194+
isConnected &&
195+
conversationId &&
196+
!initialNavigateTriggeredRef.current
197+
) {
198+
initialNavigateTriggeredRef.current = true;
199+
onInitialNavigateComplete?.();
200+
}
201+
}, [
202+
initialNavigateUrl,
203+
isConnected,
204+
conversationId,
205+
onInitialNavigateComplete,
206+
]);
207+
208+
// Reset the trigger ref when conversationId changes
209+
// biome-ignore lint/correctness/useExhaustiveDependencies: intentionally reset ref when conversationId changes
210+
useEffect(() => {
211+
initialNavigateTriggeredRef.current = false;
212+
}, [conversationId]);
213+
158214
return (
159215
<div
160216
className={cn(
@@ -329,15 +385,24 @@ export function BrowserPreviewContent({
329385
}}
330386
onFocus={() => setIsEditingUrl(true)}
331387
className="h-7 text-xs!"
332-
disabled={isNavigating || !conversationId}
388+
disabled={isNavigating || isCreatingConversation}
333389
/>
334390
<Button
335391
type="submit"
336392
size="sm"
337393
className="h-7 px-3 text-xs"
338-
disabled={isNavigating || !urlInput.trim() || !conversationId}
394+
disabled={
395+
isNavigating ||
396+
isCreatingConversation ||
397+
!urlInput.trim() ||
398+
(!conversationId && !onCreateConversationWithUrl)
399+
}
339400
>
340-
{isNavigating ? <Loader2 className="h-3 w-3 animate-spin" /> : "Go"}
401+
{isNavigating || isCreatingConversation ? (
402+
<Loader2 className="h-3 w-3 animate-spin" />
403+
) : (
404+
"Go"
405+
)}
341406
</Button>
342407
</form>
343408
</div>
@@ -356,7 +421,7 @@ export function BrowserPreviewContent({
356421
>
357422
{isConnecting && (
358423
<div className="flex items-center justify-center h-full">
359-
<div className="text-center space-y-2">
424+
<div className="text-center space-y-2 mt-20">
360425
<LoadingSpinner />
361426
<p className="text-sm text-muted-foreground">Connecting...</p>
362427
</div>
@@ -396,13 +461,6 @@ export function BrowserPreviewContent({
396461
Installing browser...
397462
</p>
398463
</>
399-
) : !conversationId ? (
400-
<>
401-
<Globe className="h-12 w-12 text-muted-foreground mx-auto" />
402-
<p className="text-sm text-muted-foreground">
403-
Send a message to start browsing
404-
</p>
405-
</>
406464
) : (
407465
<>
408466
<Globe className="h-12 w-12 text-muted-foreground mx-auto" />

0 commit comments

Comments
 (0)