diff --git a/__dlg-main.js b/__dlg-main.js new file mode 100644 index 00000000..b1bada37 --- /dev/null +++ b/__dlg-main.js @@ -0,0 +1,31 @@ +const { app, BrowserWindow } = require('electron') +const fs = require('fs') +const path = require('path') +app.disableHardwareAcceleration() +const html = fs.readFileSync(path.join(__dirname, '__dlg.html'), 'utf8') +app.whenReady().then(async () => { + const results = [] + for (const [w, h] of [[1440, 900], [1280, 700], [900, 500], [800, 380]]) { + const win = new BrowserWindow({ width: w, height: h, show: false }) + await win.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(html)) + const r = await win.webContents.executeJavaScript(`(() => { + const c = document.getElementById('content') + const b = document.getElementById('body') + const cr = c.getBoundingClientRect() + return { + viewport: innerHeight, + contentH: Math.round(cr.height), + contentTop: Math.round(cr.top), + contentBottom: Math.round(cr.bottom), + bodyClientH: b.clientHeight, + bodyScrollH: b.scrollHeight, + bodyScrolls: b.scrollHeight > b.clientHeight + 1, + overflowsViewport: cr.top < -0.5 || cr.bottom > innerHeight + 0.5, + } + })()`) + results.push({ win: w + 'x' + h, ...r }) + win.destroy() + } + console.log('RESULTS ' + JSON.stringify(results)) + app.quit() +}).catch(e => { console.log('ERR ' + e.message); app.quit() }) diff --git a/__dlg.html b/__dlg.html new file mode 100644 index 00000000..70a810f9 --- /dev/null +++ b/__dlg.html @@ -0,0 +1,30 @@ + +
+
Configure Voice Dictation
Agent Code's inline dictation streams audio to Deepgram for transcription. New Deepgram accounts get $200 in free credits, which is generally enough for very long-term personal use.
+
+

1 Create a Deepgram account

Open console.deepgram.com/signup and finish the signup — the $200 credit is applied automatically.

+

2 Create a project API key

From the console, open API Keys in the sidebar, click Create a New API Key, give it the scope Member or broader, copy the string that appears once.

+

3 Paste the key into Settings

In Agent Code, open Settings → Voice Dictation, paste the key into the Deepgram API Key row, and press Save. Your key is encrypted with your system keyring.

+

A note on the hotkey.

Dictation is triggered with Cmd+Shift+D by default: press once to record and again to finish, with no OS permission required. If you prefer holding Fn like macOS system dictation, switch the shortcut in Settings; macOS will then prompt for Accessibility permission the first time you enable dictation.

+
+
+
+ diff --git a/docs/plans_and_ideas/2026-07-28-ui-primitive-theme-fidelity-plan.md b/docs/plans_and_ideas/2026-07-28-ui-primitive-theme-fidelity-plan.md new file mode 100644 index 00000000..7f7bcb42 --- /dev/null +++ b/docs/plans_and_ideas/2026-07-28-ui-primitive-theme-fidelity-plan.md @@ -0,0 +1,207 @@ +# UI primitive theme fidelity — plan + +**Date:** 2026-07-28 +**Branch:** `feat/ui-primitives-theme-fidelity` +**Status:** plan, then implementation in the same PR + +## The finding + +`src/renderer/src/components/ui/` disagrees with itself about which theme tokens +a control is made of. + +`NumberInput` (`number-input.tsx`, the most recently added primitive) is built +entirely on the `control-*` family: + +``` +container border-control-border bg-control-bg +steppers text-control-fg hover:bg-control-hover-bg hover:text-ink + disabled:hover:bg-control-bg +field text-control-fg +``` + +`Button` (`button.tsx`) is not. Its `outline` variant hardcodes the tokens that +`control-*` happens to *alias to*, rather than the `control-*` tokens +themselves: + +| `Button` variant `outline` | what the app's control chrome uses | +| -------------------------- | ---------------------------------- | +| `border-border` | `border-control-border` | +| `bg-transparent` | `bg-control-bg` | +| `text-ink-dim` | `text-control-fg` | +| `hover:border-border-hi` | `hover:border-control-border-hover`| +| *(no hover background)* | `hover:bg-control-hover-bg` | + +`ghost` has the same problem in miniature: it already reaches for +`hover:bg-control-hover-bg`, but pairs it with `text-ink-dim` instead of +`text-control-fg`. So a single `DialogActions` footer renders a `ghost` Cancel +on `ink-dim` next to a `NumberInput` stepper on `control-fg` — two different +token families, one control strip. + +### Why this is invisible today + +`styles.css` aliases the whole family to exactly those values: + +```css +--theme-control-bg: transparent; +--theme-control-hover-bg: var(--theme-surface-hi); +--theme-control-border: var(--theme-border); +--theme-control-border-hover: var(--theme-border-hi); +--theme-control-fg: var(--theme-ink-dim); +``` + +Under every built-in theme the two columns above render identically. Nothing +looks wrong, which is why this survived. + +### Why it is a real bug anyway + +`customAppearance.ts` exposes all seven `control-*` tokens as independently +user-editable, and documents them as being about buttons specifically: + +``` +controlBg 'Resting button/toggle/select background.' +controlBorder 'Resting control border.' +controlFg 'Resting control text/icon color.' +controlBorderHover 'Hover/focusable control border before true focus.' +``` + +`theme.ts` → `applyCustomAppearance()` writes every one of them as an inline +custom property on ``, where they outrank the `[data-mode]` blocks. + +So the moment a user edits `controlBg`, `controlFg`, or `controlBorder`: + +- the 25 hand-rolled `control-*` buttons in the tree honour it; +- `NumberInput` honours it; +- **every ` + {status.configured && status.source !== 'env' ? ( - + ) : null} diff --git a/src/renderer/src/features/voice-dictation/DictationGuideModal.tsx b/src/renderer/src/features/voice-dictation/DictationGuideModal.tsx index 28cf2ab4..e7c2cb70 100644 --- a/src/renderer/src/features/voice-dictation/DictationGuideModal.tsx +++ b/src/renderer/src/features/voice-dictation/DictationGuideModal.tsx @@ -1,4 +1,13 @@ -import { useCallback, useEffect, useRef, useState } from 'react' +import { useEffect, useState } from 'react' + +import { DialogActions } from '@renderer/components/ui/dialog-actions' +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@renderer/components/ui/dialog' // Modal walking the user through Deepgram signup + key configuration. // @@ -10,6 +19,29 @@ import { useCallback, useEffect, useRef, useState } from 'react' // fully local, and reuses the same overlay stack every other Agent Code // command modal uses. // +// WHY this is a `DialogContent` and no longer a hand-rolled overlay: +// +// This file used to implement its own Escape handler, its own Tab focus +// trap, its own `fixed inset-0` backdrop, its own focus restoration, and it +// copied `data-agent-code-interaction-owner="app"` onto its root by hand. +// components/ui/README.md forbids every one of those by name — they are +// properties of DialogContent, not properties each feature reinterprets, and +// this was the last hand-rolled app modal in the tree. +// +// The hand-rolled trap was also subtly wrong in a way Radix's FocusScope is +// not: it queried focusable nodes once per keydown off a static selector, so +// the `` links inside the guide body participated but anything mounted +// into a nested portal would not have. Deleting it removes the divergence +// rather than fixing it twice. +// +// The one behaviour deliberately NOT carried over is the window-capture +// swallow of Cmd/Option chords. That existed because a hand-rolled overlay +// has no way to tell the workspace shortcut router "an app surface owns the +// turn". DialogContent mounts the ownership marker for exactly its own +// lifetime, and the router already checks that marker synchronously, so the +// chord suppression is now structural instead of a second listener racing +// the first. +// // WHY placeholder screenshots instead of committed assets: // // The user has final say on which screenshots ship — they will match a @@ -18,7 +50,6 @@ import { useCallback, useEffect, useRef, useState } from 'react' // keep the layout obviously incomplete instead of "silently missing". export function DictationGuideModal() { const [open, setOpen] = useState(false) - const dialogRef = useRef(null) useEffect(() => { const onOpen = () => setOpen(true) @@ -26,101 +57,23 @@ export function DictationGuideModal() { return () => window.removeEventListener('agent-code:open-dictation-guide', onOpen) }, []) - const close = useCallback(() => setOpen(false), []) - useEffect(() => { - if (!open) return - const previouslyFocused = document.activeElement instanceof HTMLElement - ? document.activeElement - : null - const focusFrame = requestAnimationFrame(() => dialogRef.current?.focus()) - const onKey = (event: KeyboardEvent) => { - if (event.key === 'Escape') { - event.preventDefault() - event.stopPropagation() - close() - return - } - if (event.key === 'Tab') { - const dialog = dialogRef.current - if (!dialog) return - const focusable = Array.from( - dialog.querySelectorAll( - 'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])', - ), - ) - if (focusable.length === 0) { - event.preventDefault() - dialog.focus() - return - } - const current = focusable.indexOf(document.activeElement as HTMLElement) - const next = event.shiftKey - ? current <= 0 ? focusable.length - 1 : current - 1 - : current === -1 || current === focusable.length - 1 ? 0 : current + 1 - event.preventDefault() - event.stopPropagation() - focusable[next]?.focus() - return - } - // The workspace shortcut router listens on document capture. Capturing - // app-owned Cmd/Option chords one level earlier (window) prevents a guide - // opened above the workspace from splitting/closing panes underneath it. - // Ordinary keys continue to the dialog; type/paste ingress is blocked by - // the explicit ownership marker on the dialog root below. - if (event.metaKey || event.altKey) { - event.preventDefault() - event.stopPropagation() - } - } - window.addEventListener('keydown', onKey, { capture: true }) - return () => { - cancelAnimationFrame(focusFrame) - window.removeEventListener('keydown', onKey, { capture: true }) - previouslyFocused?.focus() - } - }, [open, close]) - - if (!open) return null return ( -
{ - if (event.target === event.currentTarget) close() - }} - > -
-
-

- Configure Voice Dictation -

- -
- -
-

- Agent Code's inline dictation streams audio to{' '} - Deepgram for transcription. New - Deepgram accounts get $200 in free credits, which is generally enough - for very long-term personal use. Follow the three steps below to get - up and running. -

+ + {/* Wider than the 520px default and height-capped: this is three + illustrated steps rather than a confirm prompt, so the body scrolls + inside the dialog instead of letting the surface grow past the + viewport. */} + + + Configure Voice Dictation + + Agent Code's inline dictation streams audio to Deepgram for + transcription. New Deepgram accounts get $200 in free credits, which + is generally enough for very long-term personal use. + + +
-
- -
-
-
+ {/* Acknowledgement-only footer: there is nothing to cancel, so this is + the confirm-only shape DialogActions supports via omitting + onCancel. confirmOnEnter stays on — the body is prose and links, + nothing in it owns Enter. */} + setOpen(false)} /> + + ) } diff --git a/src/renderer/src/features/voice-dictation/DictationHistoryRow.tsx b/src/renderer/src/features/voice-dictation/DictationHistoryRow.tsx index 2c20f530..92cbad45 100644 --- a/src/renderer/src/features/voice-dictation/DictationHistoryRow.tsx +++ b/src/renderer/src/features/voice-dictation/DictationHistoryRow.tsx @@ -1,5 +1,7 @@ import { useCallback, useEffect, useRef, useState } from 'react' +import { Button } from '@renderer/components/ui/button' + import type { DictationHistoryEntry, DictationHistorySnapshot } from '@preload/api/types' // Dictation history + stats panel for the Settings page. @@ -79,13 +81,9 @@ export function DictationHistoryRow() { {error}
- +
) @@ -169,24 +167,26 @@ export function DictationHistoryRow() {
{confirming === null ? (
- - +
) : (
- - +
)}
@@ -328,22 +334,18 @@ function HistoryRow({

{entry.text}

- - +
) : null} diff --git a/src/renderer/src/features/workspace/ui/NewAgentPlacementOverlay.tsx b/src/renderer/src/features/workspace/ui/NewAgentPlacementOverlay.tsx index f9b8d2f5..ad8801f5 100644 --- a/src/renderer/src/features/workspace/ui/NewAgentPlacementOverlay.tsx +++ b/src/renderer/src/features/workspace/ui/NewAgentPlacementOverlay.tsx @@ -5,6 +5,7 @@ import { } from '@shared/types/providerKind' import type { AgentProviderKind } from '@shared/types/providerKind' import { getRendererProviderCapabilities } from '@providers/registry.renderer.capabilities' +import { Button } from '@renderer/components/ui/button' import { useEffect, useMemo, useRef, useState } from 'react' import { @@ -457,13 +458,17 @@ export function NewAgentPlacementOverlay({ itself. Placed here rather than in the kind picker because the placement step is exactly the state that had no way out. */} {selectedKind ? ( - + ) : null} @@ -513,13 +518,9 @@ export function NewAgentPlacementOverlay({ mouse-first user actually looks for — and this is the only step Dispatch and linked-agent mode ever show. */}
- +
diff --git a/src/renderer/src/workspace/tile-tree/TileLeaf/ComposerActions.tsx b/src/renderer/src/workspace/tile-tree/TileLeaf/ComposerActions.tsx index a635da95..4c242868 100644 --- a/src/renderer/src/workspace/tile-tree/TileLeaf/ComposerActions.tsx +++ b/src/renderer/src/workspace/tile-tree/TileLeaf/ComposerActions.tsx @@ -1,3 +1,4 @@ +import { Button } from '@renderer/components/ui/button' import type { DictationStatus } from '@shared/types/dictation' // ComposerActions — pointer-clickable Send and Stop, shown only in Mouse Mode. @@ -86,8 +87,9 @@ export function ComposerActions({ return (
{working ? ( - + ) : null}