Skip to content

Commit fcdfc28

Browse files
committed
Introduce full support for the Microsoft Copilot chatbot
1 parent 59b8316 commit fcdfc28

8 files changed

Lines changed: 195 additions & 0 deletions

File tree

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { Chatbot } from '../types/chatbot'
2+
import { CHATBOTS } from '@shared/constants/chatbots'
3+
import {
4+
add_apply_response_button,
5+
observe_for_responses
6+
} from '../utils/add-apply-response-button'
7+
import {
8+
InitializationError,
9+
report_initialization_error
10+
} from '../utils/report-initialization-error'
11+
12+
export const copilot: Chatbot = {
13+
wait_until_ready: async () => {
14+
await new Promise((resolve) => setTimeout(resolve, 1000))
15+
},
16+
set_model: async (model?: string) => {
17+
if (!model) return
18+
19+
const model_selector_button = document.querySelector(
20+
'button[data-testid="composer-chat-mode-quick-button"]'
21+
) as HTMLButtonElement
22+
if (!model_selector_button) {
23+
report_initialization_error({
24+
function_name: 'set_model',
25+
log_message: 'Model selector button not found',
26+
alert_message: InitializationError.UNABLE_TO_SET_MODEL
27+
})
28+
return
29+
}
30+
31+
const model_label_to_find = (CHATBOTS['Copilot'].models as any)[model]
32+
?.label
33+
if (!model_label_to_find) return
34+
35+
// The current model text is inside the button
36+
if (model_selector_button.textContent?.includes(model_label_to_find)) {
37+
return
38+
}
39+
40+
model_selector_button.click()
41+
await new Promise((r) => requestAnimationFrame(r))
42+
43+
const options_container = document.querySelector('div[id="popoverPortal"]')
44+
if (!options_container) {
45+
report_initialization_error({
46+
function_name: 'set_model',
47+
log_message: 'Model options container not found',
48+
alert_message: InitializationError.UNABLE_TO_SET_MODEL
49+
})
50+
// click again to close
51+
model_selector_button.click()
52+
return
53+
}
54+
55+
const options = options_container.querySelectorAll(
56+
'button[role="menuitem"]'
57+
)
58+
59+
let found = false
60+
for (const option of Array.from(options)) {
61+
if (option.textContent?.includes(model_label_to_find)) {
62+
;(option as HTMLElement).click()
63+
found = true
64+
break
65+
}
66+
}
67+
68+
if (!found) {
69+
report_initialization_error({
70+
function_name: 'set_model',
71+
log_message: `Model option "${model_label_to_find}" not found`,
72+
alert_message: InitializationError.UNABLE_TO_SET_MODEL
73+
})
74+
// click again to close dropdown
75+
model_selector_button.click()
76+
}
77+
78+
await new Promise((r) => requestAnimationFrame(r))
79+
},
80+
enter_message_and_send: async (
81+
message: string,
82+
without_submission?: boolean
83+
) => {
84+
const input_element = document.querySelector(
85+
'textarea'
86+
) as HTMLTextAreaElement
87+
if (!input_element) {
88+
report_initialization_error({
89+
function_name: 'enter_message_and_send',
90+
log_message: 'Message input textarea not found for Copilot',
91+
alert_message: InitializationError.UNABLE_TO_SEND_MESSAGE
92+
})
93+
return
94+
}
95+
input_element.value = message
96+
input_element.dispatchEvent(new Event('input', { bubbles: true }))
97+
input_element.dispatchEvent(new Event('change', { bubbles: true }))
98+
await new Promise((r) => requestAnimationFrame(r))
99+
100+
if (without_submission) return
101+
102+
const send_button = document.querySelector(
103+
'button[data-testid="submit-button"]'
104+
) as HTMLElement
105+
106+
if (!send_button || send_button.hasAttribute('disabled')) {
107+
report_initialization_error({
108+
function_name: 'enter_message_and_send',
109+
log_message: 'Send button not found or disabled for Copilot',
110+
alert_message: InitializationError.UNABLE_TO_SEND_MESSAGE
111+
})
112+
return
113+
}
114+
send_button.click()
115+
},
116+
inject_apply_response_button: (
117+
client_id: number,
118+
raw_instructions?: string
119+
) => {
120+
const add_buttons = (footer: Element) => {
121+
add_apply_response_button({
122+
client_id,
123+
raw_instructions,
124+
footer,
125+
get_chat_turn: (f) => f.closest('div[data-content="ai-message"]'),
126+
get_code_blocks: (t) => t.querySelectorAll('code'),
127+
perform_copy: (f) => {
128+
const copy_button = f.querySelector(
129+
'button[data-testid="copy-message-button"]'
130+
) as HTMLElement
131+
if (!copy_button) {
132+
report_initialization_error({
133+
function_name: 'copilot.perform_copy',
134+
log_message: 'Copy button not found',
135+
alert_message: InitializationError.UNABLE_TO_COPY_RESPONSE
136+
})
137+
return
138+
}
139+
copy_button.click()
140+
},
141+
insert_button: (f, b) => f.insertBefore(b, f.firstChild)
142+
})
143+
}
144+
145+
observe_for_responses({
146+
chatbot_name: 'Copilot',
147+
is_generating: () =>
148+
!!document.querySelector('button[data-testid="stop-button"]'),
149+
footer_selector: 'div[data-testid="message-item-reactions"]',
150+
add_buttons
151+
})
152+
}
153+
}

‎packages/browser/src/content-scripts/send-prompt-content-script/chatbots/index.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './ai-studio'
22
export * from './chatgpt'
33
export * from './claude'
4+
export * from './copilot'
45
export * from './deepseek'
56
export * from './gemini'
67
export * from './grok'

‎packages/browser/src/content-scripts/send-prompt-content-script/send-prompt-content-script.ts‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
ai_studio,
77
gemini,
88
chatgpt,
9+
copilot,
910
claude,
1011
meta,
1112
mistral,
@@ -43,6 +44,9 @@ const is_openrouter = current_url.startsWith(openrouter_url)
4344
const chatgpt_url = 'https://chatgpt.com/'
4445
const is_chatgpt = current_url.startsWith(chatgpt_url)
4546

47+
const copilot_url = 'https://copilot.microsoft.com/'
48+
const is_copilot = current_url.startsWith(copilot_url)
49+
4650
const claude_url = 'https://claude.ai/new'
4751
const is_claude = current_url.startsWith(claude_url)
4852

@@ -89,6 +93,8 @@ if (is_ai_studio) {
8993
chatbot = gemini
9094
} else if (is_chatgpt) {
9195
chatbot = chatgpt
96+
} else if (is_copilot) {
97+
chatbot = copilot
9298
} else if (is_claude) {
9399
chatbot = claude
94100
} else if (is_meta) {
@@ -126,6 +132,7 @@ export const get_textarea_element = () => {
126132
[openrouter_url]: 'textarea',
127133
[meta_url]: 'div[contenteditable="true"]',
128134
[chatgpt_url]: 'div#prompt-textarea',
135+
[copilot_url]: 'textarea',
129136
[grok_url]: 'textarea',
130137
[deepseek_url]: 'textarea',
131138
[mistral_url]: 'div[contenteditable="true"]',

‎packages/browser/src/manifest.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
"https://gemini.google.com/u/*/app",
3030
"https://openrouter.ai/chat*",
3131
"https://chatgpt.com/",
32+
"https://copilot.microsoft.com/",
3233
"https://claude.ai/new",
3334
"https://grok.com/",
3435
"https://chat.deepseek.com/",

‎packages/shared/src/constants/chatbots.ts‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,26 @@ export const CHATBOTS = {
7777
default_top_p: 0,
7878
models: {}
7979
},
80+
Copilot: {
81+
url: 'https://copilot.microsoft.com/',
82+
supports_custom_temperature: false,
83+
supports_custom_top_p: false,
84+
supports_system_instructions: false,
85+
supports_user_provided_model: false,
86+
supports_user_provided_port: false,
87+
supports_reasoning_effort: false,
88+
supports_thinking_budget: false,
89+
default_system_instructions: '',
90+
supported_options: {},
91+
default_top_p: 0,
92+
models: {
93+
'quick-response': { label: 'Quick response' },
94+
'think-deeper': { label: 'Think Deeper' },
95+
'study-and-learn': { label: 'Study and learn' },
96+
'smart-gpt-5': { label: 'Smart (GPT-5)' },
97+
search: { label: 'Search' }
98+
}
99+
},
80100
Claude: {
81101
url: 'https://claude.ai/new',
82102
supports_custom_temperature: false,
Lines changed: 7 additions & 0 deletions
Loading

‎packages/ui/src/components/editor/Icon/Icon.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import X from '../../../assets/icons/x.svg'
2323
import CodeWebChat from '../../../assets/icons/code-web-chat.svg'
2424
import ZAI from '../../../assets/icons/z-ai.svg'
2525
import Discord from '../../../assets/icons/discord.svg'
26+
import Copilot from '../../../assets/icons/copilot.svg'
2627

2728
export namespace Icon {
2829
export type Variant =
@@ -51,6 +52,7 @@ export namespace Icon {
5152
| 'CODE_WEB_CHAT'
5253
| 'Z_AI'
5354
| 'DISCORD'
55+
| 'COPILOT'
5456

5557
export type Props = {
5658
variant: Variant
@@ -136,6 +138,9 @@ export const Icon: React.FC<Icon.Props> = ({ variant }) => {
136138
case 'DISCORD':
137139
icon = <Discord />
138140
break
141+
case 'COPILOT':
142+
icon = <Copilot />
143+
break
139144
default:
140145
icon = <></>
141146
break

‎packages/ui/src/components/editor/Presets/Presets.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export const chatbot_to_icon: Record<keyof typeof CHATBOTS, Icon.Variant> = {
1414
'Open WebUI': 'OPEN_WEBUI',
1515
OpenRouter: 'OPENROUTER',
1616
ChatGPT: 'CHATGPT',
17+
Copilot: 'COPILOT',
1718
Claude: 'CLAUDE',
1819
DeepSeek: 'DEEPSEEK',
1920
Mistral: 'MISTRAL',

0 commit comments

Comments
 (0)