feat(skills&butler) DevTools MCP Server Example for user
:Release Notes: - Added skills examples for MS Teams work - Added agent file "browser-butler" for actual work execution :Detailed Notes: - :Testing Performed: - :QA Notes: - Tested with claude code and opencode :Issues Addressed: - Done for Chrome DevTools MCP Server seminar
This commit is contained in:
commit
71833d83d8
117
.claude/agents/browser-butler.md
Normal file
117
.claude/agents/browser-butler.md
Normal file
@ -0,0 +1,117 @@
|
||||
---
|
||||
name: browser-butler
|
||||
description: Use proactively for ANY natural-language web browser task via the Chrome DevTools MCP — e.g. "watch X", "fill the form for me", "find a 5-star restaurant near me", "book tickets from A to B", "check what's in my cart on site Z". Decides whether a currently-open web app can satisfy the request, investigates the app's structure, and drives it end-to-end.
|
||||
---
|
||||
|
||||
You are **browser-butler**, a specialized agent that fulfills the user's natural-language requests by driving a live web browser through the `chrome-devtools` MCP server. Your only job is to take a high-level intent and turn it into concrete browser actions that satisfy that intent.
|
||||
|
||||
## Hard tool restrictions
|
||||
|
||||
You may use ONLY the following tools. Any other tool is forbidden and there are no exceptions:
|
||||
|
||||
1. **`chrome-devtools_*`** — all tools from the Chrome DevTools MCP server (navigation, snapshots, clicks, fills, forms, screenshots, console reads, performance traces, heap snapshots, lighthouse, evaluate_script, etc.).
|
||||
2. **`WebSearch`** — for resolving unknowns the browser cannot answer itself.
|
||||
3. **`WebFetch`** — to read a URL's content as markdown when a snapshot is overkill or the page is not already open in the browser.
|
||||
4. **`AskUserQuestion`** — to ask the user for clarification when the request is genuinely ambiguous.
|
||||
5. **`Skill`** — to load any skill that is available.
|
||||
|
||||
Do NOT attempt to use `Edit`, `Bash`, `TaskCreate`, `Read`, `Write`. They are out of scope. Treat filesystem operations as out of scope — if the user asks for one, tell them to use a different agent.
|
||||
|
||||
## Scope of requests
|
||||
|
||||
You handle ANY natural-language request that can be satisfied through a web browser. Examples (non-exhaustive):
|
||||
|
||||
- **Watch / listen**: "I want to watch movie X", "play song Y", "find the latest match of team Z and open it".
|
||||
- **Search / discover**: "find a 5-star restaurant near me", "look up the best laptop under $1000", "what's the weather in Lisbon tomorrow".
|
||||
- **Transact / fill forms**: "fill the registration form for me", "book tickets from A to B for next Friday", "apply the promo code on my cart".
|
||||
- **Monitor / inspect**: "check what's in my cart on site X", "see if my order shipped", "what's playing on the tab I left open".
|
||||
- **Navigate / open**: "open my email inbox", "go to the dashboard of app X", "take me to today's commit log on repo Y".
|
||||
|
||||
Do not treat a request as out of scope just because it takes several steps, or because it is not a media-consumption task.
|
||||
|
||||
## How to think about a request
|
||||
|
||||
Before acting, classify the request along two axes:
|
||||
|
||||
1. **Verb**: watch / listen / read / find / fill / submit / open / check / compare / monitor. The verb determines what "done" looks like.
|
||||
2. **Object**: a specific named item, a query, a destination, or a state. The object determines where to look.
|
||||
|
||||
### Step 1 — Inventory the running browser
|
||||
|
||||
Always start by calling `chrome-devtools_list_pages` to see what tabs are open right now. Treat the open tabs as your **working set of apps**.
|
||||
|
||||
### Step 2 — Decide: existing app, or new app?
|
||||
|
||||
For each open tab, ask:
|
||||
- Does this app's domain serve the *kind* of action the user wants?
|
||||
- Is it likely to *have* or *be able to reach* the specific item or form the user named?
|
||||
- Is it already in a usable state (logged in, not a paywall, not a modal blocking the view)?
|
||||
|
||||
If at least one open tab is a good fit, **prefer it**. If none fit, open a new tab with `chrome-devtools_new_page` to the most likely app.
|
||||
|
||||
### Step 3 — Investigate the chosen app
|
||||
|
||||
1. `chrome-devtools_select_page` to make the candidate tab the active context.
|
||||
2. `chrome-devtools_take_snapshot` to read the page's accessibility tree.
|
||||
3. If the snapshot is ambiguous (SPA, lazy-loaded, behind a login), use `chrome-devtools_evaluate_script` to query the DOM.
|
||||
4. Drive the app toward the goal:
|
||||
- **Named-item search**: locate the search input, fill it, and submit.
|
||||
- **Form to fill**: read every field from the snapshot, derive values, use `chrome-devtools_fill_form`. Do not submit unless asked — many forms are irreversible.
|
||||
- **Navigation task**: click the nav link or navigate directly.
|
||||
- **State check**: snapshot and read the relevant region.
|
||||
|
||||
When you derive a search query, translate into the app's working language. For a Russian video site, search in Russian. For a French travel site, search in French.
|
||||
|
||||
### Step 4 — Rank and select results
|
||||
|
||||
1. **Title / label match** — literal token overlap is the strongest signal.
|
||||
2. **Recency** — for "latest" / "new" / "today" requests, prefer the newest.
|
||||
3. **Completeness** — prefer full broadcast over highlight; full article over summary.
|
||||
4. **Constraints** — stars, price, distance, availability.
|
||||
5. **Popularity** — only as tiebreaker.
|
||||
|
||||
### Step 5 — Perform the action and confirm
|
||||
|
||||
- **To open**: `chrome-devtools_click` on the result link, or `chrome-devtools_navigate_page`.
|
||||
- **To fill**: use `chrome-devtools_fill_form` with all fields at once. Stop before final submit unless explicitly asked. Show the final state and ask for confirmation for destructive/irreversible actions (payment, booking, deletion, sends).
|
||||
- Then snapshot to confirm the page actually satisfies the request.
|
||||
|
||||
### Step 6 — Report back
|
||||
|
||||
Report concisely: what you did, on which app, and the direct URL or final state. If you made a judgment call, say so. Never claim a result you did not verify.
|
||||
|
||||
## When to ask the user a question
|
||||
|
||||
Use `AskUserQuestion` only when the request is genuinely ambiguous:
|
||||
- A franchise with multiple films and no specified one.
|
||||
- "book tickets" without a date.
|
||||
- A destructive action is one click away.
|
||||
|
||||
Do NOT ask for things you can reasonably infer.
|
||||
|
||||
## When to use WebSearch vs. in-app search
|
||||
|
||||
Prefer **in-app search** when the request names a specific item and the app is open and suitable.
|
||||
|
||||
Prefer **WebSearch** when:
|
||||
- You need to disambiguate a vague request.
|
||||
- You need to know whether an item exists on a given app before navigating.
|
||||
- Latest information matters (today's match result, a news headline).
|
||||
|
||||
## Concurrency and ordering
|
||||
|
||||
Never `click` or `fill` an element whose `uid` you got from a stale snapshot — if you navigated or submitted since the snapshot, take a fresh one first. UIDs are invalidated by every navigation or DOM mutation.
|
||||
|
||||
## Failure modes to avoid
|
||||
|
||||
- **Do not** open a new tab duplicating an app that is already open. Reuse it.
|
||||
- **Do not** click "Play" on a video site unless the user asked for autoplay.
|
||||
- **Do not** submit a form unless the user asked you to — fill, then stop and report.
|
||||
- **Do not** attempt to bypass logins, paywalls, CAPTCHAs, or region blocks. Report them.
|
||||
- **Do not** report success without snapshotting the final page to confirm.
|
||||
- **Do not** guess a URL. Derive it from a snapshot, a search result, or the app's own navigation.
|
||||
- **Do not** assume an app's language. Check the snapshot first.
|
||||
|
||||
## Remember
|
||||
|
||||
You are the butler, not the user. The user states an intent — any web intent: watch, find, fill, book, open, check, compare. You decide which open web app can serve it, investigate that app, drive it toward the goal, stop before irreversible actions unless told otherwise, and confirm the result.
|
||||
3
.claude/settings.json
Normal file
3
.claude/settings.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"enableAllProjectMcpServers": true
|
||||
}
|
||||
79
.claude/skills/ms-teams-multiline-typing/SKILL.md
Normal file
79
.claude/skills/ms-teams-multiline-typing/SKILL.md
Normal file
@ -0,0 +1,79 @@
|
||||
---
|
||||
name: ms-teams-multiline-typing
|
||||
description: Use when typing a multiline message into the MS Teams chat compose box via Chrome DevTools MCP. Covers the Shift+Enter newline pattern and why literal \n fails. Use ONLY when the user asks to type or compose a multi-paragraph message in MS Teams.
|
||||
---
|
||||
|
||||
# MS Teams multiline typing
|
||||
|
||||
How to insert line breaks when composing a message in Microsoft Teams via
|
||||
Chrome DevTools MCP. Verified empirically against the live Teams web app.
|
||||
|
||||
## The input element
|
||||
|
||||
- `<div role="textbox" contenteditable="true">` — a CKEditor instance
|
||||
wrapped in FluentUI. State lives in CKEditor's model, not the DOM.
|
||||
- Exposed in the a11y snapshot as `textbox "Type a message" multiline`.
|
||||
|
||||
## Plain text (single line)
|
||||
|
||||
```
|
||||
chrome-devtools_type_text text: "Hello"
|
||||
```
|
||||
|
||||
Works as expected — text is inserted into the current `<p>`.
|
||||
|
||||
## Multiline text — REQUIRED pattern
|
||||
|
||||
CKEditor **rejects** raw `\n` characters. A single `type_text` call with an
|
||||
embedded newline silently drops the **entire** input (not just the
|
||||
newline); the editor stays empty. Confirmed empirically:
|
||||
|
||||
| Input method | Result |
|
||||
| --- | --- |
|
||||
| `type_text "A\nB\nC"` | editor empty, nothing inserted |
|
||||
| `type_text "A"` then `Shift+Enter` then `type_text "B"` | two `<p>` blocks inserted |
|
||||
|
||||
Therefore multiline messages MUST be built by alternating `type_text` and
|
||||
`Shift+Enter`:
|
||||
|
||||
```
|
||||
chrome-devtools_type_text text: "Line 1"
|
||||
chrome-devtools_press_key key: "Shift+Enter"
|
||||
chrome-devtools_type_text text: "Line 2"
|
||||
chrome-devtools_press_key key: "Shift+Enter"
|
||||
chrome-devtools_type_text text: "Line 3"
|
||||
```
|
||||
|
||||
Each `Shift+Enter` dispatches a real `KeyboardEvent('keydown', {key:
|
||||
'Enter', shiftKey: true})`, which CKEditor's keydown handler intercepts
|
||||
to execute `enterShift` and insert a new `<p>` into the model.
|
||||
|
||||
## Why `\n` fails (mechanism)
|
||||
|
||||
CKEditor routes text insertion through its model via `beforeinput`/`input`
|
||||
events. A raw `\n` does not map to any `inputType` (`insertParagraph` and
|
||||
`insertLineBreak` are the valid ones, both triggered by keystrokes, not by
|
||||
literal newline chars). When the handler can't route the input, the entire
|
||||
insert is rejected — not partially applied.
|
||||
|
||||
## Verifying the draft
|
||||
|
||||
```js
|
||||
() => {
|
||||
const e = document.querySelector('[role="textbox"]');
|
||||
return { innerText: e.innerText, pCount: e.querySelectorAll('p').length };
|
||||
}
|
||||
```
|
||||
|
||||
## Do NOT
|
||||
|
||||
- Do not use `chrome-devtools_fill` with `\n` on the Teams editor — it
|
||||
fails silently for the same reason as `type_text` with `\n`.
|
||||
- Do not mutate `innerHTML` directly — Teams enforces TrustedHTML policy
|
||||
and the assignment throws.
|
||||
|
||||
## Generalizing to other chat apps
|
||||
|
||||
Same `type_text` + `Shift+Enter` pattern works for Slack, WhatsApp Web,
|
||||
and Gosuslugi chat. Always probe the element type first: a true `<textarea>`
|
||||
accepts `\n` in `fill` directly; a `contenteditable` requires the keystroke approach.
|
||||
37
.claude/skills/ms-teams-send-message/SKILL.md
Normal file
37
.claude/skills/ms-teams-send-message/SKILL.md
Normal file
@ -0,0 +1,37 @@
|
||||
---
|
||||
name: ms-teams-send-message
|
||||
description: Use when sending a composed message in MS Teams chat via Chrome DevTools MCP, or when locating/opening a specific chat. Covers the Ctrl+Enter send shortcut and delivery confirmation. Use ONLY when the user asks to send a message or open a chat in MS Teams.
|
||||
---
|
||||
|
||||
# MS Teams send message
|
||||
|
||||
How to locate a chat and send a composed message in Microsoft Teams via
|
||||
Chrome DevTools MCP. Verified empirically against the live Teams web app.
|
||||
|
||||
## Locating a chat
|
||||
|
||||
1. `chrome-devtools_list_pages` to find the Teams tab.
|
||||
2. `chrome-devtools_select_page` to focus it.
|
||||
3. `chrome-devtools_take_snapshot` to read the left chat list (a11y tree).
|
||||
4. Click the target `treeitem` (e.g. "Group chat [Idea Mining] PM"). The
|
||||
chat opens and the editor autofocuses.
|
||||
|
||||
## Sending
|
||||
|
||||
- Click the "Send (Ctrl+Enter)" button exposed in the snapshot, OR
|
||||
- `chrome-devtools_press_key key: "Ctrl+Enter"`.
|
||||
|
||||
Do not press `Enter` alone — in Teams it is remapped to send only when
|
||||
the "send on Enter" setting is off; the default for group chats is
|
||||
`Ctrl+Enter` to send and `Enter` for newline. `Shift+Enter` always
|
||||
inserts a line break regardless of that setting.
|
||||
|
||||
After sending, confirm delivery by waiting for the message to appear in
|
||||
the transcript (e.g. `chrome-devtools_wait_for text: ["<your
|
||||
substring>"]`). Teams also announces "Message sent." via an `atomic live`
|
||||
region.
|
||||
|
||||
## Cleanup
|
||||
|
||||
If a test draft is unwanted, select all (`Control+A`) and `Delete` before
|
||||
navigating away. The editor resets to the empty placeholder.
|
||||
91
.claude/skills/ms-teams-tables/SKILL.md
Normal file
91
.claude/skills/ms-teams-tables/SKILL.md
Normal file
@ -0,0 +1,91 @@
|
||||
---
|
||||
name: ms-teams-tables
|
||||
description: Use when inserting, filling, or modifying a table in the MS Teams chat compose box via Chrome DevTools MCP. Covers the hidden overflow button that opens the table picker. Use ONLY when the user asks to create or edit a table in an MS Teams message.
|
||||
---
|
||||
|
||||
# MS Teams tables
|
||||
|
||||
How to insert and fill tables in the MS Teams chat compose box via Chrome
|
||||
DevTools MCP. Verified empirically against the live Teams web app.
|
||||
|
||||
Teams' chat editor DOES support tables, but the "Insert table" button is
|
||||
NOT on the main toolbar. It is hidden behind an overflow "more options"
|
||||
button.
|
||||
|
||||
## Step 1 — open the table picker
|
||||
|
||||
1. Focus the editor.
|
||||
2. Open the formatting toolbar (`Ctrl+Shift+X` or click "Show Formatting
|
||||
options").
|
||||
3. In the main FluentUI toolbar (`.ms-FocusZone.ui-toolbar` with ~24
|
||||
buttons), the overflow button is at **index 18** — it has
|
||||
`aria-haspopup="dialog"` and NO `aria-label`. Click it via
|
||||
`evaluate_script`:
|
||||
```js
|
||||
() => {
|
||||
const tb = Array.from(document.querySelectorAll('.ms-FocusZone.ui-toolbar'))
|
||||
.find(t => t.querySelectorAll('button').length > 10);
|
||||
Array.from(tb.querySelectorAll('button'))[18].click();
|
||||
}
|
||||
```
|
||||
This opens the "Insert table" dialog.
|
||||
|
||||
## Step 2 — pick a size
|
||||
|
||||
The dialog presents a grid of preset buttons, `Insert N by M table` where
|
||||
N ∈ [1..7] (columns) and M ∈ [1..5] (rows). Click the desired size:
|
||||
```js
|
||||
() => {
|
||||
const btn = Array.from(document.querySelectorAll('button'))
|
||||
.find(b => b.getAttribute('aria-label') === 'Insert 3 by 3 table');
|
||||
btn.click();
|
||||
}
|
||||
```
|
||||
A real `<table>` with `<td>` cells is inserted.
|
||||
|
||||
## Step 3 — fill cells
|
||||
|
||||
The first cell is auto-focused after insert. Use `type_text` to fill it,
|
||||
then `Tab` to move to the next cell, and repeat:
|
||||
|
||||
```
|
||||
chrome-devtools_type_text text: "A1"
|
||||
chrome-devtools_press_key key: "Tab"
|
||||
chrome-devtools_type_text text: "B1"
|
||||
chrome-devtools_press_key key: "Tab"
|
||||
chrome-devtools_type_text text: "A2"
|
||||
chrome-devtools_press_key key: "Tab"
|
||||
chrome-devtools_type_text text: "B2"
|
||||
```
|
||||
|
||||
`Tab` navigates cells left-to-right, top-to-bottom. `Shift+Tab` reverses.
|
||||
|
||||
## Step 4 — modify an existing table
|
||||
|
||||
Once a table exists, the main toolbar reveals two table-specific buttons:
|
||||
- "Insert column or row" (`aria-haspopup="dialog"`)
|
||||
- "Delete column, row, or table" (`aria-haspopup="dialog"`)
|
||||
|
||||
Use `evaluate_script` to click these by `aria-label`.
|
||||
|
||||
## Verifying table state
|
||||
|
||||
```js
|
||||
() => {
|
||||
const e = document.querySelector('[role="textbox"]');
|
||||
const t = e.querySelector('table');
|
||||
return t ? {
|
||||
rows: t.rows.length,
|
||||
cols: t.rows[0].cells.length,
|
||||
cells: Array.from(t.querySelectorAll('td')).map(c => c.innerText.trim())
|
||||
} : 'no table';
|
||||
}
|
||||
```
|
||||
|
||||
## Do NOT
|
||||
|
||||
- Do not look for "Insert table" on the main formatting toolbar — it is
|
||||
hidden in the overflow button (index 18, no aria-label,
|
||||
`haspopup="dialog"`).
|
||||
- Do not use the "Actions and apps" menu for tables — that menu only
|
||||
contains "Set delivery options" and "Record video clip".
|
||||
65
.claude/skills/ms-teams-text-formatting/SKILL.md
Normal file
65
.claude/skills/ms-teams-text-formatting/SKILL.md
Normal file
@ -0,0 +1,65 @@
|
||||
---
|
||||
name: ms-teams-text-formatting
|
||||
description: Use when applying bold, italic, underline, strikethrough, lists, quote, code, or links to text in the MS Teams chat compose box via Chrome DevTools MCP. Use ONLY when the user asks to format text in an MS Teams message.
|
||||
---
|
||||
|
||||
# MS Teams text formatting
|
||||
|
||||
How to apply inline and block formatting to text in the MS Teams chat
|
||||
compose box via Chrome DevTools MCP. Verified empirically against the live
|
||||
Teams web app (CKEditor / FluentUI).
|
||||
|
||||
## Inline formatting (bold / italic / underline / strikethrough)
|
||||
|
||||
All four work via keyboard shortcuts, applied to the current selection.
|
||||
Type the text, select it (e.g. `Control+A` for all, or `Shift+Arrow` /
|
||||
double-click for a substring), then press the shortcut.
|
||||
|
||||
| Format | Shortcut | Resulting HTML |
|
||||
| --- | --- | --- |
|
||||
| Bold | `Ctrl+B` | `<strong>...</strong>` |
|
||||
| Italic | `Ctrl+I` | `<i>...</i>` |
|
||||
| Underline | `Ctrl+U` | `<u>...</u>` |
|
||||
| Strikethrough | `Ctrl+Alt+X` | `<s>...</s>` |
|
||||
|
||||
Formats nest cleanly. Pressing a shortcut on an already-formatted selection
|
||||
toggles that format off.
|
||||
|
||||
No need to open the formatting toolbar UI; the shortcuts work while the
|
||||
editor is focused, even with the toolbar collapsed.
|
||||
|
||||
## Block formatting (lists / quote / code / link)
|
||||
|
||||
| Block | Shortcut | Notes |
|
||||
| --- | --- | --- |
|
||||
| Bulleted list | `*` or `-` then space at line start | CKEditor auto-converts the leading marker. |
|
||||
| Numbered list | `1.` or `1)` then space at line start | Same auto-convert behavior. |
|
||||
| Quote | `Ctrl+Alt+4` | Toggles quote block on current paragraph. |
|
||||
| Code (inline) | `Ctrl+Alt+Shift+C` | Wraps selection in `<code>`. |
|
||||
| Code block | `Ctrl+Alt+Shift+B` | Inserts a `<pre><code>` block. |
|
||||
| Link | `Ctrl+K` | Opens link dialog (URL + optional text). |
|
||||
|
||||
## Toolbar-only formats (no shortcut)
|
||||
|
||||
These require opening the formatting toolbar and clicking the button:
|
||||
|
||||
| Action | Toolbar button aria-label |
|
||||
| --- | --- |
|
||||
| Text highlight color | "Text highlight color" |
|
||||
| Font color | "Font color" |
|
||||
| Font size | "Font size" |
|
||||
| Paragraph style | "Paragraph" |
|
||||
| Clear all formatting | "Clear all formatting" |
|
||||
| Decrease indent | "Decrease indent" |
|
||||
| Increase indent | "Increase indent" |
|
||||
| Insert horizontal rule | "Insert horizontal rule" |
|
||||
|
||||
Open the toolbar with `Ctrl+Shift+X` (or click "Show Formatting options"),
|
||||
then click via `evaluate_script`:
|
||||
```js
|
||||
() => {
|
||||
const btn = Array.from(document.querySelectorAll('button'))
|
||||
.find(b => b.getAttribute('aria-label') === 'Font color');
|
||||
btn.click();
|
||||
}
|
||||
```
|
||||
8
.mcp.json
Normal file
8
.mcp.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"mcpServers": {
|
||||
"chrome-devtools": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "chrome-devtools-mcp@latest", "--browser-url=http://127.0.0.1:9222"]
|
||||
}
|
||||
}
|
||||
}
|
||||
303
.opencode/agent/browser-butler.md
Normal file
303
.opencode/agent/browser-butler.md
Normal file
@ -0,0 +1,303 @@
|
||||
---
|
||||
description: Use proactively for ANY natural-language web browser task via the Chrome DevTools MCP — e.g. "watch X", "fill the form for me", "find a 5-star restaurant near me", "book tickets from A to B", "check what's in my cart on site Z". Decides whether a currently-open web app can satisfy the request, investigates the app's structure, and drives it end-to-end.
|
||||
temperature: 0.2
|
||||
permission:
|
||||
read: allow
|
||||
edit: deny
|
||||
glob: allow
|
||||
grep: allow
|
||||
list: deny
|
||||
bash: deny
|
||||
task: deny
|
||||
todowrite: deny
|
||||
question: allow
|
||||
webfetch: allow
|
||||
websearch: allow
|
||||
skill: allow
|
||||
doom_loop: deny
|
||||
external_directory: deny
|
||||
lsp: deny
|
||||
"chrome-devtools_*": allow
|
||||
---
|
||||
|
||||
You are **browser-butler**, a specialized opencode agent that fulfills the user's natural-language requests by driving a live web browser through the `chrome-devtools` MCP server. Your only job is to take a high-level intent and turn it into concrete browser actions that satisfy that intent.
|
||||
|
||||
## Hard tool restrictions
|
||||
|
||||
You may use ONLY the following tools. Any other tool is forbidden and there are no exceptions:
|
||||
|
||||
1. **`chrome-devtools_*`** — all tools from the Chrome DevTools MCP server (navigation, snapshots, clicks, fills, forms, screenshots, console reads,
|
||||
performance traces, heap snapshots, lighthouse, evaluate_script, etc.).
|
||||
2. **`websearch`** — for resolving unknowns the browser cannot answer itself.
|
||||
3. **`webfetch`** — to read a URL's content as markdown when a snapshot is
|
||||
overkill or the page is not already open in the browser.
|
||||
4. **`question`** — to ask the user for clarification when the request is
|
||||
genuinely ambiguous.
|
||||
5. **`skill`** — to load any opencode skill that is available.
|
||||
|
||||
Do NOT attempt to use `edit`, `bash`, `task`, `list`, `todowrite`, `lsp`. They are disabled at the
|
||||
permission layer. Treat filesystem operations as out of scope — if the user
|
||||
asks for one, tell them to use a different agent.
|
||||
|
||||
## Scope of requests
|
||||
|
||||
You handle ANY natural-language request that can be satisfied through a web
|
||||
browser. Examples (non-exhaustive — the request type is whatever the user says):
|
||||
|
||||
- **Watch / listen**: "I want to watch movie X", "play song Y", "find the
|
||||
latest match of team Z and open it".
|
||||
- **Search / discover**: "find a 5-star restaurant near me", "look up the
|
||||
best laptop under $1000", "what's the weather in Lisbon tomorrow".
|
||||
- **Transact / fill forms**: "fill the registration form for me", "book
|
||||
tickets from A to B for next Friday", "apply the promo code on my cart".
|
||||
- **Monitor / inspect**: "check what's in my cart on site X", "see if my
|
||||
order shipped", "what's playing on the tab I left open".
|
||||
- **Navigate / open**: "open my email inbox", "go to the dashboard of app X",
|
||||
"take me to today's commit log on repo Y".
|
||||
|
||||
Do not treat a request as out of scope just because it takes several steps, or because it is not a media-consumption task. Forms, bookings, dashboards, search results, sign-in flows, and multi-page workflows are all in scope.
|
||||
|
||||
## How to think about a request
|
||||
|
||||
Before acting, classify the request along two axes:
|
||||
|
||||
1. **Verb**: watch / listen / read / find / fill / submit / open / check / compare / monitor. The verb determines what "done" looks like.
|
||||
2. **Object**: a specific named item ("Scary Movie"), a query ("5-star restaurant near me"), a destination ("tickets from A to B"), or a state ("my cart", "the open tab"). The object determines where to look.
|
||||
|
||||
Your single most important job is **app investigation** — discovering which already-open web application can serve the request, and how to drive it.
|
||||
|
||||
### Step 1 — Inventory the running browser
|
||||
|
||||
Always start by calling `chrome-devtools_list_pages` to see what tabs are open right now. Each row gives a page title and URL. These are the web applications currently available to you — a streaming site, a search engine, a booking portal, a social feed, a dashboard, an email inbox, etc.
|
||||
|
||||
Treat the open tabs as your **working set of apps**. A request like "fill the form" is most naturally served by an already-open tab that shows that form; a search request is best served by a search engine tab or an app with its own search. Only open a new app if no existing tab is suitable.
|
||||
|
||||
### Step 2 — Decide: existing app, or new app?
|
||||
|
||||
For each open tab, ask yourself:
|
||||
|
||||
- Does this app's domain serve the *kind* of action the user wants?
|
||||
(A video site serves "watch"; a travel site serves "book tickets"; a food
|
||||
delivery app serves "order"; a search engine serves everything but is one
|
||||
step further from the result.)
|
||||
- Is it likely to *have* or *be able to reach* the specific item or form the
|
||||
user named?
|
||||
- Is it already in a usable state (logged in, not a paywall, not a 404, not a
|
||||
modal blocking the view)?
|
||||
|
||||
If at least one open tab is a good fit, **prefer it** — do not open new tabs duplicatively. If none fit, open a new tab with `chrome-devtools_new_page` to
|
||||
the most likely app, or navigate an existing unrelated tab with `chrome-devtools_navigate_page` (ask the user first before clobbering a tab they may care about).
|
||||
|
||||
If you cannot decide whether an app is suitable from the URL alone, **investigate the app** (see Step 3) before concluding it does not fit. Do not assume — verify.
|
||||
|
||||
### Step 3 — Investigate the chosen app
|
||||
|
||||
Apps don't expose their content or their controls via URL alone. To discover how to satisfy the request inside an app, investigate its structure:
|
||||
|
||||
1. `chrome-devtools_select_page` to make the candidate tab the active context.
|
||||
2. `chrome-devtools_take_snapshot` to read the page's accessibility tree. This reveals: search boxes, nav links, category links, form fields, buttons,
|
||||
content listings, pagination, login walls, region restrictions, cart state, etc.
|
||||
3. If the snapshot is ambiguous (SPA, lazy-loaded, behind a login, dynamic
|
||||
content), use `chrome-devtools_evaluate_script` to query the DOM for the
|
||||
data you need — e.g. read form field values, list all `<a>` hrefs, check
|
||||
whether a "Sign in" button is present, read a cart badge count, extract a
|
||||
table of results as JSON.
|
||||
4. Drive the app toward the goal:
|
||||
- For a **named-item search**: locate the search input in the snapshot,
|
||||
`chrome-devtools_fill` it with a derived query, and submit (Enter via
|
||||
`chrome-devtools_press_key`, or click the search button).
|
||||
- For a **form to fill**: read every field's label and type from the
|
||||
snapshot, derive each value from the user's request or sensible defaults,
|
||||
and use `chrome-devtools_fill_form` to fill them all at once. Do not
|
||||
submit a form unless the user asked you to — many forms are
|
||||
irreversible (payments, bookings, deletions). Ask first.
|
||||
- For a **navigation task** ("go to my dashboard", "open my inbox"):
|
||||
either click the relevant nav link from the snapshot, or
|
||||
`chrome-devtools_navigate_page` directly to the known URL.
|
||||
- For a **state check** ("what's in my cart", "did my order ship"):
|
||||
snapshot the page and read the relevant region; use `evaluate_script`
|
||||
only if the data is not in the accessibility tree.
|
||||
- For a **comparison / research task**: snapshot each source, extract the
|
||||
relevant fields with `evaluate_script` if needed, and reason over them.
|
||||
|
||||
When you derive a search query, translate the user's request into the language
|
||||
and idiom of the app. For a Russian video site, search in Russian. For a
|
||||
French travel site, search in French. For a form field labeled "Откуда" on a
|
||||
Russian rail booking site, fill it with the city name in Russian. Keep queries
|
||||
and field values short, literal, and in the app's working language; expand
|
||||
abbreviations only when the abbreviation is unlikely to be indexed.
|
||||
|
||||
### Step 4 — Rank and select results inside the app
|
||||
|
||||
After searching inside an app, the snapshot will contain a list of result
|
||||
links, cards, or rows. Rank them against the user's request using these
|
||||
signals, in order, and pick the best one to open:
|
||||
|
||||
1. **Title / label match** — literal token overlap with the request is the
|
||||
strongest signal. Prefer exact matches; demote partial matches.
|
||||
2. **Recency** — for "latest" / "new" / "recent" / "today" requests, read the
|
||||
date stamp on each result. Prefer the newest. If the app sorts by relevance
|
||||
rather than date, look for a "sort by date" control and use it.
|
||||
3. **Completeness vs. excerpt** — for "watch the match" prefer the full-length
|
||||
broadcast over a 10-minute highlight; for "read the article" prefer the full
|
||||
text over a summary; for "book the ticket" prefer the direct booking link
|
||||
over an aggregator's landing page.
|
||||
4. **Constraints from the request** — stars (5-star restaurant), price
|
||||
(under $1000), distance (near me), availability (for next Friday). Apply
|
||||
these as filters when the app supports it; otherwise read them off each
|
||||
result and reject non-matching ones.
|
||||
5. **View count / popularity / rating** — only as a tiebreaker when other
|
||||
signals are equal.
|
||||
6. **Description link-out** — some apps put the canonical full item in the
|
||||
description of a preview (e.g. "full transmission at <url>"). Prefer that
|
||||
canonical URL over the preview when present.
|
||||
|
||||
### Step 5 — Perform the action and confirm
|
||||
|
||||
Once you have ranked results (or identified the target form / page), act:
|
||||
|
||||
- **To open an item**: `chrome-devtools_click` on the result link (stays in
|
||||
the same tab), or `chrome-devtools_navigate_page` to its URL directly.
|
||||
- **To fill a form**: use `chrome-devtools_fill_form` with all fields at once
|
||||
when possible. Prefer it over sequential `fill` calls — it is faster and
|
||||
more reliable. Stop before the final submit unless the user explicitly said
|
||||
to submit; tell them the form is filled and ready to review.
|
||||
- **To submit a non-destructive action** (search, filter, "load more"):
|
||||
submit freely.
|
||||
- **To submit a destructive / irreversible action** (payment, booking
|
||||
confirmation, deletion, sending a message): do NOT submit unless the user
|
||||
explicitly asked you to. Show them the final state with a snapshot and ask
|
||||
for confirmation via `question`.
|
||||
|
||||
Then confirm the page actually satisfies the request:
|
||||
|
||||
1. `chrome-devtools_take_snapshot` of the destination page. Check the title,
|
||||
the content, any error / paywall / region / login prompts.
|
||||
2. If the page needs a user gesture to start (common on video sites), do NOT
|
||||
press play unless the user asked for autoplay — many sites block autoplay
|
||||
and a failed click looks like a bug. Report that the item is loaded and
|
||||
ready, and let the user press play.
|
||||
3. If the page hits a paywall or region block you cannot bypass, do not try to
|
||||
bypass it — tell the user and offer the next-best result from Step 4.
|
||||
4. For form submissions, snapshot the post-submit page and confirm the
|
||||
expected outcome (success message, confirmation number, next step). If the
|
||||
outcome is an error, read the error from the snapshot and either correct
|
||||
the form or report the error to the user.
|
||||
|
||||
### Step 6 — Report back
|
||||
|
||||
Report concisely: what you did, on which app, and the direct URL or final
|
||||
state. If you made a judgment call (e.g. the user asked for "the latest match
|
||||
of team X" but the latest one is not yet on the platform, so you opened the
|
||||
most recent one that was indexed), say so explicitly so the user can correct
|
||||
you. If you filled a form but stopped short of submitting, say so and tell the
|
||||
user what the submit button is.
|
||||
|
||||
Never claim a result you did not verify. If you navigated but did not snapshot
|
||||
the landing page, you do not know what is there — snapshot first, then report.
|
||||
|
||||
## When to ask the user a question
|
||||
|
||||
Use `question` only when the request is genuinely ambiguous and a reasonable
|
||||
default would risk doing the wrong thing. Examples:
|
||||
|
||||
- "watch X" where X is a franchise with multiple films and the user did not
|
||||
specify which — offer the choices with a `question` before opening.
|
||||
- "book tickets from A to B" without a date — ask for the date.
|
||||
- "fill the form for me" without the field values — ask which fields to fill,
|
||||
or ask for the values you cannot infer.
|
||||
- A destructive action (payment, deletion, send) is one click away — confirm
|
||||
before submitting.
|
||||
|
||||
Do NOT ask a question for things you can reasonably infer: if the user says
|
||||
"watch Сокровище Нации" and only one full film with that title exists, open it.
|
||||
If a form field is obviously derivable (today's date, a default city, the
|
||||
named item in the request), fill it.
|
||||
|
||||
## When to use websearch vs. in-app search
|
||||
|
||||
Prefer **in-app search / navigation** when the request names a specific item
|
||||
and the app is already open and suitable — it is faster and respects the
|
||||
app's catalog, region, login state, and filters.
|
||||
|
||||
Prefer **websearch** when:
|
||||
|
||||
- The user's request is vague and you need to disambiguate ("that song from
|
||||
the movie with the guy" — find the song title first, then drive the music
|
||||
app).
|
||||
- You need to know whether an item exists on a given app before committing to
|
||||
navigating there ("is Сокровище Нации on rutube?").
|
||||
- The latest information matters (today's match result, a news headline, a
|
||||
current price) and the app's own search is not date-aware.
|
||||
- The user is asking a factual question ("what's the capital of X",
|
||||
"who won the match today") that a search engine answers directly.
|
||||
|
||||
Use `webfetch` only to read a specific URL's content as markdown — for
|
||||
example, to read a reference doc when you need to recall exact API parameters,
|
||||
or to read a search-result page's content without opening it in the browser.
|
||||
Do not use `webfetch` as a search engine; that is what `websearch` is for.
|
||||
|
||||
## Concurrency and ordering
|
||||
|
||||
You may send multiple independent tool calls in a single message, but respect
|
||||
ordering when there is a dependency:
|
||||
|
||||
navigate → wait_for → take_snapshot → click/fill → take_snapshot
|
||||
|
||||
For example, you may `list_pages` and `take_snapshot` in parallel only if the
|
||||
page you want to snapshot is already selected; otherwise select first.
|
||||
|
||||
Never `click` or `fill` an element whose `uid` you got from a stale snapshot
|
||||
— if you navigated, reloaded, or submitted a form since the snapshot, take a
|
||||
fresh one first. UIDs are invalidated by every navigation or DOM mutation.
|
||||
|
||||
## What "complex" means
|
||||
|
||||
A request is complex when it requires more than a single navigation. Examples
|
||||
of complex requests you should handle end-to-end without asking the user to
|
||||
intervene:
|
||||
|
||||
- "find a 5-star restaurant near me and open its menu" — inventory → app
|
||||
choice → in-app search → apply star filter → open result → snapshot menu.
|
||||
- "book train tickets from Moscow to St. Petersburg for next Friday" —
|
||||
inventory → app choice → fill From/To/Date fields → submit search → rank
|
||||
results by time or price → stop before payment, ask for confirmation.
|
||||
- "fill the registration form on the open tab" — snapshot → read each field
|
||||
label and type → derive values → `fill_form` → stop before submit, report.
|
||||
- "check what's in my cart on site X and apply promo code Y" — select tab →
|
||||
snapshot cart → read contents → locate promo field → fill → submit →
|
||||
confirm new total.
|
||||
- "compare the price of item Z across the three open shopping tabs" —
|
||||
snapshot each tab → extract price via evaluate_script if needed → report
|
||||
the comparison.
|
||||
|
||||
Do not treat a request as out of scope just because it takes several steps, or
|
||||
because it is not a media-consumption task. The whole point of this agent is
|
||||
to absorb that complexity across any kind of web task.
|
||||
|
||||
## Failure modes to avoid
|
||||
|
||||
- **Do not** open a new tab duplicating an app that is already open. Reuse it.
|
||||
- **Do not** click "Play" on a video site unless the user asked for autoplay.
|
||||
- **Do not** submit a form unless the user asked you to — fill, then stop and
|
||||
report. The same applies to payments, bookings, deletions, and sends.
|
||||
- **Do not** attempt to bypass logins, paywalls, CAPTCHAs, or region blocks.
|
||||
Report them and offer the next-best option.
|
||||
- **Do not** report success without snapshotting the final page to confirm.
|
||||
- **Do not** use `evaluate_script` to scrape a page that the snapshot already
|
||||
exposes — snapshots are faster, cheaper, and more robust.
|
||||
- **Do not** open a file or filesystem path. If the user asks for that, tell
|
||||
them to use a different agent — you have no filesystem tools and pretending
|
||||
otherwise will waste their time.
|
||||
- **Do not** guess a URL. Either derive it from a snapshot, a websearch
|
||||
result, a webfetch, or the app's own navigation. Guessing leads to 404s.
|
||||
- **Do not** assume an app's language. Check the snapshot for the working
|
||||
language and fill forms / search in that language.
|
||||
|
||||
## Remember
|
||||
|
||||
You are the butler, not the user. The user states an intent — any web intent:
|
||||
watch, find, fill, book, open, check, compare. You decide which open web app
|
||||
can serve it, investigate that app, drive it toward the goal, stop before
|
||||
irreversible actions unless told otherwise, and confirm the result. The user
|
||||
should never have to touch the browser themselves.
|
||||
9
.opencode/opencode.json
Normal file
9
.opencode/opencode.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"mcp": {
|
||||
"chrome-devtools": {
|
||||
"type": "local",
|
||||
"command": ["npx", "-y", "chrome-devtools-mcp@latest", "--browser-url=http://127.0.0.1:9222"]
|
||||
}
|
||||
}
|
||||
}
|
||||
95
.opencode/skills/ms-teams-multiline-typing/SKILL.md
Normal file
95
.opencode/skills/ms-teams-multiline-typing/SKILL.md
Normal file
@ -0,0 +1,95 @@
|
||||
---
|
||||
name: ms-teams-multiline-typing
|
||||
description: Use when typing a multiline message into the MS Teams chat compose box via Chrome DevTools MCP. Covers the Shift+Enter newline pattern and why literal \n fails. Use ONLY when the user asks to type or compose a multi-paragraph message in MS Teams.
|
||||
---
|
||||
|
||||
# MS Teams multiline typing
|
||||
|
||||
How to insert line breaks when composing a message in Microsoft Teams via
|
||||
Chrome DevTools MCP. Verified empirically against the live Teams web app.
|
||||
|
||||
## The input element
|
||||
|
||||
- `<div role="textbox" contenteditable="true">` — a CKEditor instance
|
||||
wrapped in FluentUI. State lives in CKEditor's model, not the DOM.
|
||||
- Exposed in the a11y snapshot as `textbox "Type a message" multiline`.
|
||||
|
||||
## Plain text (single line)
|
||||
|
||||
```
|
||||
chrome-devtools_type_text text: "Hello"
|
||||
```
|
||||
|
||||
Works as expected — text is inserted into the current `<p>`.
|
||||
|
||||
## Multiline text — REQUIRED pattern
|
||||
|
||||
CKEditor **rejects** raw `\n` characters. A single `type_text` call with an
|
||||
embedded newline silently drops the **entire** input (not just the
|
||||
newline); the editor stays empty. Confirmed empirically:
|
||||
|
||||
| Input method | Result |
|
||||
| --- | --- |
|
||||
| `type_text "A\nB\nC"` | editor empty, nothing inserted |
|
||||
| `type_text "A"` then `Shift+Enter` then `type_text "B"` | two `<p>` blocks inserted |
|
||||
|
||||
Therefore multiline messages MUST be built by alternating `type_text` and
|
||||
`Shift+Enter`:
|
||||
|
||||
```
|
||||
chrome-devtools_type_text text: "Line 1"
|
||||
chrome-devtools_press_key key: "Shift+Enter"
|
||||
chrome-devtools_type_text text: "Line 2"
|
||||
chrome-devtools_press_key key: "Shift+Enter"
|
||||
chrome-devtools_type_text text: "Line 3"
|
||||
```
|
||||
|
||||
Each `Shift+Enter` dispatches a real `KeyboardEvent('keydown', {key:
|
||||
'Enter', shiftKey: true})`, which CKEditor's keydown handler intercepts
|
||||
to execute `enterShift` and insert a new `<p>` into the model.
|
||||
|
||||
## Why `\n` fails (mechanism)
|
||||
|
||||
CKEditor routes text insertion through its model via `beforeinput`/`input`
|
||||
events. A raw `\n` does not map to any `inputType` (`insertParagraph` and
|
||||
`insertLineBreak` are the valid ones, both triggered by keystrokes, not by
|
||||
literal newline chars). When the handler can't route the input, the entire
|
||||
insert is rejected — not partially applied. This is stricter than a plain
|
||||
`<textarea>`, where `fill` with `\n` works fine.
|
||||
|
||||
## Verifying the draft
|
||||
|
||||
```js
|
||||
() => {
|
||||
const e = document.querySelector('[role="textbox"]');
|
||||
return { innerText: e.innerText, pCount: e.querySelectorAll('p').length };
|
||||
}
|
||||
```
|
||||
|
||||
The a11y snapshot will also show `paragraph` children with `LineBreak`
|
||||
nodes between them.
|
||||
|
||||
## Do NOT
|
||||
|
||||
- Do not use `chrome-devtools_fill` with `\n` on the Teams editor — it
|
||||
fails silently for the same reason as `type_text` with `\n`.
|
||||
- Do not mutate `innerHTML` directly — Teams enforces TrustedHTML policy
|
||||
and the assignment throws. Even if it didn't, React/CKEditor state would
|
||||
desync from the DOM and the Send button would stay disabled.
|
||||
|
||||
## Generalizing to other chat apps
|
||||
|
||||
Same `type_text` + `Shift+Enter` pattern works for Slack, WhatsApp Web,
|
||||
and Gosuslugi chat — they are all contenteditable under the hood. What
|
||||
differs:
|
||||
|
||||
| App | Newline shortcut | Send shortcut |
|
||||
| --- | --- | --- |
|
||||
| MS Teams | `Shift+Enter` | `Ctrl+Enter` (or click Send) |
|
||||
| Slack | `Shift+Enter` | `Enter` (configurable) |
|
||||
| WhatsApp Web | `Shift+Enter` | `Enter` |
|
||||
| Gosuslugi | `Enter` (if `<textarea>`) / `Shift+Enter` (if contenteditable) | Send button click |
|
||||
|
||||
Always probe the element type first: a true `<textarea>` accepts `\n` in
|
||||
`fill` directly and needs no `Shift+Enter`; a `contenteditable` requires
|
||||
the keystroke approach.
|
||||
37
.opencode/skills/ms-teams-send-message/SKILL.md
Normal file
37
.opencode/skills/ms-teams-send-message/SKILL.md
Normal file
@ -0,0 +1,37 @@
|
||||
---
|
||||
name: ms-teams-send-message
|
||||
description: Use when sending a composed message in MS Teams chat via Chrome DevTools MCP, or when locating/opening a specific chat. Covers the Ctrl+Enter send shortcut and delivery confirmation. Use ONLY when the user asks to send a message or open a chat in MS Teams.
|
||||
---
|
||||
|
||||
# MS Teams send message
|
||||
|
||||
How to locate a chat and send a composed message in Microsoft Teams via
|
||||
Chrome DevTools MCP. Verified empirically against the live Teams web app.
|
||||
|
||||
## Locating a chat
|
||||
|
||||
1. `chrome-devtools_list_pages` to find the Teams tab.
|
||||
2. `chrome-devtools_select_page` to focus it.
|
||||
3. `chrome-devtools_take_snapshot` to read the left chat list (a11y tree).
|
||||
4. Click the target `treeitem` (e.g. "Group chat [Idea Mining] PM"). The
|
||||
chat opens and the editor autofocuses.
|
||||
|
||||
## Sending
|
||||
|
||||
- Click the "Send (Ctrl+Enter)" button exposed in the snapshot, OR
|
||||
- `chrome-devtools_press_key key: "Ctrl+Enter"`.
|
||||
|
||||
Do not press `Enter` alone — in Teams it is remapped to send only when
|
||||
the "send on Enter" setting is off; the default for group chats is
|
||||
`Ctrl+Enter` to send and `Enter` for newline. `Shift+Enter` always
|
||||
inserts a line break regardless of that setting.
|
||||
|
||||
After sending, confirm delivery by waiting for the message to appear in
|
||||
the transcript (e.g. `chrome-devtools_wait_for text: ["<your
|
||||
substring>"]`). Teams also announces "Message sent." via an `atomic live`
|
||||
region.
|
||||
|
||||
## Cleanup
|
||||
|
||||
If a test draft is unwanted, select all (`Control+A`) and `Delete` before
|
||||
navigating away. The editor resets to the empty placeholder.
|
||||
98
.opencode/skills/ms-teams-tables/SKILL.md
Normal file
98
.opencode/skills/ms-teams-tables/SKILL.md
Normal file
@ -0,0 +1,98 @@
|
||||
---
|
||||
name: ms-teams-tables
|
||||
description: Use when inserting, filling, or modifying a table in the MS Teams chat compose box via Chrome DevTools MCP. Covers the hidden overflow button that opens the table picker. Use ONLY when the user asks to create or edit a table in an MS Teams message.
|
||||
---
|
||||
|
||||
# MS Teams tables
|
||||
|
||||
How to insert and fill tables in the MS Teams chat compose box via Chrome
|
||||
DevTools MCP. Verified empirically against the live Teams web app.
|
||||
|
||||
Teams' chat editor DOES support tables, but the "Insert table" button is
|
||||
NOT on the main toolbar. It is hidden behind an overflow "more options"
|
||||
button.
|
||||
|
||||
## Step 1 — open the table picker
|
||||
|
||||
1. Focus the editor.
|
||||
2. Open the formatting toolbar (`Ctrl+Shift+X` or click "Show Formatting
|
||||
options").
|
||||
3. In the main FluentUI toolbar (`.ms-FocusZone.ui-toolbar` with ~24
|
||||
buttons), the overflow button is at **index 18** — it has
|
||||
`aria-haspopup="dialog"` and NO `aria-label`. Click it via
|
||||
`evaluate_script`:
|
||||
```js
|
||||
() => {
|
||||
const tb = Array.from(document.querySelectorAll('.ms-FocusZone.ui-toolbar'))
|
||||
.find(t => t.querySelectorAll('button').length > 10);
|
||||
Array.from(tb.querySelectorAll('button'))[18].click();
|
||||
}
|
||||
```
|
||||
This opens the "Insert table" dialog.
|
||||
|
||||
Alternatively, once the dialog is open, locate any button whose
|
||||
`aria-label` matches `/^Insert \d+ by \d+ table$/`.
|
||||
|
||||
## Step 2 — pick a size
|
||||
|
||||
The dialog presents a grid of preset buttons, `Insert N by M table` where
|
||||
N ∈ [1..7] (columns) and M ∈ [1..5] (rows), plus an "Allow edits"
|
||||
checkbox. Click the desired size:
|
||||
```js
|
||||
() => {
|
||||
const btn = Array.from(document.querySelectorAll('button'))
|
||||
.find(b => b.getAttribute('aria-label') === 'Insert 3 by 3 table');
|
||||
btn.click();
|
||||
}
|
||||
```
|
||||
A real `<table>` with `<td>` cells is inserted. Each cell is its own
|
||||
`contenteditable` nested editable.
|
||||
|
||||
## Step 3 — fill cells
|
||||
|
||||
The first cell is auto-focused after insert. Use `type_text` to fill it,
|
||||
then `Tab` to move to the next cell, and repeat:
|
||||
|
||||
```
|
||||
chrome-devtools_type_text text: "A1"
|
||||
chrome-devtools_press_key key: "Tab"
|
||||
chrome-devtools_type_text text: "B1"
|
||||
chrome-devtools_press_key key: "Tab"
|
||||
chrome-devtools_type_text text: "A2"
|
||||
chrome-devtools_press_key key: "Tab"
|
||||
chrome-devtools_type_text text: "B2"
|
||||
```
|
||||
|
||||
`Tab` navigates cells left-to-right, top-to-bottom. `Shift+Tab` reverses.
|
||||
Inline formatting shortcuts (`Ctrl+B` etc.) work inside cells.
|
||||
|
||||
## Step 4 — modify an existing table
|
||||
|
||||
Once a table exists, the main toolbar reveals two table-specific buttons:
|
||||
- "Insert column or row" (`aria-haspopup="dialog"`)
|
||||
- "Delete column, row, or table" (`aria-haspopup="dialog"`)
|
||||
|
||||
Use `evaluate_script` to click these by `aria-label`. They open their own
|
||||
dialogs with row/column insert/delete options.
|
||||
|
||||
## Verifying table state
|
||||
|
||||
```js
|
||||
() => {
|
||||
const e = document.querySelector('[role="textbox"]');
|
||||
const t = e.querySelector('table');
|
||||
return t ? {
|
||||
rows: t.rows.length,
|
||||
cols: t.rows[0].cells.length,
|
||||
cells: Array.from(t.querySelectorAll('td')).map(c => c.innerText.trim())
|
||||
} : 'no table';
|
||||
}
|
||||
```
|
||||
|
||||
## Do NOT
|
||||
|
||||
- Do not look for "Insert table" on the main formatting toolbar — it is
|
||||
hidden in the overflow button (index 18, no aria-label,
|
||||
`haspopup="dialog"`).
|
||||
- Do not use the "Actions and apps" menu for tables — that menu only
|
||||
contains "Set delivery options" and "Record video clip".
|
||||
68
.opencode/skills/ms-teams-text-formatting/SKILL.md
Normal file
68
.opencode/skills/ms-teams-text-formatting/SKILL.md
Normal file
@ -0,0 +1,68 @@
|
||||
---
|
||||
name: ms-teams-text-formatting
|
||||
description: Use when applying bold, italic, underline, strikethrough, lists, quote, code, or links to text in the MS Teams chat compose box via Chrome DevTools MCP. Use ONLY when the user asks to format text in an MS Teams message.
|
||||
---
|
||||
|
||||
# MS Teams text formatting
|
||||
|
||||
How to apply inline and block formatting to text in the MS Teams chat
|
||||
compose box via Chrome DevTools MCP. Verified empirically against the live
|
||||
Teams web app (CKEditor / FluentUI).
|
||||
|
||||
## Inline formatting (bold / italic / underline / strikethrough)
|
||||
|
||||
All four work via keyboard shortcuts, applied to the current selection.
|
||||
Type the text, select it (e.g. `Control+A` for all, or `Shift+Arrow` /
|
||||
double-click for a substring), then press the shortcut.
|
||||
|
||||
| Format | Shortcut | Resulting HTML |
|
||||
| --- | --- | --- |
|
||||
| Bold | `Ctrl+B` | `<strong>...</strong>` |
|
||||
| Italic | `Ctrl+I` | `<i>...</i>` |
|
||||
| Underline | `Ctrl+U` | `<u>...</u>` |
|
||||
| Strikethrough | `Ctrl+Alt+X` | `<s>...</s>` |
|
||||
|
||||
Formats nest cleanly — e.g. `Ctrl+B` then `Ctrl+I` on the same selection
|
||||
yields `<i><strong>text</strong></i>`. Pressing a shortcut on an already-
|
||||
formatted selection toggles that format off.
|
||||
|
||||
No need to open the formatting toolbar UI; the shortcuts work while the
|
||||
editor is focused, even with the toolbar collapsed.
|
||||
|
||||
## Block formatting (lists / quote / code / link)
|
||||
|
||||
Also driven by shortcuts, no toolbar interaction required:
|
||||
|
||||
| Block | Shortcut | Notes |
|
||||
| --- | --- | --- |
|
||||
| Bulleted list | `*` or `-` then space at line start | CKEditor auto-converts the leading marker. |
|
||||
| Numbered list | `1.` or `1)` then space at line start | Same auto-convert behavior. |
|
||||
| Quote | `Ctrl+Alt+4` | Toggles quote block on current paragraph. |
|
||||
| Code (inline) | `Ctrl+Alt+Shift+C` | Wraps selection in `<code>`. |
|
||||
| Code block | `Ctrl+Alt+Shift+B` | Inserts a `<pre><code>` block. |
|
||||
| Link | `Ctrl+K` | Opens link dialog (URL + optional text). |
|
||||
|
||||
## Toolbar-only formats (no shortcut)
|
||||
|
||||
These require opening the formatting toolbar and clicking the button:
|
||||
|
||||
| Action | Toolbar button aria-label |
|
||||
| --- | --- |
|
||||
| Text highlight color | "Text highlight color" |
|
||||
| Font color | "Font color" |
|
||||
| Font size | "Font size" |
|
||||
| Paragraph style | "Paragraph" |
|
||||
| Clear all formatting | "Clear all formatting" |
|
||||
| Decrease indent | "Decrease indent" |
|
||||
| Increase indent | "Increase indent" |
|
||||
| Insert horizontal rule | "Insert horizontal rule" |
|
||||
|
||||
Open the toolbar with `Ctrl+Shift+X` (or click "Show Formatting options"),
|
||||
then click via `evaluate_script`:
|
||||
```js
|
||||
() => {
|
||||
const btn = Array.from(document.querySelectorAll('button'))
|
||||
.find(b => b.getAttribute('aria-label') === 'Font color');
|
||||
btn.click();
|
||||
}
|
||||
```
|
||||
Loading…
x
Reference in New Issue
Block a user