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
14 changes: 14 additions & 0 deletions extension/src/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -595,6 +595,20 @@ async function handleTabs(cmd: Command, workspace: string): Promise<Result> {
await chrome.tabs.update(target.id, { active: true });
return { id: cmd.id, ok: true, data: { selected: target.id } };
}
case 'open-user-tab': {
if (!cmd.url || !isSafeNavigationUrl(cmd.url)) {
return { id: cmd.id, ok: false, error: 'Missing or invalid URL' };
}
// Find a user window that is NOT an automation window
const automationWindowIds = new Set([...automationSessions.values()].map(s => s.windowId));
const allWindows = await chrome.windows.getAll({ windowTypes: ['normal'] });
const userWindow = allWindows.find(w => w.id !== undefined && !automationWindowIds.has(w.id));
if (!userWindow?.id) {
return { id: cmd.id, ok: false, error: 'No existing Chrome window found. Open a Chrome window first.' };
}
const tab = await chrome.tabs.create({ windowId: userWindow.id, url: cmd.url, active: true });
return { id: cmd.id, ok: true, data: { tabId: tab.id, windowId: userWindow.id } };
}
default:
return { id: cmd.id, ok: false, error: `Unknown tabs op: ${cmd.op}` };
}
Expand Down
4 changes: 2 additions & 2 deletions extension/src/protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ export interface Command {
workspace?: string;
/** URL to navigate to (navigate action) */
url?: string;
/** Sub-operation for tabs: list, new, close, select */
op?: 'list' | 'new' | 'close' | 'select';
/** Sub-operation for tabs: list, new, close, select, open-user-tab */
op?: 'list' | 'new' | 'close' | 'select' | 'open-user-tab';
/** Tab index for tabs select/close */
index?: number;
/** Cookie domain filter */
Expand Down
5 changes: 5 additions & 0 deletions src/browser/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,11 @@ export class Page extends BasePage {
}
}

/** Open a URL as a new tab in the user's existing Chrome window (not the automation window). */
async openUserTab(url: string): Promise<void> {
await sendCommand('tabs', { op: 'open-user-tab', url, ...this._wsOpt() });
}

async tabs(): Promise<unknown[]> {
const result = await sendCommand('tabs', { op: 'list', ...this._wsOpt() });
return Array.isArray(result) ? result : [];
Expand Down
1 change: 1 addition & 0 deletions src/capabilityRouting.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const BROWSER_ONLY_STEPS = new Set([
'evaluate',
'intercept',
'tap',
'open-user-tab',
]);

function pipelineNeedsBrowserSession(pipeline: Record<string, unknown>[]): boolean {
Expand Down
3 changes: 2 additions & 1 deletion src/pipeline/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import type { IPage } from '../types.js';

// Import core steps
import { stepNavigate, stepClick, stepType, stepWait, stepPress, stepSnapshot, stepEvaluate } from './steps/browser.js';
import { stepNavigate, stepClick, stepType, stepWait, stepPress, stepSnapshot, stepEvaluate, stepOpenUserTab } from './steps/browser.js';
import { stepFetch } from './steps/fetch.js';
import { stepSelect, stepMap, stepFilter, stepSort, stepLimit } from './steps/transform.js';
import { stepIntercept } from './steps/intercept.js';
Expand Down Expand Up @@ -60,3 +60,4 @@ registerStep('limit', stepLimit);
registerStep('intercept', stepIntercept);
registerStep('tap', stepTap);
registerStep('download', stepDownload);
registerStep('open-user-tab', stepOpenUserTab);
6 changes: 6 additions & 0 deletions src/pipeline/steps/browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,9 @@ export async function stepEvaluate(page: IPage | null, params: unknown, data: un
}
return result;
}

export async function stepOpenUserTab(page: IPage | null, params: unknown, data: unknown, args: Record<string, unknown>): Promise<unknown> {
const url = String(render(params, { args, data }));
await page!.openUserTab?.(url);
return data;
}
2 changes: 2 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ export interface IPage {
*/
setFileInput?(files: string[], selector?: string): Promise<void>;
closeWindow?(): Promise<void>;
/** Open a URL as a new tab in the user's existing Chrome window (not the automation window). */
openUserTab?(url: string): Promise<void>;
/** Returns the current page URL, or null if unavailable. */
getCurrentUrl?(): Promise<string | null>;
/** Returns the active tab ID, or undefined if not yet resolved. */
Expand Down