Adds a Settings > AI switch to hide the automatic Add to Conversation button on terminal selection while keeping the context menu action available.
Closes#1397
Introduce workspace-aware System side panel with remote process/tmux/Docker management, terminal popup for interactive attach, capability warmup, review-hardened IPC, performance optimizations, toast action errors, and SSH channel recovery on reconnect.
Fixes#1375 by letting Cmd/Ctrl+[1...9] target only work tabs when enabled, while keeping the existing Terminus-style default.
Co-authored-by: Cursor <cursoragent@cursor.com>
* perf(settings): reduce Mac settings window input lag (#1347)
Debounce custom CSS commits, memoize heavy tabs, and replace Radix ScrollArea
with native scrolling so typing and navigation stay responsive on macOS.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(settings): flush debounced textarea on unmount
Avoid losing custom CSS edits when the settings window closes before the
debounce timer fires.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Introduce shared SettingCard and SettingsSection primitives so AI, SFTP,
system, and terminal tabs use the same white-card + row control pattern.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(sftp): add follow terminal directory mode for sidebar (#1266)
Add a toolbar toggle that keeps the side-panel SFTP browser synced with the linked SSH terminal cwd, inspired by MobaXterm's follow-folder behavior.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): probe pwd after commands when follow mode lacks OSC 7
Add a deferred getSessionPwd fallback after terminal commands when follow-terminal-cwd is enabled and the shell did not report OSC 7, and fix settings sync hook dependencies.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): repair follow toggle UI and backend cwd sync fallback
Fix SftpPaneView memo skipping follow prop updates, probe fresh pwd when OSC 7 is missing, and broaden linked terminal session resolution for sidebar follow mode.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(sftp): harden follow sync after review findings
Reuse shouldFollowTerminalCwdNavigate in production path, re-read connection
after async cwd probe, skip redundant navigate on toggle, and only probe pwd
when the SFTP side panel is open.
Co-authored-by: Cursor <cursoragent@cursor.com>
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Expose whole-window transparency via setOpacity with settings and a top-bar quick control, persisting across restarts and syncing across windows.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat(ai): provider switcher chip in the Catty Agent chat input
Catty Agent currently has no model chip in the chat input — modelPresets
is empty for the catty agentId so `hasModelPicker` is false. The
provider/model the chat ends up using comes from the global
`activeProviderId` / `activeModelId`, which means switching providers
requires opening Settings → AI → Providers. Closes the gap surfaced in
the original feedback on #1101 (problem 2) and partially addresses
#986 (per-context model switching) by going per-agent.
State layer adds an `agentProviderMap` (`Record<agentId, providerId>`)
alongside the existing `agentModelMap`. Both are written together when
the user picks from the new dropdown; together they form a per-agent
override that beats the global active provider/model when set. Cross-
window sync mirrors the agentModelMap pattern.
ChatInput grows a `providerSwitcher` prop carrying the enabled
ProviderConfigs, the selected (providerId, modelId), and an onSelect
callback. When supplied, the model chip switches from the Cpu glyph
to the provider's ProviderIconBadge + `providerName · modelId`, and
the popover renders a two-column layout — providers on the left
(icon + name + default model caption), that provider's known model(s)
on the right. ACP agents (Claude/Codex) are untouched because they
plumb their provider through the CLI, not the Vercel AI SDK.
AIChatSidePanel resolves `effectiveActiveProvider` /
`effectiveActiveModelId` for the catty agent from the per-agent map,
falling back to the global selection. handleSend now passes those
through to sendToCattyAgent so a provider picked in one Catty session
applies everywhere Catty runs.
For v1, each provider's right column shows just its configured
`defaultModel`. The two-level structure is in place to absorb richer
listings (cached /models, manual additions) later without UI
surgery — left a note in the popover code.
Refs #1101, #986.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* polish(ai): flatten provider picker to a single list
Two-level dropdown was theatre — each ProviderConfig exposes exactly
one model (its `defaultModel`), so the right column ended up showing
the same string already captioned on the left row. Picking a provider
implicitly picks its model; one click is enough.
Also drops the `p.enabled !== false` filter on the picker list. The
user's expectation, confirmed by feedback on the first cut, is that
the chat-input list mirrors what Settings → AI → Providers shows. The
per-provider `enabled` toggle is an "active-ish" flag, not a
visibility gate; hiding disabled providers in the picker silently
made everything but the one currently active disappear, which is what
the reviewer hit.
Pares the popover down to a vertical list — provider icon + name +
default-model caption (mono if set, italic "configure default model"
hint if not) + a Check on the bound row. Removes the `hoveredProviderId`
state along with the right-column rendering since it's no longer
driving anything.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* polish(ai): tune provider icon sizes in the chat input
Chip icon was sitting at sm (20px badge) and crowded the h-6 toolbar
chip alongside the truncated label; popover rows used the same sm, so
nothing differentiated the picker from the chip visually.
Adds an xs size to ProviderIconBadge (16px badge, 10px glyph, 9px
letter fallback) and uses it on the chip. Popover rows step up to md
(32px badge, 16px glyph) and the row padding grows accordingly so the
larger brand mark has room to breathe — the picker now reads like a
list of choices, the chip reads like a status line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): keep per-agent override honest in model fallback + chip label
Two codex P1/P2 findings on #1107:
P1 — When Catty has a per-agent provider override but no per-agent
model (agentModelMap['catty'] empty), cattyAgentModelId fell through
to the global `activeModelId`. That id belongs to whichever provider
was globally active, not the one Catty is now bound to, so e.g.
overriding to DeepSeek without a defaultModel happily sent gpt-4o
(global) to DeepSeek and produced a wrong-model error. The fallback
is now: stored agent model → override provider's defaultModel → empty.
Only the no-override path keeps the activeModelId tail; the
no-provider send guard catches the empty case for everyone else.
P2 — ChatInput's selectedSwitcherProvider used `?? providers[0]` so
the chip always displayed *some* provider even when none was actually
bound (selectedProviderId missing). The rest of the pipeline (send
guard, agentProviderMap) treats that as "no provider", so the chip
was lying. Dropped the fallback; when nothing is bound the chip now
shows a generic Cpu glyph + "Select provider" label until the user
picks one from the popover. Adds `ai.chat.selectProvider` in en /
zh-CN / ru.
Refs codex review on #1107.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): purge per-agent binding when the bound provider is deleted
Codex local review (round 2) flagged that a saved Catty model id could
outlive its provider. The chain:
- catty bound to provider A (DeepSeek): agentProviderMap['catty']='A',
agentModelMap['catty']='deepseek-v4-flash'
- user deletes provider A
- cattyAgentProvider falls back to the global active provider (B, OpenAI)
- cattyAgentModelId still returned the saved 'deepseek-v4-flash'
- send dispatched the DeepSeek model id to OpenAI → wrong-model error
Two layers of fix:
1. **Defensive resolution** (AIChatSidePanel) — cattyAgentModelId now
looks at the override provider before trusting the stored model id.
If the override is stale (provider deleted), the stored model id is
treated as orphan and the resolution falls back to the global active
selection just like cattyAgentProvider already does.
2. **Cleanup on remove** (useAIState.removeProvider) — when a provider
is deleted, any agent whose agentProviderMap entry pointed at it
gets both maps cleared (provider override + saved model). Same
rationale: that model id is now meaningless against every other
provider. Mirrors the existing activeProviderId cleanup.
Adds a small agentProviderMapRef so removeProvider can snapshot which
agents to clean up without relying on the captured-at-callback-create
state (which would be stale).
Refs codex local review on #1107.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): include agentProviderMap in settings sync + block empty-model send
Codex local review (round 3) found two issues:
P2 — `agentProviderMap` was missing from the cross-device sync surface.
Added it to the sync key list, the build path, the apply path, and the
SyncPayload type alongside the existing `agentModelMap`. Sync tests in
`syncPayload.test.ts` now cover the new field on both the build and
apply sides.
P2 — Provider rows with no `defaultModel` were still clickable in the
chat-input picker, so selecting one would save a binding with empty
model id; the send path then dispatched an empty model name and the
SDK would surface a vague backend error. Two changes:
1. ChatInput disables the row (\`disabled\`, \`aria-disabled\`, dimmed
styling, tooltip pointing the user to Settings) when the provider
has no \`defaultModel\`. Click is suppressed.
2. AIChatSidePanel \`handleSend\` adds a model-required guard mirroring
the existing no-provider guard — surfaces \`ai.chat.noProviderModel\`
as an assistant message. Catches stale bindings (e.g. user edited
the provider's \`defaultModel\` to empty after binding) before they
reach the SDK.
Refs codex local review on #1107.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): trim whitespace-only model ids + reconcile orphan bindings on sync
Codex local review (round 4) found two more leaks:
P2 — Whitespace-only `defaultModel` passed the picker's `disabled` gate
(which trims) but every other layer (cattyAgentModelId resolution,
the send-guard `!sendActiveModelId` check, the SDK call) compared the
raw string. A provider whose default model was " " could still reach
the SDK and surface a vague backend error. Normalized: trim at the
resolution boundary (catty model fallback chain) and trim before the
send-guard's existence check.
P2 — Sync apply did not reconcile per-agent bindings against the
incoming provider set. A payload could change `providers` without
shipping a fresh `agentProviderMap`, leaving local overrides pointing
to provider ids the synced set no longer includes — the same ghost-
binding bug that `removeProvider` already handles for explicit user
deletes. Added `pruneOrphanPerAgentBindings()` that runs after every
AI settings apply: it walks `agentProviderMap`, drops any entry whose
provider id isn't in the current `providers` list, and clears the
saved model id for those agents alongside it (mirroring removeProvider
cleanup). A new test in `syncPayload.test.ts` exercises the ghost-
binding case end-to-end.
Refs codex local review on #1107.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): notify open AI state of cross-device sync apply in the same window
Codex local review (round 4) flagged that `applySyncPayload` writes
straight to localStorage but `useAIState` only re-reads on browser
`storage` events. Those events only fire in *other* windows — the
window doing the apply (the one with the active chat panel) keeps
showing pre-sync providers and per-agent bindings until reload.
Concrete leak: cloud sync swaps in a new provider set, pruning runs,
localStorage is correct, but the open Catty chip still references the
ghost provider binding until the user reloads.
Extracts the existing `AI_STATE_CHANGED_EVENT` constant +
`emitAIStateChanged` helper out of `useAIState` and into a new
`application/state/aiStateEvents.ts` so non-React call sites (sync
apply, future IPC handlers) can fire it without pulling in the hook.
After AI settings apply, `syncPayload.ts` walks every AI key it
touched (including the agentProviderMap / agentModelMap that the
reconcile step may have mutated even when the payload didn't ship
them) and emits a same-window nudge. `useAIState`'s existing local
listener routes unknown keys back through its storage handler, so
each affected piece of React state rehydrates from localStorage.
Adds a regression test in `syncPayload.test.ts` that asserts the
expected `netcatty:ai-state-changed` keys are dispatched for the
providers + per-agent map keys.
Refs codex local review on #1107.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The icon badge next to the Display Name input opens the icon picker
but had no visual cue — users had no reason to suspect it was
clickable, and reviewer feedback flagged it as too hidden.
Adds a primary-tinted ring on hover/focus plus a small pencil glyph
overlaid on the bottom-right corner of the badge (also hover/focus
gated). The badge itself doesn't change shape, so the layout stays
stable; the affordance is purely additive.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ai): customizable provider name, icon, and protocol style
Issue #1101 reported that the only way to wire DeepSeek through the
Catty Agent was to add an "Anthropic" provider with a DeepSeek base URL,
which (a) wasn't relabelable except on the "custom" providerId and
(b) implicitly conflated wire-protocol style with providerId, locking
"custom" to the OpenAI-compatible client.
ProviderConfig now carries three optional fields — `style`
(anthropic/openai/google), `iconId` (built-in brand glyph key), and
`iconDataUrl` (user upload). createModelFromConfig routes on the
resolved style first, falling back to providerId only for per-vendor
quirks (ollama's throwaway apiKey and openrouter's baseURL). Display
name becomes editable for every provider, not just custom.
The icon picker exposes a curated lobe-icons subset (MIT) covering the
common Anthropic/OpenAI-compatible third parties — DeepSeek, Moonshot,
Kimi, Qwen, Zhipu, Doubao, Mistral, Cohere, Grok, Perplexity, Groq,
Hugging Face — plus the existing six built-ins. Uploads are downsampled
to 64×64 WebP on a canvas so localStorage doesn't blow up. See
public/ai/providers/NOTICE.md for attribution.
Closes part of #1101 (problem 2). PR2 will reuse the new name/icon
plumbing in the Catty Agent chat input to surface a provider switcher.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* polish(ai): bigger labelled icon tiles and auto-fill display name
The first pass shipped the icon picker as an 8-column grid of bare
20px badges. Reviewer feedback: the icons are too small to read at a
glance, every preset should announce its brand name, and a second
click on a selected preset should let the user back out.
Switches the grid to auto-fill 120px tiles with a 32px icon plus a
truncated label so the picker reads like a brand list, not a sprite
sheet. Selecting a preset now also writes the brand's canonical
English display name into the Display Name field — the picker labels
keep their "/ 中文" suffix as a bilingual hint, but a new `name` field
on each catalog entry supplies a clean string for autofill (e.g.
"Qwen / 通义" → "Qwen"). Re-clicking the selected tile clears
iconId/iconDataUrl but leaves the typed name alone, so users who
already edited the name don't lose their edit on accidental toggle.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* polish(ai): add explicit close button to icon picker
The picker only opened via the icon badge above the Display Name field,
which left no obvious affordance to collapse it once a preset was
chosen — users either had to scroll past or click back up to the icon.
Drops a ghost Close button in the bottom-right of the picker's action
row (next to Upload/Reset) so dismissing the panel is one click and
visible without hunting.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): wire model discovery to the resolved provider style
Codex review on #1105 flagged that ModelSelector still derives auth
headers from `providerId` (`x-api-key` only when providerId is the
literal "anthropic"). Now that ProviderConfig lets the user override
`style` independently, the form persists the override but the model
discovery call ignored it — so picking an Anthropic providerId pointed
at an OpenAI-compatible backend (or vice versa) would send the wrong
header and fail to list models even though chat routing already used
the override.
Extracts `buildModelDiscoveryHeaders(style, apiKey)` as a pure helper
in infrastructure/ai/. ModelSelector now resolves the protocol family
via `resolveProviderStyle` with the explicit `style` prop taking
precedence, then asks the helper for headers. ProviderConfigForm
passes the resolved style through. The `needsApiKey = providerId !==
"ollama"` shortcut stays as-is — that's a per-vendor concession, not a
style decision.
Refs codex review on #1105.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): apply per-vendor URL fallbacks across every style override
Codex review on #1105 caught that the openrouter (and ollama) baseURL
fallback was sitting inside the `style === 'openai'` branch, so a user
who picked OpenRouter providerId + Anthropic/Google style with an
empty baseURL would skip the fallback entirely. The SDK then routed
to its own default endpoint (api.anthropic.com /
generativelanguage.googleapis.com) using the user's OpenRouter key —
silent misrouting plus auth failures.
Pulls the per-vendor quirks out into a new pure helper
`resolveProviderEndpoint(config, style, safeApiKey)`. The URL
fallback now fires for every style — the user picked openrouter for a
reason, even if they overrode the wire format. The ollama-only
`'ollama'` literal apiKey swap stays gated on `style === 'openai'`
because Anthropic/Google clients need a real key, not the throwaway
placeholder.
Refs codex review on #1105.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): send x-goog-api-key for google-style model discovery
Codex review on #1105 flagged that buildModelDiscoveryHeaders bucketed
google-style providers with the OpenAI-compat default, so a model
refresh against any google-routed endpoint was sending `Authorization:
Bearer …`. That's the wrong auth header — Google Generative AI
rejects Bearer entirely and expects `x-goog-api-key` (or `?key=`).
The runtime chat path already uses `createGoogleGenerativeAI`, which
authenticates with the Google header family, so discovery was the
odd one out.
Adds an explicit `google` branch returning `{ "x-goog-api-key":
<apiKey> }` and updates the unit test. The default google providerId
never hits this path today (PROVIDER_PRESETS["google"] has no
modelsEndpoint), but the style override surface created in this PR
lets users compose providerId + style pairs where discovery does
fire — e.g. an openai/anthropic-style providerId pointed at a Google
endpoint via baseURL override.
Refs codex review on #1105.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): pick discovery endpoint from resolved provider style
Codex review on #1105 pointed out the lopsided fix in ae7394c8: I
switched discovery *headers* to honor the resolved `style` but left
the URL path coming straight from `PROVIDER_PRESETS[providerId]`. So
the moment style and providerId disagree the request shape is half
correct — e.g. Anthropic providerId + style=openai sends Bearer
(right) at `/v1/models` (wrong; the OpenAI-compat backend exposes
/models). 404s either way.
Adds `STYLE_DEFAULT_MODELS_ENDPOINT` (`anthropic → /v1/models`,
`openai → /models`, `google → undefined`) and a
`resolveModelsDiscoveryEndpoint(style, presetEndpoint)` helper. The
style's convention wins; the caller-supplied `presetEndpoint` is the
fallback for styles with no listing convention (currently just
google).
Behavior for stock configs is unchanged — every preset's existing
modelsEndpoint matches its style's default. Mismatches now line up
(headers + path together), and `custom` providers gain a sensible
discovery attempt when the user sets a style. My earlier inline reply
ducking this was wrong; codex's call was right.
Added three unit tests covering style defaults, the override-on-flip,
and the google-style passthrough.
Refs codex review on #1105.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(terminal): add global startupCommandDelayMs setting
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(terminal): add startup-command line-split and delay-clamp helpers
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(terminal): run multi-line startup commands in sequence with configurable delay
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(terminal): expose startup command delay in Terminal settings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(terminal): keep startup-command line content verbatim
splitStartupCommandLines now only drops blank/whitespace-only lines and
normalizes CRLF, but no longer trims each line's content. This keeps a
single-line startup command byte-identical to what the user typed (e.g. a
leading space for HISTCONTROL=ignorespace is preserved), while still
supporting multi-line sequencing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(sync): include startupCommandDelayMs in synced terminal settings
Terminal settings sync via the SYNCABLE_TERMINAL_KEYS allowlist; the new
startupCommandDelayMs preference was missing, so it wouldn't propagate across
devices. Add it (it's a user preference like keepaliveInterval, not
device-specific).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ai): add Claude auth-presence detection helper
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(ai): surface actionable Claude auth errors and reap stuck agent processes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ai): add pure helpers for Claude config dir + env editor
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(ai): let users set Claude config dir and env vars in settings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* polish(ai): harden Claude config env, de-dupe error text, label a11y
- buildClaudeEnv: drop managed keys (CLAUDE_CONFIG_DIR/CLAUDE_CODE_EXECUTABLE)
if a user types them into the free-text env editor, so they can't clobber
the config-dir field or the discovered executable path (+ regression test).
- bridge: only append error data fields not already shown as message/code,
so the actionable error text doesn't echo the same code/message twice.
- ClaudeCodeCard: associate the new config-dir/env labels with their inputs
via htmlFor/id.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(ai): make the Claude auth & config section collapsible
The optional "Authentication & config" section now has a collapsible
header (chevron toggle). Collapsed by default to keep the card tidy, but
auto-expanded when the user already has a config directory or env vars set
so existing config isn't hidden. Local UI state, not persisted.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): preserve raw env editor text; don't encourage plaintext secrets
P1: the env textarea was bound to the persisted value, which is the parsed
env re-serialized — so typing a key before its "=" was erased mid-entry
(buildClaudeEnv drops lines without "="). Keep the raw typed text in local
draft state and only resync from the persisted value on genuine external
changes (not our own parse→serialize round-trip).
P2: the env editor persists to localStorage in plaintext (no credential
encryption). Stop suggesting ANTHROPIC_API_KEY in the placeholder and warn
that values are stored in plaintext, steering credentials to the config
directory (a `claude` login) — consistent with keeping Claude auth
CLI/config-owned (#705).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): expand ~ in Claude config directory before passing to the agent
CLAUDE_CONFIG_DIR is handed to the spawned agent as an env var, which is not
shell-expanded — so "~/.claude" was treated as a literal "~" directory.
Expand a leading ~ at consume time (normalizeAgentEnv + getClaudeConfigDir)
rather than on save, so the stored value stays portable across machines
(cloud sync) and each device expands to its own home.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ai): recreate cached Claude provider when its env config changes
Claude ACP providers are cached per chat session and reused unless one of
the fingerprinted dimensions changes. authFingerprint was null for Claude,
so editing the config directory / env vars in Settings didn't take effect on
an already-running session. Fingerprint the Claude agent env so a config
change invalidates the cached provider and the next turn respawns with it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(terminal): add per-mode follow-theme resolver and storage keys
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(terminal): persist per-mode follow-theme selections in settings state
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(sync): include per-mode follow terminal themes in cloud sync
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* i18n(terminal): add per-mode follow-theme picker strings
* feat(terminal): add type filter and auto option to theme picker
* feat(terminal): pick dark/light terminal theme when following app theme
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(terminal): don't flag the auto sentinel as a missing theme in the picker
The per-mode follow-theme pickers default to the 'auto' sentinel, which is
not a real theme id, so ThemeList's deletedSelectedTheme check classified it
as a deleted custom theme and rendered a spurious "Missing Theme" banner above
the Auto entry on first open. Exclude TERMINAL_THEME_AUTO from that check.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* i18n(terminal): align zh-CN dark/light wording with app convention
Use 深色/浅色 (matching the theme picker section headers and global
appearance settings) instead of 暗色/亮色 for the per-mode terminal
theme labels, so the picker modal reads consistently.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(terminal): match follow-theme preview fallback to runtime resolution
The per-mode preview memos fell back straight to TERMINAL_THEMES[0] when a
selection resolved to a deleted theme, while the runtime currentTerminalTheme
memo falls back to the manual terminalThemeId first. Mirror the runtime chain
so the Settings preview matches the actual terminal for users with a
non-default manual theme.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat #826: optional Option+←/→ word jump on macOS
Adds a Terminal → Keyboard toggle "Option+←/→ jumps by word" (off by default,
synced). When on, a bare Option+Left/Right sends Meta-b / Meta-f instead of
xterm's default ^[[1;3D / ^[[1;3C, so readline/zle moves by word without
per-host bindkey setup (Termius-style).
The key→sequence mapping is a tested pure function; the handler reads the
setting live (no reconnect) and runs after kitty mode + autocomplete so it
doesn't override them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix#826: gate Option+←/→ word jump to macOS
The setting is syncable, so without a platform gate, enabling it on a Mac
would also rewrite Alt+←/→ to Meta-b/f on synced Linux/Windows devices,
breaking apps/shells that expect the default ^[[1;3D / ^[[1;3C. Pass
isMacPlatform() into the mapping so it only applies on macOS; add a test
for the non-macOS case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>