From bf414d4583bf51ba320213fb7ee8e329419d8ec9 Mon Sep 17 00:00:00 2001 From: Giuseppe Cianci Date: Fri, 24 Apr 2026 11:03:34 +0200 Subject: [PATCH] issue reporter: use context menu for delay dropdown, fix validation - Replace custom delay dropdown with IContextMenuService.showContextMenu() using proper Action.checked for selected delay indicator - Anchor dropdown to floating bar for correct positioning - Dispatch synthetic mousedown on drag handle pointerdown to close the context menu (real mousedown suppressed by preventDefault) - Add CSS rule for .wizard-describe-content.invalid-input to show red borders on child textareas in step 2 validation - Remove old custom delay menu CSS (.wizard-delay-menu etc.) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .agents/skills/launch/SKILL.md | 374 ------ .vscode/tasks.json | 9 +- .../issue/browser/issueReporterEditorPane.ts | 7 +- .../issue/browser/issueReporterOverlay.ts | 1002 ++++++++--------- .../browser/media/issueReporterOverlay.css | 256 ++--- 5 files changed, 570 insertions(+), 1078 deletions(-) delete mode 100644 .agents/skills/launch/SKILL.md diff --git a/.agents/skills/launch/SKILL.md b/.agents/skills/launch/SKILL.md deleted file mode 100644 index 2b564d812bb..00000000000 --- a/.agents/skills/launch/SKILL.md +++ /dev/null @@ -1,374 +0,0 @@ ---- -name: launch -description: "Launch and automate VS Code (Code OSS) using agent-browser via Chrome DevTools Protocol. Use when you need to interact with the VS Code UI, automate the chat panel, test UI features, or take screenshots of VS Code. Triggers include 'automate VS Code', 'interact with chat', 'test the UI', 'take a screenshot', 'launch Code OSS with debugging'." -metadata: - allowed-tools: Bash(agent-browser:*), Bash(npx agent-browser:*) ---- - -# VS Code Automation - -Automate VS Code (Code OSS) using agent-browser. VS Code is built on Electron/Chromium and exposes a Chrome DevTools Protocol (CDP) port that agent-browser can connect to, enabling the same snapshot-interact workflow used for web pages. - -## Prerequisites - -- **`agent-browser` must be installed.** It's listed in devDependencies โ€” run `npm install` in the repo root. Use `npx agent-browser` if it's not on your PATH, or install globally with `npm install -g agent-browser`. -- **For Code OSS (VS Code dev build):** The repo must be built before launching. `./scripts/code.sh` runs the build automatically if needed, or set `VSCODE_SKIP_PRELAUNCH=1` to skip the compile step if you've already built. -- **CSS selectors are internal implementation details.** Selectors like `.interactive-input-part`, `.interactive-input-editor`, and `.part.auxiliarybar` used in `eval` commands are VS Code internals that may change across versions. If they stop working, use `agent-browser snapshot -i` to re-discover the current DOM structure. - -## Core Workflow - -1. **Launch** Code OSS with remote debugging enabled -2. **Connect** agent-browser to the CDP port -3. **Snapshot** to discover interactive elements -4. **Interact** using element refs -5. **Re-snapshot** after navigation or state changes - -> **๐Ÿ“ธ Take screenshots for a paper trail.** Use `agent-browser screenshot ` at key moments โ€” after launch, before/after interactions, and when something goes wrong. Screenshots provide visual proof of what the UI looked like and are invaluable for debugging failures or documenting what was accomplished. -> -> Save screenshots inside a timestamped subfolder so each run is isolated and nothing gets overwritten: -> -> ```bash -> # Create a timestamped folder for this run's screenshots -> SCREENSHOT_DIR="/tmp/code-oss-screenshots/$(date +%Y-%m-%dT%H-%M-%S)" -> mkdir -p "$SCREENSHOT_DIR" -> -> # Save a screenshot (path is a positional argument โ€” use ./ or absolute paths) -> # Bare filenames without ./ may be misinterpreted as CSS selectors -> agent-browser screenshot "$SCREENSHOT_DIR/after-launch.png" -> ``` - -```bash -# Launch Code OSS with remote debugging -./scripts/code.sh --remote-debugging-port=9224 - -# Wait for Code OSS to start, retry until connected -for i in 1 2 3 4 5; do agent-browser connect 9224 2>/dev/null && break || sleep 3; done - -# Verify you're connected to the right target (not about:blank) -# If `tab` shows the wrong target, run `agent-browser close` and reconnect -agent-browser tab - -# Discover UI elements -agent-browser snapshot -i - -# Focus the chat input (macOS) -agent-browser press Control+Meta+i -``` - -## Connecting - -```bash -# Connect to a specific port -agent-browser connect 9222 - -# Or use --cdp on each command -agent-browser --cdp 9222 snapshot -i - -# Auto-discover a running Chromium-based app -agent-browser --auto-connect snapshot -i -``` - -After `connect`, all subsequent commands target the connected app without needing `--cdp`. - -## Tab Management - -Electron apps often have multiple windows or webviews. Use tab commands to list and switch between them: - -```bash -# List all available targets (windows, webviews, etc.) -agent-browser tab - -# Switch to a specific tab by index -agent-browser tab 2 - -# Switch by URL pattern -agent-browser tab --url "*settings*" -``` - -## Launching Code OSS (VS Code Dev Build) - -The VS Code repository includes `scripts/code.sh` which launches Code OSS from source. It passes all arguments through to the Electron binary, so `--remote-debugging-port` works directly: - -```bash -cd # the root of your VS Code checkout -./scripts/code.sh --remote-debugging-port=9224 -``` - -Wait for the window to fully initialize, then connect: - -```bash -# Wait for Code OSS to start, retry until connected -for i in 1 2 3 4 5; do agent-browser connect 9224 2>/dev/null && break || sleep 3; done - -# Verify you're connected to the right target (not about:blank) -# If `tab` shows the wrong target, run `agent-browser close` and reconnect -agent-browser tab -agent-browser snapshot -i -``` - -**Tips:** -- Set `VSCODE_SKIP_PRELAUNCH=1` to skip the compile step if you've already built: `VSCODE_SKIP_PRELAUNCH=1 ./scripts/code.sh --remote-debugging-port=9224` (from the repo root) -- Code OSS uses the default user data directory. Unlike VS Code Insiders, you don't typically need `--user-data-dir` since there's usually only one Code OSS instance running. -- If you see "Sent env to running instance. Terminating..." it means Code OSS is already running and forwarded your args to the existing instance. Quit Code OSS and relaunch with the flag, or use `--user-data-dir=/tmp/code-oss-debug` to force a new instance. - -## Launching the Agents App (Agents Window) - -The Agents app is a separate workbench mode launched with the `--agents` flag. It uses a dedicated user data directory to avoid conflicts with the main Code OSS instance. - -```bash -cd # the root of your VS Code checkout -./scripts/code.sh --agents --remote-debugging-port=9224 -``` - -Wait for the window to fully initialize, then connect: - -```bash -# Wait for Agents app to start, retry until connected -for i in 1 2 3 4 5; do agent-browser connect 9224 2>/dev/null && break || sleep 3; done - -# Verify you're connected to the right target (not about:blank) -agent-browser tab -agent-browser snapshot -i -``` - -**Tips:** -- The `--agents` flag launches the Agents workbench instead of the standard VS Code workbench. -- Set `VSCODE_SKIP_PRELAUNCH=1` to skip the compile step if you've already built. - -## Launching VS Code Extensions for Debugging - -To debug a VS Code extension via agent-browser, launch VS Code Insiders with `--extensionDevelopmentPath` and `--remote-debugging-port`. Use `--user-data-dir` to avoid conflicting with an already-running instance. - -```bash -# Build the extension first -cd # e.g., the root of your extension checkout -npm run compile - -# Launch VS Code Insiders with the extension and CDP -code-insiders \ - --extensionDevelopmentPath="" \ - --remote-debugging-port=9223 \ - --user-data-dir=/tmp/vscode-ext-debug - -# Wait for VS Code to start, retry until connected -for i in 1 2 3 4 5; do agent-browser connect 9223 2>/dev/null && break || sleep 3; done - -# Verify you're connected to the right target (not about:blank) -# If `tab` shows the wrong target, run `agent-browser close` and reconnect -agent-browser tab -agent-browser snapshot -i -``` - -**Key flags:** -- `--extensionDevelopmentPath=` โ€” loads your extension from source (must be compiled first) -- `--remote-debugging-port=9223` โ€” enables CDP (use 9223 to avoid conflicts with other apps on 9222) -- `--user-data-dir=` โ€” uses a separate profile so it starts a new process instead of sending to an existing VS Code instance - -**Without `--user-data-dir`**, VS Code detects the running instance, forwards the args to it, and exits immediately โ€” you'll see "Sent env to running instance. Terminating..." and CDP never starts. - -## Restarting After Code Changes - -**After making changes to Code OSS source code, you must restart to pick up the new build.** The workbench loads the compiled JavaScript at startup โ€” changes are not hot-reloaded. - -### Restart Workflow - -1. **Rebuild** the changed code -2. **Kill** the running Code OSS instance -3. **Relaunch** with the same flags - -```bash -# 1. Ensure your build is up to date. -# Normally you can skip a manual step here and let ./scripts/code.sh in step 3 -# trigger the build when needed (or run `npm run watch` in another terminal). - -# 2. Kill the Code OSS instance listening on the debug port (if running) -pids=$(lsof -t -i :9224) -if [ -n "$pids" ]; then - kill $pids -fi - -# 3. Relaunch -./scripts/code.sh --remote-debugging-port=9224 - -# 4. Reconnect agent-browser -for i in 1 2 3 4 5; do agent-browser connect 9224 2>/dev/null && break || sleep 3; done -agent-browser tab -agent-browser snapshot -i -``` - -> **Tip:** If you're iterating frequently, run `npm run watch` in a separate terminal so compilation happens automatically. You still need to kill and relaunch Code OSS to load the new build. - -## Interacting with Monaco Editor (Chat Input, Code Editors) - -VS Code uses Monaco Editor for all text inputs including the Copilot Chat input. Monaco editors require specific agent-browser techniques โ€” standard `click`, `fill`, and `keyboard type` commands may not work depending on the VS Code build. - -### The Universal Pattern: Focus via Keyboard Shortcut + `press` - -This works on **all** VS Code builds (Code OSS, Insiders, stable): - -```bash -# 1. Open and focus the chat input with the keyboard shortcut -# macOS: -agent-browser press Control+Meta+i -# Linux / Windows: -agent-browser press Control+Alt+i - -# 2. Type using individual press commands -agent-browser press H -agent-browser press e -agent-browser press l -agent-browser press l -agent-browser press o -agent-browser press Space # Use "Space" for spaces -agent-browser press w -agent-browser press o -agent-browser press r -agent-browser press l -agent-browser press d - -# Verify text appeared (optional) -agent-browser eval ' -(() => { - const sidebar = document.querySelector(".part.auxiliarybar"); - const viewLines = sidebar.querySelectorAll(".interactive-input-editor .view-line"); - return Array.from(viewLines).map(vl => vl.textContent).join("|"); -})()' - -# 3. Send the message (same on all platforms) -agent-browser press Enter -``` - -**Chat focus shortcut by platform:** -- **macOS:** `Ctrl+Cmd+I` โ†’ `agent-browser press Control+Meta+i` -- **Linux:** `Ctrl+Alt+I` โ†’ `agent-browser press Control+Alt+i` -- **Windows:** `Ctrl+Alt+I` โ†’ `agent-browser press Control+Alt+i` - -This shortcut focuses the chat input and sets `document.activeElement` to a `DIV` with class `native-edit-context` โ€” VS Code's native text editing surface that correctly processes key events from `agent-browser press`. - -### `type @ref` โ€” Works on Some Builds - -On VS Code Insiders (extension debug mode), `type @ref` handles focus and input in one step: - -```bash -agent-browser snapshot -i -# Look for: textbox "The editor is not accessible..." [ref=e62] -agent-browser type @e62 "Hello from George!" -``` - -> **Tip:** If `type @ref` silently drops text (the editor stays empty), the ref may be stale or the editor not yet ready. Re-snapshot to get a fresh ref and try again. You can verify text was entered using the snippet in "Verifying Text and Clearing" below. - -However, **`type @ref` silently fails on Code OSS** โ€” the command completes without error but no text appears. This also applies to `keyboard type` and `keyboard inserttext`. Always verify text appeared after typing, and fall back to the keyboard shortcut + `press` pattern if it didn't. The `press`-per-key approach works universally across all builds. - -> **โš ๏ธ Warning:** `keyboard type` can hang indefinitely in some focus states (e.g., after JS mouse events). If it doesn't return within a few seconds, interrupt it and fall back to `press` for individual keystrokes. - -### Compatibility Matrix - -| Method | VS Code Insiders | Code OSS | -|--------|-----------------|----------| -| `press` per key (after focus shortcut) | โœ… Works | โœ… Works | -| `type @ref` | โœ… Works | โŒ Silent fail | -| `keyboard type` (after focus) | โœ… Works | โŒ Silent fail | -| `keyboard inserttext` (after focus) | โœ… Works | โŒ Silent fail | -| `click @ref` | โŒ Blocked by overlay | โŒ Blocked by overlay | -| `fill @ref` | โŒ Element not visible | โŒ Element not visible | - -### Fallback: Focus via JavaScript Mouse Events - -If the keyboard shortcut doesn't work (e.g., chat panel isn't configured), you can focus the editor via JavaScript: - -```bash -agent-browser eval ' -(() => { - const inputPart = document.querySelector(".interactive-input-part"); - const editor = inputPart.querySelector(".monaco-editor"); - const rect = editor.getBoundingClientRect(); - const x = rect.x + rect.width / 2; - const y = rect.y + rect.height / 2; - editor.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, clientX: x, clientY: y })); - editor.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, clientX: x, clientY: y })); - editor.dispatchEvent(new MouseEvent("click", { bubbles: true, clientX: x, clientY: y })); - return "activeElement: " + document.activeElement?.className; -})()' - -# Then use press for each character -agent-browser press H -agent-browser press e -# ... -``` - -### Verifying Text and Clearing - -```bash -# Verify text in the chat input -agent-browser eval ' -(() => { - const sidebar = document.querySelector(".part.auxiliarybar"); - const viewLines = sidebar.querySelectorAll(".interactive-input-editor .view-line"); - return Array.from(viewLines).map(vl => vl.textContent).join("|"); -})()' - -# Clear the input (Select All + Backspace) -# macOS: -agent-browser press Meta+a -# Linux / Windows: -agent-browser press Control+a -# Then delete: -agent-browser press Backspace -``` - -### Screenshot Tips for VS Code - -On ultrawide monitors, the chat sidebar may be in the far-right corner of the CDP screenshot. Options: -- Use `agent-browser screenshot --full` to capture the entire window -- Use element screenshots: `agent-browser screenshot ".part.auxiliarybar" sidebar.png` -- Use `agent-browser screenshot --annotate` to see labeled element positions -- Maximize the sidebar first: click the "Maximize Secondary Side Bar" button - -> **macOS:** If `agent-browser screenshot` returns "Permission denied", your terminal needs Screen Recording permission. Grant it in **System Settings โ†’ Privacy & Security โ†’ Screen Recording**. As a fallback, use the `eval` verification snippet to confirm text was entered โ€” this doesn't require screen permissions. - -## Troubleshooting - -### "Connection refused" or "Cannot connect" - -- Make sure Code OSS was launched with `--remote-debugging-port=NNNN` -- If Code OSS was already running, quit and relaunch with the flag -- Check that the port isn't in use by another process: - - macOS / Linux: `lsof -i :9224` - - Windows: `netstat -ano | findstr 9224` - -### Elements not appearing in snapshot - -- VS Code uses multiple webviews. Use `agent-browser tab` to list targets and switch to the right one -- Use `agent-browser snapshot -i -C` to include cursor-interactive elements (divs with onclick handlers) - -### Cannot type in Monaco Editor inputs - -- Use `agent-browser press` for individual keystrokes after focusing the input. Focus the chat input with the keyboard shortcut (macOS: `Ctrl+Cmd+I`, Linux/Windows: `Ctrl+Alt+I`). -- `type @ref`, `keyboard type`, and `keyboard inserttext` work on VS Code Insiders but **silently fail on Code OSS** โ€” they complete without error but no text appears. The `press`-per-key approach works universally. -- See the "Interacting with Monaco Editor" section above for the full compatibility matrix. - -## Cleanup - -**Always kill the Code OSS instance when you're done.** Code OSS is a full Electron app that consumes significant memory (often 1โ€“4 GB+). Leaving it running wastes resources and holds the CDP port. - -```bash -# Disconnect agent-browser -agent-browser close - -# Kill the Code OSS instance listening on the debug port (if running) -# macOS / Linux: -pids=$(lsof -t -i :9224) -if [ -n "$pids" ]; then - kill $pids -fi - -# Windows: -# taskkill /F /PID -# Or use Task Manager to end "Code - OSS" -``` - -Verify it's gone: -```bash -# Confirm no process is listening on the debug port -lsof -i :9224 # should return nothing -``` diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ec0da3bf408..87402a4acff 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -115,7 +115,8 @@ "kind": "build", "isDefault": true }, - "problemMatcher": [] + "problemMatcher": [], + "inAgents": true }, { "type": "npm", @@ -174,7 +175,8 @@ "Kill Copilot - Build" ], "group": "build", - "problemMatcher": [] + "problemMatcher": [], + "inAgents": true }, { "label": "Restart VS Code - Build", @@ -254,7 +256,8 @@ "windows": { "command": ".\\scripts\\code.bat" }, - "problemMatcher": [] + "problemMatcher": [], + "inAgents": true }, { "label": "Run Dev Agents", diff --git a/src/vs/workbench/contrib/issue/browser/issueReporterEditorPane.ts b/src/vs/workbench/contrib/issue/browser/issueReporterEditorPane.ts index 86af4eaaac2..97773d8ce2a 100644 --- a/src/vs/workbench/contrib/issue/browser/issueReporterEditorPane.ts +++ b/src/vs/workbench/contrib/issue/browser/issueReporterEditorPane.ts @@ -32,6 +32,7 @@ import { IWorkbenchAssignmentService } from '../../../services/assignment/common import product from '../../../../platform/product/common/product.js'; import { isLinuxSnap } from '../../../../base/common/platform.js'; import { INativeHostService } from '../../../../platform/native/common/native.js'; +import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js'; /** @@ -61,6 +62,7 @@ export class IssueReporterEditorPane extends EditorPane { @IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService, @INativeHostService private readonly nativeHostService: INativeHostService, @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, + @IContextMenuService private readonly contextMenuService: IContextMenuService, ) { super(IssueReporterEditorPane.ID, group, telemetryService, themeService, storageService); } @@ -102,6 +104,7 @@ export class IssueReporterEditorPane extends EditorPane { data, this.recordingService.isSupported, this.container, + this.contextMenuService, ); this.inputDisposables.add(this.wizard); @@ -122,10 +125,6 @@ export class IssueReporterEditorPane extends EditorPane { this.wizard.show(); - // Set active theme name - const colorTheme = this.themeService.getColorTheme(); - this.wizard.setActiveThemeName(colorTheme.label); - // Populate system info in background (non-blocking) this.populateSystemInfo(); diff --git a/src/vs/workbench/contrib/issue/browser/issueReporterOverlay.ts b/src/vs/workbench/contrib/issue/browser/issueReporterOverlay.ts index 1722988ce6d..d2d7997513b 100644 --- a/src/vs/workbench/contrib/issue/browser/issueReporterOverlay.ts +++ b/src/vs/workbench/contrib/issue/browser/issueReporterOverlay.ts @@ -5,12 +5,18 @@ import './media/issueReporterOverlay.css'; import { $, addDisposableListener, append, EventType, getWindow } from '../../../../base/browser/dom.js'; +import { Button } from '../../../../base/browser/ui/button/button.js'; +import { IContextMenuProvider } from '../../../../base/browser/contextmenu.js'; import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js'; +import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js'; +import { Action } from '../../../../base/common/actions.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js'; import { isMacintosh } from '../../../../base/common/platform.js'; import { localize } from '../../../../nls.js'; +import { defaultButtonStyles, defaultCheckboxStyles, defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; import { IssueReporterData, IssueType } from '../common/issue.js'; import { IssueReporterModel } from './issueReporterModel.js'; import { RecordingState } from './recordingService.js'; @@ -72,10 +78,8 @@ export class IssueReporterOverlay { // Step 3: Screenshots & Recording private screenshotContainer!: HTMLElement; private screenshotDelay = 0; - private captureBtn!: HTMLElement; - private captureLabel!: HTMLElement; - private recordBtn: HTMLElement | undefined; - private recordLabel!: HTMLElement; + private captureBtn!: Button; + private recordBtn: Button | undefined; private recordingElapsedLabel!: HTMLElement; private recordingElapsedTimer: ReturnType | undefined; private recordingStartTime = 0; @@ -83,7 +87,7 @@ export class IssueReporterOverlay { private readonly recordings: { filePath: string; durationMs: number; thumbnailDataUrl?: string }[] = []; // Step 4: Review - private titleInput!: HTMLInputElement; + private titleInput!: InputBox; private reviewThumbCards: HTMLElement[] = []; private uploading = false; private includeSystemInfo = true; @@ -92,14 +96,12 @@ export class IssueReporterOverlay { private includeSettings = true; private settingsContent: string | undefined; private workspaceSettingsContent: string | undefined; - private activeThemeName: string | undefined; // Navigation private stepIndicator!: HTMLElement; private stepLabel!: HTMLElement; - private backButton!: HTMLElement; - private nextButton!: HTMLElement; - private nextShortcutBadge!: HTMLElement; + private backButton!: Button; + private nextButton!: Button; // Progress dots private readonly progressDots: HTMLElement[] = []; @@ -115,6 +117,7 @@ export class IssueReporterOverlay { private readonly data: IssueReporterData, private readonly recordingSupported: boolean = false, private readonly container: HTMLElement, + private readonly contextMenuProvider?: IContextMenuProvider, ) { this.model = new IssueReporterModel({ ...data, @@ -163,28 +166,16 @@ export class IssueReporterOverlay { // โ”€โ”€ Bottom navigation โ”€โ”€ const nav = append(this.wizardPanel, $('div.wizard-nav')); - this.backButton = append(nav, $('div.wizard-nav-btn.wizard-back')); - const backArrow = append(this.backButton, $('span')); - backArrow.textContent = '\u2190'; // โ† - const backLabel = append(this.backButton, $('span')); - backLabel.textContent = localize('back', "Back"); - const backShortcut = append(this.backButton, $('span.wizard-shortcut-badge')); - backShortcut.textContent = 'Esc'; - this.backButton.setAttribute('role', 'button'); - this.backButton.setAttribute('tabindex', '0'); - this.backButton.title = localize('backEscape', "Back (Escape)"); + this.backButton = this.disposables.add(new Button(nav, { ...defaultButtonStyles, secondary: true })); + this.backButton.label = localize('back', "Back"); + this.backButton.element.classList.add('wizard-back'); + this.backButton.element.title = localize('backEscape', "Back (Escape)"); - this.nextButton = append(nav, $('div.wizard-nav-btn.wizard-next.primary')); - const nextLabel = append(this.nextButton, $('span.wizard-next-label')); - nextLabel.textContent = localize('next', "Next"); - const nextArrow = append(this.nextButton, $('span.wizard-next-arrow')); - nextArrow.textContent = ' \u2192'; // โ†’ - this.nextShortcutBadge = append(this.nextButton, $('span.wizard-shortcut-badge')); - this.nextShortcutBadge.textContent = isMacintosh ? '\u2318\u23CE' : 'Ctrl+\u23CE'; - this.nextButton.setAttribute('role', 'button'); - this.nextButton.setAttribute('tabindex', '0'); + this.nextButton = this.disposables.add(new Button(nav, { ...defaultButtonStyles })); + this.nextButton.label = localize('next', "Next"); + this.nextButton.element.classList.add('wizard-next'); const ctrlKey = isMacintosh ? '\u2318' : 'Ctrl'; - this.nextButton.title = localize('nextCtrlEnter', "Next ({0}+Enter)", ctrlKey); + this.nextButton.element.title = localize('nextCtrlEnter', "Next ({0}+Enter)", ctrlKey); this.registerEventHandlers(); this.updateStepUI(); @@ -342,14 +333,12 @@ export class IssueReporterOverlay { } }; - for (const { type, label, icon, shortcut } of types) { + for (const { type, label, icon } of types) { const btn = append(this.typeButtonGroup, $('div.wizard-type-btn')); btn.setAttribute('role', 'button'); btn.setAttribute('tabindex', '0'); btn.setAttribute('data-type', String(type)); - const shortcutBadge = append(btn, $('span.wizard-shortcut-badge')); - shortcutBadge.textContent = shortcut; const iconEl = append(btn, $('span.wizard-type-icon')); iconEl.appendChild(renderIcon(icon)); const labelEl = append(btn, $('span')); @@ -416,31 +405,31 @@ export class IssueReporterOverlay { this.screenshotDelay = parseInt(delaySelect.value); })); - this.captureBtn = append(actions, $('div.wizard-nav-btn.wizard-capture-btn.primary')); - this.captureBtn.setAttribute('role', 'button'); - this.captureBtn.setAttribute('tabindex', '0'); - const cameraIcon = append(this.captureBtn, $('span.wizard-capture-icon')); + this.captureBtn = this.disposables.add(new Button(actions, { ...defaultButtonStyles })); + this.captureBtn.label = localize('addScreenshot', "Add screenshot"); + this.captureBtn.element.classList.add('wizard-capture-btn'); + const cameraIcon = document.createElement('span'); + cameraIcon.className = 'wizard-capture-icon'; cameraIcon.appendChild(renderIcon(Codicon.deviceCamera)); - this.captureLabel = append(this.captureBtn, $('span.wizard-capture-label')); - this.captureLabel.textContent = localize('addScreenshot', "Add screenshot"); + this.captureBtn.element.insertBefore(cameraIcon, this.captureBtn.element.firstChild); - this.disposables.add(addDisposableListener(this.captureBtn, EventType.CLICK, () => { - if (this.getTotalAttachments() >= MAX_ATTACHMENTS || this.captureBtn.classList.contains('disabled')) { + this.disposables.add(this.captureBtn.onDidClick(() => { + if (this.getTotalAttachments() >= MAX_ATTACHMENTS || !this.captureBtn.enabled) { return; } if (this.screenshotDelay > 0) { - this.captureBtn.classList.add('disabled'); - const origText = this.captureLabel.textContent; + this.captureBtn.enabled = false; + const origLabel = this.captureBtn.label as string; let remaining = this.screenshotDelay; - this.captureLabel.textContent = `${remaining}...`; + this.captureBtn.label = `${remaining}...`; const interval = setInterval(() => { remaining--; if (remaining > 0) { - this.captureLabel.textContent = `${remaining}...`; + this.captureBtn.label = `${remaining}...`; } else { clearInterval(interval); - this.captureLabel.textContent = origText; - this.captureBtn.classList.remove('disabled'); + this.captureBtn.label = origLabel; + this.captureBtn.enabled = true; this._onDidRequestScreenshot.fire(); } }, 1000); @@ -451,18 +440,18 @@ export class IssueReporterOverlay { // Record video button (only when supported) if (this.recordingSupported) { - this.recordBtn = append(actions, $('div.wizard-nav-btn.wizard-record-btn')); - this.recordBtn.setAttribute('role', 'button'); - this.recordBtn.setAttribute('tabindex', '0'); - const recordIcon = append(this.recordBtn, $('span.wizard-record-icon')); + this.recordBtn = this.disposables.add(new Button(actions, { ...defaultButtonStyles, secondary: true })); + this.recordBtn.label = localize('recordVideo', "Record video"); + this.recordBtn.element.classList.add('wizard-record-btn'); + const recordIcon = document.createElement('span'); + recordIcon.className = 'wizard-record-icon'; recordIcon.appendChild(renderIcon(Codicon.record)); - this.recordLabel = append(this.recordBtn, $('span.wizard-record-label')); - this.recordLabel.textContent = localize('recordVideo', "Record video"); + this.recordBtn.element.insertBefore(recordIcon, this.recordBtn.element.firstChild); - this.recordingElapsedLabel = append(this.recordBtn, $('span.wizard-recording-elapsed')); + this.recordingElapsedLabel = append(this.recordBtn.element, $('span.wizard-recording-elapsed')); this.recordingElapsedLabel.style.display = 'none'; - this.disposables.add(addDisposableListener(this.recordBtn, EventType.CLICK, () => { + this.disposables.add(this.recordBtn.onDidClick(() => { if (this.currentRecordingState === RecordingState.Recording) { this._onDidRequestStopRecording.fire(); } else if (this.currentRecordingState === RecordingState.Idle && this.getTotalAttachments() < MAX_ATTACHMENTS) { @@ -478,8 +467,7 @@ export class IssueReporterOverlay { this.createFloatingCaptureBar(); } - private captureStripRecordBtn: HTMLElement | undefined; - private captureStripRecordLbl: HTMLElement | undefined; + private captureStripRecordBtn: Button | undefined; private captureStripRecordElapsed: HTMLElement | undefined; private createFloatingCaptureBar(): void { @@ -503,63 +491,55 @@ export class IssueReporterOverlay { const captureLbl = append(captureBtn, $('span')); captureLbl.textContent = localize('screenshot', "Screenshot"); - const delayBtn = append(segmented, $('div.wizard-segmented-dropdown.primary')); - delayBtn.setAttribute('role', 'button'); - delayBtn.setAttribute('tabindex', '0'); - const delayChevron = append(delayBtn, $('span')); - delayChevron.appendChild(renderIcon(Codicon.chevronDown)); - - // Delay label shown when delay > 0 - const delayIndicator = append(segmented, $('span.wizard-segmented-delay-label')); - delayIndicator.style.display = 'none'; - - // Delay dropdown menu (hidden by default) - const delayMenu = append(this.floatingBar, $('div.wizard-delay-menu')); - delayMenu.style.display = 'none'; + // Delay dropdown using VS Code's DropdownMenu const delayOptions = [ { label: localize('noDelay', "No delay"), value: 0 }, { label: localize('threeSecDelay', "3 second delay"), value: 3 }, { label: localize('fiveSecDelay', "5 second delay"), value: 5 }, { label: localize('tenSecDelay', "10 second delay"), value: 10 }, ]; - for (const opt of delayOptions) { - const item = append(delayMenu, $('div.wizard-delay-menu-item')); - item.textContent = opt.label; - if (opt.value === this.screenshotDelay) { - item.classList.add('selected'); - } - this.disposables.add(addDisposableListener(item, EventType.CLICK, () => { - this.screenshotDelay = opt.value; - delayMenu.style.display = 'none'; - // Update selection state - for (const el of delayMenu.children) { - (el as HTMLElement).classList.remove('selected'); - } - item.classList.add('selected'); - // Update delay indicator - if (opt.value > 0) { - delayIndicator.textContent = `${opt.value}s`; - delayIndicator.style.display = ''; - } else { - delayIndicator.style.display = 'none'; - } + const delayDropdownContainer = append(segmented, $('div.wizard-segmented-dropdown.primary')); + delayDropdownContainer.setAttribute('role', 'button'); + delayDropdownContainer.setAttribute('tabindex', '0'); + append(delayDropdownContainer, $('span')).appendChild(renderIcon(Codicon.chevronDown)); + + if (this.contextMenuProvider) { + this.disposables.add(addDisposableListener(delayDropdownContainer, EventType.CLICK, (e) => { + e.stopPropagation(); + const actions = delayOptions.map(opt => { + const action = new Action( + `delay-${opt.value}`, + opt.label, + undefined, + true, + async () => { this.screenshotDelay = opt.value; } + ); + action.checked = opt.value === this.screenshotDelay; + return action; + }); + this.contextMenuProvider!.showContextMenu({ + getAnchor: () => this.floatingBar!, + getActions: () => actions, + skipTelemetry: true, + onHide: () => { for (const a of actions) { a.dispose(); } }, + }); + })); + + // Close the delay menu when drag starts. + // The drag handler calls e.preventDefault() on pointerdown which + // suppresses the mousedown event that the context menu uses for + // outside-click detection โ€” so we dispatch a synthetic one. + this.disposables.add(addDisposableListener(dragArea, EventType.POINTER_DOWN, () => { + dragArea.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); })); } - this.disposables.add(addDisposableListener(delayBtn, EventType.CLICK, (e) => { - e.stopPropagation(); - delayMenu.style.display = delayMenu.style.display === 'none' ? '' : 'none'; - })); - - // Close delay menu when clicking elsewhere - this.disposables.add(addDisposableListener(this.floatingBar, EventType.CLICK, () => { - delayMenu.style.display = 'none'; - })); - this.disposables.add(addDisposableListener(captureBtn, EventType.CLICK, () => { - delayMenu.style.display = 'none'; if (this.getTotalAttachments() < MAX_ATTACHMENTS && !captureBtn.classList.contains('disabled')) { if (this.screenshotDelay > 0) { + // Lock width so button doesn't shrink during countdown + captureBtn.style.minWidth = `${captureBtn.offsetWidth}px`; + captureBtn.style.textAlign = 'center'; captureBtn.classList.add('disabled'); let remaining = this.screenshotDelay; captureLbl.textContent = `${remaining}...`; @@ -571,6 +551,8 @@ export class IssueReporterOverlay { clearInterval(interval); captureLbl.textContent = localize('screenshot', "Screenshot"); captureBtn.classList.remove('disabled'); + captureBtn.style.minWidth = ''; + captureBtn.style.textAlign = ''; this._onDidRequestScreenshot.fire(); } }, 1000); @@ -582,16 +564,12 @@ export class IssueReporterOverlay { // Record button if (this.recordingSupported) { - this.captureStripRecordBtn = append(this.floatingBar, $('div.wizard-nav-btn.wizard-record-btn')); - this.captureStripRecordBtn.setAttribute('role', 'button'); - this.captureStripRecordBtn.setAttribute('tabindex', '0'); - const recordIcon = append(this.captureStripRecordBtn, $('span.wizard-record-icon')); - recordIcon.appendChild(renderIcon(Codicon.record)); - this.captureStripRecordLbl = append(this.captureStripRecordBtn, $('span')); - this.captureStripRecordLbl.textContent = localize('recordVideo', "Record video"); - this.captureStripRecordElapsed = append(this.captureStripRecordBtn, $('span.wizard-recording-elapsed')); + this.captureStripRecordBtn = this.disposables.add(new Button(this.floatingBar, { ...defaultButtonStyles, secondary: true, supportIcons: true })); + this.captureStripRecordBtn.label = `$(record) ${localize('recordVideo', "Record video")}`; + this.captureStripRecordBtn.element.classList.add('wizard-record-btn'); + this.captureStripRecordElapsed = append(this.captureStripRecordBtn.element, $('span.wizard-recording-elapsed')); this.captureStripRecordElapsed.style.display = 'none'; - this.disposables.add(addDisposableListener(this.captureStripRecordBtn, EventType.CLICK, () => { + this.disposables.add(this.captureStripRecordBtn.onDidClick(() => { if (this.currentRecordingState === RecordingState.Recording) { this._onDidRequestStopRecording.fire(); } else if (this.currentRecordingState === RecordingState.Idle && this.getTotalAttachments() < MAX_ATTACHMENTS) { @@ -674,15 +652,16 @@ export class IssueReporterOverlay { const titleGroup = append(page, $('div.wizard-field')); const titleLabel = append(titleGroup, $('label.wizard-field-label')); titleLabel.textContent = localize('issueTitle', "Issue title"); - this.titleInput = append(titleGroup, $('input.wizard-title-input')) as HTMLInputElement; - this.titleInput.type = 'text'; - this.titleInput.placeholder = localize('issueTitlePlaceholder', "Brief summary of the issue"); + this.titleInput = this.disposables.add(new InputBox(titleGroup, undefined, { + placeholder: localize('issueTitlePlaceholder', "Brief summary of the issue"), + inputBoxStyles: defaultInputBoxStyles, + })); if (this.data.issueTitle) { this.titleInput.value = this.data.issueTitle; } - this.disposables.add(addDisposableListener(this.titleInput, EventType.INPUT, () => { - this.titleInput.classList.remove('invalid-input'); + this.disposables.add(this.titleInput.onDidChange(() => { + this.titleInput.element.classList.remove('invalid-input'); })); // Review details (filled dynamically) โ€” compact horizontal layout @@ -691,22 +670,10 @@ export class IssueReporterOverlay { private registerEventHandlers(): void { // Back - this.disposables.add(addDisposableListener(this.backButton, EventType.CLICK, () => this.goBack())); - this.disposables.add(addDisposableListener(this.backButton, EventType.KEY_DOWN, (e: KeyboardEvent) => { - if ((e.key === 'Enter' && !e.ctrlKey && !e.metaKey) || e.key === ' ') { - e.preventDefault(); - this.goBack(); - } - })); + this.disposables.add(this.backButton.onDidClick(() => this.goBack())); // Next - this.disposables.add(addDisposableListener(this.nextButton, EventType.CLICK, () => this.goNext())); - this.disposables.add(addDisposableListener(this.nextButton, EventType.KEY_DOWN, (e: KeyboardEvent) => { - if ((e.key === 'Enter' && !e.ctrlKey && !e.metaKey) || e.key === ' ') { - e.preventDefault(); - this.goNext(); - } - })); + this.disposables.add(this.nextButton.onDidClick(() => this.goNext())); // Ctrl+Enter to advance / submit this.disposables.add(addDisposableListener(this.wizardPanel, EventType.KEY_DOWN, (e: KeyboardEvent) => { @@ -777,21 +744,13 @@ export class IssueReporterOverlay { const oldStep = this.currentStep; this.currentStep = step; - const direction = step > oldStep ? 1 : -1; const oldPage = this.stepPages[oldStep]; const newPage = this.stepPages[step]; - oldPage.classList.add(direction > 0 ? 'slide-out-left' : 'slide-out-right'); - newPage.classList.remove('slide-out-left', 'slide-out-right', 'slide-in-left', 'slide-in-right'); - newPage.classList.add(direction > 0 ? 'slide-in-right' : 'slide-in-left'); + // Immediate transition โ€” no animation + oldPage.style.display = 'none'; newPage.style.display = 'flex'; - setTimeout(() => { - oldPage.style.display = 'none'; - oldPage.classList.remove('slide-out-left', 'slide-out-right'); - newPage.classList.remove('slide-in-left', 'slide-in-right'); - }, 250); - this.updateStepUI(); if (step === WizardStep.Describe) { @@ -837,42 +796,21 @@ export class IssueReporterOverlay { } // Back button visibility - this.backButton.style.display = this.currentStep === WizardStep.Categorize ? 'none' : ''; + this.backButton.element.style.display = this.currentStep === WizardStep.Categorize ? 'none' : ''; // Next button label const ctrlKey = isMacintosh ? '\u2318' : 'Ctrl'; - const nextLabel = this.nextButton.querySelector('.wizard-next-label'); - const nextArrow = this.nextButton.querySelector('.wizard-next-arrow'); if (this.currentStep === WizardStep.Review) { - if (nextLabel) { - nextLabel.textContent = localize('previewOnGitHub', "Preview on GitHub"); - } - if (nextArrow) { - nextArrow.textContent = ' \u2192'; // โ†’ - } - this.nextButton.classList.remove('submit'); - this.nextButton.classList.add('primary'); - this.nextButton.title = localize('submitCtrlEnter', "Preview on GitHub ({0}+Enter)", ctrlKey); + this.nextButton.label = localize('previewOnGitHub', "Preview on GitHub"); + this.nextButton.element.title = localize('submitCtrlEnter', "Preview on GitHub ({0}+Enter)", ctrlKey); } else if (this.currentStep === WizardStep.Screenshots) { - if (nextLabel) { - nextLabel.textContent = this.screenshots.length === 0 - ? localize('skip', "Skip") - : localize('next', "Next"); - } - if (nextArrow) { - nextArrow.textContent = ' \u00BB'; // ยป - } - this.nextButton.classList.remove('submit'); - this.nextButton.title = localize('nextCtrlEnter', "Next ({0}+Enter)", ctrlKey); + this.nextButton.label = this.screenshots.length === 0 + ? localize('skip', "Skip") + : localize('next', "Next"); + this.nextButton.element.title = localize('nextCtrlEnter', "Next ({0}+Enter)", ctrlKey); } else { - if (nextLabel) { - nextLabel.textContent = localize('next', "Next"); - } - if (nextArrow) { - nextArrow.textContent = ' \u2192'; // โ†’ - } - this.nextButton.classList.remove('submit'); - this.nextButton.title = localize('nextCtrlEnter', "Next ({0}+Enter)", ctrlKey); + this.nextButton.label = localize('next', "Next"); + this.nextButton.element.title = localize('nextCtrlEnter', "Next ({0}+Enter)", ctrlKey); } // Show/hide capture strip (only on step 3) @@ -1119,478 +1057,446 @@ export class IssueReporterOverlay { title.textContent = opts.label; const checkWrap = append(header, $('div.review-diag-check-wrap')); - const checkbox = append(checkWrap, $('input.review-diag-checkbox')) as HTMLInputElement; - checkbox.type = 'checkbox'; - checkbox.checked = opts.checked; - checkbox.id = `diag-${opts.id}`; + const checkbox = this.disposables.add(new Checkbox(localize('includeInIssue', "Include in issue"), opts.checked, defaultCheckboxStyles)); + checkWrap.appendChild(checkbox.domNode); const checkLabel = append(checkWrap, $('label.review-diag-check-label')); - checkLabel.setAttribute('for', `diag-${opts.id}`); checkLabel.textContent = localize('includeInIssue', "Include in issue"); - this.disposables.add(addDisposableListener(checkbox, EventType.CHANGE, () => { + this.disposables.add(checkbox.onChange(() => { opts.onToggle(checkbox.checked); })); - const toggleBtn = append(header, $('div.wizard-nav-btn.review-diag-toggle')); - toggleBtn.setAttribute('role', 'button'); - toggleBtn.setAttribute('tabindex', '0'); - const toggleIcon = append(toggleBtn, $('span')); - toggleIcon.appendChild(renderIcon(Codicon.chevronUp)); - const toggleLabel = append(toggleBtn, $('span')); - toggleLabel.textContent = localize('minimize', "Minimize"); + const toggleBtn = this.disposables.add(new Button(header, { ...defaultButtonStyles, secondary: true, supportIcons: true })); + toggleBtn.label = `$(chevron-up) ${localize('minimize', "Minimize")}`; + toggleBtn.element.classList.add('review-diag-toggle'); // Content const content = append(group, $('div.review-diag-content')); opts.renderContent(content); let expanded = true; - this.disposables.add(addDisposableListener(toggleBtn, EventType.CLICK, () => { + this.disposables.add(toggleBtn.onDidClick(() => { expanded = !expanded; content.style.display = expanded ? '' : 'none'; - toggleIcon.textContent = ''; - toggleIcon.appendChild(renderIcon(expanded ? Codicon.chevronUp : Codicon.chevronDown)); - toggleLabel.textContent = expanded - ? localize('minimize', "Minimize") - : localize('expand', "Expand"); + toggleBtn.label = expanded + ? `$(chevron-up) ${localize('minimize', "Minimize")}` + : `$(chevron-down) ${localize('expand', "Expand")}`; })); } private addDiagRow(table: HTMLElement, label: string, value: string): void { - const row = append(table, $('tr')); - const th = append(row, $('td.review-diag-key')); - th.textContent = label; - const td = append(row, $('td.review-diag-val')); - td.textContent = value; -} + const row = append(table, $('tr')); + const th = append(row, $('td.review-diag-key')); + th.textContent = label; + const td = append(row, $('td.review-diag-val')); + td.textContent = value; + } -/** Called by the form service to show upload progress */ -setUploading(uploading: boolean): void { - this.uploading = uploading; - const nextLabel = this.nextButton.querySelector('.wizard-next-label'); - const nextArrow = this.nextButton.querySelector('.wizard-next-arrow'); + /** Called by the form service to show upload progress */ + setUploading(uploading: boolean): void { + this.uploading = uploading; - if(uploading) { - this.nextButton.classList.add('uploading'); - if (nextLabel) { - nextLabel.textContent = localize('uploading', "Uploading..."); - } - if (nextArrow) { - nextArrow.textContent = ''; - const spinner = document.createElement('span'); - spinner.className = 'wizard-btn-spinner'; - nextArrow.appendChild(spinner); - } - this.backButton.style.display = 'none'; - } else { - this.nextButton.classList.remove('uploading'); - if(nextLabel) { - nextLabel.textContent = localize('previewOnGitHub', "Preview on GitHub"); - } - if(nextArrow) { - nextArrow.textContent = ' \u2192'; + if (uploading) { + this.nextButton.element.classList.add('uploading'); + this.nextButton.label = localize('uploading', "Uploading..."); + this.nextButton.enabled = false; + this.backButton.element.style.display = 'none'; + } else { + this.nextButton.element.classList.remove('uploading'); + this.nextButton.label = localize('previewOnGitHub', "Preview on GitHub"); + this.nextButton.enabled = true; } } -} -/** Mark a specific attachment as uploading / done */ -setAttachmentUploadState(index: number, state: 'pending' | 'uploading' | 'done'): void { - const card = this.reviewThumbCards[index]; - if(!card) { - return; - } + /** Mark a specific attachment as uploading / done */ + setAttachmentUploadState(index: number, state: 'pending' | 'uploading' | 'done'): void { + const card = this.reviewThumbCards[index]; + if (!card) { + return; + } card.classList.remove('upload-pending', 'upload-uploading', 'upload-done'); - card.classList.add(`upload-${state}`); + card.classList.add(`upload-${state}`); - const overlay = card.querySelector('.review-progress-overlay') as HTMLElement | null; - if(!overlay) { - return; - } + const overlay = card.querySelector('.review-progress-overlay') as HTMLElement | null; + if (!overlay) { + return; + } - if(state === 'done') { - // Replace ring with checkmark - overlay.textContent = ''; - const check = document.createElement('span'); - check.className = 'review-progress-check'; - check.appendChild(renderIcon(Codicon.check)); - overlay.appendChild(check); -} + if (state === 'done') { + // Replace ring with checkmark + overlay.textContent = ''; + const check = document.createElement('span'); + check.className = 'review-progress-check'; + check.appendChild(renderIcon(Codicon.check)); + overlay.appendChild(check); + } } private submit(): void { - const title = this.titleInput.value.trim(); - if(!title) { - this.titleInput.classList.add('invalid-input'); - this.titleInput.focus(); - return; - } + const title = this.titleInput.value.trim(); + if (!title) { + this.titleInput.element.classList.add('invalid-input'); + this.titleInput.focus(); + return; + } const description = this.composeDescription(); - this.model.update({ issueDescription: description, issueTitle: title, ...(this.selectedIssueType !== undefined ? { issueType: this.selectedIssueType } : {}) }); + this.model.update({ issueDescription: description, issueTitle: title, ...(this.selectedIssueType !== undefined ? { issueType: this.selectedIssueType } : {}) }); - const body = this.buildIssueBody(); - this._onDidSubmit.fire({ title, body }); -} - -show(): void { - if(this.visible) { - return; -} -this.visible = true; - -this.wizardPanel.classList.add('open', 'wizard-embedded'); -this.wizardPanel.style.maxHeight = 'none'; -append(this.container, this.wizardPanel); -this.wizardPanel.focus(); + const body = this.buildIssueBody(); + this._onDidSubmit.fire({ title, body }); } -close(): void { - this.visible = false; - this._onDidClose.fire(); -} + show(): void { + if (this.visible) { + return; + } + this.visible = true; + + this.wizardPanel.classList.add('open', 'wizard-embedded'); + this.wizardPanel.style.maxHeight = 'none'; + append(this.container, this.wizardPanel); + this.wizardPanel.focus(); + } + + close(): void { + this.visible = false; + this._onDidClose.fire(); + } private getTotalAttachments(): number { - return this.screenshots.length + this.recordings.length; -} + return this.screenshots.length + this.recordings.length; + } -addScreenshot(screenshot: IScreenshot): void { - if(this.getTotalAttachments() >= MAX_ATTACHMENTS) { - return; -} -this.screenshots.push(screenshot); -this.updateScreenshotThumbnails(); -this.updateAttachmentButtons(); -this.updateStepUI(); + addScreenshot(screenshot: IScreenshot): void { + if (this.getTotalAttachments() >= MAX_ATTACHMENTS) { + return; + } + this.screenshots.push(screenshot); + this.updateScreenshotThumbnails(); + this.updateAttachmentButtons(); + this.updateStepUI(); -// Immediately open the annotation editor for the new screenshot -this.openAnnotationEditor(this.screenshots.length - 1); + // Immediately open the annotation editor for the new screenshot + this.openAnnotationEditor(this.screenshots.length - 1); } private updateAttachmentButtons(): void { - const atMax = this.getTotalAttachments() >= MAX_ATTACHMENTS; + const atMax = this.getTotalAttachments() >= MAX_ATTACHMENTS; - this.captureBtn.classList.toggle('disabled', atMax); - this.captureLabel.textContent = atMax - ? localize('maxAttachmentsReached', "Max attachments reached") - : localize('addScreenshot', "Add screenshot"); - - if(this.recordBtn) { - this.recordBtn.classList.toggle('disabled', atMax); - if (this.currentRecordingState !== RecordingState.Recording) { - this.recordLabel.textContent = atMax + this.captureBtn.enabled = !atMax; + this.captureBtn.label = atMax ? localize('maxAttachmentsReached', "Max attachments reached") - : localize('recordVideo', "Record video"); - } -} + : localize('addScreenshot', "Add screenshot"); + + if (this.recordBtn) { + this.recordBtn.enabled = !atMax; + if (this.currentRecordingState !== RecordingState.Recording) { + this.recordBtn.label = atMax + ? localize('maxAttachmentsReached', "Max attachments reached") + : localize('recordVideo', "Record video"); + } + } } private updateScreenshotThumbnails(): void { - this.screenshotContainer.textContent = ''; + this.screenshotContainer.textContent = ''; - if(this.screenshots.length === 0 && this.recordings.length === 0) { - const empty = append(this.screenshotContainer, $('div.wizard-screenshots-empty')); - empty.textContent = localize('noScreenshots', "No screenshots or recordings added yet"); - return; -} + if (this.screenshots.length === 0 && this.recordings.length === 0) { + const empty = append(this.screenshotContainer, $('div.wizard-screenshots-empty')); + empty.textContent = localize('noScreenshots', "No screenshots or recordings added yet"); + return; + } -for (let i = 0; i < this.screenshots.length; i++) { - const screenshot = this.screenshots[i]; - const card = append(this.screenshotContainer, $('div.wizard-screenshot-card')); + for (let i = 0; i < this.screenshots.length; i++) { + const screenshot = this.screenshots[i]; + const card = append(this.screenshotContainer, $('div.wizard-screenshot-card')); - const img = append(card, $('img')) as HTMLImageElement; - img.src = screenshot.annotatedDataUrl ?? screenshot.dataUrl; - img.alt = localize('screenshotAlt', "Screenshot {0}", i + 1); + const img = append(card, $('img')) as HTMLImageElement; + img.src = screenshot.annotatedDataUrl ?? screenshot.dataUrl; + img.alt = localize('screenshotAlt', "Screenshot {0}", i + 1); - this.disposables.add(addDisposableListener(card, EventType.CLICK, () => { - this._onDidRequestOpenScreenshot.fire(screenshot); - })); + this.disposables.add(addDisposableListener(card, EventType.CLICK, () => { + this._onDidRequestOpenScreenshot.fire(screenshot); + })); - const deleteBtn = append(card, $('div.wizard-screenshot-delete')); - deleteBtn.setAttribute('role', 'button'); - deleteBtn.setAttribute('aria-label', localize('deleteScreenshot', "Delete screenshot")); - deleteBtn.appendChild(renderIcon(Codicon.close)); - this.disposables.add(addDisposableListener(deleteBtn, EventType.CLICK, e => { - e.stopPropagation(); - this.screenshots.splice(i, 1); - this.updateScreenshotThumbnails(); - this.updateAttachmentButtons(); - this.updateStepUI(); - })); -} + const deleteBtn = append(card, $('div.wizard-screenshot-delete')); + deleteBtn.setAttribute('role', 'button'); + deleteBtn.setAttribute('aria-label', localize('deleteScreenshot', "Delete screenshot")); + deleteBtn.appendChild(renderIcon(Codicon.close)); + this.disposables.add(addDisposableListener(deleteBtn, EventType.CLICK, e => { + e.stopPropagation(); + this.screenshots.splice(i, 1); + this.updateScreenshotThumbnails(); + this.updateAttachmentButtons(); + this.updateStepUI(); + })); + } -// Recording thumbnails -for (let i = 0; i < this.recordings.length; i++) { - const rec = this.recordings[i]; - const card = append(this.screenshotContainer, $('div.wizard-screenshot-card.wizard-recording-card')); + // Recording thumbnails + for (let i = 0; i < this.recordings.length; i++) { + const rec = this.recordings[i]; + const card = append(this.screenshotContainer, $('div.wizard-screenshot-card.wizard-recording-card')); - // Show video thumbnail if available - if (rec.thumbnailDataUrl) { - const thumbImg = append(card, $('img.wizard-screenshot-img')); - thumbImg.setAttribute('src', rec.thumbnailDataUrl); - thumbImg.setAttribute('draggable', 'false'); - } + // Show video thumbnail if available + if (rec.thumbnailDataUrl) { + const thumbImg = append(card, $('img.wizard-screenshot-img')); + thumbImg.setAttribute('src', rec.thumbnailDataUrl); + thumbImg.setAttribute('draggable', 'false'); + } - // Dark overlay with play icon - const playOverlay = append(card, $('div.wizard-recording-play')); - playOverlay.appendChild(renderIcon(Codicon.play)); + // Dark overlay with play icon + const playOverlay = append(card, $('div.wizard-recording-play')); + playOverlay.appendChild(renderIcon(Codicon.play)); - const durSec = Math.floor(rec.durationMs / 1000); - const durLabel = append(card, $('div.wizard-recording-duration')); - durLabel.textContent = `${Math.floor(durSec / 60)}:${(durSec % 60).toString().padStart(2, '0')}`; + const durSec = Math.floor(rec.durationMs / 1000); + const durLabel = append(card, $('div.wizard-recording-duration')); + durLabel.textContent = `${Math.floor(durSec / 60)}:${(durSec % 60).toString().padStart(2, '0')}`; - // Click to open from OS - this.disposables.add(addDisposableListener(card, EventType.CLICK, () => { - this._onDidRequestOpenRecording.fire(rec.filePath); - })); + // Click to open from OS + this.disposables.add(addDisposableListener(card, EventType.CLICK, () => { + this._onDidRequestOpenRecording.fire(rec.filePath); + })); - const deleteBtn = append(card, $('div.wizard-screenshot-delete')); - deleteBtn.setAttribute('role', 'button'); - deleteBtn.setAttribute('aria-label', localize('deleteRecording', "Remove recording")); - deleteBtn.appendChild(renderIcon(Codicon.close)); - this.disposables.add(addDisposableListener(deleteBtn, EventType.CLICK, e => { - e.stopPropagation(); - this.recordings.splice(i, 1); - this.updateScreenshotThumbnails(); - this.updateAttachmentButtons(); - this.updateStepUI(); - })); -} + const deleteBtn = append(card, $('div.wizard-screenshot-delete')); + deleteBtn.setAttribute('role', 'button'); + deleteBtn.setAttribute('aria-label', localize('deleteRecording', "Remove recording")); + deleteBtn.appendChild(renderIcon(Codicon.close)); + this.disposables.add(addDisposableListener(deleteBtn, EventType.CLICK, e => { + e.stopPropagation(); + this.recordings.splice(i, 1); + this.updateScreenshotThumbnails(); + this.updateAttachmentButtons(); + this.updateStepUI(); + })); + } -if (this.getTotalAttachments() < MAX_ATTACHMENTS) { - const addCard = append(this.screenshotContainer, $('div.wizard-screenshot-card.wizard-screenshot-add')); - const plus = append(addCard, $('div.wizard-screenshot-plus')); - plus.appendChild(renderIcon(Codicon.add)); - this.disposables.add(addDisposableListener(addCard, EventType.CLICK, () => { - this._onDidRequestScreenshot.fire(); - })); -} + if (this.getTotalAttachments() < MAX_ATTACHMENTS) { + const addCard = append(this.screenshotContainer, $('div.wizard-screenshot-card.wizard-screenshot-add')); + const plus = append(addCard, $('div.wizard-screenshot-plus')); + plus.appendChild(renderIcon(Codicon.add)); + this.disposables.add(addDisposableListener(addCard, EventType.CLICK, () => { + this._onDidRequestScreenshot.fire(); + })); + } } private openAnnotationEditor(index: number): void { - if(index < 0 || index >= this.screenshots.length) { - return; -} + if (index < 0 || index >= this.screenshots.length) { + return; + } -const screenshot = this.screenshots[index]; -const targetWindow = getWindow(this.wizardPanel); -const editor = new ScreenshotAnnotationEditor(screenshot, targetWindow.document.body); + const screenshot = this.screenshots[index]; + const targetWindow = getWindow(this.wizardPanel); + const editor = new ScreenshotAnnotationEditor(screenshot, targetWindow.document.body); -editor.onDidSave(annotatedDataUrl => { - screenshot.annotatedDataUrl = annotatedDataUrl; - this.updateScreenshotThumbnails(); -}); + editor.onDidSave(annotatedDataUrl => { + screenshot.annotatedDataUrl = annotatedDataUrl; + this.updateScreenshotThumbnails(); + }); -editor.onDidCancel(() => { - // nothing to do, editor disposes itself -}); + editor.onDidCancel(() => { + // nothing to do, editor disposes itself + }); } -getScreenshots(): readonly IScreenshot[] { - return this.screenshots; -} + getScreenshots(): readonly IScreenshot[] { + return this.screenshots; + } -getRecordings(): readonly { filePath: string; durationMs: number; thumbnailDataUrl ?: string } [] { - return this.recordings; -} + getRecordings(): readonly { filePath: string; durationMs: number; thumbnailDataUrl?: string }[] { + return this.recordings; + } private buildIssueBody(): string { - const description = this.composeDescription(); - this.model.update({ issueDescription: description }); + const description = this.composeDescription(); + this.model.update({ issueDescription: description }); - let body = this.model.serialize(); + let body = this.model.serialize(); - if (this.includeSettings && this.settingsContent) { - body += `\n
User Settings\n\n\`\`\`json\n${this.settingsContent}\n\`\`\`\n\n
`; - if (this.workspaceSettingsContent) { - body += `\n
Workspace Settings\n\n\`\`\`json\n${this.workspaceSettingsContent}\n\`\`\`\n\n
`; + if (this.includeSettings && this.settingsContent) { + body += `\n
User Settings\n\n\`\`\`json\n${this.settingsContent}\n\`\`\`\n\n
`; + if (this.workspaceSettingsContent) { + body += `\n
Workspace Settings\n\n\`\`\`json\n${this.workspaceSettingsContent}\n\`\`\`\n\n
`; + } + } + + if (this.screenshots.length > 0) { + body += '\n\n### Screenshots\n\n'; + for (let i = 0; i < this.screenshots.length; i++) { + body += `\n`; + } + } + + return body; + } + + isVisible(): boolean { + return this.visible; + } + + focus(): void { + this.wizardPanel.focus(); + } + + getPanel(): HTMLElement { + return this.wizardPanel; + } + + hideFloatingBar(): void { + if (this.floatingBar) { + this.floatingBar.style.display = 'none'; } } - if (this.screenshots.length > 0) { - body += '\n\n### Screenshots\n\n'; - for (let i = 0; i < this.screenshots.length; i++) { - body += `\n`; + showFloatingBar(): void { + if (this.floatingBar && this.currentStep === WizardStep.Screenshots) { + this.floatingBar.style.display = ''; } } - return body; -} - -isVisible(): boolean { - return this.visible; -} - -focus(): void { - this.wizardPanel.focus(); -} - -getPanel(): HTMLElement { - return this.wizardPanel; -} - -hideFloatingBar(): void { - if(this.floatingBar) { - this.floatingBar.style.display = 'none'; -} + /** Update the internal model with additional data loaded asynchronously */ + updateModel(newData: Record): void { + this.model.update(newData); + // Refresh review details if we're on the review step (async data may have arrived) + if (this.currentStep === WizardStep.Review) { + this.updateReviewDetails(); + } } -showFloatingBar(): void { - if(this.floatingBar && this.currentStep === WizardStep.Screenshots) { - this.floatingBar.style.display = ''; -} + setSettingsContent(userSettings: string, workspaceSettings?: string): void { + this.settingsContent = userSettings; + this.workspaceSettingsContent = workspaceSettings; + if (this.currentStep === WizardStep.Review) { + this.updateReviewDetails(); + } } -/** Update the internal model with additional data loaded asynchronously */ -updateModel(newData: Record): void { - this.model.update(newData); - // Refresh review details if we're on the review step (async data may have arrived) - if(this.currentStep === WizardStep.Review) { - this.updateReviewDetails(); -} + hasUnsavedChanges(): boolean { + if (this.submitted) { + return false; + } + return this.hasUserInput(); } -setSettingsContent(userSettings: string, workspaceSettings ?: string): void { - this.settingsContent = userSettings; - this.workspaceSettingsContent = workspaceSettings; - if(this.currentStep === WizardStep.Review) { - this.updateReviewDetails(); -} - } - -setActiveThemeName(name: string): void { - this.activeThemeName = name; -} - -hasUnsavedChanges(): boolean { - if (this.submitted) { - return false; - } - return this.hasUserInput(); -} - private hasUserInput(): boolean { - return !!( - this.hasDescriptionContent() || - this.titleInput.value.trim() || - this.selectedIssueType !== undefined || - this.screenshots.length > 0 || - this.recordings.length > 0 - ); -} - -/** Mark as submitted โ€” locks navigation and disables discard dialog */ -markAsSubmitted(): void { - this.submitted = true; -} - -/** Show a "Close" button next to the submit button after successful submission */ -showCloseButton(): void { - this.backButton.style.display = 'none'; - - // Add close button next to the existing preview button - const nav = this.nextButton.parentElement; - if(nav && !nav.querySelector('.wizard-close-btn')) { - const closeBtn = append(nav, $('div.wizard-nav-btn.wizard-close-btn')); - closeBtn.setAttribute('role', 'button'); - closeBtn.setAttribute('tabindex', '0'); - const closeLbl = append(closeBtn, $('span')); - closeLbl.textContent = localize('closeTab', "Close"); - this.disposables.add(addDisposableListener(closeBtn, EventType.CLICK, () => { - this._onDidClose.fire(); - })); -} + return !!( + this.hasDescriptionContent() || + this.titleInput.value.trim() || + this.selectedIssueType !== undefined || + this.screenshots.length > 0 || + this.recordings.length > 0 + ); } -getWizardHeight(): number { - return this.wizardPanel.offsetHeight; -} + /** Mark as submitted โ€” locks navigation and disables discard dialog */ + markAsSubmitted(): void { + this.submitted = true; + } -setRecordingState(state: RecordingState): void { - this.currentRecordingState = state; + /** Show a "Close" button next to the submit button after successful submission */ + showCloseButton(): void { + this.backButton.element.style.display = 'none'; - if(state === RecordingState.Recording) { - // Switch to recording mode: disable all wizard UI except stop button - this.wizardPanel.classList.add('wizard-recording'); - if (this.recordBtn) { - this.recordBtn.classList.add('recording'); - this.recordLabel.textContent = localize('stopRecording', "Stop recording"); - this.recordingElapsedLabel.style.display = ''; - this.recordingStartTime = Date.now(); - this.recordingElapsedLabel.textContent = '0:00'; - this.recordingElapsedTimer = setInterval(() => { - const elapsed = Math.floor((Date.now() - this.recordingStartTime) / 1000); - const mins = Math.floor(elapsed / 60); - const secs = elapsed % 60; - const timeStr = `${mins}:${secs.toString().padStart(2, '0')}`; - this.recordingElapsedLabel.textContent = timeStr; - if (this.captureStripRecordElapsed) { - this.captureStripRecordElapsed.textContent = timeStr; + // Add close button next to the existing preview button + const nav = this.nextButton.element.parentElement; + if (nav && !nav.querySelector('.wizard-close-btn')) { + const closeBtn = this.disposables.add(new Button(nav, { ...defaultButtonStyles, secondary: true })); + closeBtn.label = localize('closeTab', "Close"); + closeBtn.element.classList.add('wizard-close-btn'); + this.disposables.add(closeBtn.onDidClick(() => { + this._onDidClose.fire(); + })); + } + } + + getWizardHeight(): number { + return this.wizardPanel.offsetHeight; + } + + setRecordingState(state: RecordingState): void { + this.currentRecordingState = state; + + if (state === RecordingState.Recording) { + // Switch to recording mode: disable all wizard UI except stop button + this.wizardPanel.classList.add('wizard-recording'); + if (this.recordBtn) { + this.recordBtn.element.classList.add('recording'); + this.recordBtn.label = localize('stopRecording', "Stop recording"); + this.recordingElapsedLabel.style.display = ''; + this.recordingStartTime = Date.now(); + this.recordingElapsedLabel.textContent = '0:00'; + this.recordingElapsedTimer = setInterval(() => { + const elapsed = Math.floor((Date.now() - this.recordingStartTime) / 1000); + const mins = Math.floor(elapsed / 60); + const secs = elapsed % 60; + const timeStr = `${mins}:${secs.toString().padStart(2, '0')}`; + this.recordingElapsedLabel.textContent = timeStr; + if (this.captureStripRecordElapsed) { + this.captureStripRecordElapsed.textContent = timeStr; + } + }, 1000); } - }, 1000); - } - // Update floating bar record button - if (this.captureStripRecordBtn) { - this.captureStripRecordBtn.classList.add('recording'); - this.captureStripRecordBtn.title = localize('stopRecording', "Stop recording"); - if (this.captureStripRecordLbl) { - this.captureStripRecordLbl.textContent = localize('stopRecording', "Stop recording"); - } - if (this.captureStripRecordElapsed) { - this.captureStripRecordElapsed.style.display = ''; - this.captureStripRecordElapsed.textContent = '0:00'; + // Update floating bar record button + if (this.captureStripRecordBtn) { + this.captureStripRecordBtn.element.classList.add('recording'); + this.captureStripRecordBtn.element.title = localize('stopRecording', "Stop recording"); + this.captureStripRecordBtn.label = `$(stop-circle) ${localize('stopRecording', "Stop recording")}`; + if (this.captureStripRecordElapsed) { + this.captureStripRecordElapsed.style.display = ''; + this.captureStripRecordElapsed.textContent = '0:00'; + } + } + // Dim other capture strip controls during recording + if (this.floatingBar) { + this.floatingBar.classList.add('wizard-strip-recording'); + } + } else { + // Back to idle + this.wizardPanel.classList.remove('wizard-recording'); + if (this.recordBtn) { + this.recordBtn.element.classList.remove('recording'); + this.recordBtn.label = localize('recordVideo', "Record video"); + this.recordingElapsedLabel.style.display = 'none'; + } + if (this.recordingElapsedTimer !== undefined) { + clearInterval(this.recordingElapsedTimer); + this.recordingElapsedTimer = undefined; + } + + // Update floating bar record button + if (this.captureStripRecordBtn) { + this.captureStripRecordBtn.element.classList.remove('recording'); + this.captureStripRecordBtn.element.title = localize('recordVideo', "Record video"); + this.captureStripRecordBtn.label = `$(record) ${localize('recordVideo', "Record video")}`; + if (this.captureStripRecordElapsed) { + this.captureStripRecordElapsed.style.display = 'none'; + } + } + if (this.floatingBar) { + this.floatingBar.classList.remove('wizard-strip-recording'); + } } } - // Dim other capture strip controls during recording - if (this.floatingBar) { - this.floatingBar.classList.add('wizard-strip-recording'); - } -} else { - // Back to idle - this.wizardPanel.classList.remove('wizard-recording'); - if (this.recordBtn) { - this.recordBtn.classList.remove('recording'); - this.recordLabel.textContent = localize('recordVideo', "Record video"); - this.recordingElapsedLabel.style.display = 'none'; - } - if (this.recordingElapsedTimer !== undefined) { - clearInterval(this.recordingElapsedTimer); - this.recordingElapsedTimer = undefined; + + addRecording(filePath: string, durationMs: number, thumbnailDataUrl?: string): void { + this.recordings.push({ filePath, durationMs, thumbnailDataUrl }); + this.updateScreenshotThumbnails(); + this.updateAttachmentButtons(); + this.updateStepUI(); } - // Update floating bar record button - if (this.captureStripRecordBtn) { - this.captureStripRecordBtn.classList.remove('recording'); - this.captureStripRecordBtn.title = localize('recordVideo', "Record video"); - if (this.captureStripRecordLbl) { - this.captureStripRecordLbl.textContent = localize('recordVideo', "Record video"); + dispose(): void { + if (this.recordingElapsedTimer !== undefined) { + clearInterval(this.recordingElapsedTimer); } - if (this.captureStripRecordElapsed) { - this.captureStripRecordElapsed.style.display = 'none'; - } - } - if (this.floatingBar) { - this.floatingBar.classList.remove('wizard-strip-recording'); - } -} - } - -addRecording(filePath: string, durationMs: number, thumbnailDataUrl ?: string): void { - this.recordings.push({ filePath, durationMs, thumbnailDataUrl }); - this.updateScreenshotThumbnails(); - this.updateAttachmentButtons(); - this.updateStepUI(); -} - -dispose(): void { - if(this.recordingElapsedTimer !== undefined) { - clearInterval(this.recordingElapsedTimer); -} -this.disposables.dispose(); -this._onDidClose.dispose(); -this._onDidSubmit.dispose(); -this._onDidRequestScreenshot.dispose(); -this._onDidRequestStartRecording.dispose(); -this._onDidRequestStopRecording.dispose(); -this._onDidRequestOpenRecording.dispose(); -this._onDidRequestOpenScreenshot.dispose(); + this.disposables.dispose(); + this._onDidClose.dispose(); + this._onDidSubmit.dispose(); + this._onDidRequestScreenshot.dispose(); + this._onDidRequestStartRecording.dispose(); + this._onDidRequestStopRecording.dispose(); + this._onDidRequestOpenRecording.dispose(); + this._onDidRequestOpenScreenshot.dispose(); } } diff --git a/src/vs/workbench/contrib/issue/browser/media/issueReporterOverlay.css b/src/vs/workbench/contrib/issue/browser/media/issueReporterOverlay.css index 8c31a75d27b..1c8e7fde3b6 100644 --- a/src/vs/workbench/contrib/issue/browser/media/issueReporterOverlay.css +++ b/src/vs/workbench/contrib/issue/browser/media/issueReporterOverlay.css @@ -44,6 +44,14 @@ height: 100%; } +.issue-reporter-editor-tab, +.issue-reporter-editor-tab:focus, +.issue-reporter-editor-tab:focus-visible, +.issue-reporter-wizard:focus, +.issue-reporter-wizard:focus-visible { + outline: none !important; +} + /* โ”€โ”€ Floating Capture Bar (debug toolbar style) โ”€โ”€ */ .wizard-floating-bar { position: fixed; @@ -88,6 +96,20 @@ line-height: 1; } +.wizard-floating-bar .monaco-button { + display: flex; + flex-direction: row; + align-items: center; + gap: 5px; + padding: 4px 10px; + font-size: 12px; + line-height: 1; + width: auto; + flex: none; + height: 26px; + box-sizing: border-box; +} + .wizard-floating-bar .wizard-nav-btn > * { display: flex; align-items: center; @@ -117,7 +139,7 @@ display: flex; align-items: center; gap: 5px; - padding: 3px 10px; + padding: 4px 10px; cursor: pointer; font-size: 12px; border-radius: 4px 0 0 4px; @@ -125,6 +147,8 @@ background: var(--vscode-button-background, #0e639c); border: 1px solid var(--vscode-button-background, #0e639c); border-right: none; + height: 26px; + box-sizing: border-box; } .wizard-segmented-main:hover { @@ -139,13 +163,15 @@ .wizard-segmented-dropdown { display: flex; align-items: center; - padding: 3px 4px; + padding: 4px 4px; cursor: pointer; border-radius: 0 4px 4px 0; color: var(--vscode-button-foreground, #fff); background: var(--vscode-button-background, #0e639c); border: 1px solid var(--vscode-button-background, #0e639c); border-left: 1px solid rgba(255, 255, 255, 0.2); + height: 26px; + box-sizing: border-box; } .wizard-segmented-dropdown:hover { @@ -157,53 +183,15 @@ align-items: center; } -.wizard-segmented-delay-label { - font-size: 11px; - color: var(--vscode-button-foreground, #fff); - background: var(--vscode-button-background, #0e639c); - padding: 1px 5px; - border-radius: 8px; - margin-left: -6px; - margin-right: 2px; - position: relative; - top: -8px; - font-weight: 600; -} - -/* Delay dropdown menu */ -.wizard-delay-menu { - position: absolute; - top: 100%; - right: 0; - margin-top: 4px; - background: var(--vscode-menu-background, var(--vscode-editor-background, #252526)); - border: 1px solid var(--vscode-menu-border, var(--vscode-input-border, #444)); - border-radius: 4px; - box-shadow: var(--vscode-shadow-lg, 0 2px 8px rgba(0, 0, 0, 0.4)); - z-index: 10; - min-width: 140px; - padding: 4px 0; -} - -.wizard-delay-menu-item { - padding: 4px 12px; - font-size: 12px; - cursor: pointer; - color: var(--vscode-menu-foreground, var(--vscode-foreground, #ccc)); - white-space: nowrap; -} - -.wizard-delay-menu-item:hover { - background: var(--vscode-menu-selectionBackground, var(--vscode-list-hoverBackground, rgba(255, 255, 255, 0.08))); - color: var(--vscode-menu-selectionForeground, var(--vscode-foreground, #fff)); -} - -.wizard-delay-menu-item.selected { - font-weight: 600; +/* Dim screenshot button during recording (keep drag handle interactive) */ +.wizard-floating-bar.wizard-strip-recording .wizard-segmented-btn { + opacity: 0.3; + pointer-events: none; } /* Recording state in floating bar */ -.wizard-floating-bar .wizard-nav-btn.recording { +.wizard-floating-bar .wizard-nav-btn.recording, +.wizard-floating-bar .monaco-button.recording { color: var(--vscode-testing-iconErrored, #f14c4c); animation: recording-pulse 1.5s ease-in-out infinite; } @@ -285,49 +273,6 @@ overflow-y: auto; position: absolute; inset: 0; - transition: transform 0.25s ease, opacity 0.25s ease; -} - -.wizard-step.slide-out-left { - transform: translateX(-100%); - opacity: 0; -} - -.wizard-step.slide-out-right { - transform: translateX(100%); - opacity: 0; -} - -.wizard-step.slide-in-right { - animation: slideInRight 0.25s ease forwards; -} - -.wizard-step.slide-in-left { - animation: slideInLeft 0.25s ease forwards; -} - -@keyframes slideInRight { - from { - transform: translateX(100%); - opacity: 0; - } - - to { - transform: translateX(0); - opacity: 1; - } -} - -@keyframes slideInLeft { - from { - transform: translateX(-100%); - opacity: 0; - } - - to { - transform: translateX(0); - opacity: 1; - } } /* โ”€โ”€ Wizard Typography โ”€โ”€ */ @@ -372,13 +317,17 @@ color: var(--vscode-input-placeholderForeground, #888); } -.wizard-textarea.invalid-input, -.wizard-title-input.invalid-input { +.wizard-textarea.invalid-input { border-color: var(--vscode-inputValidation-errorBorder, #be1100) !important; background: var(--vscode-inputValidation-errorBackground, rgba(190, 17, 0, 0.1)); } -/* โ”€โ”€ Category Buttons (Step 2) โ”€โ”€ */ +.wizard-describe-content.invalid-input .wizard-textarea { + border-color: var(--vscode-inputValidation-errorBorder, #be1100) !important; + background: var(--vscode-inputValidation-errorBackground, rgba(190, 17, 0, 0.1)); +} + +/* โ”€โ”€ Category Buttons (Step 1) โ€” radio button style โ”€โ”€ */ .wizard-type-buttons { display: flex; gap: 10px; @@ -390,17 +339,49 @@ display: flex; align-items: center; gap: 8px; - padding: 10px 20px; - border-radius: 20px; + padding: 8px 16px; + border-radius: 4px; border: 1px solid var(--vscode-input-border, #555); background: transparent; cursor: pointer; font-size: 13px; color: var(--vscode-foreground, #ccc); - transition: all 0.15s ease; + transition: border-color 0.15s ease; user-select: none; } +.wizard-type-btn::before { + content: ''; + display: inline-block; + width: 14px; + height: 14px; + border-radius: 50%; + border: 2px solid var(--vscode-input-border, #555); + background: transparent; + flex-shrink: 0; + transition: border-color 0.15s ease; +} + +.wizard-type-btn:hover { + border-color: var(--vscode-focusBorder, #007acc); +} + +.wizard-type-btn:hover::before { + border-color: var(--vscode-focusBorder, #007acc); +} + +.wizard-type-btn.selected { + border-color: var(--vscode-focusBorder, #007acc); + background: transparent; + color: var(--vscode-foreground, #ccc); +} + +.wizard-type-btn.selected::before { + border-color: var(--vscode-focusBorder, #007acc); + background: var(--vscode-focusBorder, #007acc); + box-shadow: inset 0 0 0 2px var(--vscode-editor-background, #1e1e1e); +} + /* Shortcut badge โ€” shared style for keyboard shortcut indicators */ .wizard-shortcut-badge { display: inline-flex; @@ -418,33 +399,17 @@ margin-left: 2px; } -.wizard-nav-btn.primary .wizard-shortcut-badge { +.wizard-nav-btn.primary .wizard-shortcut-badge, +.monaco-button .wizard-shortcut-badge { border-color: rgba(255, 255, 255, 0.3); color: var(--vscode-button-foreground, #fff); opacity: 0.7; } -.wizard-type-btn.selected .wizard-shortcut-badge { - border-color: rgba(255, 255, 255, 0.4); - color: var(--vscode-button-foreground, #fff); - opacity: 0.8; -} - .wizard-type-buttons.invalid-input .wizard-type-btn { border-color: var(--vscode-inputValidation-errorBorder, #be1100); } -.wizard-type-btn:hover { - border-color: var(--vscode-focusBorder, #007acc); - background: rgba(255, 255, 255, 0.04); -} - -.wizard-type-btn.selected { - border-color: var(--vscode-focusBorder, #007acc); - background: var(--vscode-focusBorder, #007acc); - color: var(--vscode-button-foreground, #fff); -} - .wizard-type-icon { display: flex; align-items: center; @@ -570,7 +535,8 @@ gap: 6px; } -.wizard-capture-btn.disabled { +.wizard-capture-btn.disabled, +.monaco-button.wizard-capture-btn:disabled { opacity: 0.5; pointer-events: none; } @@ -605,26 +571,10 @@ flex-direction: column; } -.wizard-title-input { - width: 100%; - height: 32px; - font-size: 13px; - font-family: inherit; - color: var(--vscode-input-foreground, #ccc); - background: var(--vscode-input-background, #3c3c3c); - border: 1px solid var(--vscode-input-border, #555); - border-radius: 4px; - padding: 0 10px; - outline: none; - box-sizing: border-box; -} - -.wizard-title-input:focus { - border-color: var(--vscode-focusBorder, #007acc); -} - -.wizard-title-input::placeholder { - color: var(--vscode-input-placeholderForeground, #888); +.wizard-field .monaco-inputbox.invalid-input, +.wizard-field .monaco-inputbox.invalid-input .input { + border-color: var(--vscode-inputValidation-errorBorder, #be1100) !important; + background: var(--vscode-inputValidation-errorBackground, rgba(190, 17, 0, 0.1)); } .wizard-review-details { @@ -700,7 +650,8 @@ } /* Button spinner */ -.wizard-nav-btn.uploading { +.wizard-nav-btn.uploading, +.monaco-button.uploading { pointer-events: none; opacity: 0.8; } @@ -837,17 +788,12 @@ flex-shrink: 0; } -.review-diag-toggle { +.review-diag-toggle.monaco-button { flex-shrink: 0; - font-size: 12px !important; - padding: 2px 6px !important; - box-sizing: border-box; - justify-content: center; -} - -.review-diag-toggle > * { - display: flex; - align-items: center; + font-size: 12px; + padding: 2px 8px; + width: auto; + flex: none; } .review-diag-content { @@ -926,6 +872,14 @@ background: var(--vscode-editor-background, #1e1e1e); } +/* Button widget overrides for wizard nav */ +.wizard-nav .monaco-button { + padding: 6px 16px; + white-space: nowrap; + width: auto; + flex: none; +} + /* โ”€โ”€ Navigation buttons โ”€โ”€ */ .wizard-nav-btn { display: flex; @@ -1148,13 +1102,15 @@ Recording UI โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• */ -.wizard-record-btn { +.wizard-record-btn, +.monaco-button.wizard-record-btn { gap: 6px; border-color: #c33 !important; color: #c33 !important; } -.wizard-record-btn:hover { +.wizard-record-btn:hover, +.monaco-button.wizard-record-btn:hover { background: rgba(204, 51, 51, 0.1) !important; } @@ -1164,7 +1120,8 @@ color: #c33; } -.wizard-record-btn.recording { +.wizard-record-btn.recording, +.monaco-button.wizard-record-btn.recording { background: #c33 !important; border-color: #c33 !important; color: #fff !important; @@ -1175,7 +1132,8 @@ color: #fff; } -.wizard-record-btn.recording:hover { +.wizard-record-btn.recording:hover, +.monaco-button.wizard-record-btn.recording:hover { background: #a22 !important; }