From 02a7b1608a3e58d77fb7701d56b79c03edb85790 Mon Sep 17 00:00:00 2001 From: Mateusz Kupczyk Date: Tue, 8 Sep 2026 09:05:10 +0200 Subject: [PATCH 1/2] feat: Add propagation of openFullTabView parameter to Opera tools --- README.md | 3 ++ SKILL.md | 1 + src/cli.ts | 87 ++++++++++++++++++++++++++++++++++++++++++------ test/cli.test.ts | 51 ++++++++++++++++++++++++++++ 4 files changed, 131 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index ddeb71f..9b6183a 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,8 @@ opera-browser-cli make "a todo app in vanilla JS" # generate and open a webpage opera-browser-cli research "solid-state batteries" # in-depth research across multiple sources ``` +Add `--open-fulltab-view` to any AI command to open the created tab in the foreground (default: background). Not available in headless mode. + Run `opera-browser-cli setup` to get started, or `opera-browser-cli doctor` to check your configuration. ## Install @@ -231,6 +233,7 @@ opera-browser-cli eval "(() => { const rows = [...document.querySelectorAll('tr' | `make ` | Ask the AI to build a webpage or app | Opera Neon | | `research ` | Ask the AI to research a topic in depth | Opera Neon | +All four AI commands accept `--open-fulltab-view` to open the created tab in the foreground (default: background). Not available in headless mode. `research` accepts `--type local` (default), `--type one-minute`, or `--type deep`. ### Configuration diff --git a/SKILL.md b/SKILL.md index aa3cce3..21d0687 100644 --- a/SKILL.md +++ b/SKILL.md @@ -12,6 +12,7 @@ metadata: {"openclaw": {"requires": {"bins": ["opera-browser-cli"]}}} - **`chat`** — available on any Opera browser. Use `--model ` to select an AI model, `--conversation-id ` to continue a conversation. - **`models`** — list available AI models for chat (shows IDs and which is the default). - **`invoke-do`, `make`, `research`** — require **Opera Neon** with an active sign-in. `make` accepts `--conversation-id ` to continue an existing conversation. +- **`--open-fulltab-view`** — applicable to `chat`, `invoke-do`, `make`, `research`. Opens the created tab in the foreground so the user sees the AI UI immediately (default: background). Not available in headless mode. - **Research conversation IDs**: Each research session creates a fresh, unique conversation. The conversation ID is **not resumable** for research (no `--conversation-id` flag). However, the conversation ID from a research can be used with `chat` or `make` for follow-up questions/actions in the same research context. - **`mcp-servers`** — list MCP servers registered in the browser (requires Opera Neon). - **`mcp-tools --server `** — list tools exposed by a specific MCP server (requires Opera Neon). diff --git a/src/cli.ts b/src/cli.ts index fcaa977..a2b34e4 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -112,8 +112,10 @@ commands[55]: network-get [id], lighthouse, perf-start, perf-stop, perf-insight , heap , start, stop, restart, status, attach, launch-args, login, - chat [--model ] [--conversation-id ] , invoke-do , - make [--conversation-id ] , research , models, + chat [--model ] [--conversation-id ] [--open-fulltab-view] , + invoke-do [--open-fulltab-view] , + make [--conversation-id ] [--open-fulltab-view] , + research [--open-fulltab-view] , models, mcp-servers, mcp-tools --server , mcp-call --server --tool , mcp-add , mcp-auth , mcp-remove , mcp-enable , mcp-disable , @@ -152,6 +154,8 @@ opera ai: mcp-remove, mcp-enable, and mcp-disable require Opera Neon with an active sign-in. Run \`opera-browser-cli setup\` to configure the executable path, or set OPERA_CLI_EXECUTABLE_PATH="/Applications/Opera Neon.app/Contents/MacOS/Opera". + chat, invoke-do, make, and research accept --open-fulltab-view to open the + created tab in the foreground (default: background). Not available in headless mode. gpu: Headless Chrome cannot access hardware GPU on most Linux systems. @@ -664,6 +668,7 @@ args: options: --model AI model to use (run "opera-browser-cli models" to list) --conversation-id, -c Continue an existing conversation (omit to start a new one) + --open-fulltab-view Open the chat tab in the foreground (default: background) examples: opera-browser-cli chat "Hello, who are you?" @@ -677,6 +682,9 @@ Requires Opera Neon with an active sign-in. Run \`opera-browser-cli setup\` to c args: Task to perform (required) +flags: + --open-fulltab-view Open the do tab in the foreground (default: background) + examples: opera-browser-cli invoke-do "Find the cheapest flight from London to Tokyo next month" opera-browser-cli invoke-do "Log in to my account and check my order history"`, @@ -690,6 +698,7 @@ args: flags: --conversation-id, -c Continue an existing make conversation (omit to start a new one) + --open-fulltab-view Open the make tab in the foreground (default: background) examples: opera-browser-cli make "A landing page for a coffee shop with a menu and contact form" @@ -705,6 +714,7 @@ args: flags: --type Research depth: local, one-minute, or deep (default: local) + --open-fulltab-view Open the research tab in the foreground (default: background) examples: opera-browser-cli research "the history of the Roman Empire" @@ -2931,6 +2941,25 @@ function requireNeon(command: string): void { } } +/** + * Reject `--open-fulltab-view` in headless mode. Activating a foreground tab + * in a headless window is nonsensical; the extension cannot distinguish headed + * from headless, so the CLI owns this validation. Mirrors the contract in + * docs/cdp-fulltab-view-contract.md §4. + */ +function requireHeadedForFullTabView(command: string): void { + if (!shouldRunHeaded()) { + throw new CdpError( + "--open-fulltab-view is not available in headless mode.", + "VALIDATION_ERROR", + [ + "Run with `OPERA_CLI_HEADED=1` to use a visible browser", + `Or omit the flag to run ${command} in the background`, + ], + ); + } +} + // --- Login --- const OPERA_ACCOUNT_URL = "https://auth.opera.com/account/"; @@ -3163,13 +3192,14 @@ function formatConversationResult(result: string): string { } async function handleChat(args: string[]): Promise { - const { prompt, model, conversationId } = parseChatOrMakeArgs(args); + const { prompt, model, conversationId, openFullTabView } = parseChatOrMakeArgs(args); if (!prompt) { throw new CdpError("Missing prompt", "VALIDATION_ERROR", [ 'Run `opera-browser-cli chat "What is on this page?"` to chat with Opera AI', "Use --model to select a model (run `opera-browser-cli models` to list)", ]); } + if (openFullTabView) requireHeadedForFullTabView("chat"); const toolArgs: Record = { prompt }; if (model !== undefined) { toolArgs["model"] = model; @@ -3177,6 +3207,9 @@ async function handleChat(args: string[]): Promise { if (conversationId !== undefined) { toolArgs["conversationId"] = conversationId; } + if (openFullTabView) { + toolArgs["openFullTabView"] = true; + } const result = await callAiTool("chat", "opera_chat", toolArgs); // CDP errors are raw strings checked first; only success responses are JSON. checkAiResultForCdpError("chat", result); @@ -3184,30 +3217,40 @@ async function handleChat(args: string[]): Promise { } async function handleInvokeDo(args: string[]): Promise { - const prompt = args.join(" "); + const openFullTabView = args.includes("--open-fulltab-view"); + const prompt = args.filter(a => a !== "--open-fulltab-view").join(" "); if (!prompt) { throw new CdpError("Missing prompt", "VALIDATION_ERROR", [ 'Run `opera-browser-cli invoke-do "Click the login button"` to perform an action', ]); } + if (openFullTabView) requireHeadedForFullTabView("invoke-do"); requireNeon("invoke-do"); - const result = await callAiTool("invoke-do", "opera_do", { prompt }); + const toolArgs: Record = { prompt }; + if (openFullTabView) { + toolArgs["openFullTabView"] = true; + } + const result = await callAiTool("invoke-do", "opera_do", toolArgs); checkAiResultForCdpError("invoke-do", result); return formatMcpResult("result", result, [], true); } async function handleMake(args: string[]): Promise { - const { prompt, conversationId } = parseMakeArgs(args); + const { prompt, conversationId, openFullTabView } = parseMakeArgs(args); if (!prompt) { throw new CdpError("Missing prompt", "VALIDATION_ERROR", [ 'Run `opera-browser-cli make "A summary of this page"` to create something', ]); } + if (openFullTabView) requireHeadedForFullTabView("make"); requireNeon("make"); const toolArgs: Record = { prompt }; if (conversationId !== undefined) { toolArgs["conversationId"] = conversationId; } + if (openFullTabView) { + toolArgs["openFullTabView"] = true; + } const result = await callAiTool("make", "opera_make", toolArgs); checkAiResultForCdpError("make", result); return formatConversationResult(result); @@ -3220,9 +3263,11 @@ export function parseChatOrMakeArgs(args: string[]): { prompt: string; model?: string; conversationId?: string; + openFullTabView?: boolean; } { let model: string | undefined; let conversationId: string | undefined; + let openFullTabView = false; const promptParts: string[] = []; for (let i = 0; i < args.length; i++) { const arg = args[i]; @@ -3241,16 +3286,24 @@ export function parseChatOrMakeArgs(args: string[]): { if (i + 1 < args.length) { conversationId = args[++i]; } + } else if (arg === "--open-fulltab-view") { + openFullTabView = true; } else { promptParts.push(arg); } } - return { prompt: promptParts.join(" "), model, conversationId }; + return { + prompt: promptParts.join(" "), + model, + conversationId, + openFullTabView: openFullTabView || undefined, + }; } export function parseMakeArgs(args: string[]): { prompt: string; conversationId?: string; + openFullTabView?: boolean; } { if (args.includes("--model")) { throw new CdpError( @@ -3259,28 +3312,36 @@ export function parseMakeArgs(args: string[]): { ["Run `opera-browser-cli models` to list the models available for chat"], ); } - const { prompt, conversationId } = parseChatOrMakeArgs(args); - return { prompt, conversationId }; + const { prompt, conversationId, openFullTabView } = parseChatOrMakeArgs(args); + return { prompt, conversationId, openFullTabView }; } export function parseResearchArgs(args: string[]): { prompt: string; researchType?: ResearchType; + openFullTabView?: boolean; } { let researchType: ResearchType | undefined; + let openFullTabView = false; const promptParts: string[] = []; for (let i = 0; i < args.length; i++) { if (args[i] === "--type" && i + 1 < args.length) { researchType = args[++i] as ResearchType; + } else if (args[i] === "--open-fulltab-view") { + openFullTabView = true; } else { promptParts.push(args[i]); } } - return { prompt: promptParts.join(" "), researchType }; + return { + prompt: promptParts.join(" "), + researchType, + openFullTabView: openFullTabView || undefined, + }; } async function handleResearch(args: string[]): Promise { - const { prompt, researchType } = parseResearchArgs(args); + const { prompt, researchType, openFullTabView } = parseResearchArgs(args); if (!prompt) { throw new CdpError("Missing prompt", "VALIDATION_ERROR", [ 'Run `opera-browser-cli research "quantum computing"` to research a topic', @@ -3297,9 +3358,13 @@ async function handleResearch(args: string[]): Promise { ["Valid types: local, one-minute, deep"], ); } + if (openFullTabView) requireHeadedForFullTabView("research"); requireNeon("research"); const toolArgs: Record = { prompt }; if (researchType !== undefined) toolArgs.researchType = researchType; + if (openFullTabView) { + toolArgs["openFullTabView"] = true; + } const result = await callAiTool("research", "opera_research", toolArgs); checkAiResultForCdpError("research", result); return formatMcpResult("result", result, [], true); diff --git a/test/cli.test.ts b/test/cli.test.ts index 9b67e6a..07a19f1 100644 --- a/test/cli.test.ts +++ b/test/cli.test.ts @@ -6,6 +6,7 @@ import { getCommandHelp, parseChatOrMakeArgs, parseMakeArgs, + parseResearchArgs, parseScreenshotArgs, parseSetupArgs, } from "../src/cli.js"; @@ -202,6 +203,21 @@ describe("parseChatOrMakeArgs", () => { const result = parseChatOrMakeArgs(["--model", "gpt-4o", "--conversation-id", "conv-1", "Hello"]); expect(result).toEqual({ prompt: "Hello", model: "gpt-4o", conversationId: "conv-1" }); }); + + it("parses --open-fulltab-view flag", () => { + const result = parseChatOrMakeArgs(["Hello", "--open-fulltab-view"]); + expect(result).toEqual({ prompt: "Hello", model: undefined, conversationId: undefined, openFullTabView: true }); + }); + + it("omits openFullTabView when flag absent", () => { + const result = parseChatOrMakeArgs(["Hello"]); + expect(result).toEqual({ prompt: "Hello", model: undefined, conversationId: undefined }); + }); + + it("parses --open-fulltab-view with --model and --conversation-id", () => { + const result = parseChatOrMakeArgs(["--model", "claude-sonnet-4", "-c", "conv-1", "Hello", "--open-fulltab-view"]); + expect(result).toEqual({ prompt: "Hello", model: "claude-sonnet-4", conversationId: "conv-1", openFullTabView: true }); + }); }); describe("handleChat JSON response parsing", () => { @@ -265,4 +281,39 @@ describe("parseMakeArgs", () => { "make does not accept --model", ); }); + + it("parses --open-fulltab-view flag", () => { + expect(parseMakeArgs(["Build a todo app", "--open-fulltab-view"])).toEqual({ + prompt: "Build a todo app", + conversationId: undefined, + openFullTabView: true, + }); + }); +}); + +describe("parseResearchArgs", () => { + it("parses prompt only", () => { + const result = parseResearchArgs(["quantum computing"]); + expect(result).toEqual({ prompt: "quantum computing", researchType: undefined }); + }); + + it("parses --type flag", () => { + const result = parseResearchArgs(["quantum computing", "--type", "deep"]); + expect(result).toEqual({ prompt: "quantum computing", researchType: "deep" }); + }); + + it("parses --open-fulltab-view flag", () => { + const result = parseResearchArgs(["quantum computing", "--open-fulltab-view"]); + expect(result).toEqual({ prompt: "quantum computing", researchType: undefined, openFullTabView: true }); + }); + + it("omits openFullTabView when flag absent", () => { + const result = parseResearchArgs(["quantum computing"]); + expect(result).toEqual({ prompt: "quantum computing", researchType: undefined }); + }); + + it("parses --type and --open-fulltab-view together", () => { + const result = parseResearchArgs(["--type", "one-minute", "quantum", "--open-fulltab-view"]); + expect(result).toEqual({ prompt: "quantum", researchType: "one-minute", openFullTabView: true }); + }); }); From 91cff8b7d1a730cfd8fa8ab1878c73e5c87fde07 Mon Sep 17 00:00:00 2001 From: Mateusz Kupczyk Date: Tue, 8 Sep 2026 13:05:20 +0200 Subject: [PATCH 2/2] fixup! feat: Add propagation of openFullTabView parameter to Opera tools --- src/cli.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/cli.ts b/src/cli.ts index a2b34e4..6f6e0d7 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -2944,8 +2944,7 @@ function requireNeon(command: string): void { /** * Reject `--open-fulltab-view` in headless mode. Activating a foreground tab * in a headless window is nonsensical; the extension cannot distinguish headed - * from headless, so the CLI owns this validation. Mirrors the contract in - * docs/cdp-fulltab-view-contract.md §4. + * from headless, so the CLI owns this validation. */ function requireHeadedForFullTabView(command: string): void { if (!shouldRunHeaded()) { @@ -3217,8 +3216,7 @@ async function handleChat(args: string[]): Promise { } async function handleInvokeDo(args: string[]): Promise { - const openFullTabView = args.includes("--open-fulltab-view"); - const prompt = args.filter(a => a !== "--open-fulltab-view").join(" "); + const { prompt, openFullTabView } = parseChatOrMakeArgs(args); if (!prompt) { throw new CdpError("Missing prompt", "VALIDATION_ERROR", [ 'Run `opera-browser-cli invoke-do "Click the login button"` to perform an action',