feat: redesign settings panel — sidebar navigation, pill tags, toggle switches, repo cards - #56
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add Tauri command to read the git origin remote URL from a local repo, plus a TypeScript utility to parse GitHub owner/repo from remote URLs. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace the 380px single-panel form with a 680px sidebar-navigated settings dialog. Three categories (General, Git & GitHub, Notifications) are navigable via a left sidebar. Tracked repos use card-based UI with add-from-disk auto-detection. Labels and ignored folders use TagInput. Notification toggles use ToggleSwitch components. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Global token bumps: - --text-primary: 0.9 → 0.92 - --text-secondary: 0.6 → 0.65 - --text-muted: 0.3 → 0.42 - --text-ghost: 0.15 → 0.25 Settings panel: - Section headers: ghost → muted, 10px → 11px - Field labels: muted → secondary, 11px → 12px - Inputs: 12px → 13px - Hints: ghost → muted, 10px → 11px - Toggle labels: 12px → 13px - Toggle descriptions: ghost → muted, 10px → 11px Launch screen: - Tip text: ghost → muted, 12px → 13px - Hero hint: ghost → muted, 12px → 13px - Session card time: ghost → muted - Session card meta: ghost → muted, 11px → 12px - Column labels: muted → secondary, 11px → 12px Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR redesigns the Settings dialog into a sidebar-navigated panel with reusable UI controls and adds a Tauri command to auto-detect GitHub remotes when adding repos from disk.
Changes:
- Replaced the single settings form with a 3-category (General / Git & GitHub / Notifications) sidebar layout and new repo-card based tracked repo UI.
- Added reusable
TagInput(pill editor) andToggleSwitchcomponents for a denser, more modern settings UX. - Added
get_git_remote_url(Rust) +getGitRemoteUrl(TS) andparseGitHubRepoto support “Add from disk” repo detection.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 9 comments.
Show a summary per file
| File | Description |
|---|---|
| src/lib/utils/parseGitRemote.ts | Adds parseGitHubRepo helper to extract owner/repo from remote URLs. |
| src/lib/tauri.ts | Adds getGitRemoteUrl() invoke wrapper for the new Rust command. |
| src/components/ui/ToggleSwitch.svelte | New toggle switch component for Notifications. |
| src/components/ui/TagInput.svelte | New pill-tag editor component for labels/ignored folders. |
| src/components/SettingsDialog.svelte | Major settings UI redesign: sidebar navigation, repo cards, tag inputs, toggle rows, “Add from disk” flow. |
| src-tauri/src/lib.rs | Registers the new get_git_remote_url command. |
| src-tauri/src/commands/git.rs | Implements get_git_remote_url using git2 to read the origin remote URL. |
| docs/superpowers/specs/2026-03-30-settings-redesign-design.md | Adds a design spec documenting the new settings layout and flows. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const remoteUrl = await getGitRemoteUrl(selected); | ||
| if (remoteUrl) { | ||
| const parsed = parseGitHubRepo(remoteUrl); | ||
| if (parsed) { | ||
| trackedReposList = [...trackedReposList, { repo: parsed, localPath: selected }]; |
There was a problem hiding this comment.
In addRepoFromDisk, the tracked repo is saved with localPath: selected, but get_git_remote_url uses Repository::discover, so selected may be a subdirectory of the repo. Downstream code opens tracked repos via Repository::open(local_path), which will fail if local_path is not the repo root. Consider resolving the git root (e.g., via the existing get_git_root command or by returning the discovered workdir from Rust) and storing that path instead of the raw selection.
| <label>Author name</label> | ||
| <input bind:value={author} /> |
There was a problem hiding this comment.
The "Author name" <label> is no longer associated with its <input> (no for/id, and the input is not nested inside the label). This breaks screen reader labeling and also causes existing tests that use findByLabelText("Author name") to fail. Add an id on the input and for on the label (or wrap the input in the label).
| <label>Author name</label> | |
| <input bind:value={author} /> | |
| <label for="author-name">Author name</label> | |
| <input id="author-name" bind:value={author} /> |
| <label>Default labels</label> | ||
| <TagInput | ||
| tags={labelsList} | ||
| onAdd={(tag) => { labelsList = [...labelsList, tag]; }} | ||
| onRemove={(i) => { labelsList = labelsList.filter((_, idx) => idx !== i); }} | ||
| placeholder="Add label..." | ||
| /> | ||
| </div> | ||
| <div class="settings-field"> | ||
| <label>Ignored folders</label> | ||
| <TagInput | ||
| tags={ignoredFoldersList} | ||
| onAdd={(tag) => { ignoredFoldersList = [...ignoredFoldersList, tag]; }} | ||
| onRemove={(i) => { ignoredFoldersList = ignoredFoldersList.filter((_, idx) => idx !== i); }} | ||
| placeholder="Add folder..." | ||
| mono |
There was a problem hiding this comment.
TagInput is rendered under a plain <label> that isn't programmatically connected to the actual <input> inside TagInput, so the tag editor has no accessible name. Consider adding a prop to TagInput to pass an aria-label/aria-labelledby through to its inner <input> (and wire the label up here), so screen readers can identify the control.
| <label>Default labels</label> | |
| <TagInput | |
| tags={labelsList} | |
| onAdd={(tag) => { labelsList = [...labelsList, tag]; }} | |
| onRemove={(i) => { labelsList = labelsList.filter((_, idx) => idx !== i); }} | |
| placeholder="Add label..." | |
| /> | |
| </div> | |
| <div class="settings-field"> | |
| <label>Ignored folders</label> | |
| <TagInput | |
| tags={ignoredFoldersList} | |
| onAdd={(tag) => { ignoredFoldersList = [...ignoredFoldersList, tag]; }} | |
| onRemove={(i) => { ignoredFoldersList = ignoredFoldersList.filter((_, idx) => idx !== i); }} | |
| placeholder="Add folder..." | |
| mono | |
| <label id="default-labels-label">Default labels</label> | |
| <TagInput | |
| tags={labelsList} | |
| onAdd={(tag) => { labelsList = [...labelsList, tag]; }} | |
| onRemove={(i) => { labelsList = labelsList.filter((_, idx) => idx !== i); }} | |
| placeholder="Add label..." | |
| aria-labelledby="default-labels-label" | |
| /> | |
| </div> | |
| <div class="settings-field"> | |
| <label id="ignored-folders-label">Ignored folders</label> | |
| <TagInput | |
| tags={ignoredFoldersList} | |
| onAdd={(tag) => { ignoredFoldersList = [...ignoredFoldersList, tag]; }} | |
| onRemove={(i) => { ignoredFoldersList = ignoredFoldersList.filter((_, idx) => idx !== i); }} | |
| placeholder="Add folder..." | |
| mono | |
| aria-labelledby="ignored-folders-label" |
| <div class="settings-repo-name">{repo.repo}</div> | ||
| <div class="settings-repo-path">{repo.localPath}</div> | ||
| </div> | ||
| <button class="settings-repo-remove" onclick={() => removeRepo(i)}>×</button> |
There was a problem hiding this comment.
The repo remove button only contains "×" and has no accessible name. Add an aria-label (e.g., "Remove tracked repo") so screen readers can announce the action.
| <button class="settings-repo-remove" onclick={() => removeRepo(i)}>×</button> | |
| <button | |
| class="settings-repo-remove" | |
| onclick={() => removeRepo(i)} | |
| aria-label="Remove tracked repo" | |
| >×</button> |
| <input bind:value={newRepoName} placeholder="owner/repo" /> | ||
| <div class="settings-path-picker-inline"> | ||
| <input bind:value={newRepoPath} placeholder="/path/to/checkout" /> | ||
| <button onclick={browseRepoPath}>📁</button> |
There was a problem hiding this comment.
The folder browse button is icon-only (📁) and has no accessible name. Add an aria-label (e.g., "Browse for repository path") so it’s usable with assistive tech.
| <button onclick={browseRepoPath}>📁</button> | |
| <button onclick={browseRepoPath} aria-label="Browse for repository path">📁</button> |
| <div>Agent replied to annotation</div> | ||
| <div class="settings-toggle-desc">When an agent responds to one of your comments</div> | ||
| </div> | ||
| <ToggleSwitch checked={notifyAnnotationReply} onToggle={() => notifyAnnotationReply = !notifyAnnotationReply} /> | ||
| </div> |
There was a problem hiding this comment.
Each ToggleSwitch is adjacent to visible text, but the switch itself has no accessible name (aria-label/aria-labelledby). For role="switch" controls, ensure the switch is labeled (e.g., pass an ariaLabel prop or wire aria-labelledby to the text element here).
| let activeCategory = $state<"general" | "git" | "notifications">("general"); | ||
|
|
There was a problem hiding this comment.
This redesign introduces new UI behaviors (sidebar category switching, tag editing, repo card add/remove, "Add from disk" flow). There is an existing SettingsDialog.test.ts in the repo that asserts on the old labeled inputs/buttons; it will now fail and does not cover the new flows. Update/add tests to match the new structure and mock getGitRemoteUrl + the dialog picker.
| {#each tags as tag, i} | ||
| <span class="tag-pill"> | ||
| {tag} | ||
| <button class="tag-remove" onclick={() => onRemove(i)}>×</button> |
There was a problem hiding this comment.
The tag remove button only renders "×" and has no accessible name. Add an aria-label (e.g., "Remove tag") and set type="button" so it can’t accidentally submit a surrounding form.
| <button class="tag-remove" onclick={() => onRemove(i)}>×</button> | |
| <button | |
| class="tag-remove" | |
| type="button" | |
| aria-label="Remove tag" | |
| onclick={() => onRemove(i)} | |
| > | |
| × | |
| </button> |
| onToggle, | ||
| }: { | ||
| checked: boolean; | ||
| onToggle: () => void; | ||
| } = $props(); | ||
| </script> | ||
|
|
||
| <button | ||
| class="toggle-switch" | ||
| class:toggle-switch-on={checked} | ||
| onclick={onToggle} | ||
| role="switch" | ||
| aria-checked={checked} |
There was a problem hiding this comment.
ToggleSwitch renders a <button role="switch"> without an accessible name and without type="button". For accessibility, expose aria-label/aria-labelledby (or accept a label prop) so the switch can be named by consumers; adding type="button" prevents accidental form submission when used inside a form.
| onToggle, | |
| }: { | |
| checked: boolean; | |
| onToggle: () => void; | |
| } = $props(); | |
| </script> | |
| <button | |
| class="toggle-switch" | |
| class:toggle-switch-on={checked} | |
| onclick={onToggle} | |
| role="switch" | |
| aria-checked={checked} | |
| onToggle, | |
| label, | |
| ariaLabel, | |
| ariaLabelledby, | |
| }: { | |
| checked: boolean; | |
| onToggle: () => void; | |
| label?: string; | |
| ariaLabel?: string; | |
| ariaLabelledby?: string; | |
| } = $props(); | |
| </script> | |
| <button | |
| class="toggle-switch" | |
| class:toggle-switch-on={checked} | |
| type="button" | |
| onclick={onToggle} | |
| role="switch" | |
| aria-checked={checked} | |
| aria-label={ariaLabel ?? label} | |
| aria-labelledby={ariaLabelledby} |
Summary
Replace the single-panel settings form with a professional sidebar-navigated settings panel.
New UI components:
TagInput.svelte— pill tag editor with Enter/comma to add, × to remove, backspace on empty, monospace variantToggleSwitch.svelte— accessible toggle with amber/grey statesThree categories:
New Rust command:
get_git_remote_url— reads the origin remote URL from a local git repo (used by "Add from disk" flow)New utility:
parseGitHubRepo— extractsowner/repofrom HTTPS and SSH GitHub URLsTest plan
🤖 Generated with Claude Code