diff --git a/src/renderer/src/components/NewWorkspaceComposerCard.tsx b/src/renderer/src/components/NewWorkspaceComposerCard.tsx
index 478494916..2447e45f2 100644
--- a/src/renderer/src/components/NewWorkspaceComposerCard.tsx
+++ b/src/renderer/src/components/NewWorkspaceComposerCard.tsx
@@ -1219,7 +1219,7 @@ export default function NewWorkspaceComposerCard({
diff --git a/src/renderer/src/components/contextual-tours/ContextualTourControl.tsx b/src/renderer/src/components/contextual-tours/ContextualTourControl.tsx
index 7898b5795..0a2186c50 100644
--- a/src/renderer/src/components/contextual-tours/ContextualTourControl.tsx
+++ b/src/renderer/src/components/contextual-tours/ContextualTourControl.tsx
@@ -66,7 +66,7 @@ function AutoRenameBranchFromWorkControl(): JSX.Element {
diff --git a/src/renderer/src/components/feature-wall/AiCommitPrSettingsSwitch.tsx b/src/renderer/src/components/feature-wall/AiCommitPrSettingsSwitch.tsx
index 7655cd88e..751722d95 100644
--- a/src/renderer/src/components/feature-wall/AiCommitPrSettingsSwitch.tsx
+++ b/src/renderer/src/components/feature-wall/AiCommitPrSettingsSwitch.tsx
@@ -27,7 +27,7 @@ export function AiCommitPrSettingsSwitch({
diff --git a/src/renderer/src/components/feature-wall/KeepAwakeCard.tsx b/src/renderer/src/components/feature-wall/KeepAwakeCard.tsx
index 6340d2f1b..2a7b57adc 100644
--- a/src/renderer/src/components/feature-wall/KeepAwakeCard.tsx
+++ b/src/renderer/src/components/feature-wall/KeepAwakeCard.tsx
@@ -38,7 +38,7 @@ export function KeepAwakeCard(props: {
diff --git a/src/renderer/src/components/settings/AccountsPane.tsx b/src/renderer/src/components/settings/AccountsPane.tsx
index d9db52dd9..0a3dd5ae7 100644
--- a/src/renderer/src/components/settings/AccountsPane.tsx
+++ b/src/renderer/src/components/settings/AccountsPane.tsx
@@ -1570,7 +1570,7 @@ export function AccountsPane({
>
diff --git a/src/renderer/src/components/settings/AgentAwakeSetting.tsx b/src/renderer/src/components/settings/AgentAwakeSetting.tsx
index 66e56c32a..28cd7cc01 100644
--- a/src/renderer/src/components/settings/AgentAwakeSetting.tsx
+++ b/src/renderer/src/components/settings/AgentAwakeSetting.tsx
@@ -50,7 +50,7 @@ export function AgentAwakeSetting({
>
diff --git a/src/renderer/src/components/settings/AutoRenameBranchFromWorkSetting.tsx b/src/renderer/src/components/settings/AutoRenameBranchFromWorkSetting.tsx
index c4d520cbd..2cbdf227e 100644
--- a/src/renderer/src/components/settings/AutoRenameBranchFromWorkSetting.tsx
+++ b/src/renderer/src/components/settings/AutoRenameBranchFromWorkSetting.tsx
@@ -186,7 +186,7 @@ export function AutoRenameBranchFromWorkSetting({
>
diff --git a/src/renderer/src/components/settings/BrowserLinkRoutingSetting.tsx b/src/renderer/src/components/settings/BrowserLinkRoutingSetting.tsx
index 6b0885a86..b26c1e2fd 100644
--- a/src/renderer/src/components/settings/BrowserLinkRoutingSetting.tsx
+++ b/src/renderer/src/components/settings/BrowserLinkRoutingSetting.tsx
@@ -54,7 +54,7 @@ export function BrowserLinkRoutingSetting({
>
diff --git a/src/renderer/src/components/settings/BrowserUseEnableSwitch.tsx b/src/renderer/src/components/settings/BrowserUseEnableSwitch.tsx
index cbb4c3361..28a071b15 100644
--- a/src/renderer/src/components/settings/BrowserUseEnableSwitch.tsx
+++ b/src/renderer/src/components/settings/BrowserUseEnableSwitch.tsx
@@ -21,7 +21,7 @@ export function BrowserUseEnableSwitch({
>
diff --git a/src/renderer/src/components/settings/CliSection.tsx b/src/renderer/src/components/settings/CliSection.tsx
index 41984fe20..86a5c89f3 100644
--- a/src/renderer/src/components/settings/CliSection.tsx
+++ b/src/renderer/src/components/settings/CliSection.tsx
@@ -299,7 +299,7 @@ export function CliSection({
>
diff --git a/src/renderer/src/components/settings/CommitMessageAiPane.tsx b/src/renderer/src/components/settings/CommitMessageAiPane.tsx
index e24fc97ee..13f1b10cf 100644
--- a/src/renderer/src/components/settings/CommitMessageAiPane.tsx
+++ b/src/renderer/src/components/settings/CommitMessageAiPane.tsx
@@ -209,7 +209,7 @@ export function CommitMessageAiPane({
>
diff --git a/src/renderer/src/components/settings/ExperimentalPane.tsx b/src/renderer/src/components/settings/ExperimentalPane.tsx
index 62d8420be..9760591d4 100644
--- a/src/renderer/src/components/settings/ExperimentalPane.tsx
+++ b/src/renderer/src/components/settings/ExperimentalPane.tsx
@@ -99,7 +99,7 @@ export function ExperimentalPane({
>
@@ -144,7 +144,7 @@ export function ExperimentalPane({
>
@@ -203,7 +203,7 @@ export function ExperimentalPane({
>
diff --git a/src/renderer/src/components/settings/GitPane.tsx b/src/renderer/src/components/settings/GitPane.tsx
index da1a34c3f..490cef11e 100644
--- a/src/renderer/src/components/settings/GitPane.tsx
+++ b/src/renderer/src/components/settings/GitPane.tsx
@@ -291,7 +291,7 @@ export function GitPane({
>
@@ -385,7 +385,7 @@ export function GitPane({
>
diff --git a/src/renderer/src/components/settings/InputPane.tsx b/src/renderer/src/components/settings/InputPane.tsx
index 55094ec4a..125e90cc0 100644
--- a/src/renderer/src/components/settings/InputPane.tsx
+++ b/src/renderer/src/components/settings/InputPane.tsx
@@ -70,7 +70,7 @@ export function InputPane({ settings, updateSettings }: InputPaneProps): React.J
>
diff --git a/src/renderer/src/components/settings/NotificationSettingToggle.tsx b/src/renderer/src/components/settings/NotificationSettingToggle.tsx
index 4de95f53b..935023924 100644
--- a/src/renderer/src/components/settings/NotificationSettingToggle.tsx
+++ b/src/renderer/src/components/settings/NotificationSettingToggle.tsx
@@ -39,7 +39,7 @@ export function NotificationSettingToggle({
>
diff --git a/src/renderer/src/components/settings/PrivacyPane.tsx b/src/renderer/src/components/settings/PrivacyPane.tsx
index f2334e867..8a096f8c2 100644
--- a/src/renderer/src/components/settings/PrivacyPane.tsx
+++ b/src/renderer/src/components/settings/PrivacyPane.tsx
@@ -127,7 +127,7 @@ export function PrivacyPane({ settings }: PrivacyPaneProps): React.JSX.Element {
>
diff --git a/src/renderer/src/components/settings/SettingsFormControls.tsx b/src/renderer/src/components/settings/SettingsFormControls.tsx
index a2d57a6ac..27bcf7acc 100644
--- a/src/renderer/src/components/settings/SettingsFormControls.tsx
+++ b/src/renderer/src/components/settings/SettingsFormControls.tsx
@@ -52,7 +52,7 @@ export function SettingsSwitch({
diff --git a/src/renderer/src/components/settings/TerminalWindowSection.tsx b/src/renderer/src/components/settings/TerminalWindowSection.tsx
index 0cd000602..3ca33d25c 100644
--- a/src/renderer/src/components/settings/TerminalWindowSection.tsx
+++ b/src/renderer/src/components/settings/TerminalWindowSection.tsx
@@ -133,7 +133,7 @@ export function TerminalWindowSection({
>
@@ -270,7 +270,7 @@ export function TerminalWindowSection({
diff --git a/src/renderer/src/components/settings/VoiceDictationSettingsSection.tsx b/src/renderer/src/components/settings/VoiceDictationSettingsSection.tsx
index f148e631d..3747781b9 100644
--- a/src/renderer/src/components/settings/VoiceDictationSettingsSection.tsx
+++ b/src/renderer/src/components/settings/VoiceDictationSettingsSection.tsx
@@ -51,7 +51,7 @@ export function VoiceDictationSettingsSection({
>
diff --git a/src/renderer/src/components/settings/WslCliRegistration.tsx b/src/renderer/src/components/settings/WslCliRegistration.tsx
index 7d01d96d5..586d30684 100644
--- a/src/renderer/src/components/settings/WslCliRegistration.tsx
+++ b/src/renderer/src/components/settings/WslCliRegistration.tsx
@@ -198,7 +198,7 @@ export function WslCliRegistration({
>
diff --git a/src/renderer/src/components/sidebar/HostRemoveDialog.tsx b/src/renderer/src/components/sidebar/HostRemoveDialog.tsx
index 97135f97e..d99f5e860 100644
--- a/src/renderer/src/components/sidebar/HostRemoveDialog.tsx
+++ b/src/renderer/src/components/sidebar/HostRemoveDialog.tsx
@@ -251,7 +251,7 @@ export function HostRemoveDialog({
diff --git a/src/renderer/src/components/stats/ClaudeUsageLoadingState.tsx b/src/renderer/src/components/stats/ClaudeUsageLoadingState.tsx
index e459ab9c6..59c378cc8 100644
--- a/src/renderer/src/components/stats/ClaudeUsageLoadingState.tsx
+++ b/src/renderer/src/components/stats/ClaudeUsageLoadingState.tsx
@@ -21,7 +21,7 @@ export function ClaudeUsageLoadingState({
diff --git a/src/renderer/src/components/stats/ClaudeUsagePane.tsx b/src/renderer/src/components/stats/ClaudeUsagePane.tsx
index 32536628f..dfd83b666 100644
--- a/src/renderer/src/components/stats/ClaudeUsagePane.tsx
+++ b/src/renderer/src/components/stats/ClaudeUsagePane.tsx
@@ -233,7 +233,7 @@ export function ClaudeUsagePane(): React.JSX.Element {
onClick={() => handleSetEnabled(false)}
className="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent bg-foreground transition-colors"
>
-
+
diff --git a/src/renderer/src/components/stats/CodexUsagePane.tsx b/src/renderer/src/components/stats/CodexUsagePane.tsx
index 2ebd6913d..4ae5074de 100644
--- a/src/renderer/src/components/stats/CodexUsagePane.tsx
+++ b/src/renderer/src/components/stats/CodexUsagePane.tsx
@@ -235,7 +235,7 @@ export function CodexUsagePane(): React.JSX.Element {
onClick={() => handleSetEnabled(false)}
className="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent bg-foreground transition-colors"
>
-
+
diff --git a/src/renderer/src/components/stats/OpenCodeUsagePane.tsx b/src/renderer/src/components/stats/OpenCodeUsagePane.tsx
index c09f2a5a1..b70b06593 100644
--- a/src/renderer/src/components/stats/OpenCodeUsagePane.tsx
+++ b/src/renderer/src/components/stats/OpenCodeUsagePane.tsx
@@ -246,7 +246,7 @@ export function OpenCodeUsagePane(): React.JSX.Element {
onClick={() => handleSetEnabled(false)}
className="relative inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border border-transparent bg-foreground transition-colors"
>
-
+
diff --git a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandAppendEnterSwitch.tsx b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandAppendEnterSwitch.tsx
index ad45db4b0..0ddd62224 100644
--- a/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandAppendEnterSwitch.tsx
+++ b/src/renderer/src/components/terminal-quick-commands/TerminalQuickCommandAppendEnterSwitch.tsx
@@ -39,7 +39,7 @@ export function TerminalQuickCommandAppendEnterSwitch({
>
diff --git a/src/renderer/src/components/toggle-switch-handle-alignment.test.ts b/src/renderer/src/components/toggle-switch-handle-alignment.test.ts
new file mode 100644
index 000000000..b0a5574dd
--- /dev/null
+++ b/src/renderer/src/components/toggle-switch-handle-alignment.test.ts
@@ -0,0 +1,231 @@
+import { readdirSync, readFileSync, statSync } from 'node:fs'
+import { relative, resolve } from 'node:path'
+// TypeScript 7 is a native CLI; AST tests still need the legacy JavaScript API.
+import ts from 'typescript-api'
+import { describe, expect, it } from 'vitest'
+
+// Why: the h-5 w-9 switch is hand-rolled at every call site instead of living in a
+// primitive, so the handle offset drifts. The 1px-bordered track leaves a 34px content
+// box for the 14px (`size-3.5`) handle, and `translate-x-0.5` insets the off state by
+// 2px, so a symmetric on state is 34 - 14 - 2 = 18px (`translate-x-4.5`).
+
+const RENDERER_ROOT = resolve('src/renderer/src')
+
+const OFF_STATE = 'translate-x-0.5'
+const ON_STATE = 'translate-x-4.5'
+
+/** The track geometry the arithmetic above is derived from; other switch sizes are not ours to check. */
+const TRACK = ['h-5', 'w-9']
+
+/** The handle's 14px size, in both the shorthand and long-hand spellings in use. */
+const HANDLE_SIZE = [['size-3.5'], ['h-3.5', 'w-3.5']]
+
+/** Every current handle is the round knob; this is what separates it from a size-3.5 icon in the same track. */
+const HANDLE_SHAPE = 'rounded-full'
+
+/** Any horizontal translate — negative, arbitrary, variant-prefixed or named — so no respelling reads as "no offset". */
+const OFFSET = /(?:^|:)-?translate-x-\S+$/
+
+/** Anything that moves the track off the 34px content box the 18px on-state is derived from. */
+const BORDER_WIDTH = /^border(?:-[xytrbles])?-(?:\d+|\[)/
+const TRACK_PADDING = /^p[xytrbles]?-(?:\d|\[)/
+
+function collectSourceFiles(dir: string, files: string[] = []): string[] {
+ for (const name of readdirSync(dir)) {
+ const filePath = resolve(dir, name)
+ if (statSync(filePath).isDirectory()) {
+ collectSourceFiles(filePath, files)
+ } else if (name.endsWith('.tsx') && !name.endsWith('.test.tsx')) {
+ files.push(filePath)
+ }
+ }
+ return files
+}
+
+/** Class tokens anywhere under a node, flattening the ternaries, template holes and `cn()` args a className is built from. */
+function tokensIn(node: ts.Node): string[] {
+ const tokens: string[] = []
+ const read = (child: ts.Node): void => {
+ if (ts.isStringLiteral(child) || ts.isTemplateLiteralToken(child)) {
+ tokens.push(...child.text.split(/\s+/).filter(Boolean))
+ }
+ ts.forEachChild(child, read)
+ }
+ read(node)
+ return tokens
+}
+
+/** Reading the className attribute's own subtree keeps a child element's classes out of its parent's token set. */
+function classNameOf(element: ts.JsxOpeningLikeElement): ts.Node | undefined {
+ const className = element.attributes.properties.find(
+ (property): property is ts.JsxAttribute =>
+ ts.isJsxAttribute(property) && property.name.getText() === 'className'
+ )
+ return className?.initializer
+}
+
+function hasAll(tokens: string[], required: string[]): boolean {
+ return required.every((token) => tokens.includes(token))
+}
+
+/**
+ * The offsets each side of a conditional className contributes. A handle that toggles is
+ * two-state by construction, so this tells a deliberately single-state knob apart from one
+ * that lost a branch — which a flat token list cannot.
+ */
+function offsetBranches(node: ts.Node): [string[], string[]][] {
+ const branches: [string[], string[]][] = []
+ const read = (child: ts.Node): void => {
+ if (ts.isConditionalExpression(child)) {
+ const whenTrue = tokensIn(child.whenTrue).filter((token) => OFFSET.test(token))
+ const whenFalse = tokensIn(child.whenFalse).filter((token) => OFFSET.test(token))
+ if (whenTrue.length > 0 || whenFalse.length > 0) {
+ branches.push([whenTrue, whenFalse])
+ return
+ }
+ }
+ ts.forEachChild(child, read)
+ }
+ read(node)
+ return branches
+}
+
+function trackDrift(tokens: string[]): string | undefined {
+ if (!tokens.includes('border')) {
+ return 'no 1px border'
+ }
+ if (tokens.includes('box-content')) {
+ return 'box-content'
+ }
+ return tokens.find((token) => BORDER_WIDTH.test(token) || TRACK_PADDING.test(token))
+}
+
+type Handle = { at: string; offsets: string[] }
+type Toggle = { at: string; branches: [string[], string[]] }
+type Scan = { handles: Handle[]; toggles: Toggle[]; drifted: string[]; knobless: string[] }
+
+/**
+ * Handles are found *inside* a track element rather than by scanning source text forward, so
+ * neither a sibling element nor a same-file icon can be mistaken for the knob.
+ */
+function collectFile(filePath: string, source: string, scan: Scan): void {
+ const sourceFile = ts.createSourceFile(
+ filePath,
+ source,
+ ts.ScriptTarget.Latest,
+ true,
+ ts.ScriptKind.TSX
+ )
+
+ const at = (node: ts.Node): string => {
+ const { line } = sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile))
+ return `${relative(RENDERER_ROOT, filePath)}:${line + 1}`
+ }
+
+ const readHandles = (node: ts.Node): void => {
+ if (ts.isJsxOpeningLikeElement(node)) {
+ const className = classNameOf(node)
+ const tokens = className ? tokensIn(className) : []
+ if (
+ className &&
+ HANDLE_SIZE.some((size) => hasAll(tokens, size)) &&
+ tokens.includes(HANDLE_SHAPE)
+ ) {
+ scan.handles.push({ at: at(node), offsets: tokens.filter((token) => OFFSET.test(token)) })
+ for (const branches of offsetBranches(className)) {
+ scan.toggles.push({ at: at(node), branches })
+ }
+ }
+ }
+ ts.forEachChild(node, readHandles)
+ }
+
+ const visit = (node: ts.Node): void => {
+ if (ts.isJsxOpeningLikeElement(node)) {
+ const className = classNameOf(node)
+ const tokens = className ? tokensIn(className) : []
+ if (hasAll(tokens, TRACK)) {
+ const drift = trackDrift(tokens)
+ if (drift) {
+ scan.drifted.push(`${at(node)}: ${drift}`)
+ }
+ // The knob lives in the track's subtree; a self-closing track has no subtree to walk.
+ const found = scan.handles.length
+ if (ts.isJsxOpeningElement(node)) {
+ readHandles(node.parent)
+ }
+ // A track whose knob moved into a child component would otherwise leave the scan silently.
+ if (scan.handles.length === found) {
+ scan.knobless.push(at(node))
+ }
+ return
+ }
+ }
+ ts.forEachChild(node, visit)
+ }
+
+ visit(sourceFile)
+}
+
+let cached: Scan | undefined
+
+/** Parsing every gated file is the expensive part, so all three assertions share one scan. */
+function scanRenderer(): Scan {
+ if (!cached) {
+ const scan: Scan = { handles: [], toggles: [], drifted: [], knobless: [] }
+
+ for (const filePath of collectSourceFiles(RENDERER_ROOT)) {
+ const source = readFileSync(filePath, 'utf8')
+ if (TRACK.every((token) => source.includes(token))) {
+ collectFile(filePath, source, scan)
+ }
+ }
+
+ // A track nested inside another h-5 w-9 element would otherwise be collected twice.
+ const seen = new Set()
+ scan.handles = scan.handles.filter((handle) => !seen.has(handle.at) && seen.add(handle.at))
+ cached = scan
+ }
+
+ // A scan that stops finding switches has lost its bounds, not proven the tree clean. The
+ // floor only has to catch a collapse — `knobless` and `drifted` catch the structural cases —
+ // so it sits well under the ~34 handles in the tree rather than pinning the current count.
+ expect(cached.handles.length, 'the switch scan lost its bounds').toBeGreaterThan(25)
+
+ return cached
+}
+
+describe('toggle switch handle alignment', () => {
+ it('insets the handle by 2px at both ends of the track', () => {
+ const offenders = scanRenderer().handles.flatMap((handle) =>
+ handle.offsets.length === 0
+ ? [`${handle.at}: no translate-x offset`]
+ : handle.offsets
+ .filter((offset) => offset !== OFF_STATE && offset !== ON_STATE)
+ .map((offset) => `${handle.at}: ${offset}`)
+ )
+
+ expect(offenders).toEqual([])
+ })
+
+ it('gives every toggling handle both ends of the pair', () => {
+ const pairs = (on: string[], off: string[]): boolean =>
+ on.length === 1 && on[0] === ON_STATE && off.length === 1 && off[0] === OFF_STATE
+
+ const offenders = scanRenderer()
+ .toggles.filter(({ branches: [a, b] }) => !pairs(a, b) && !pairs(b, a))
+ .map(
+ ({ at, branches }) => `${at}: ${branches.map((side) => side.join(' ') || '—').join(' / ')}`
+ )
+
+ expect(offenders).toEqual([])
+ })
+
+ it('keeps every track on the geometry the 18px on-state is derived from', () => {
+ expect(scanRenderer().drifted).toEqual([])
+ })
+
+ it('keeps every track a knob the scan can see', () => {
+ expect(scanRenderer().knobless).toEqual([])
+ })
+})