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
38 changes: 38 additions & 0 deletions test/unit/webview/mode-icons.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { describe, it, expect } from "vitest";
import { readFileSync } from "node:fs";
import path from "node:path";

/**
* The chat UI's mode picker (agent / ask / manual / plan / composer) must
* use standard VS Code codicon glyphs, not emoji. Emoji render differently
* across platforms and never match the editor theme's icon font.
*/
const ROOT = path.resolve(__dirname, "../../..");
const MAIN_JS = readFileSync(
path.join(ROOT, "webview-ui/static/main.js"),
"utf8",
);

const MODE_SECTION = MAIN_JS.slice(
MAIN_JS.indexOf("const modeIcons"),
MAIN_JS.indexOf("const modeDescs"),
);

describe("mode picker icons use standard codicons", () => {
it("maps every mode to a codicon name (no emoji)", () => {
const names = ["agent", "ask", "manual", "plan", "composer"];
for (const mode of names) {
expect(MODE_SECTION, `mode '${mode}' must have a codicon entry`).toMatch(
new RegExp(`${mode}:\\s*'[a-z-]+'`),
);
}
});

it("does not use emoji glyphs in the mode icon map", () => {
expect(MODE_SECTION).not.toMatch(/[\u{1F300}-\u{1FAFF}]/u);
});

it("renders mode icons through the codicon() helper", () => {
expect(MAIN_JS).toMatch(/codicon\(modeIcons\[m\] \|\| 'circuit-board'\)/);
});
});
37 changes: 27 additions & 10 deletions webview-ui/static/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -673,15 +673,30 @@
const bottomBar = el('div', { class: 'bottom-bar' });

// Mode picker β€” styled popup replacing native <select>.
const modeIcons = { agent: 'βš™', ask: 'πŸ’¬', manual: 'πŸ›‘', plan: 'πŸ“‹', composer: '🎼' };
// Standard VS Code codicons (not emoji) so they match the theme's icon font.
const modeIcons = {
agent: 'circuit-board',
ask: 'comment-discussion',
manual: 'shield',
plan: 'list-unordered',
composer: 'multiple-windows',
};
const modeDescs = {
agent: 'πŸ”΄ Autonomous β€” reads & edits files, runs commands',
ask: '🟒 Read-only β€” answers questions, never edits files',
manual: '🟑 Step-by-step β€” asks approval before every action',
plan: '🟑 Plan only β€” researches and proposes, no file changes',
composer: 'πŸ”΄ Multi-file β€” edits multiple files with diff review',
agent: 'Autonomous β€” reads & edits files, runs commands',
ask: 'Read-only β€” answers questions, never edits files',
manual: 'Step-by-step β€” asks approval before every action',
plan: 'Plan only β€” researches and proposes, no file changes',
composer: 'Multi-file β€” edits multiple files with diff review',
};
const modePickerBtn = el('button', { class: 'mode-picker-btn' }, [`${modeIcons[state.mode] || 'βš™'} ${state.mode.charAt(0).toUpperCase() + state.mode.slice(1)} β–Ύ`]);
/** Label for a mode: codicon glyph + capitalized name + caret. */
function modeLabel(m) {
const span = el('span', { class: 'mode-btn-label' });
span.append(codicon(modeIcons[m] || 'circuit-board'));
span.append(document.createTextNode(` ${m.charAt(0).toUpperCase() + m.slice(1)} β–Ύ`));
return span;
}
const modePickerBtn = el('button', { class: 'mode-picker-btn' });
modePickerBtn.append(modeLabel(state.mode));
const modePickerPopup = el('div', { class: 'mode-picker-popup', hidden: 'true' });
modePickerBtn.addEventListener('click', (ev) => {
ev.stopPropagation();
Expand All @@ -693,7 +708,7 @@
modeTooltipShown = true;
sessionStorage.setItem('champ-mode-tip', 'true');
const tip = el('div', { class: 'mode-tip' }, [
'🟒 Ask = read-only safe 🟑 Plan/Manual = supervised πŸ”΄ Agent/Composer = edits files'
'Ask = read-only safe Plan/Manual = supervised Agent/Composer = edits files'
]);
modePickerPopup.insertBefore(tip, modePickerPopup.firstChild);
}
Expand All @@ -709,7 +724,8 @@
modePickerPopup.innerHTML = '';
for (const m of ['agent', 'ask', 'manual', 'plan', 'composer']) {
const row = el('div', { class: `mode-row${m === state.mode ? ' active' : ''}` });
const icon = el('span', { class: 'mode-icon' }, [modeIcons[m] || '']);
const icon = el('span', { class: 'mode-icon' });
icon.append(codicon(modeIcons[m] || 'circuit-board'));
const textCol = el('div', { class: 'mode-text' });
const nameEl = el('span', { class: 'mode-name' }, [m.charAt(0).toUpperCase() + m.slice(1)]);
const descEl = el('span', { class: 'mode-desc' }, [modeDescs[m] || '']);
Expand All @@ -723,7 +739,8 @@
row.addEventListener('click', () => {
state.mode = m;
vscode.postMessage({ type: 'setMode', mode: m });
modePickerBtn.textContent = `${modeIcons[m]} ${m.charAt(0).toUpperCase() + m.slice(1)} β–Ύ`;
modePickerBtn.textContent = '';
modePickerBtn.append(modeLabel(m));
modePickerPopup.setAttribute('hidden', 'true');
if (state.messages.length === 0) renderEmptyState();
});
Expand Down
Loading