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
358 changes: 347 additions & 11 deletions src/browser/features/RightSidebar/BrowserTab/BrowserTab.test.tsx

Large diffs are not rendered by default.

408 changes: 406 additions & 2 deletions src/browser/features/RightSidebar/BrowserTab/BrowserTab.tsx

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ function createSession(overrides: Partial<BrowserSession> = {}): BrowserSession
function renderViewport(session: BrowserSession, overrides?: { screenshotSrc?: string | null }) {
return render(
<BrowserViewport
panelId="browser-preview-viewport"
workspaceId="workspace-1"
session={session}
screenshotSrc={overrides?.screenshotSrc ?? "data:image/jpeg;base64,frame-data"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import type {
} from "./browserBridgeTypes";

interface BrowserViewportProps {
panelId?: string;
workspaceId: string;
session: BrowserSession | null;
screenshotSrc: string | null;
Expand Down Expand Up @@ -551,7 +552,12 @@ export function BrowserViewport(props: BrowserViewportProps) {
);

return (
<div className="bg-background-secondary relative min-h-0 flex-1 overflow-hidden">
<div
id={props.panelId}
role={props.panelId != null ? "tabpanel" : undefined}
aria-label={props.panelId != null ? "Browser viewport" : undefined}
className="bg-background-secondary relative min-h-0 flex-1 overflow-hidden"
>
{interactiveSurface}
{blockingOverlay}
{props.visibleError && props.screenshotSrc && (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export interface BrowserDiscoveredOtherSession extends BrowserDiscoveredSession
cwd: string;
}

export type { BrowserPageTab } from "@/common/orpc/schemas/api";

export interface BrowserSessionAttachOptions {
allowOtherWorkspaceSession?: boolean;
}
Expand Down
45 changes: 41 additions & 4 deletions src/common/orpc/schemas/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2269,6 +2269,22 @@ const BrowserDiscoveredOtherSessionSchema = BrowserDiscoveredSessionSchema.exten
cwd: z.string(),
});

export const BrowserPageTabSchema = z.object({
tabId: z.string(),
label: z.string().nullable(),
title: z.string(),
url: z.string(),
active: z.boolean(),
type: z.enum(["page", "webview"]),
});

export type BrowserPageTab = z.infer<typeof BrowserPageTabSchema>;

const BrowserCommandResultSchema = z.object({
success: z.boolean(),
error: z.string().nullish(),
});

const BrowserControlActionSchema = z.enum(["open", "back", "forward", "reload"]);

export const browser = {
Expand All @@ -2283,6 +2299,19 @@ export const browser = {
otherSessions: z.array(BrowserDiscoveredOtherSessionSchema),
}),
},
listTabs: {
input: z
.object({
workspaceId: z.string(),
sessionName: z.string(),
allowOtherWorkspaceSession: z.boolean().nullish(),
})
.strict(),
output: z.object({
tabs: z.array(BrowserPageTabSchema),
error: z.string().nullish(),
}),
},
getBootstrap: {
input: z
.object({
Expand All @@ -2307,10 +2336,18 @@ export const browser = {
allowOtherWorkspaceSession: z.boolean().nullish(),
})
.strict(),
output: z.object({
success: z.boolean(),
error: z.string().nullish(),
}),
output: BrowserCommandResultSchema,
},
selectTab: {
input: z
.object({
workspaceId: z.string(),
sessionName: z.string(),
tabRef: z.string(),
allowOtherWorkspaceSession: z.boolean().nullish(),
})
.strict(),
output: BrowserCommandResultSchema,
},
getUrl: {
input: z
Expand Down
140 changes: 87 additions & 53 deletions src/node/orpc/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,70 @@ async function withGoalErrorTranslation<T>(fn: () => Promise<T>): Promise<T> {
}
}

interface BrowserCommandLoadingParams<T extends { success: boolean }> {
context: ORPCContext;
workspaceId: string;
sessionName: string;
run: () => Promise<T>;
}

// Browser commands mark the preview loading before they run; callers validate the session
// before invoking this helper, so the URL refresh can skip the duplicate discovery pass.
async function markBrowserCommandLoadedFromCurrentUrl(params: {
context: ORPCContext;
workspaceId: string;
sessionName: string;
commandToken: number;
}): Promise<void> {
try {
const urlResult = await params.context.browserControlService.getUrl(
params.workspaceId,
params.sessionName,
{ skipSessionValidation: true }
Comment thread
ThomasK33 marked this conversation as resolved.
);
params.context.browserSessionStateHub.markLoaded(
params.workspaceId,
params.sessionName,
urlResult.error == null ? urlResult.url : undefined,
params.commandToken
);
} catch {
params.context.browserSessionStateHub.markLoaded(
params.workspaceId,
params.sessionName,
undefined,
params.commandToken
);
}
}

async function runBrowserCommandWithLoadingState<T extends { success: boolean }>(
Comment thread
ThomasK33 marked this conversation as resolved.
params: BrowserCommandLoadingParams<T>
): Promise<T> {
const commandToken = params.context.browserSessionStateHub.markLoading(
params.workspaceId,
params.sessionName
);

try {
const result = await params.run();
if (result.success) {
await markBrowserCommandLoadedFromCurrentUrl({ ...params, commandToken });
} else {
params.context.browserSessionStateHub.markLoaded(
Comment thread
ThomasK33 marked this conversation as resolved.
params.workspaceId,
params.sessionName,
undefined,
commandToken
);
}
return result;
} catch (error) {
await markBrowserCommandLoadedFromCurrentUrl({ ...params, commandToken });
throw error;
}
}

export const router = (authToken?: string) => {
const t = os.$context<ORPCContext>().use(createAuthMiddleware(authToken));

Expand Down Expand Up @@ -1300,6 +1364,12 @@ export const router = (authToken?: string) => {
})),
};
}),
listTabs: t
.input(schemas.browser.listTabs.input)
.output(schemas.browser.listTabs.output)
.handler(async ({ context, input }) => {
return await context.browserControlService.listTabs(input);
}),
getBootstrap: t
.input(schemas.browser.getBootstrap.input)
.output(schemas.browser.getBootstrap.output)
Expand Down Expand Up @@ -1333,59 +1403,23 @@ export const router = (authToken?: string) => {
.input(schemas.browser.control.input)
.output(schemas.browser.control.output)
.handler(async ({ context, input }) => {
const commandToken = context.browserSessionStateHub.markLoading(
input.workspaceId,
input.sessionName
);

try {
const result = await context.browserControlService.executeControl(input);
if (result.success) {
// executeControl already validated the selected session with the explicit scope flag.
const urlResult = await context.browserControlService.getUrl(
input.workspaceId,
input.sessionName,
{ skipSessionValidation: true }
);
context.browserSessionStateHub.markLoaded(
input.workspaceId,
input.sessionName,
urlResult.error == null ? urlResult.url : undefined,
commandToken
);
} else {
context.browserSessionStateHub.markLoaded(
input.workspaceId,
input.sessionName,
undefined,
commandToken
);
}
return result;
} catch (error) {
try {
// executeControl already validated the selected session with the explicit scope flag.
const urlResult = await context.browserControlService.getUrl(
input.workspaceId,
input.sessionName,
{ skipSessionValidation: true }
);
context.browserSessionStateHub.markLoaded(
input.workspaceId,
input.sessionName,
urlResult.error == null ? urlResult.url : undefined,
commandToken
);
} catch {
context.browserSessionStateHub.markLoaded(
input.workspaceId,
input.sessionName,
undefined,
commandToken
);
}
throw error;
}
return await runBrowserCommandWithLoadingState({
context,
workspaceId: input.workspaceId,
sessionName: input.sessionName,
run: () => context.browserControlService.executeControl(input),
});
}),
selectTab: t
.input(schemas.browser.selectTab.input)
.output(schemas.browser.selectTab.output)
.handler(async ({ context, input }) => {
return await runBrowserCommandWithLoadingState({
context,
workspaceId: input.workspaceId,
sessionName: input.sessionName,
run: () => context.browserControlService.selectTab(input),
});
}),
getUrl: t
.input(schemas.browser.getUrl.input)
Expand Down
Loading
Loading