Commit Graph

169 Commits

Author SHA1 Message Date
bincxz e9e8c35178 Add terminal and log timestamps 2026-06-06 17:04:33 +08:00
bincxz 73fd091b80 Cover provider dropdown presentation 2026-06-05 16:43:52 +08:00
bincxz 5bb4052f3d Keep preset model suggestions visible 2026-06-05 16:39:39 +08:00
bincxz 36e7e3cb7f Keep add provider menu inside settings panel 2026-06-05 16:37:54 +08:00
bincxz c9216b32ab Add domestic AI provider presets 2026-06-05 16:28:21 +08:00
陈大猫 bb67aa77f5 [codex] compact long Catty agent context (#1237)
* feat: compact long Catty agent context

* fix: tighten context compaction review issues

* fix: preserve tool context during compaction

* fix: clear stale model metadata on key edits
2026-06-04 17:56:39 +08:00
陈大猫 3fc56df111 refactor(ai): migrate agent backends from ACP to official SDKs (claude/codex/copilot) (#1229) 2026-06-04 17:11:02 +08:00
陈大猫 008890a688 feat(terminal): 自定义本地 Shell 支持启动参数 (#1221) (#1225)
* feat(terminal): 支持为自定义本地 Shell 配置启动参数 (#1221)

自定义本地 Shell 此前只能填可执行文件路径,无法指定启动参数,
导致接入 msys2 bash 时缺少 `--login -i`,shell 不经 profile 初始化、
环境变量缺失而无法使用。

- TerminalSettings 新增 localShellArgs(string[],默认 []),随设置自动迁移
- 新增 domain/shellArgs:引号感知的命令行分词/回显格式化
- resolveShellSetting 透传自定义参数;参数为空时回退到 bridge 默认参数,
  命中已发现 shell(WSL/Git Bash)时忽略自定义参数,避免串味
- 自定义 Shell 弹窗新增「启动参数」输入,设置行展示完整命令(en/zh-CN/ru)

参数复用已有管线 session.localShellArgs → startLocalSession → pty.spawn,
不涉及云同步(与机器相关的 localShell 一致,均不同步)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(terminal): 修复自定义 Shell 参数的两处 review 问题

- 切换到默认/已发现 shell 时清空 localShellArgs:自定义参数仅对自定义
  路径有意义,清空可避免在发现列表尚未加载(启动竞态)或所选 shell ID
  在本机不可用时,旧参数被当作该 shell 的启动参数而泄漏导致启动失败
- formatShellArgs 对含双引号的参数改用单引号包裹,修复 `-c "..."`
  这类参数重新打开弹窗保存时被破坏的往返问题

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(terminal): shellArgs 量化处理含两种引号的参数往返

采用单引号优先的引号策略:单引号内全字面(Windows 路径、双引号原样保留,
无需转义);仅当 token 自身含单引号时改用双引号包裹并转义 \ 与 "。
解析端仅在双引号区内将 \" / \\ 视为转义,其余反斜杠保持字面,
确保未加引号的 Windows 路径不被破坏。修复 `echo "it's ok"` 这类同时含
单双引号的参数在弹窗重新保存时被破坏的问题。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(terminal): 自定义 Shell 已选中后可重新打开编辑参数

自定义 shell 选中时下拉框值已是 __custom__,再次点选「自定义…」不会触发
onValueChange,导致无法重新打开弹窗修改参数/路径。改为把自定义 shell 概要
做成可点击的编辑入口(铅笔图标),点击即重新填充草稿并打开弹窗;
打开逻辑抽到 openCustomShellModal 复用。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(terminal): shellArgs 改用 POSIX 单引号方案,修复尾部反斜杠与空参数

- 解析:两种引号区内均全字面(双引号不再把 \" 当转义),保留 Windows
  路径尾部反斜杠等手输入;引号外仅 \' 转义为字面单引号(支撑 '\'' 习语),
  其余反斜杠保持字面,未加引号的 Windows 路径不受影响
- 格式化:统一单引号包裹(内容全字面),内嵌单引号用 POSIX '\'' 习语;
  显式空参数输出为 '' 以免重新保存时被丢弃

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(terminal): customArgs take precedence over discovered shell defaults when user explicitly sets them

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-04 11:07:13 +08:00
bincxz afe959835d Add SSH debug log setting 2026-06-02 12:01:40 +08:00
陈大猫 1fec5925eb Refactor large modules and fix runtime errors (#1136) 2026-05-28 15:12:19 +08:00
陈大猫 53d3e05bb4 Per-agent provider switcher chip in the Catty Agent chat input (#1107)
* 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>
2026-05-27 00:03:08 +08:00
陈大猫 0c4de74c84 polish(ai): hint that the provider icon is clickable (#1106)
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>
2026-05-26 23:13:15 +08:00
陈大猫 2a4feea40f Customizable provider name, icon, and protocol style (#1105)
* 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>
2026-05-26 22:41:19 +08:00
陈大猫 67448cea65 feat(terminal): configurable startup-command delay + multi-line sequencing (#1096)
* 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>
2026-05-26 00:47:00 +08:00
陈大猫 770b06a9ee fix(ai): make Claude Code agent diagnosable + configurable (auth) (#1095)
* 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>
2026-05-25 23:59:48 +08:00
陈大猫 1d50b2c4a1 feat(terminal): choose dark/light terminal theme when following app theme (#1094)
* 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>
2026-05-25 22:20:45 +08:00
陈大猫 bf1c95500a feat #826: optional Option+←/→ word jump on macOS (#1082)
* 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>
2026-05-25 00:10:40 +08:00
陈大猫 7bd5d6465a fix claude system cli detection (#1033) 2026-05-21 00:11:51 +08:00
yuzifu 3e8965f9a9 Fix pr987 (#1010) 2026-05-19 20:13:16 +08:00
陈大猫 b30696c98b Clean up dead code and duplicated helpers (#1001) 2026-05-18 20:00:10 +08:00
yuzifu 88732040aa fix(terminal): separate prompt after unterminated command output (#987)
* fix(terminal): separate prompt after unterminated command output
Add a display-layer prompt line break handler so recognized shell prompts move to the next visual line when the final command output line is not newline terminated.

Also add a terminal setting to toggle the behavior, sync support, i18n copy, and focused tests for prompt insertion.

* fix review issue

* Fix prompt cache initialization

* Serialize terminal output writes for prompt breaks

* Keep terminal status lines ordered with output

* Fix prompt arming without command callback

* Keep prompt display breaks out of session logs

* Avoid prompt breaks for output suffix matches

---------

Co-authored-by: yuzifu <yuzifu@TB16PGen5.Info>
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
2026-05-18 18:45:41 +08:00
陈大猫 67c5571df5 Fix #958: highlight IPv6 + allow editing built-in keyword rules (#962)
Two changes addressing both halves of #958:

1. IPv6 highlighting
   The built-in 'URL, IP & MAC' rule only shipped URL, IPv4 and MAC
   patterns, so compressed IPv6 addresses such as 2001:11:22:33::5 or
   fe80::d2dd:bff:fe79:f2bb were never highlighted. Add an IPv6 regex
   covering full and compressed forms (including ::1 and leading-/trailing-
   :: variants) and merge it into the same 'ip-mac' rule's patterns. The
   normalizer's existing "fill missing defaults" path means existing users
   pick this up on next start with no migration step.

2. Editable built-in rules
   Add an optional `customized` flag to KeywordHighlightRule. When false /
   absent, normalize re-syncs the rule's label/patterns with the shipped
   defaults (so future default-pattern upgrades reach users automatically).
   When true, normalize keeps the user's label/patterns/color/enabled
   verbatim, allowing built-ins like 'ip-mac' to be tailored.

   SettingsTerminalTab:
   - Pencil icon now appears on built-ins too. Editing one routes through
     the same dialog and flips `customized` on save.
   - The pattern field becomes a Textarea so multi-pattern built-ins (e.g.
     'error' ships seven spellings) can all be edited in one go.
   - A per-rule "↺" reset icon appears on customized built-ins and restores
     the shipped label/patterns while preserving the user's color/enabled.
   - The footer's "Reset to default colors" button is broadened into
     "Reset built-ins to defaults", restoring every built-in to shipped
     label/patterns/color and clearing `customized`.

Tests:
   New domain/keywordHighlight.test.ts (6 tests) covers IPv6 matches for
   both #958 examples plus loopback and full-form, IPv4/MAC still match,
   normalize migrates legacy non-customized 'ip-mac' to include IPv6,
   normalize preserves customized patterns, and normalize keeps user
   custom rules verbatim. Full suite: 808/0/3.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:35:07 +08:00
陈大猫 ea5320d94a Fix #954: unify Tooltip styling + replace native selects (#961)
* Fix #954: unify Tooltip styling + replace native selects

Replace native HTML title= tooltips and native <select> dropdowns
with the existing Radix-based Tooltip / Select components so they
share the app's rounded styling, theme tokens and i18n pipeline.
Adds a global TooltipProvider in AppWithProviders so every
descendant Tooltip works without a per-file Provider wrapper.

Scope (driven by the issue #954 examples and "全部都处理" follow-up):

- TerminalLayer toolbar: Add Terminal / Split View / SFTP / Scripts
  / Theme / AI Chat / Move panel / Close panel.
- TopTabs middle bar: quick switcher, more tabs, AI assistant, theme
  toggle, settings; window-control buttons (min/max/close), tray
  close and hotkey reset/disable have their native title dropped per
  the user's explicit opt-out ("可以不用Tooltip,直接全局禁用
  原生title 属性").
- AI panels: AIChatSidePanel session history / new chat / delete,
  ConversationExport, AgentSelector, ChatInput attach / expand /
  permission, ModelSelector, ProviderCard, ai-elements/tool-call.
- SFTP: SftpSidePanel header, SftpBreadcrumb, SftpFileRow,
  SftpPaneToolbar, SftpTabBar, SftpTransferQueue.
- Settings: SettingsPage close, SettingsAppearanceTab theme/accent
  swatches, SettingsFileAssociationsTab edit/remove, SettingsSystemTab
  crash-log paths and global hotkey reset.
- Host vault: HostDetailsPanel (clear / suggestions / show-password /
  key path / browse key), GroupDetailsPanel, KnownHostsManager,
  ConnectionLogsManager, KeychainManager, SyncStatusButton,
  CloudSyncSettings, LogView, QuickSwitcher, ScriptsSidePanel,
  Terminal status bar copy-host + broadcast/focus, ZmodemProgressIndicator.
- Terminal subcomponents: HostKeywordHighlightPopover, TerminalComposeBar,
  TerminalConnectionDialog, TerminalSearchBar.
- Editor: TextEditorPane (subtitle, search, wrap, promote-to-tab).
- TrayPanel session rows and port-forwarding rows.

Native <select> migrated to custom Select component:
- SerialConnectModal (data bits, stop bits, parity, flow control)
- SerialHostDetailsPanel (same four fields)
- HostDetailsPanel backspace behavior
- GroupDetailsPanel backspace behavior
- SettingsTerminalTab local shell picker
- terminal/ThemeSidePanel font weight

Hardcoded English strings extracted to i18n. New keys for both
en and zh-CN: terminal.layer.*, topTabs.*, ai.chat.* (sessionHistory,
attach, collapse, expand, enableAgent), zmodem.*, settings.shortcuts.
resetToDefault. Inline help text on SnippetsManager package-name input
removed because the same hint is already shown in a visible <p> below
the input.

Existing per-file <TooltipProvider> wrappers (SnippetsManager,
ScriptsSidePanel, SelectHostPanel, RuleCard, HostDetailsPanel proxy
section) are left in place — they nest harmlessly under the global
provider and stay self-sufficient for component tests.

Tests:
- tsc clean for changed files (pre-existing repo-wide errors
  unrelated to this PR).
- All 802 tests pass (3 skipped pre-existing).
- HostDetailsPanel.proxyProfile.test and TextEditorPane.test
  updated to wrap with TooltipProvider, matching the runtime
  context now needed by the migrated components.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Fix #954: wrap Settings + Tray windows with TooltipProvider

Settings and the tray panel mount as separate Electron windows with
their own React root in index.tsx, so they do not inherit the global
TooltipProvider added under AppWithProviders. After the unified
Tooltip migration, any settings tab that used a Tooltip (Appearance,
Application, FileAssociations, System, Shortcuts, Terminal, AI
ProviderCard, AI ModelSelector) — and TrayPanel — threw
"Tooltip must be used within TooltipProvider" and rendered nothing.

Wrap both branches with TooltipProvider at the same level as
ToastProvider in index.tsx.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 20:14:24 +08:00
陈大猫 92ecd84edf Fix #939: per-host SSH keepalive override + cloud-friendly defaults (#947)
* fix(ssh): per-host keepalive override + cloud-friendly defaults (#939, #581)

Issues #939 (cloud / Aliyun sessions silently freezing after 15-20 min idle
because no SSH keepalive packets are sent) and #581 (older routers like
NOKIA / ALCATEL being killed by ssh2 after a few unanswered keepalives) are
in direct tension at the global-setting level: cloud users want keepalive
ON, embedded-device users want it OFF, and any single global default hurts
the other group.

Resolves the conflict by moving keepalive to a per-host setting (mirroring
the existing `legacyAlgorithms` per-host pattern), with cloud-friendly
global defaults:

Domain:
  - Host gains `keepaliveOverride?: boolean` + `keepaliveInterval?: number`
    + `keepaliveCountMax?: number`. When override is true, the host's
    values are used; otherwise the global TerminalSettings values apply.
    Per-field fallback so a host can override interval only or countMax only.
  - TerminalSettings gains `keepaliveCountMax: number` so the second knob
    (number of unanswered keepalives before declaring dead) is no longer
    hardcoded at 3 in the bridge.
  - DEFAULT_TERMINAL_SETTINGS: keepaliveInterval bumped from 0 to 30, and
    keepaliveCountMax = 10. Cloud LBs / NAT tables stay populated; brief
    network glitches don't trip the dead-connection check; an actually
    dead session is detected within ~5 minutes. Existing users with 0
    saved keep their value (no migration) — they were the #581 router
    cohort and their setup still works untouched.

Plumbing:
  - domain/host.ts adds resolveHostKeepalive(host, globalSettings) with
    five unit tests covering both directions of the override flag and
    per-field fallback.
  - components/terminal/runtime/createTerminalSessionStarters.ts uses the
    resolver when building startSSHSession options.
  - electron/bridges/sshBridge.cjs reads keepaliveCountMax from options
    (defaulting to 10) at both connection sites (direct + jump host) and
    still routes interval=0 through to a fully disabled keepalive
    (preserving #581's escape hatch).

UI:
  - Settings → Terminal → Connection grows a second input next to the
    existing interval: "Max unanswered keepalives".
  - Host details panel gains a Keepalive section with a "Override global
    keepalive" toggle that, when on, exposes per-host interval +
    countMax inputs and an inline hint when interval = 0 (explaining
    the implications). Same visual pattern as the existing Legacy
    Algorithms section.

Sync:
  - keepaliveCountMax added to SYNCABLE_TERMINAL_KEYS so the new global
    field rides existing sync infrastructure. Per-host fields ride the
    hosts array passthrough automatically (older clients receiving them
    ignore unknown fields, per the existing lenient sync contract).

i18n: en + zh-CN strings for the new settings row, the host section
header, and the override toggle / inputs / disabled hint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ssh): resolve keepalive per jump host, not just the final target

Addresses codex review on PR #947:
  https://github.com/binaricat/Netcatty/pull/947#discussion_r3217027xxx

The first cut only resolved keepalive for the final target host and
forwarded a single interval/countMax pair across the whole start-SSH
call. connectThroughChain in sshBridge.cjs then applied that one pair
to every hop, so a chain like:

   router (bastion, needs keepalive=0)  →  cloud target (needs 30s)

would either kill the router (with cloud-friendly defaults) or fail
to keep the target alive (with router-friendly 0). The per-host
override was effectively useless for bastion hosts.

Fix:
  - NetcattyJumpHost gains optional keepaliveInterval / keepaliveCountMax.
  - createTerminalSessionStarters runs resolveHostKeepalive() per
    jumpHost when building the chain, so each hop carries its own
    resolved pair.
  - sshBridge.cjs's chain connector reads jump.keepaliveInterval /
    jump.keepaliveCountMax for each hop, falling back to the call's
    target-level options for backward compatibility with older
    serializers that don't yet populate the per-hop fields.

The final target's keepalive path is unchanged — it still reads
options.keepaliveInterval / options.keepaliveCountMax that the
session starter resolves from the target host.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ssh): per-host keepalive for SFTP + port forwarding too

Follow-up to the maintainer review on PR #947 — terminal SSH was the
only path that honored per-host keepalive overrides. SFTP and port
forwarding share the same NetcattyJumpHost type but their builders
weren't resolving keepalive per-hop, and their bridges hardcoded the
old 10s/3 defaults. Net result: a router-as-bastion in a chain still
got killed when reached via the SFTP file panel or a port-forwarding
tunnel, even though the user had toggled per-host override.

Plumbing:
  - useSftpHostCredentials / buildSftpHostCredentials: accept optional
    terminalSettings; call resolveHostKeepalive() for the target and
    each jump entry; emit keepaliveInterval / keepaliveCountMax in the
    returned NetcattySSHOptions.
  - useSftpConnections + useSftpState + SftpStateOptions thread the
    setting down. SftpSidePanel passes the global terminalSettings prop
    it already has from TerminalLayer.
  - portForwardingService.startPortForward: accepts terminalSettings
    as an 8th argument, resolves per-host (target + each jump), and
    populates the bridge payload.
  - usePortForwardingState.startTunnel and usePortForwardingAutoStart
    forward the new parameter; App.tsx supplies terminalSettings (via
    a ref in the once-on-launch auto-start effect so changing global
    keepalive later doesn't re-fire it).

Bridges:
  - sftpBridge.cjs target connect: now also reads keepaliveCountMax
    from options (was hardcoded 3). 10s/3 stays as the bridge-level
    fallback to preserve the #669 protection when the renderer hasn't
    supplied a value.
  - sftpBridge.cjs jump hop: reads jump.keepaliveInterval /
    jump.keepaliveCountMax, then falls back to the target-call options
    (matches the symmetric SSH bridge change).
  - portForwardingBridge.cjs: reads keepaliveInterval /
    keepaliveCountMax from the IPC payload; same 10s/3 fallback.

Types:
  - NetcattyJumpHost already grew keepalive fields earlier; this
    commit also adds them to PortForwardOptions so the IPC contract
    is explicit.

End-to-end: a chain `[router-as-bastion, cloud-host]` with the
router host's keepaliveOverride=true / interval=0 now correctly
disables keepalive on the router hop for terminal SSH AND SFTP AND
port forwarding, while the cloud target still gets the resolved
30s/10 default for each path.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ssh): honor explicit keepalive=0 in SFTP + port forwarding bridges

Addresses codex review on PR #947:
  - https://github.com/binaricat/Netcatty/pull/947#discussion_r3217448xxx
  - https://github.com/binaricat/Netcatty/pull/947#discussion_r3217449xxx

The previous follow-up commit (5c8bc923) plumbed per-host keepalive
into SFTP / port forwarding but kept the existing bridge-level
"if interval > 0 use it, else 10s" fallback. That collapsed two
semantically distinct inputs:

  - "user explicitly resolved interval = 0" (host with keepaliveOverride
    + interval=0; the whole point of the override)
  - "no value supplied at all" (legacy serializer)

Both ended up as 10s in the bridge, so a router-as-bastion / direct
router connection through SFTP or a port-forward tunnel still got
ssh2-killed after countMax unanswered probes — exactly the case
per-host override was supposed to fix.

Fix: bridges now distinguish on `== null`:
  - positive value → honor it
  - explicit 0 → truly disabled (0 ms, 0 countMax — ssh2 skips its
    dead-connection check entirely on this connection)
  - undefined / null → fall back to 10s/3 (preserves #669 idle-NAT
    protection for older callers that pre-date per-host plumbing)

Applies to both SFTP target connect and SFTP jump hop builders, plus
the port forwarding target builder. Terminal SSH bridge is unchanged
since it already treated 0 as disabled.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ssh): plumb terminalSettings to all remaining keepalive call sites

Addresses codex review on PR #947:
  - PortForwardingNew + TrayPanel were not passing terminalSettings into
    startTunnel, so tunnels started from the main port-forwarding UI or
    from the tray menu silently used the FALLBACK 30/10 instead of the
    user's actual global keepalive settings. Hosts inheriting global
    policy could see different behavior depending on the entry point.
  - SftpView was not threading terminalSettings into useSftpState, so
    SFTP connections opened from the main tab UI also fell back to the
    same hardcoded default and ignored the user's settings.

Wiring:
  - PortForwardingProps gains `terminalSettings`; VaultView accepts it
    on the same prop and forwards from its own new prop; App.tsx
    supplies it from useSettingsState. The startTunnel call site uses
    it directly and includes it in the useCallback dep list so the
    handler updates when settings change.
  - SftpViewProps gains `terminalSettings`; SftpViewMount accepts and
    forwards it; the sftpOptions memo includes it in its dep list.
  - TrayPanelContent gains a `terminalSettings` prop; the TrayPanel
    wrapper (which already calls useSettingsState for uiLanguage)
    passes it down so the standalone tray window agrees with the main
    window's settings.

Also updates the explicit `startTunnel` signature in
UsePortForwardingStateResult so callers see the new 8th parameter
through the hook's return type, not just through the implementation.

Net result: every place that starts an SSH-derived connection
(terminal session, SFTP browse, port-forward tunnel) now consistently
sees the user's configured global keepalive policy and any per-host
overrides; the FALLBACK_KEEPALIVE constants in the service /
credentials builder are now only reached by genuinely-decoupled call
sites (tests, headless usage) rather than masking missing wiring.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(ssh): include terminalSettings keepalive fields in memo comparators

Addresses codex review on PR #947 — all three components that grew a
`terminalSettings` prop (SftpView, SftpSidePanel, VaultView) are wrapped
in React.memo with manual equality comparators, and none of those
comparators were updated to include the new prop. React would skip the
re-render when global keepalive changed, so new SFTP / port-forwarding
connections from those subtrees would silently keep using the old
keepalive policy until some other tracked prop happened to flip.

Each comparator now compares the keepalive fields directly rather than
the whole terminalSettings object — only those two fields drive
connection resolution in this subtree, and ignoring the rest avoids
unnecessary re-renders for unrelated terminal-setting changes (fonts,
themes, etc.) that already have their own targeted comparator entries.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:22:12 +08:00
陈大猫 92dd898eb4 Fix #931: let users pick a CJK font + per-font smart pairing (#940)
* feat(fonts): add CJK font pairing composition module

Introduces composeFontFamilyStack() which builds the xterm fontFamily
CSS string at runtime from:
  - the user's primary Latin font
  - an explicit CJK font (TerminalSettings.fallbackFont) if set
  - otherwise a per-Latin-font recommended CJK pairing
  - a hardcoded system CJK fallback stack
  - a Nerd Font icon fallback stack
  - the universal monospace generic

14 unit tests cover composition order, deduplication, OS defaults,
quoting, and recommendation override behavior.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(fonts): expose raw Latin families and add CJK-coverage entries

- TERMINAL_FONTS[].family no longer bakes in the CJK fallback stack;
  composition is deferred to runtime via composeFontFamilyStack().
- Drops withCjkFallback helper from this module and its caller in
  lib/localFonts.ts.
- Adds 6 CJK-coverage primary fonts to the dropdown: Sarasa Mono SC/TC,
  Maple Mono CN, LXGW WenKai Mono, Microsoft YaHei UI, PingFang SC.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(terminal): compose font-family stack with user-configurable CJK fallback

resolvedFontFamily now passes through composeFontFamilyStack(), which
prepends the user's TerminalSettings.fallbackFont (if set) ahead of the
per-Latin-font recommended CJK pairing and the system fallback stack.

The platform argument is derived from navigator.platform inside the
useMemo, so the same Latin font may pair with PingFang SC on macOS and
Microsoft YaHei UI on Windows out of the box.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(settings): add CJK font picker to terminal settings

Adds a new "CJK font" select row right under the main font selector in
the Terminal settings tab. Bound to TerminalSettings.fallbackFont (an
already-existing-but-unused field), so this needs no schema or sync
payload change.

Default value "Auto" leaves fallbackFont empty, which lets the new
per-Latin-font pairing in cjkFonts.ts pick a CJK font automatically.
Selecting any explicit option (Sarasa Mono SC, PingFang SC, Microsoft
YaHei UI, etc.) takes precedence over the per-font pairing.

Includes en + zh-CN i18n strings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(sync): cover fallbackFont round-trip + legacy payload tolerance

Four new test cases verify cloud-sync compatibility for the new CJK
font setting:

  - buildSyncPayload includes fallbackFont when set
  - buildSyncPayload omits fallbackFont when unset
  - applySyncPayload writes incoming fallbackFont to TERM_SETTINGS
  - applySyncPayload from a legacy client (no fallbackFont) does NOT
    wipe the local value — critical for old-to-new upgrades

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(fonts): add font availability detection (canvas + document.fonts API)

Three-layer detection used by isFontInstalled(family):
  1. Known @fontsource-bundled families (e.g. JetBrains Mono) always
     count as installed.
  2. document.fonts.check() — picks up @font-face and system-loaded fonts.
  3. Canvas width measurement against serif / sans-serif / monospace
     fallbacks; only counts if the target font produces a width that
     differs from ALL three generics for a probe string.

detectInstalledWithContext is a pure function taking an injected
measurement context, which keeps the canvas / DOM behind a seam and
lets the logic be unit-tested without a browser. 11 tests cover
quoted-family parsing, the three-generic-fallback rule, bundled
short-circuit, and document.fonts.check fast-path.

Results are cached per process; clearFontAvailabilityCache() invalidates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(fonts): filter dropdowns to fonts actually installed on this machine

Layer 3 of #931 added Sarasa Mono SC / Maple Mono CN / Microsoft YaHei UI
/ PingFang SC etc. to the terminal font dropdown, but users who don't
have these installed would still see them and pick them — resulting in
"I changed the font and nothing happened" confusion.

This commit filters both dropdowns through isFontInstalled():

  - TerminalFontSelect: drops any built-in or system-discovered font
    that detection can't render. If filtering would leave fewer than 4
    fonts (detection misfire safety net), shows the full list.

  - TerminalCjkFontSelect: keeps the "Auto" sentinel always, drops
    concrete CJK choices that aren't present on this machine.

Both selects always keep the currently-selected value visible — even
when the underlying font is missing — so users can read and clear
their setting without surprise.

Also expands `npm test` globs to pick up infrastructure/config/*.test.ts
and lib/*.test.ts, which previously matched no patterns and meant the
new cjkFonts and fontAvailability suites were silently excluded from
CI runs.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(fonts): never recommend proportional CJK fonts for terminal use

The previous PingFang SC / Microsoft YaHei UI / Hiragino Sans GB choices
were proportional sans-serif fonts whose CJK glyphs aren't designed to
fit a terminal's 2x cell grid — the rendered Chinese ended up visibly
wider than its allocated cells, breaking grid alignment (reported on
macOS with PingFang SC selected as the CJK font).

Changes:
  - TerminalCjkFontSelect: drops PingFang SC / Microsoft YaHei UI /
    Hiragino Sans GB from the dropdown. Legacy explicit selections
    still surface as a synthetic "not recommended" option so users can
    see and re-pick.
  - CJK_SYSTEM_FALLBACK_FONTS: monospace-only list. Sarasa Mono SC/TC,
    Maple Mono CN, LXGW WenKai Mono, Noto Sans Mono CJK SC, Source Han
    Mono SC, NSimSun, SimSun. Proportional fonts removed.
  - PER_FONT_CJK_PAIRING: every entry now points at a true monospace
    CJK font. Cascadia / Consolas / Menlo etc. all recommend Sarasa
    Mono SC, which the next commit bundles via @font-face.
  - getDefaultCjkFallback: Windows = SimSun (always installed,
    monospace); macOS = Sarasa Mono SC (will be bundled); Linux =
    Noto Sans Mono CJK SC. A regression test enforces that no
    per-OS default is a known proportional font.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(fonts): bundle Sarasa Mono SC as the universal CJK monospace

Previous commit removed proportional CJK fonts (PingFang SC, etc.)
from the picker and switched per-OS defaults to true monospace, but
macOS ships NO system-installed monospace CJK font — leaving macOS
users with a broken default unless they manually install Sarasa or
similar. This commit closes that gap by bundling Sarasa Mono SC as
an @font-face webfont, so the recommended pairings and macOS default
"just work" out of the box.

Details:
  - public/fonts/SarasaMonoSC-Regular.woff2 (~4.8 MB): subsetted from
    be5invis/Sarasa-Gothic v1.0.37 SarasaMonoSC-Regular.ttf (24 MB).
    Covers ASCII, Latin-1, common punctuation/symbols, CJK Unified
    Ideographs main block, Hiragana/Katakana, halfwidth/fullwidth,
    box-drawing — the everyday-Chinese coverage that matters for a
    terminal. Rare CJK Ext-A/B/historical chars fall through to the
    system fallback stack.
  - public/fonts/SarasaMono-LICENSE.txt: OFL-1.1 verbatim, required
    by the license.
  - index.css: @font-face declaration with font-display: swap so the
    user doesn't see a flash of nothing while the woff2 loads.
  - KNOWN_BUNDLED_FAMILIES: "Sarasa Mono SC" added so the dropdown
    availability filter doesn't hide it.

Installer impact: ~+4.8 MB (vs current ~100-200 MB Electron baseline).
The font replaces what would otherwise have been "Chinese chars look
broken in the terminal" for every macOS user without a manually
installed CJK monospace font.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(fonts): use Local Font Access API as the authoritative install check

document.fonts.check() turned out to be unreliable as an installed-font
signal in Chromium — it returns true for any syntactically-valid family
name regardless of whether the font is actually installed, as a
deliberate fingerprinting-mitigation. The previous detector took it as
a positive signal and ended up keeping uninstalled fonts in the dropdown
(reported by a macOS user seeing dozens of fonts they don't have).

This commit pivots the detection chain:

  - lib/localFonts.ts: getAllSystemFontFamilies() exposes the unfiltered
    set of installed family names from queryLocalFonts(), reusing the
    same underlying call as getMonospaceFonts() via a shared cache.

  - lib/fontAvailability.ts: drops the document.fonts.check fast-path.
    Adds setSystemFamilies() / hasAuthoritativeData(). When the set has
    been populated, isFontInstalled answers from membership lookup
    directly — no canvas guessing. Canvas remains as a fallback for
    environments where the Local Font Access API is unavailable or
    permission is denied.

  - application/state/fontStore.ts: during initialize(), runs the
    monospace-only query and the full-system-families query together,
    then pipes the result into fontAvailability.

  - TerminalFontSelect: with authoritative data, drops the "if filtered
    list is suspiciously small, show all" safety net. Empty would now
    really mean empty (highly unlikely since Sarasa Mono SC is bundled).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(fonts): drop PingFang SC / Microsoft YaHei UI from primary dropdown

Step 1 of this PR removed proportional CJK fonts from the CJK fallback
picker but left them in BASE_TERMINAL_FONTS, so PingFang SC and
Microsoft YaHei UI were still selectable as the *primary* terminal
font. Picking PingFang SC as primary produced visibly bloated Latin
character spacing (xterm.js samples cell width from the primary font;
the wide proportional 'M' inflates every cell), reported by a macOS
user in the same thread that opened #931.

Both entries are removed from BASE_TERMINAL_FONTS. A new
infrastructure/config/fonts.test.ts asserts that no known proportional
CJK font name (including PingFang TC/HK, Microsoft YaHei variants,
Hiragino Sans GB, Heiti SC/TC) is ever shipped in TERMINAL_FONTS as a
primary choice.

Migration for users already saved to one of the removed ids:
useSettingsState rewrites STORAGE_KEY_TERM_FONT_FAMILY to the default
(Menlo) on read when it sees a deprecated id, so the bad value also
stops getting carried into cloud-sync uploads. Per-host fontFamily
overrides are NOT migrated automatically — they still gracefully
fall through to the dropdown's first entry via the existing
getFontById fallback; users can re-pick from the host settings UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(fonts): drop Comic Sans MS — it's a proportional handwriting font

Same symptom as the PingFang SC / Microsoft YaHei UI removal: Comic
Sans MS was historically in the primary font dropdown labeled
"Casual, non-traditional terminal font", but Comic Sans is a
handwriting-style proportional sans-serif. Picking it as the terminal
primary inflates cell width and spaces every Latin character far
apart (reported in the same #931 thread).

- BASE_TERMINAL_FONTS: comic-sans-ms entry removed.
- DEPRECATED_PRIMARY_FONT_IDS: gains comic-sans-ms so existing
  selections silently migrate to Menlo on read.
- fonts.test.ts: the proportional-font ban list now also covers
  Latin proportional fonts (Comic Sans MS, Arial, Helvetica, Times
  New Roman, Georgia, Verdana, Trebuchet MS, Tahoma) so the test
  catches any future mislabeled body-text font from being added to
  the terminal dropdown.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(fonts): keep monospace ahead of CJK fallbacks in composed stack

Addresses codex P1 review comment on PR #940
(https://github.com/binaricat/Netcatty/pull/940#discussion_r3216017737).

The previous behavior of withCjkFallback() had monospace immediately
after the primary family, before any CJK fallback. composeFontFamilyStack
had moved monospace to the very end, which means: when the primary
font isn't installed on the user's machine (common for Layer 3 CJK
choices that aren't bundled and not present on a given OS, or for any
built-in id like cascadia-code on a Linux system without it), CSS
per-glyph fallback resolves Latin glyphs from a CJK font's full-width
Latin variants before ever reaching monospace generic. That breaks
xterm.js's fixed cell-grid alignment.

The composed stack now reads:
  <primary>, monospace, <userFallback>, <recommended-cjk>,
  <system-cjk-stack>, <nerd-font-stack>

Per-glyph CSS fallback behavior:
  - Latin → primary if installed → monospace generic. Cell width
    stays consistent.
  - CJK → primary (no) → monospace (no Chinese glyphs) → walks into
    CJK fallbacks.
  - Nerd PUA → falls past all of the above into the Nerd Font stack.

Updates the position-invariant tests and adds a regression test that
explicitly asserts monospace appears before every CJK family in the
output stack.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(fonts): dedupe Local Font Access API calls under concurrent init

Addresses codex P2 review on PR #940:
  https://github.com/binaricat/Netcatty/pull/940#discussion_r3216246xxx

fontStore.initialize() runs getMonospaceFonts() and
getAllSystemFontFamilies() in Promise.all; both internally called
queryAllSystemFontsOnce(), whose cache check (`if (cache) return`) was
only useful once the result had been written. Concurrent callers both
passed the empty-cache check and fired their own queryLocalFonts()
request — two real Local Font Access API invocations on cold start,
with the risk of one succeeding while the other was denied (leaving
the authoritative set unset).

Fix: cache the *in-flight promise itself*, so subsequent callers
await the same single invocation. The first await populates the
family-set cache as a side effect, and the resolved promise keeps
returning the same value to every subsequent caller.

Adds lib/localFonts.test.ts with three regression tests:
  - concurrent getMonospaceFonts + getAllSystemFontFamilies = 1 API call
  - sequential repeats also reuse the resolved promise
  - missing API returns null authoritative set (canvas fallback signal)

Exports __resetLocalFontsCacheForTesting() so each test gets a fresh
module-level state.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(fonts): retry LFA on transient failure + notify on availability changes

Two follow-up fixes from codex P2 review on PR #940:

1) queryAllSystemFontsOnce() previously kept its in-flight promise even
   when queryLocalFonts threw. Subsequent callers reused the cached
   empty result for the rest of the session, so any transient failure
   at boot (permission state not ready, AbortError, etc.) permanently
   blinded the rest of the app to installed fonts. Catch now clears
   queryPromise so the next caller retries. Regression test added.

2) TerminalCjkFontSelect.visibleOptions and TerminalFontSelect
   .visibleFonts were memoized on [value] / [fonts, value] only, but
   the filter calls isFontInstalled() which reads module-level
   systemFamilies — a value that arrives asynchronously after the
   initial render. The memos never recomputed when authoritative
   availability data landed, so the dropdowns could continue showing
   stale "filtered" results until the user changed selection.

   fontAvailability now exposes subscribeFontAvailability() and
   getFontAvailabilityVersion() (monotonic counter bumped on
   setSystemFamilies / clearFontAvailabilityCache). Both selects
   subscribe via useSyncExternalStore and include the version in
   their memo deps; tests cover subscriber notification and version
   monotonicity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(fonts): migrate host/group deprecated font ids + localize CJK labels

Two follow-up fixes from codex review on PR #940:

P2 — Host/group level font migration
====================================
The earlier deprecated-id migration only rewrote
STORAGE_KEY_TERM_FONT_FAMILY, so hosts and group configs that had
explicitly opted into a now-removed font id (e.g. pingfang-sc,
microsoft-yahei, comic-sans-ms) kept `fontFamily` set with
`fontFamilyOverride=true`. After the dropdown entries were dropped
in 9f2bd282/c9b622d8, those records silently fell through to the
first font in the registry (Menlo) while the override flag still
read "true" — users saw a host claiming a custom font but rendering
the global default with no way to tell what happened.

Fix:
  - infrastructure/config/fonts.ts gains migrateDeprecatedFontOverride(),
    a structurally-shared helper that drops fontFamily and clears
    fontFamilyOverride when the id is deprecated.
  - sanitizeHost now runs it on every host load.
  - domain/groupConfig.ts grows sanitizeGroupConfig(); useVaultState
    applies it both on initial load and on cross-tab storage events.
  - Existing decrypt → sanitize → encrypt round-trip in useVaultState
    means the migrated values are persisted back to localStorage and
    propagate through cloud sync naturally.

Tests: two each in domain/host.test.ts and domain/groupConfig.test.ts
covering deprecated-id reset and untouched-valid-id preservation.

P3 — Localize CJK font option labels
====================================
TerminalCjkFontSelect previously hardcoded Chinese option labels
("Auto · 按主字体智能搭配", "Sarasa Mono SC (更纱黑体 简)", etc.) and
the synthetic "not recommended" warning. Non-Chinese locales saw a
mixed-language UI despite the rest of the setting going through i18n.

OPTIONS now references i18n keys; the component looks them up via
useI18n(). Both en and zh-CN locales gain matching keys, including
`...option.legacy` with `{font}` interpolation for the synthetic
"not recommended" item that surfaces saved-but-removed values.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(fonts): also sanitize group configs on the write/import path

Addresses codex P2 review on PR #940:
  https://github.com/binaricat/Netcatty/pull/940#discussion_r3216314xxx

The previous commit (09c87820) added sanitizeGroupConfig() but only
plumbed it into the decrypt paths (initial load + storage event).
updateGroupConfigs() — which is also the write path used by
applySyncPayload / importVaultData when ingesting a legacy payload —
still set state from raw input. A sync from an older client carrying
{ fontFamily: "pingfang-sc", fontFamilyOverride: true } would land in
memory unsanitized AND be re-persisted with the bad override active
until the next reload re-ran the decrypt path.

Fix mirrors updateHosts → sanitizeHost: map every incoming entry
through sanitizeGroupConfig before both setGroupConfigs and the
encrypt-and-persist step. Same call site now feeds the cleaned data
to localStorage, so legacy values are scrubbed on first import.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(fonts): migrate deprecated terminal font ids on every ingest path

Addresses codex P2 review on PR #940:
  https://github.com/binaricat/Netcatty/pull/940#discussion_r3216517xxx

The previous migration only ran in the initial useState() initializer
for terminalFontFamilyId, so deprecated ids (pingfang-sc /
microsoft-yahei / comic-sans-ms) could still re-enter state via:

  - rehydrateAllFromStorage() at line ~527 — runs on remote-import
    completion and re-reads STORAGE_KEY_TERM_FONT_FAMILY raw.
  - The notifySettingsChanged IPC handler at line ~663 — fires when a
    cloud sync or programmatic localStorage write announces a change.
  - The cross-window storage event handler at line ~873.

Any of these paths could pull a deprecated id back into state after
the initial migration ran, leaving the font selector with no matching
option and silently rendering the global default while continuing to
propagate the stale value through subsequent sync uploads.

Centralizes the migration in migrateIncomingTerminalFontId(raw):
  - returns null when raw is empty
  - if raw is deprecated, writes DEFAULT_FONT_FAMILY back to
    localStorage AND returns it
  - otherwise returns raw unchanged

All four ingest sites (initial init, rehydrate, IPC, storage event)
now route through this helper. The rewrite-on-deprecated semantics
also guarantee that the moment any path sees a bad value, the next
sync upload carries the cleaned default — not the deprecated id.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(fonts): use bundled Latin-only fallback instead of monospace generic

Resolves the tension between codex's two P1 reviews on PR #940:

  Round 1 (da1fe4cd): "monospace must come BEFORE CJK fallbacks" —
    otherwise Latin glyphs fall into a CJK font's full-width Latin
    when the primary font is missing.

  Round 2 (this commit): "monospace must come AFTER CJK fallbacks" —
    otherwise on macOS Chrome, the generic `monospace` pulls in
    PingFang via Chromium's CJK system fallback and silently masks
    the user's CJK picker.

Both are right; using a single `monospace` token can't satisfy both
roles because `monospace` is a generic family whose CJK-glyph
coverage is platform-dependent.

Fix mirrors Tabby's approach (their "monospace-fallback" SourceCodePro
sitting before any CJK in the chain): insert a known Latin-only
bundled font between the primary and CJK fallbacks. JetBrains Mono is
already shipped via @fontsource/jetbrains-mono and carries no CJK
glyphs, so it catches Latin without intercepting Chinese.

New stack order:
  <primary>, "JetBrains Mono", <userFallback>, <recommended-cjk>,
  <system-cjk-stack>, <nerd-font-stack>, monospace

Per-glyph CSS fallback now behaves as intended on every platform:
  - Latin: primary (if installed) → JetBrains Mono. Cells stay aligned.
  - CJK: primary (no) → JetBrains Mono (no CJK glyphs) → user CJK pick.
  - Nerd PUA: all of the above → Nerd Font stack.

Replaces the two prior positional-invariant tests with one for each
codex review concern: JetBrains Mono precedes every CJK family
(Latin alignment), and user CJK precedes generic monospace (CJK
picker effectiveness).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(fonts): use OR-of-fallbacks for canvas font detection

Addresses codex P2 review on PR #940:
  https://github.com/binaricat/Netcatty/pull/940#discussion_r3216556xxx

detectInstalledWithContext required the target font to produce a
different rendered width from *all three* generic fallbacks (serif,
sans-serif, monospace) to be counted as installed. That's too strict:
on macOS the `monospace` generic resolves to Menlo itself, so
measure(`"Menlo", monospace`) === measure(`monospace`), and the
detector reported Menlo as missing even when it was clearly installed.
The same false-negative trap exists for any font that happens to
share metrics with one of the three generics on a given platform.

Switches to OR-of-fallbacks: a font counts as installed if its
rendered width differs from at least one generic baseline. A truly
uninstalled font still falls through to each generic in turn and
matches all three baselines, so this doesn't introduce false positives.

Regression tests added for both directions:
  - Menlo with metrics identical to `monospace` generic → installed.
  - "Definitely Not Installed" font → still reported missing.

The path only fires when the Local Font Access API is unavailable or
denied — when LFA succeeds, `setSystemFamilies` short-circuits ahead of
canvas — so this primarily improves the degraded-permission scenario.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(fonts): quote-aware tokenizer for font-family lists

Addresses codex P2 review on PR #940:
  https://github.com/binaricat/Netcatty/pull/940#discussion_r3216559xxx

composeFontFamilyStack and extractPrimaryFamily both tokenized their
input with a raw String.split(',') — which corrupts any CSS family
list whose quoted family name contains a comma (CSS allows that, e.g.
`"Foo, Inc. Mono"` is a single family). A naive split would shred
that into `"Foo` / `Inc. Mono"` and emit a malformed font-family back
out.

No current TERMINAL_FONTS entry hits this case, but lib/localFonts.ts
builds family strings from arbitrary system fonts via the Local Font
Access API — a user with a comma-bearing family name would have
silently broken filtering until now.

Adds splitFontFamilyList(css) in cjkFonts.ts: an exported quote-aware
tokenizer that splits on commas only when outside quoted segments
(handles both " and '). composeFontFamilyStack uses it instead of raw
split; extractPrimaryFamily in lib/fontAvailability.ts imports it for
symmetry so the two call sites can't drift.

Tests cover the tokenizer directly (simple list, quoted-with-comma,
single quotes, double commas) and end-to-end (a quoted primary with
an internal comma survives composition intact).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(fonts): translate Layer 3 CJK font descriptions to English

The 4 CJK-coverage entries added in earlier commits (Sarasa Mono SC,
Sarasa Mono TC, Maple Mono CN, LXGW WenKai Mono) had hardcoded Chinese
description strings, while every other TERMINAL_FONTS entry uses
English ('Adobe's professional programming font', 'Iosevka variant
mimicking Berkeley Mono style', etc.). The dropdown rendered a
mixed-language list — flagged by the maintainer.

Converted the 4 descriptions to English in the same style as the
existing entries. No i18n scaffolding added; the existing convention
is "English-only `description` field, not routed through t()", and
the rest of the registry stays consistent with that.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 14:07:15 +08:00
bincxz 72e305fb7a Add reusable proxy profiles 2026-05-06 17:33:46 +08:00
陈大猫 470d9b5aae [codex] Improve ACP agent error diagnostics (#880) 2026-05-01 08:00:50 +08:00
陈大猫 d86c5ed05a [codex] Remove mosh client path setting (#878)
* fix(terminal): remove mosh client path setting

* fix(terminal): remove stale mosh detection bridge
2026-04-30 17:54:35 +08:00
陈大猫 4dd2465388 Keep known hosts local during sync (#863) 2026-04-29 11:01:21 +08:00
陈大猫 b6734b9ef9 Show auto-detected mosh path (#858) 2026-04-28 21:38:10 +08:00
陈大猫 74d56cdcb8 [codex] Settings: detect & override mosh client path (#849)
* Add Mosh client detection and override in Settings → Terminal

Builds on PR #847 (auto-detection across PATH gaps). Power users with
non-standard install locations (containers, custom builds, multiple
mosh versions) can now point the app at a specific mosh binary; less
technical users get a one-click "Detect" button to confirm where mosh
was found, with a Browse fallback for clicker-only flows.

Backend (electron/bridges/terminalBridge.cjs):
- detectMoshClient() returns { platform, found, path, searchedPaths }.
  Reuses resolvePosixExecutable; surfaces the searched dirs so the UI
  can tell users where to look when nothing was found.
- pickMoshClient() opens a native file picker via dialog.showOpenDialog.
- startMoshSession honors options.moshClientPath when provided. Strict
  failure: a missing/non-executable explicit path produces a clear
  error instead of falling back to auto-detect, so users notice typos
  and stale paths instead of getting silent recovery.

UI (components/settings/tabs/SettingsTerminalTab.tsx):
- New SettingRow under "Connection" with text input + Detect + Browse
  buttons, mirroring the localShell validation pattern. Shows inline
  validation (notFound/isDirectory) and the last detect result with
  searched directories on miss.

Plumbing:
- TerminalSettings.moshClientPath: string field with default "" so
  empty == auto-detect (matches existing PR #847 semantics).
- preload exposes detectMoshClient + pickMoshClient.
- createTerminalSessionStarters passes terminalSettings.moshClientPath
  into the IPC call, undefined when blank.
- en.ts / zh-CN.ts get the 9 new strings.

Verified locally:
- vite build succeeds; settings tab renders.
- detectMoshClient() against the live machine returns
  /opt/homebrew/bin/mosh with the expected searchedPaths list.
- Existing PR #847 auto-detection path is unchanged when the field is
  empty.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Skip POSIX execute-bit check for explicit Windows mosh path

Address Codex P2 on PR #849 commit 88e5c596. isExecutableFile used
`(stat.mode & 0o111) !== 0` to gate the explicit moshClientPath in
startMoshSession, but Windows Node returns mode 0o100666 even for
.exe / .bat / .cmd files (NTFS has no POSIX execute bits). Result:
a Windows user who picked a perfectly valid `mosh.exe` via the new
Browse dialog or typed an absolute path was rejected with
"Configured Mosh client not usable…" — making the manual override
unusable on Windows.

Make isExecutableFile platform-aware: still require isFile() and
the Unix execute bit on POSIX, but treat any regular file as
executable on Win32 and let spawn-time PATHEXT / extension handling
filter non-executables.

Resolver paths are unaffected — resolvePosixExecutable returns null
on Win32 before isExecutableFile is reached.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Augment Windows env when explicit mosh path is outside PATH

Address Codex P2 on PR #849 commit 69782471. When a Windows user
selected a mosh.exe outside %PATH% via Browse / custom path, the
explicit-client branch left resolvedMoshDir null, so the later
PATH/MOSH_CLIENT injection was skipped. The Mosh wrapper still
exec's `mosh-client` (and `ssh`) by name, so a valid selection
failed unless that directory was already on PATH.

- Always set resolvedMoshDir for explicit moshClientPath, regardless
  of platform.
- Use path.delimiter so PATH composition uses ";" on Win32 and ":"
  on POSIX. Compare directory membership with path.normalize so
  trailing-slash / case differences don't double-add.
- When picking mosh-client, try .exe / .bat / .cmd extensions on
  Win32 before the bare name; POSIX still uses just `mosh-client`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Validate Mosh client is executable in Settings UI

Address Codex P2 on PR #849 commit b6c384af. UI's debounced validator
called validatePath which only reported exists / isFile / isDirectory,
so a regular file without the POSIX execute bit (e.g. a stray
/etc/hosts-style path) was marked as valid in Settings — but
startMoshSession's isExecutableFile check then rejected the same path
at connect time, deferring the error until the user actually tried to
use Mosh.

- validatePath now returns `isExecutable: boolean`, mirroring
  isExecutableFile semantics (POSIX: stat.mode & 0o111; Win32: any
  regular file is treated as executable since NTFS lacks POSIX bits).
  Existing callers (localShell, localStartDir) ignore the new field.
- global.d.ts ValidatePath return type extended.
- SettingsTerminalTab Mosh validator surfaces a `notExecutable`
  message when the file exists but lacks exec permissions, keeping
  the UI in lockstep with main-process gating.
- en / zh-CN strings for the new state.

Verified: /bin/sh -> isExecutable:true, /etc/hosts -> false, /etc ->
false (directory). UI now warns immediately on the regression case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Require absolute Mosh client paths in Settings UI and main

Address Codex P2 on PR #849 commit 2eba549e. The shared validatePath
bridge resolves bare names through PATH (necessary for localShell
where 'powershell.exe' is a valid choice), so a user typing 'mosh' or
'mosh.exe' into the new Mosh field would get a green check in
Settings — but startMoshSession treats moshClientPath as a literal
filesystem path and calls isExecutableFile on the raw value. The
saved setting then disables auto-detection and Mosh sessions fail
unless a matching file happens to exist in the app's cwd.

Gate on absolute paths at both layers so UI validation and the
runtime check agree:

- startMoshSession: path.isAbsolute(expanded) before isExecutableFile,
  with a distinct error message naming the constraint.
- SettingsTerminalTab: same shape — UI checks looksAbsolute (POSIX
  /, leading ~, Windows drive letter, or UNC \\\\) before sending the
  IPC, surfacing notAbsolute inline. Tolerant across platforms so
  pasting a Windows-style path on macOS still produces a real
  downstream error rather than a misleading 'not absolute'.
- en / zh-CN strings.

Verified against the full case matrix (relative names, ./, ../, bare
basenames, POSIX absolute, ~/, Windows drive, UNC) — UI flags every
relative entry without an IPC round-trip, and any value that passes
UI also passes main-process validation (or both reject).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 14:39:37 +08:00
陈大猫 08e566adb0 [codex] Add X11 forwarding support (#835)
* Add X11 forwarding support

* Address X11 forwarding review feedback

* Handle X11 auth for unix socket display paths

* Tighten X11 forwarding compatibility handling
2026-04-28 07:54:26 +08:00
陈大猫 487b7adf3e Add 'Set to disabled' button to individual keybindings (#781) (#782)
The keybinding recorder couldn't assign the 'Disabled' sentinel — pressing
Esc just cancels. Add a Ban-icon button next to 'Reset to default' that
writes 'Disabled' for the active scheme, and render the button label using
the localized 'Disabled' string instead of the raw sentinel.
2026-04-21 16:57:56 +08:00
陈大猫 a1648adf12 Add opt-in setting to preserve mouse selection across keystrokes (#755) (#763)
* Add opt-in setting to preserve mouse selection across keystrokes

Closes #755.

xterm.js hardcodes a "clear selection on user input" listener
(SelectionService.ts: coreService.onUserInput → clearSelection) with
no public option to disable. The user-reported workflow this breaks:
select a path with the mouse, type a command prefix like `sz `, then
middle-click-paste the still-live selection — but the very first
keystroke wipes the selection, so there's nothing left to paste.

Modern terminals (iTerm2, GNOME Terminal, Windows Terminal) preserve
the selection across input by default. We expose this as an opt-in
toggle for now since the visual semantics are a behavior change.

Implementation is capture-and-restore via xterm.js public APIs
(getSelectionPosition / select); xterm clears the selection
synchronously, then a queueMicrotask reapplies it on the next tick.
A ref (isRestoringSelectionRef) gates copy-on-select so the restore
doesn't redundantly rewrite the clipboard and clobber whatever the
user copied elsewhere in between.

Defaults to false (opt-in); can flip to default-on later if reception
is positive. Selection still clears on:

- Mouse click in empty space (xterm's mouse-driven path is untouched)
- Terminal scroll past the selected rows (existing buffer-trim logic)
- Programmatic clearSelection() callers

Files:
- domain/models.ts — new field, default false
- application/syncPayload.ts — added to SYNCABLE_TERMINAL_KEYS
- components/terminal/runtime/createXTermRuntime.ts — capture in
  attachCustomKeyEventHandler, restore via queueMicrotask
- components/Terminal.tsx — owns isRestoringSelectionRef, passes it
  through context, checks in copy-on-select listener
- components/settings/tabs/SettingsTerminalTab.tsx — UI toggle
- application/i18n/locales/{en,zh-CN}.ts — labels

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Trim verbose i18n descriptions to match neighboring rows

Both clearWipesScrollback and preserveSelectionOnInput descriptions
were too long. Cut to one sentence each, matching the brevity of
adjacent rows like Bracketed paste and OSC-52. Historical context and
edge-case caveats belong in the changelog/PR, not the settings UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 16:22:48 +08:00
陈大猫 484ac5f463 Honor CSI 3 J by default; add toggle to preserve scrollback on clear (#761)
* Honor CSI 3 J by default; add toggle to preserve scrollback on `clear`

Default `clear` (ncurses ≥ 2013) emits CSI 2 J + CSI 3 J to wipe both
visible screen and scrollback. PR #633 unconditionally intercepted CSI
3 J to keep history across `clear`, which broke POSIX semantics — users
running standard `clear` could not wipe scrollback at all (#757).

Restore the standard behavior as the default and expose a toggle for
the iTerm2-style "preserve history" preference (matches what #622
asked for):

- domain/models.ts: add `clearWipesScrollback: boolean` (default true)
- createXTermRuntime.ts: CSI 3 J handler now reads the setting and
  only intercepts when the user opts out
- SettingsTerminalTab.tsx + i18n: expose the toggle with a description
  explaining the tradeoff
- The right-click "Clear Buffer" menu action keeps its independent
  semantics (always preserves scrollback) regardless of this setting,
  since it goes through `clearTerminalViewport`, not the CSI path

Closes #757

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix: include clearWipesScrollback in cloud-sync terminal keys

Codex review on PR #761 caught that the new toggle was added to
TerminalSettings but not to SYNCABLE_TERMINAL_KEYS, so it would never
travel across devices via cloud sync — users disabling it on one
device would silently get the default back on another after sync.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-18 15:17:33 +08:00
陈大猫 db69d5ac39 [codex] Harden sync overwrite protection and add local restore history (#720)
* fix: harden sync overwrite recovery

* refactor: separate backup retention settings

* refactor: align backup retention controls

* refactor: simplify backup retention card

* fix: address PR #720 deep-review findings

- Close the cross-window restore race by holding a time-bounded barrier
  in localStorage during every destructive apply; useAutoSync skips
  pushes while it's set, preventing a pre-restore snapshot from
  clobbering just-restored cloud data.
- Round-trip startup three-way merges so merged-in local additions
  actually reach the cloud instead of living only on the device that
  ran the merge until the next edit.
- Upgrade sync signatures from a 64-char ciphertext prefix to full
  SHA-256 (v3), closing the tail-mutation replay weakness.
- Harden the vault-backup IPC: payload size cap, enum-validated reason,
  sanitized version strings, strict maxCount, concurrent-call mutex,
  monotonic createdAt to avoid same-ms ordering ties.
- Extract the anchor-change decision into a pure module with unit tests
  covering no-anchor, resource-id drift, and signature mismatch paths.
- Capture the protective backup from the pre-apply closure snapshot so
  it reflects what's being replaced rather than what was imported.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address PR #720 follow-up review findings

Make protective backup abort-on-failure (was best-effort console.error),
preserve nested syncedAt in fingerprint, use UTF-8 byte length for size
guard, throw on conflict-inspect failure so stale uploads can't leak
through, treat unreadable remote as changed, canonical-JSON signature
meta, and hold the version stamp on transient backup failures so the
retry path still fires.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address second-pass review findings on PR #720

- Hold version-change stamp when payload is non-meaningful (covers the
  startup vault-rehydrate race where a transient empty snapshot would
  permanently skip the upgrade backup).
- readBackupRecord stat-checks before readFile so an oversized file in
  the backup dir cannot OOM the renderer on enumeration.
- Reject maxBackups input outside 1..100 instead of silently clamping
  (matches the i18n error copy and the main-process sanitizer bound).
- Wrap USE_LOCAL conflict-resolution push in withRestoreBarrier so a
  concurrent auto-sync in another window cannot interleave.
- sha256Hex throws SyncSignatureUnavailableError on missing WebCrypto
  subtle; createSyncedFileSignature returns null, forcing the
  unreadable-remote → three-way-merge path instead of a weak
  length-only pseudo-signature.
- Document that array order in normalizePayloadForHash is an invariant
  enforced by producers, not the hash function.
- Drop three-way-merge completion logs from console.log to console.info.
- Comment the implicit restore → store-listener refresh chain so
  future refactors don't silently break the UI reload path.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address third-pass review findings on PR #720

Resolves I-3 through I-8 and related cleanup items identified in the
deep review. Highlights:

- replace setTimeout(0) post-merge round-trip with a direct
  syncAllProviders call using the already-computed merged payload,
  removing the React-commit race
- resolve the empty-vault confirmation promise on unmount so a
  mid-dialog window teardown doesn't leak the resolver
- retry the version-change backup as hosts/keys hydrate, instead of
  latching on the first (possibly empty) snapshot
- heartbeat-refresh the cross-window restore barrier so long applies
  cannot expose a post-60s window to concurrent auto-sync
- add a diagnostic warning when connected providers hold divergent
  bases (multi-account configurations)
- surface a user-visible "Sync paused" toast when startup inspect
  fails, replacing the previous silent gate-open
- tie-break backup list sort by id when createdAt collides
- extract applyProtectedSyncPayload so the main and settings windows
  cannot drift on restore-barrier / protective-backup handling

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address deep-review findings on PR #720

Deep re-review surfaced six Important issues that survived the prior
four review rounds. All are hardened here:

- I1: fsync the protective backup file AND its directory before the
  rename completes, so a system crash between backup creation and the
  restore it guards cannot leave a torn/zero-length safety net.
- I3: persist an apply-in-progress sentinel across the non-atomic
  localStorage writes in applySyncPayload. A crash mid-apply now
  surfaces on the next startup (toast + refuse auto-push) instead of
  silently pushing the half-applied state over an intact cloud copy.
- I2: only open the auto-sync gate (remoteCheckDoneRef) when the
  startup inspect validated cleanly. Add a bounded exponential-backoff
  retry so a transient inspect failure self-heals instead of wedging
  auto-sync until restart.
- I5: save the sync base BEFORE advancing the per-provider anchor
  inside uploadToProvider. A renderer crash between the two writes
  now degrades to "stale anchor forces re-inspect on next run," which
  re-merges against the fresh base — eliminating the silent
  base-drift window where a 3rd-device race could misclassify
  entries.
- I6: main process broadcasts a vaultBackups:changed IPC event on
  every mutation; useLocalVaultBackups subscribes so protective
  backups created from the main window show up in the Settings
  backup list without manual refresh.
- I4: update PR description + code comment to match the actual
  (safer) design: auto-sync gate opens on vault init, with
  hasMeaningfulSyncData + restore barrier preventing empty-push; the
  version-change backup is best-effort and retries as data hydrates.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: serialize startup checkRemoteVersion and stabilize its deps

Re-review flagged that checkRemoteVersion's useCallback depended on
`config` — a fresh object literal from App.tsx on every render — so
the retry effect restarted with attempt=0 on every vault edit and
could spawn overlapping in-flight inspect+apply runs. Two concurrent
commitRemoteInspection + onApplyPayload calls could race on the
apply-in-progress sentinel around interleaved writes.

Route `buildPayload`, `config.onApplyPayload`, and `config.startupReady`
through refs so checkRemoteVersion's identity no longer churns with
unrelated App state. Add an in-flight guard that returns early when a
previous invocation is still awaiting the network, closing the
same-window re-entry gap that withRestoreBarrier intentionally doesn't
cover.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: release in-flight lock on no-connected-provider early return

Third-pass review caught that `checkRemoteInFlightRef` was acquired
before the `!connectedProvider` check, so that early return leaked
the lock and every subsequent retry-timer tick silently no-op'd.
Move the acquisition past the early return so the only path that
takes the lock reaches the finally-release.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 03:09:55 +08:00
bincxz cc2702b825 fix: decouple Codex agent auth from netcatty provider list (#705)
Codex agent auth must be determined entirely by ~/.codex/auth.json or
~/.codex/config.toml. Before this change, if the user configured any
OpenAI-compatible API provider in netcatty settings (for Catty agent use),
useAIChatStreaming would silently hand that provider's apiKey to the Codex
agent too, causing aiBridge to spawn codex-acp with authMethodId
"codex-api-key" and completely override the user's ChatGPT login.

The regression was introduced in PR #702 (v1.0.89) when findManagedAgent
Provider started matching generic "custom" providers for Codex. Users who
logged into Codex via ChatGPT and also had a netcatty-configured custom
provider saw the UI flip to "API mode" on refresh and their ChatGPT
session get ignored.

Remove the codex branch from the agentProviderId resolver and from
findManagedAgentProvider itself. Also drop the now-meaningless
hasCompatibleProvider hint on the Codex settings card and its i18n copy.
Claude agent behavior is unchanged.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 19:08:18 +08:00
陈大猫 af2589e60b Merge pull request #704 from tces1/MoreSkills
feat: add Netcatty user skills scanning and chat selection flow
2026-04-13 13:33:12 +08:00
Eric Chan 1f3e531d7b Fix AI skill selection handling 2026-04-13 11:03:43 +08:00
bincxz 1c9c4fcec3 fix: address second-round review feedback
- Extract fail-loud check to shared getCodexCustomConfigPreflightError so
  the list-models handler (aiBridge.cjs:2149) enforces the same up-front
  error as the stream handler. Previously a user whose config.toml
  env_key was unexported would get the targeted message on chat send but
  a generic "Missing env var" from model-list probes (once the probe was
  rewired for Codex in a future change).

- Wire Settings "Refresh Status" to also invalidate the shell-env cache.
  New invalidateShellEnvCache() helper in shellUtils; aiCodexGetIntegration
  now accepts an optional { refreshShellEnv } flag; the button passes it
  so a user who just exported OPENROUTER_API_KEY in their rc file can
  click Refresh instead of having to restart netcatty.

- Declare authHash in CodexCustomProviderConfig (types.ts + global.d.ts)
  so renderer TS actually sees the field instead of needing a cast.

- DRY the 360 magic number in ChatInput: extract
  MODEL_PICKER_MAX_WIDTH, use it in both the className max-width and the
  left-clamp math so the two can't drift.

- Move codexCustomConfigResolved useState declaration next to its
  companion codexConfigModel, above the effect that invokes its setter,
  and drop the duplicate declaration further down. Pure code-organization
  cleanup but removes a use-before-declaration nit.

No functional changes beyond the fail-loud parity and the refresh-shell-env
path. ACP behavior when authMethodId is omitted still requires a
real-world OpenRouter config.toml validation, which the user is running.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 02:30:29 +08:00
bincxz fea8e8b305 fix: stop probing codex-acp for models; show config.toml model when custom
Two issues the user flagged with the previous round:

1. Probing codex-acp for available models returned the stock ChatGPT
   catalog (GPT 5.4, Codex 5.x, o3, o4-mini) regardless of the active
   provider. For a user with a custom model_provider in
   ~/.codex/config.toml (OpenRouter + Qwen), those IDs are meaningless
   on their endpoint. Roll back the managed-Codex probe hook and go
   back to static CODEX_MODEL_PRESETS for the stock / ChatGPT path.

2. The fixed w-[300px] popover left empty space on the right whenever
   the longest row was narrower than 300px.

Instead of the probe, teach readCodexCustomProviderConfig to also
return the top-level `model` from config.toml and expose it on the
integration response. In AIChatSidePanel, call aiCodexGetIntegration
when Codex is the active agent and, if customConfig.model is present,
override agentModelPresets with a single-entry list pinned to that
model. Otherwise fall back to the static presets as before — so
ChatGPT users see GPT 5.x / Codex 5.x etc. exactly like before, while
custom-config users see just the model their provider is actually
pinned to.

Popover switches from fixed width to `w-max min-w-[160px] max-w-[360px]`
so it hugs content (great for short single-model lists) while still
capping very long rows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 02:10:35 +08:00
bincxz 18c001e9c5 fix: show custom config even when env_key is not exported yet
The first pass required both a custom model_provider in ~/.codex/config.toml
AND the referenced env_key to already be present in the shell environment.
If a user had the config file set up but hadn't (yet) exported the key in
their shell, detection returned null and the UI fell back to "Not
connected" + "Connect ChatGPT" — which is the exact flow they were trying
to avoid.

The config.toml is a strong enough signal of intent on its own. Keep the
integration in the connected_custom_config state regardless of env_key
availability, but expose envKeyPresent on the response so the UI can
explicitly warn "Warning: $MY_KEY is not set in your shell — export it".
Status label and color also flip to amber ("Custom config detected — env
var missing") so the state is easy to spot without dropping back to the
login prompt.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 01:53:33 +08:00
bincxz c2c6b265d4 feat: detect user's ~/.codex/config.toml custom provider as ready state
Users who hand-configure ~/.codex/config.toml with a custom model_provider
and matching [model_providers.<name>] entry are fully functional from the
Codex CLI, but netcatty only looked at codex login status — which reports
on ~/.codex/auth.json alone — and would therefore push them into the
ChatGPT login flow even though the CLI works for them.

Add a minimal TOML parser for the narrow subset we need (top-level keys
plus [model_providers.<name>] string tables), and readCodexCustomProvider
Config() to detect a usable custom-provider setup: an active model_provider
that isn't the built-in openai preset, pointing at a provider entry whose
env_key is set in the shell env (or api_key is hardcoded).

Surface this as a new integration state "connected_custom_config", add a
customConfig summary on the IPC response, and tweak the Codex settings
card so it shows the custom-provider name, hides the Connect ChatGPT
button, and drops the stale "OpenAI-compatible provider" hint when this
path is active.

At Codex-ACP spawn time, introduce getCodexAuthOverride() so we only pass
authMethodId: "chatgpt" when we truly have no other option. When a
netcatty-managed API key is present we still use "codex-api-key"; when the
user has a custom config we omit authMethodId entirely so codex-acp
resolves auth from the shell env / config.toml itself. Fold the detected
custom config (provider name, base url, env key presence) into the
provider reuse fingerprint so edits to config.toml invalidate cached ACP
instances.

Fixes the Codex half of #677 for users who skip Settings → AI providers.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 01:49:23 +08:00
bincxz 1e50b66407 fix: finish Codex provider follow-up for #677 2026-04-13 01:21:05 +08:00
bincxz f89afc0e05 feat: add setting to hide the standalone SFTP top tab
Adds a "Show SFTP tab" toggle in Settings → Appearance (under the
Vault section) that controls visibility of the standalone SFTP view
in the top tab bar. When disabled:

- The SFTP tab is removed from the top tab strip.
- The openSftp hotkey (Ctrl+Shift+O / ⌘⇧O) becomes a no-op.
- If the user is currently on the SFTP tab, the active tab auto-
  switches to Vaults.

The in-session SFTP side panel (opened from the terminal toolbar) is
unaffected — that is the surface users keep when they hide the
top-level tab.

Setting persists via localStorage, syncs across windows, and is
included in the cloud SyncPayload alongside the existing Vault
visibility toggles (showRecentHosts,
showOnlyUngroupedHostsInRoot). Default: on.

Addresses the first ask in #690.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-12 23:57:15 +08:00
bincxz 8b99f2411f fix vault root host filter sync and empty states 2026-04-12 23:27:36 +08:00
bincxz 3089cab88d add vault root ungrouped host toggle 2026-04-12 23:09:03 +08:00
Eric Chan 50b20eaa05 chore: triple-pass review and hardening of AI Skills logic 2026-04-12 17:25:45 +08:00
Eric Chan 58bc08a045 Add user skills injection and picker UI 2026-04-10 20:53:39 +08:00
bincxz fc32b44d8e fix: replace missing ToggleRow with SettingRow + Toggle
ToggleRow is a locally-defined component in HostDetailsPanel and
GroupDetailsPanel — it is NOT exported or available in the terminal
settings tab. Using it caused a white-screen crash. Replaced with
the existing SettingRow + Toggle pattern that's already used
throughout the terminal settings tab.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 20:19:54 +08:00