Skip to content
Merged
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
4 changes: 4 additions & 0 deletions docs/tool-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

---

Expand Down Expand Up @@ -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.

---

Expand Down Expand Up @@ -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.

---

Expand All @@ -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.

---
Expand Down
45 changes: 41 additions & 4 deletions src/opera/tools/opera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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) {
Expand All @@ -166,19 +175,29 @@ 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
const session = getCDPSession(
request.page.pptrPage as unknown as {_client(): CDPSession},
);
try {
const payload: Record<string, unknown> = {
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,
);
Expand Down Expand Up @@ -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
Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand Down
170 changes: 170 additions & 0 deletions tests/opera/opera.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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 () => {
Comment thread
mateuszk-opera marked this conversation as resolved.
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', () => {
Expand Down
Loading