diff --git a/docs/tool-reference.md b/docs/tool-reference.md index b8404f7..dba0026 100644 --- a/docs/tool-reference.md +++ b/docs/tool-reference.md @@ -648,6 +648,7 @@ in the DevTools Elements panel (if any). - **prompt** (string) **(required)**: The prompt to send to Opera AI. - **conversationId** (string) _(optional)_: Conversation ID to continue an existing conversation. Omit to start a new conversation. - **model** (string) _(optional)_: Model ID to use for the chat. Omit to use the browser default. Use [`opera_list_models`](#opera_list_models) to discover available IDs. +- **openFullTabView** (boolean) _(optional)_: When true, activate the created chat tab so the user sees it immediately. Default false (tab created in the background). Only meaningful for headed sessions. --- @@ -678,6 +679,7 @@ in the DevTools Elements panel (if any). **Parameters:** - **prompt** (string) **(required)**: The action to perform, described in natural language. +- **openFullTabView** (boolean) _(optional)_: When true, activate the created do tab so the user sees it immediately. Default false (tab created in the background). Only meaningful for headed sessions. --- @@ -725,6 +727,7 @@ in the DevTools Elements panel (if any). - **prompt** (string) **(required)**: Description of what to create or generate. - **conversationId** (string) _(optional)_: Conversation ID to continue an existing conversation. Omit to start a new conversation. +- **openFullTabView** (boolean) _(optional)_: When true, activate the created make tab so the user sees it immediately. Default false (tab created in the background). Only meaningful for headed sessions. --- @@ -746,6 +749,7 @@ in the DevTools Elements panel (if any). **Parameters:** - **prompt** (string) **(required)**: The topic or question to research. +- **openFullTabView** (boolean) _(optional)_: When true, activate the created research tab so the user sees it immediately. Default false (tab created in the background). Only meaningful for headed sessions. - **researchType** (enum: "local", "one-minute", "deep") _(optional)_: Depth of research. "local" uses only on-page context, "one-minute" performs a quick web search, "deep" performs a thorough multi-source search. --- diff --git a/src/opera/tools/opera.ts b/src/opera/tools/opera.ts index 52c5138..30249c4 100644 --- a/src/opera/tools/opera.ts +++ b/src/opera/tools/opera.ts @@ -125,6 +125,12 @@ export const operaChat = definePageTool({ .describe( 'Conversation ID to continue an existing conversation. Omit to start a new conversation.', ), + openFullTabView: zod + .boolean() + .optional() + .describe( + 'When true, activate the created chat tab so the user sees it immediately. Default false (tab created in the background). Only meaningful for headed sessions.', + ), }, handler: async (request, response) => { // puppeteer's _client() is internal; cast to the shape getCDPSession needs @@ -142,6 +148,9 @@ export const operaChat = definePageTool({ if (request.params.conversationId !== undefined) { payload['conversationId'] = request.params.conversationId; } + if (request.params.openFullTabView === true) { + payload['openFullTabView'] = true; + } const result = await dispatchAction(session, payload); response.appendResponseLine(result); } catch (e) { @@ -166,6 +175,12 @@ export const operaDo = definePageTool({ prompt: zod .string() .describe('The action to perform, described in natural language.'), + openFullTabView: zod + .boolean() + .optional() + .describe( + 'When true, activate the created do tab so the user sees it immediately. Default false (tab created in the background). Only meaningful for headed sessions.', + ), }, handler: async (request, response) => { // puppeteer's _client() is internal; cast to the shape getCDPSession needs @@ -173,12 +188,16 @@ export const operaDo = definePageTool({ request.page.pptrPage as unknown as {_client(): CDPSession}, ); try { + const payload: Record = { + action: 'do', + prompt: request.params.prompt, + }; + if (request.params.openFullTabView === true) { + payload['openFullTabView'] = true; + } const result = await dispatchWithStreamedResponse( session, - { - action: 'do', - prompt: request.params.prompt, - }, + payload, chunk => response.sendLog(chunk), request.signal, ); @@ -212,6 +231,12 @@ export const operaMake = definePageTool({ .describe( 'Conversation ID to continue an existing conversation. Omit to start a new conversation.', ), + openFullTabView: zod + .boolean() + .optional() + .describe( + 'When true, activate the created make tab so the user sees it immediately. Default false (tab created in the background). Only meaningful for headed sessions.', + ), }, handler: async (request, response) => { // puppeteer's _client() is internal; cast to the shape getCDPSession needs @@ -226,6 +251,9 @@ export const operaMake = definePageTool({ if (request.params.conversationId !== undefined) { payload['conversationId'] = request.params.conversationId; } + if (request.params.openFullTabView === true) { + payload['openFullTabView'] = true; + } const result = await dispatchAction(session, payload); response.appendResponseLine(result); } catch (e) { @@ -254,6 +282,12 @@ export const operaResearch = definePageTool({ .describe( 'Depth of research. "local" uses only on-page context, "one-minute" performs a quick web search, "deep" performs a thorough multi-source search.', ), + openFullTabView: zod + .boolean() + .optional() + .describe( + 'When true, activate the created research tab so the user sees it immediately. Default false (tab created in the background). Only meaningful for headed sessions.', + ), }, handler: async (request, response) => { // puppeteer's _client() is internal; cast to the shape getCDPSession needs @@ -267,6 +301,9 @@ export const operaResearch = definePageTool({ if (request.params.researchType !== undefined) { payload['researchType'] = request.params.researchType; } + if (request.params.openFullTabView === true) { + payload['openFullTabView'] = true; + } try { const result = await dispatchWithStreamedResponse( session, diff --git a/tests/opera/opera.test.ts b/tests/opera/opera.test.ts index 2afea24..8612eaa 100644 --- a/tests/opera/opera.test.ts +++ b/tests/opera/opera.test.ts @@ -233,6 +233,44 @@ describe('opera tools', () => { assert.ok(!('conversationId' in session.payloadAt(0))); }); + it('forwards openFullTabView when true', async () => { + const session = new FakeCDPSession(); + const {response} = makeResponse(); + + await operaChat.handler( + makeRequest(session, {prompt: 'hi', openFullTabView: true}), + response, + context, + ); + + assert.strictEqual(session.payloadAt(0)['openFullTabView'], true); + }); + + it('omits openFullTabView when not set', async () => { + const session = new FakeCDPSession(); + const {response} = makeResponse(); + + await operaChat.handler( + makeRequest(session, {prompt: 'hi'}), + response, + context, + ); + + assert.ok(!('openFullTabView' in session.payloadAt(0))); + }); + + it('omits openFullTabView when false', async () => { + const session = new FakeCDPSession(); + const {response} = makeResponse(); + + await operaChat.handler( + makeRequest(session, {prompt: 'hi', openFullTabView: false}), + response, + context, + ); + + assert.ok(!('openFullTabView' in session.payloadAt(0))); + }); }); describe('opera_make', () => { @@ -271,6 +309,44 @@ describe('opera tools', () => { 'conversation-123', ); }); + it('forwards openFullTabView when true', async () => { + const session = new FakeCDPSession().resolveWith({result: 'made it'}); + const {response} = makeResponse(); + + await operaMake.handler( + makeRequest(session, {prompt: 'a poem', openFullTabView: true}), + response, + context, + ); + + assert.strictEqual(session.payloadAt(0)['openFullTabView'], true); + }); + + it('omits openFullTabView for opera_make when not set', async () => { + const session = new FakeCDPSession().resolveWith({result: 'made it'}); + const {response} = makeResponse(); + + await operaMake.handler( + makeRequest(session, {prompt: 'a poem'}), + response, + context, + ); + + assert.ok(!('openFullTabView' in session.payloadAt(0))); + }); + + it('omits openFullTabView for opera_make when false', async () => { + const session = new FakeCDPSession().resolveWith({result: 'made it'}); + const {response} = makeResponse(); + + await operaMake.handler( + makeRequest(session, {prompt: 'a poem', openFullTabView: false}), + response, + context, + ); + + assert.ok(!('openFullTabView' in session.payloadAt(0))); + }); }); describe('opera_list_models', () => { @@ -461,6 +537,100 @@ describe('opera tools', () => { assert.ok(!('researchType' in session.payloadAt(0))); }); + it('forwards openFullTabView when true for opera_do', async () => { + const session = new FakeCDPSession().resolveWith({correlationId: 'c1'}); + const {response} = makeResponse(); + + const pending = operaDo.handler( + makeRequest(session, {prompt: 'go', openFullTabView: true}), + response, + context, + ); + await waitForStreamListeners(session); + session.emit('Opera.actionCompleted', { + correlationId: 'c1', + result: 'done', + }); + await pending; + + assert.strictEqual(session.payloadAt(0)['openFullTabView'], true); + }); + + it('omits openFullTabView for opera_do when not set', async () => { + const session = new FakeCDPSession().resolveWith({correlationId: 'c1'}); + const {response} = makeResponse(); + + const pending = operaDo.handler( + makeRequest(session, {prompt: 'go'}), + response, + context, + ); + await waitForStreamListeners(session); + session.emit('Opera.actionCompleted', { + correlationId: 'c1', + result: 'done', + }); + await pending; + + assert.ok(!('openFullTabView' in session.payloadAt(0))); + }); + + it('forwards openFullTabView when true for opera_research', async () => { + const session = new FakeCDPSession().resolveWith({correlationId: 'c1'}); + const {response} = makeResponse(); + + const pending = operaResearch.handler( + makeRequest(session, {prompt: 'quantum', openFullTabView: true}), + response, + context, + ); + await waitForStreamListeners(session); + session.emit('Opera.actionCompleted', { + correlationId: 'c1', + result: 'summary', + }); + await pending; + + assert.strictEqual(session.payloadAt(0)['openFullTabView'], true); + }); + + it('omits openFullTabView for opera_research when not set', async () => { + const session = new FakeCDPSession().resolveWith({correlationId: 'c1'}); + const {response} = makeResponse(); + + const pending = operaResearch.handler( + makeRequest(session, {prompt: 'quantum'}), + response, + context, + ); + await waitForStreamListeners(session); + session.emit('Opera.actionCompleted', { + correlationId: 'c1', + result: 'summary', + }); + await pending; + + assert.ok(!('openFullTabView' in session.payloadAt(0))); + }); + + it('omits openFullTabView for opera_research when false', async () => { + const session = new FakeCDPSession().resolveWith({correlationId: 'c1'}); + const {response} = makeResponse(); + + const pending = operaResearch.handler( + makeRequest(session, {prompt: 'quantum', openFullTabView: false}), + response, + context, + ); + await waitForStreamListeners(session); + session.emit('Opera.actionCompleted', { + correlationId: 'c1', + result: 'summary', + }); + await pending; + + assert.ok(!('openFullTabView' in session.payloadAt(0))); + }); }); describe('opera_register_mcp_server', () => {