Commit Graph

44 Commits

Author SHA1 Message Date
bincxz 90b6e514ef Address terminal side panel auto-open review feedback 2026-06-29 22:24:07 +08:00
bincxz 05231d3fba Add terminal side panel auto-open setting 2026-06-29 22:08:07 +08:00
陈大猫 0c8370477c Unify AI permission auto mode (#1688) 2026-06-26 00:26:30 +08:00
陈大猫 f191a9b449 fix terminal dynamic tab title settings
Add a global dynamic tab title mode, default to agent-only titles, remove the per-host disable control, and keep terminal UI refresh paths in sync with the setting.
2026-06-25 14:34:40 +08:00
bincxz cff74b6e49 polish terminal and settings UI 2026-06-22 22:04:33 +08:00
陈大猫 b74b5a442a [codex] Add vault notes markdown workspace (#1558)
* Add vault notes markdown workspace

* Improve notes tree drag interactions

* Harden notes tree move behavior

* Refine vault notes interactions

* Address vault notes review feedback

* Tighten notes tree review fixes
2026-06-22 18:23:58 +08:00
陈大猫 982569532c Sync AI API keys across devices
Sync AI API keys through cloud sync while keeping device-local external tool paths local. Also preserves local AI keys in backups and stops sync if a local encrypted key cannot be read.
2026-06-22 11:45:07 +08:00
陈大猫 05b713ab18 [codex] Add configurable middle-click terminal behavior (#1443)
* Add configurable middle-click terminal behavior

* Fix middle-click terminal behavior edge cases
2026-06-12 16:28:09 +08:00
陈大猫 4459aa4ef3 Add terminal zoom disable setting
Adds a Shortcuts setting to disable terminal zoom shortcuts.
2026-06-12 14:33:17 +08:00
陈大猫 64a6986d01 Sync terminal selection AI preference (#1441)
Syncs the terminal selection Add to Conversation preference with AI settings so cloud sync, local backups, restores, and settings-only auto-sync detect the value.

Follow-up for #1436.
2026-06-12 14:28:33 +08:00
bincxz 1ebcd017bd Make terminal timestamps per-host 2026-06-11 15:25:34 +08:00
陈大猫 36267717ac feat(terminal): add system manager side panel for processes, tmux, and Docker
Introduce workspace-aware System side panel with remote process/tmux/Docker management, terminal popup for interactive attach, capability warmup, review-hardened IPC, performance optimizations, toast action errors, and SSH channel recovery on reconnect.
2026-06-11 04:19:21 +08:00
陈大猫 c0efc9d5c1 feat(ai): add quick messages with slash command picker (#691)
Closes #691
2026-06-11 03:11:39 +08:00
bincxz 14fe1e3ecb feat(shortcuts): add option to skip pinned tabs for number keys
Fixes #1375 by letting Cmd/Ctrl+[1...9] target only work tabs when enabled, while keeping the existing Terminus-style default.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 19:15:15 +08:00
bincxz 037b85bd66 fix(ui): smooth work-tab chrome transitions and split-pane autocomplete
Replace immersive instant-switch with animated active chrome theme sync so
top tabs match terminal sessions immediately on tab click, and clamp
autocomplete popups to the active pane so they stay anchored to the cursor
in split workspaces.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-10 01:10:33 +08:00
陈大猫 4b5993cad6 fix host sidebar behavior for editor tabs (#1348) 2026-06-09 21:24:16 +08:00
陈大猫 b1f930a995 feat(sftp): 侧栏 SFTP 增加追随终端目录模式 (#1266) (#1310)
* feat(sftp): add follow terminal directory mode for sidebar (#1266)

Add a toolbar toggle that keeps the side-panel SFTP browser synced with the linked SSH terminal cwd, inspired by MobaXterm's follow-folder behavior.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(sftp): probe pwd after commands when follow mode lacks OSC 7

Add a deferred getSessionPwd fallback after terminal commands when follow-terminal-cwd is enabled and the shell did not report OSC 7, and fix settings sync hook dependencies.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(sftp): repair follow toggle UI and backend cwd sync fallback

Fix SftpPaneView memo skipping follow prop updates, probe fresh pwd when OSC 7 is missing, and broaden linked terminal session resolution for sidebar follow mode.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(sftp): harden follow sync after review findings

Reuse shouldFollowTerminalCwdNavigate in production path, re-read connection
after async cwd probe, skip redundant navigate on toggle, and only probe pwd
when the SFTP side panel is open.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-08 19:28:30 +08:00
bincxz 7d30b19421 Harden timestamp edge cases 2026-06-06 17:31:41 +08:00
陈大猫 e3f68e1a3f [codex] fix cloud sync master key refresh (#1175)
* fix cloud sync master key refresh

* Protect first sync from default settings

* Avoid recovery prompt for settings-only sync

* Cancel stale syncs after master key changes
2026-06-01 19:29:53 +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
陈大猫 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
陈大猫 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
陈大猫 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
陈大猫 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
bincxz 684c094d40 Drop externalAgents from cloud sync (device-local config)
ExternalAgentConfig.command/acpCommand/args/env are OS- and
machine-specific (binary paths, .exe suffixes, platform-dependent
environment values). Pushing them to other devices either fails to
resolve or silently runs the wrong thing.

Stop collecting/applying STORAGE_KEY_AI_EXTERNAL_AGENTS and remove the
field from the SyncPayload type. apply silently ignores the field on
legacy snapshots that still carry it, so existing remote data is safe.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 23:16:37 +08:00
bincxz d84c2cc902 Preserve local AI apiKeys when applying synced settings
`collectSyncableSettings` strips device-bound encrypted apiKeys from
provider entries and webSearchConfig before upload, but
`applySyncableSettings` was writing them back wholesale, silently wiping
local credentials whenever any other setting changed on a second device.

Merge by id (providers) and by providerId (web search) so a synced
payload only overrides the apiKey when it explicitly carries one.

Also include `application/*.test.ts` in the npm test glob so the
syncPayload tests added in this PR actually run in CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 22:17:41 +08:00
bincxz c9da2a5893 Sync AI/UI settings and fix multi-display settings window
Extend cloud sync to cover AI provider config, external agents,
permission/tool modes, command policy, web search settings,
workspace focus style, terminal follow-app theme, SFTP default view,
and additional terminal options. Device-bound encrypted apiKey
placeholders are stripped from providers and webSearchConfig before
upload. Auto-sync now reacts to syncable localStorage changes via a
new adapter-level event.

Center the Settings window on the display of the window that opened
it instead of always using the main window, fixing issue #920.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-10 00:41:14 +08:00
bincxz 72e305fb7a Add reusable proxy profiles 2026-05-06 17:33:46 +08:00
bincxz 155463f77c add reusable proxy profiles 2026-05-06 15:20:23 +08:00
陈大猫 4dd2465388 Keep known hosts local during sync (#863) 2026-04-29 11:01:21 +08:00
陈大猫 c30d872852 fix(settings): guard customKeyBindings sync against echo loop (closes #818) (#821)
* fix(settings): guard customKeyBindings cross-window sync against echo loop (closes #818)

customKeyBindings was the only synced setting whose two cross-window
handlers (DOM storage event + IPC onSettingsChanged) called
setCustomKeyBindings unconditionally. Every broadcast landed with a
fresh parsed object reference, so React re-rendered and the persist
effect re-broadcast, echoing across windows indefinitely.

While the echoes carry the same content, a rapid second click from
the user can arrive between the outbound broadcast and an older
in-flight echo — the echo's setState then clobbers the latest click
and the UI "bounces" from Disabled back to the original binding.
This matches the report in #818 (disable and reset operations
flicker between values when clicked in quick succession).

Fix: mirror the equality guards used by every other synced field.
Compare the incoming payload (stringified for objects) against the
current value from settingsSnapshotRef, and skip setCustomKeyBindings
when they match. Add customKeyBindings to settingsSnapshotRef so the
IPC handler has access without pulling it into the effect's closure.

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

* fix(settings): stop shortcut sync bounce flicker

* fix(settings): harden shortcut sync ordering

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 13:34:38 +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 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 3089cab88d add vault root ungrouped host toggle 2026-04-12 23:09:03 +08:00
bincxz 33f8221d5c refactor: remove immersive mode toggle remnants — always enabled
Immersive mode was already hardcoded to true with a no-op setter.
Clean up all dead code:
- Remove isImmersive param from useImmersiveMode hook
- Remove immersiveMode/setImmersiveMode from useSettingsState
- Remove toggle from SettingsPage and SettingsAppearanceTab
- Remove sync read/write of immersiveMode setting
- Remove i18n keys for the removed toggle
- Simplify App.tsx conditionals

Kept: useImmersiveMode hook (core logic), CSS classes (fade overlay),
sync type field (backward compat), storage key.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 13:49:03 +08:00
陈大猫 2f314c3588 feat: group configuration inheritance (#220) (#593)
* feat(i18n): add translations for group config panel

* feat(models): add GroupConfig data model, resolution logic, and encryption

Add the GroupConfig interface for group-level default settings that hosts
inherit. Includes ancestor-chain resolution (A/B/C merges from A, A/B,
A/B/C), host-level application logic, storage key, and secure field
encryption/decryption for sensitive GroupConfig fields.

Part of #220.

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

* feat(state): add groupConfigs state management with encryption

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

* feat(ui): create GroupDetailsPanel with full config editing

Side panel for editing group-level default configuration using AsidePanel.
Includes General, SSH, Telnet, Advanced, Mosh, and Appearance sections
with sub-panel navigation for Proxy, Chain, EnvVars, and Theme selection.

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

* feat(vault): wire GroupDetailsPanel, replace rename dialog with full config panel

Replace all group rename dialog triggers with the new GroupDetailsPanel sidebar.
The hover edit button, context menu, and tree view edit callbacks now open the
full group configuration panel instead of a simple rename dialog.

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

* feat(connect): apply group config defaults at connection time

When connecting to a host, merge group-level default configuration so
hosts inherit their group's settings for auth, protocol, appearance,
and other inheritable fields. Connection logs still reference the
original host's label/hostname.

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

* feat(sync): include groupConfigs in sync and export payloads

Add groupConfigs to SyncPayload, SyncableVaultData, buildSyncPayload,
and applySyncPayload so group connection defaults are preserved during
cloud sync and data import/export. Also wire groupConfigs into the
vault object in SettingsPage so it flows through to the sync payload
builder.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(vault): update group configs on move and delete

* feat(host-panel): show inherited group defaults as placeholders

When editing a host that belongs to a group with configuration, group
default values now appear as placeholder text in username, startup
command, and charset fields where the host doesn't have its own value.

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

* fix: clean up unused imports in GroupDetailsPanel

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

* feat(group-panel): add/remove protocol sections, editable parent group

- SSH and Telnet sections are now add/remove — click "Add Protocol"
  to enable, "..." menu to remove. Only enabled protocols override hosts.
- Parent Group is now editable via Combobox dropdown for quick
  group moving.

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

* fix: move SSH-specific fields into SSH protocol section

Startup Command, Legacy Algorithms, Proxy, Host Chaining,
Environment Variables, and Mosh are all SSH-specific and now only
visible when SSH protocol is added. Only Charset remains as a
shared field in the Advanced section.

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

* fix: hide charset and appearance when no protocol is added

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

* fix: close Add Protocol dropdown after selection

Use controlled open state to explicitly close the dropdown when a
protocol is selected, preventing residual content from overlapping
the newly rendered section.

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

* fix: apply group defaults in TerminalLayer sessionHostsMap

Terminal component was re-reading the original host from the hosts
array by hostId, bypassing the group defaults applied in
handleConnectToHost. Now sessionHostsMap applies resolveGroupDefaults
+ applyGroupDefaults when building the host object for each session,
so Terminal sees the merged credentials/settings.

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

* fix: move Add Protocol to bottom, fix i18n for protocol/font labels

- Add Protocol button moved below Appearance section
- Added i18n keys: addProtocol, removeProtocol, fontFamily, fontSize
- All hardcoded English strings replaced with t() calls

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

* fix: replace font family text input with TerminalFontSelect dropdown

Use the same font selector component as settings, showing available
terminal fonts with preview. Includes "Use Global" reset button.

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

* feat(group-panel): match HostDetailsPanel key/certificate selection pattern

Replace the simple Combobox key selector with the same credential selection
flow used in HostDetailsPanel: a popover with Key/Certificate options,
inline combobox per type, and proper badge display with certificate icon.

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

* feat(group-panel): add Local Key File option to credential selection

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(group-panel): add identityFilePaths to GroupConfig and Local Key File option

- Added identityFilePaths to GroupConfig interface and INHERITABLE_KEYS
- GroupDetailsPanel now supports Key, Certificate, and Local Key File
  credential selection, matching HostDetailsPanel's full credential flow

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

* fix: prevent local key file input from overflowing panel width

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

* fix: constrain local key file input width with w-0 flex-1

Native input elements have a large default min-width. Using w-0 with
flex-1 forces the input to shrink within the flex container.

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

* fix: add overflow-hidden to SSH Card to contain local key file input

Matches HostDetailsPanel's Card which uses overflow-hidden on the
credentials section to prevent long file paths from overflowing.

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

* fix: add min-w-0 to key file path row for proper text truncation

Flex children need min-w-0 for truncate to work correctly,
otherwise the text pushes the container wider.

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

* fix: force key file path text truncation with inline max-width calc

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

* fix: use fixed 320px max-width on key file path text to force truncation

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

* fix: add overflow-hidden to AsidePanelContent to prevent content overflow

The root cause was the inner div of AsidePanelContent only had
overflow-x-hidden which was being overridden by ScrollArea's viewport.
Changed to full overflow-hidden with w-full box-border.

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

* fix: override Radix ScrollArea viewport's display:table in AsidePanel

Radix ScrollArea Viewport wraps content in a div with
display:table and min-width:100%, causing content to expand beyond
the panel width. Override this on AsidePanelContent's ScrollArea
to use display:block and min-width:0 instead.

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

* fix: critical issues — seed new hosts from group defaults, validate group names, fix empty import

- HostDetailsPanel: When groupDefaults has values for port/username/charset,
  new hosts start with undefined/empty so group defaults take effect via
  applyGroupDefaults() instead of being blocked by hardcoded values
- GroupDetailsPanel: Validate group name in handleSubmit to reject '/' and
  '\' characters, matching the old rename dialog behavior, with visual error
- useVaultState: Check groupConfigs !== undefined instead of truthy so that
  importing an empty array [] properly clears all group configs

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

* fix: safe prefix replacement, remove dead code, extract shared resolveEffectiveHost

- Replace all .replace(oldPath, newPath) / .replace(sourcePath, newPath) with
  explicit prefix slicing (newPath + str.slice(oldPath.length)) in handleSaveGroupConfig
  and moveGroup for more robust path renaming
- Remove dead c.path === oldPath branch in finalConfigs mapping since updatedConfigs
  already contains the config with newPath
- Extract resolveEffectiveHost helper in App.tsx to deduplicate group defaults
  resolution in _handleTrayPanelConnect and handleConnectToHost

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

* fix: preserve undefined port on save when group has port default

form.port || 22 was forcing port to 22 even when intentionally left
undefined for group inheritance. Now uses nullish coalescing and only
defaults to 22 when no group port default exists.

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

* fix: SSH-adjacent field detection, chain host defaults, telnet inheritance, theme clear

- hasSshFields() now checks proxyConfig, hostChain, startupCommand,
  legacyAlgorithms, environmentVariables, moshEnabled, moshServerPath,
  and identityFilePaths so the SSH section auto-opens when editing
- Chain hosts in sessionChainHostsMap now get group defaults applied
  via resolveGroupDefaults + applyGroupDefaults
- Added telnetEnabled to GroupConfig interface and INHERITABLE_KEYS;
  save handler sets telnetEnabled: true when Telnet section is on
- Theme/font "Use global" clear now sets override to false instead of
  undefined, preventing parent group theme from leaking through

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

* fix: review round 4 — sync, SFTP, port forwarding, type safety, UX

- Scan groupConfigs in encrypted credential guard (P1 security)
- Add groupConfigs to auto-sync payload and three-way merge (P1 sync)
- Apply group defaults in SFTP connections (P1 SFTP)
- Apply group defaults in all port forwarding paths (P1 port forwarding)
- Make Host.port optional to fix unsafe type cast (P1 type safety)
- Fix port input empty → 0 instead of undefined (P2)
- Add port placeholder showing inherited value (P2)
- Mutual exclusion of group/host detail panels (P2)
- Fix sub-panel width jump 420px → 380px (P2)
- Validate duplicate group path on rename/reparent (P2)

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

* fix: review round 5 — null guard, empty array inheritance, memo comparator, form reset

- Guard groupConfigs import against null payload (P1 crash)
- Validate duplicate path on moveGroup drag-drop (P2 data corruption)
- Clear empty environmentVariables to undefined for group inheritance (P1)
- Clear empty hostChain to undefined for group inheritance (P2)
- Add groupConfigs to SftpView memo comparator (P1 stale defaults)
- Add key={editingGroupPath} to GroupDetailsPanel for form reset (P1)

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

* fix: review round 6 — copy credentials, protocol dialog use effective host

- Apply group defaults in handleCopyCredentials (P2)
- Apply group defaults in hasMultipleProtocols check (P2)
- Pass effective host to ProtocolSelectDialog (P2)

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

* fix: serialize protocol:'ssh' marker to persist SSH section in group config

- Add protocol:'ssh' as marker field in handleSubmit SSH block
- Detect protocol:'ssh' in hasSshFields() to preserve section on reopen
- Clean up protocol field in removeSsh()

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-01 12:40:40 +08:00
陈大猫 c7ae51b952 feat: host/group management improvements (#506) (#589)
* feat(models): add pinned and lastConnectedAt fields to Host

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(i18n): add translations for pinned and recently connected sections

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(vault): add pin toggle, lastConnectedAt tracking, and computed sections

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

* feat(vault): render Pinned and Recently Connected sections at root level

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

* feat(vault): add pin/unpin context menus and hover edit buttons in all views

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

* feat(vault): make breadcrumb a drop target for moving groups back to root

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(settings): add toggle for showing recently connected hosts section

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: resolve lint warnings for unused vars and unnecessary dependency

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

* fix: improve pin performance and add pop-in animation

- Use ref for hosts in callbacks to avoid stale closures and
  unnecessary re-renders when hosts array changes
- Add pop-in spring animation on pinned host cards with staggered
  delay for a satisfying visual effect

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

* fix: fix pop-in animation visibility and improve pin responsiveness

- Move @keyframes pop-in out of @layer base to global scope so inline
  styles can reference it
- Add translateY to animation for a bouncier, more satisfying feel
- Use pinnedAnimKey to force card remount on pin changes so animation
  replays each time
- Wrap onUpdateHosts in startTransition for non-blocking pin updates

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

* fix: only animate newly pinned card, increase section spacing

- Track lastPinnedId instead of global animKey so only the newly pinned
  card gets the pop-in animation, not all existing pinned cards
- Clear animation state via onAnimationEnd for clean re-trigger
- Add mb-4 to Pinned and Recent sections for better visual separation

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

* feat(vault): show pin indicator icon on pinned host cards

Small semi-transparent pin icon in top-right corner of pinned host
cards in the Hosts section (grid view only).

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

* style: use solid amber/yellow pin indicator icon

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

* style: tilt pin indicator icon 45 degrees

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

* style: replace pin indicator with filled amber star on all pinned cards

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

* fix: move lastConnectedAt tracking to App-level handleConnectToHost

Previously updating lastConnectedAt in VaultView's handleHostConnect
which could be lost during tab switches. Now tracked at the App level
where all connections are handled, ensuring the timestamp persists
regardless of UI navigation state.

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

* fix: address Codex review findings (P2 issues)

1. useStoredBoolean now syncs across same-window components via
   CustomEvent dispatch, so Settings toggle immediately updates VaultView
2. lastConnectedAt updated after connectToHost succeeds, not before
3. Pinned and Recently Connected sections now respect active search
   and tag filters

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

* fix: address second round Codex review findings

1. Track lastConnectedAt on actual 'connected' status instead of
   session creation - handles via handleSessionStatusChange wrapper
2. Covers tray panel connections since all paths go through
   updateSessionStatus
3. Pinned/Recent cards now honor multi-select mode with checkbox
   UI instead of triggering connections

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

* fix: address third round Codex review findings

1. [P1] Use hostsRef in handleSessionStatusChange to avoid
   overwriting concurrent host changes with stale snapshot
2. [P2] Exclude pinned/recent hosts from main host list at root
   level to prevent duplicate cards on screen
3. [P2] Remove Pin action from tree view context menu since tree
   view has no pinned ordering/indicator support

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

* fix: address fourth round Codex review findings

1. [P1] Remove leftover onToggleHostPinned references in HostTreeView
   root-level component that were missed in previous cleanup
2. [P2] Add draggable + onDragStart to pinned/recent host cards so
   drag-and-drop between groups still works
3. [P3] Fix grouped view header count to exclude hosts already shown
   in pinned/recent sections

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

* fix: use functional state update for lastConnectedAt, dedupe pinned from recent

1. [P2] Add updateHostLastConnected using setHosts(prev => ...) functional
   update pattern (same as updateHostDistro) to avoid overwriting concurrent
   host changes when multiple sessions connect simultaneously
2. [P3] Exclude pinned hosts from Recently Connected section to prevent
   duplicate cards between the two top sections

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

* fix: wire showRecentHosts into settings sync, clear pin on duplicate

1. [P2] Add showRecentHosts to SyncPayload settings so the preference
   survives cloud sync and settings export/import
2. [P2] Clear pinned and lastConnectedAt on duplicated hosts so copies
   don't inherit pin/recent status from the original

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 23:55:45 +08:00
陈大猫 aeacd913f5 feat: sync global SFTP bookmarks via cloud sync (#553)
* feat: sync global SFTP bookmarks via cloud sync

Global SFTP path bookmarks were stored only in localStorage and not
included in the cloud sync payload, so they could not be synced across
devices. Add them to the sync settings, with auto-sync detection via
a custom event and in-memory snapshot rehydration on import.

Local bookmarks remain device-specific by design.

Closes #548

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

* fix: deduplicate global SFTP bookmarks by path during merge

When the same path is bookmarked independently on two devices, each
generates a different random ID. The entity-array merge preserves both,
creating duplicates. Add path-based deduplication after settings merge,
following the same pattern used for known hosts.

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

* fix: sync global bookmarks across renderer windows via storage event

When cloud sync imports bookmarks in the Settings window, the main
window's in-memory snapshot stays stale. Listen for cross-window
storage events on the bookmark key to auto-rehydrate.

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-03-28 18:59:58 +08:00
陈大猫 9a7d4decff feat: 终端命令自动补全系统 (#527)
* feat: 终端命令自动补全系统

实现类似 WindTerm/Fish 的终端命令自动补全功能,不依赖机器学习:

- 历史命令持久化存储:按主机分组,频率+时间衰减排序,跨会话共享
- 前缀匹配引擎:支持精确前缀匹配和模糊匹配(首字符+连续字符+词边界加权)
- Prompt 检测器:识别 bash/$、zsh/%、fish/> 等常见 prompt 模式,排除 vim/less 等程序
- Ghost Text 插件:xterm.js 自定义 addon,光标后灰色行内建议,→ 接受全部,Ctrl+→ 接受一词
- 弹出补全菜单:浮动列表 UI,↑↓ 导航,Tab/Enter 选中,Esc 关闭,来源标记(h/c/s/o/a)
- @withfig/autocomplete 集成:600+ 命令规范的子命令、选项、参数补全
- 上下文感知:解析命令行 token,根据当前位置提供对应类型的补全
- 用户配置:启用/禁用、Ghost Text、弹出菜单、防抖延迟、最小字符数等
- 快速打字防误触:检测打字速度,快速输入时抑制建议
- 输入防抖 100ms,异步匹配不阻塞 UI

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

* fix: 补全菜单混合展示历史命令和 spec 子命令

- 输入已知命令名(如 docker)时即使没有空格也预览子命令
- 历史命令条数从 8 降为 5,留空间给 spec 建议
- 修复 wordIndex === 0 时 spec 补全被跳过的问题

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

* fix: 补全菜单在终端底部时向上展开

当光标在终端下方、空间不足时,弹出菜单向上展开(底边对齐光标行),
避免溢出终端区域。列表顺序和选中逻辑不变——最可能的选项始终在顶部,
用户初始向下选择。参考 Termius 的做法。

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

* fix: 补全菜单跟随终端主题 + Enter 直接执行命令

1. 补全菜单颜色从终端主题动态派生(color-mix),不再硬编码色值,
   确保与任何主题视觉一致
2. 在弹出菜单中按 Enter 选择命令时,直接插入并发送 \r 执行,
   无需用户再按一次回车
3. Tab/鼠标点击仍然只插入不执行(保留选择后编辑的能力)

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

* fix: 修复 PR review 发现的全部 20 个问题

功能修复:
- #1 修复 selectAndExecute 导致命令双重录入历史:用 suppressNextEnterRecordRef
  标志位让 handleInput 的 Enter 分支跳过已经录入过的命令
- #2 修复 Prompt 末尾 $ 误判:重写 findPromptBoundary 为从左到右逐字符扫描,
  排除 $HOME/$PATH 等变量引用(检查 $ 前是否有空格、是否在 token 内部)
- #6 快速打字检测实际生效:快速打字时 debounce 延迟翻倍(200ms),等用户停顿
- #8 resolveSpecContext 处理带参数的 option(如 --name value):
  识别 option 的 args 字段,自动跳过下一个 token
- #9 Ghost text 位置随终端滚动/渲染更新:注册 term.onRender 回调
- #13 Escape 键不再拦截 vi-mode:仅在 popup 可见时消费 Escape,
  ghost text 显示时不拦截(ghost text 是被动的,不应阻止 shell 交互)
- #14 所有 setState 统一使用 EMPTY_STATE 常量,不再遗漏 expandUpward 字段

架构修复:
- #3 消除 CustomEvent 通信:改为 onAcceptText 回调注入,
  Terminal.tsx 直接传 writeToSession 回调给 hook,
  删除 createXTermRuntime 中约 20 行 listener 代码和 cleanupAutocompleteListener 字段
- #7 xterm 私有 API 访问集中到 xtermUtils.ts:getCellDimensions 统一入口,
  带缓存机制,仅在首次访问或 terminal 切换时触发 DOM 测量
- #16 删除 getCommandNameSuggestions 中多余的动态自导入 await import("./figSpecLoader")

性能修复:
- #5 合并 ghost text 和 popup 的查询路径:删除独立的 getInlineSuggestion,
  fetchSuggestions 只调一次 getCompletions,ghost text 取 completions[0]
- #10 preloadCommonSpecs 分批加载(每批 8 个,requestIdleCallback 间隔),
  延迟 200ms 启动,且检查 enabled 才执行
- #11 scoreEntry 改为 scoreEntryAt(entry, now),now 在查询开始时缓存一次
- #15 scrollIntoView 从 smooth 改为 instant,消除快速导航动画排队
- #19 loadSpec 添加 in-flight 去重(inFlightLoads Map),同一 spec 并发加载只触发一次 import
- #20 存储满时淘汰改为按 score 排序后保留前半,而非按插入顺序

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

* fix: 修复二审发现的全部 10 个问题

功能修复:
- #1(高) insertSuggestion 改用实时 detectPrompt 而非过时的 lastPromptRef,
  修复用户继续打字后 Tab 选择建议导致字符重复插入的 bug
- #2(中) handleInput Enter 录入历史优先用实时 detectPrompt,
  修复快速打字场景下 recordCommand 记录不完整命令
- #9 suppressNextEnterRecordRef 添加 100ms 安全超时清除,防止 flag 残留
- #10 getNextWord 从 index 1 开始搜索分隔符,修复 ghost text 以 / 开头时
  一次接受全部而非逐段的问题

性能修复:
- #3(中) GhostTextAddon 注册 term.onResize 调用 invalidateCellDimensionCache,
  确保 resize/字体变化后 cell 尺寸缓存正确失效
- #4 updatePosition 缓存 lastLeft/lastTop,位置无变化时跳过 DOM 写入;
  字体属性移到 show() 中只设置一次,不再每帧写 6 个 style
- #5 统一 clearState() 函数替代所有 setState({...EMPTY_STATE}),
  带 popupVisible 守卫避免无效 re-render
- #6 hasSpec 中 specs.includes() 改为 Set.has(),O(1) 查找

架构修复:
- #7 Terminal.tsx 中 autocompleteAcceptTextRef 去掉多余的 useCallback 包装
- #8 删除 AutocompletePopup 的 onClose 死代码 prop

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

* fix: popup 默认不选中任何项,用户按 ↑/↓ 后才选中

修复输入 ls 等简单命令时回车误执行联想结果的问题:
- selectedIndex 初始为 -1(无选中),Enter 直接执行用户输入的命令
- 用户按 ↑/↓ 导航后 selectedIndex >= 0,此时 Enter 才执行选中的建议
- Tab 仍然可以直接接受第一条建议(主动接受行为)
- Enter 无选中时关闭 popup 并让按键透传到终端

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

* fix: fig spec 改为从静态资源 fetch 加载,修复生产构建中补全不工作

根因:@vite-ignore 动态 import 在 Electron 生产构建中无法解析
node_modules 路径(app:// 协议只能访问 dist/ 目录)。

修复方案(与 Monaco 编辑器相同的模式):
- 新增 scripts/copy-fig-specs.cjs,prebuild 时将全部 739 个 fig spec
  从 node_modules/@withfig/autocomplete/build/ 复制到 public/fig-specs/
- Vite 自动将 public/ 内容复制到 dist/,app:// 协议可以正常访问
- figSpecLoader.ts 改用 fetch + Blob URL + dynamic import 加载 spec,
  同时保留 @vite-ignore import 作为 fallback(兼容 dev 模式)
- public/fig-specs 加入 .gitignore(构建时生成,不进版本控制)

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

* fix: ESLint 忽略 public/fig-specs 目录(第三方生成代码)

与 public/monaco 相同的处理方式——这些是从 node_modules 复制的
第三方构建产物,不应被项目 ESLint 规则检查。

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

* fix: 输入完整子命令名时展示其选项(如 git commit 显示 --message 等)

当 currentToken 完全匹配一个子命令时(如 "git commit" 中的 "commit"),
导航进入该子命令并展示其 options 和 sub-subcommands 作为预览。

之前的逻辑因为 name !== currentToken 过滤掉了完全匹配的项,
且 resolveSpecContext 的 consumedTokens 不包含当前 token,
导致停留在父级而看不到子级的选项。

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

* fix: 修复 fig spec index.js 解析失败导致补全不工作

根因:index.js 格式为 var e=[...],diffVersionedCompletions=[...];
正则 /var\s+\w+\s*=\s*(\[[\s\S]*?\]);/ 要求 ] 后紧跟 ;,
但第一个数组后面是 , 不是 ;,导致非贪婪匹配跳到第二个 ];,
捕获了两个数组拼在一起,JSON.parse 失败,spec 列表为空。

修复:改用 indexOf 找第一个 [ 和对应的 ],直接截取子串解析。
fig spec 的 index 是简单的字符串平坦数组,无嵌套括号。

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

* fix: fig spec 改用 URL 直接 dynamic import,移除 fetch+Blob 方案

fetch + Blob URL + import() 方案可能被 Electron CSP 策略阻止。
改为直接用完整 URL 做 dynamic import:
- dev: import("http://localhost:5173/fig-specs/git.js")
- prod: import("app://./fig-specs/git.js")

两种环境下动态 import 都能正常解析模块,无需 fetch 中间步骤。
同时简化 getAvailableSpecs 也用同样方式,移除 fetch+正则解析。

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

* fix: fig spec 改为通过 Electron IPC 加载,彻底解决 dev/prod 加载问题

之前的方案(静态文件 + dynamic import / fetch + Blob URL)都因为
Vite dev server 对 .js 文件的模块转换和 Electron CSP 限制而失败。

新方案:通过 main process 的 Node.js require() 加载 fig spec,
通过 IPC 传给 renderer:
- main.cjs: 添加 netcatty:figspec:list 和 netcatty:figspec:load handler
- preload.cjs: 暴露 listFigSpecs() 和 loadFigSpec() API
- figSpecLoader.ts: 通过 window.netcatty bridge 调用 IPC

优势:
- main process 直接访问 node_modules,dev 和 production 都可靠
- 无需复制文件到 public/、无需 @vite-ignore hack
- spec 数据通过 IPC 序列化传输,无 CSP 限制
- 删除了 scripts/copy-fig-specs.cjs 和 public/fig-specs/ 相关代码

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

* fix: main process fig spec 加载改用 import() 替代 require()

@withfig/autocomplete 是 ESM 包("type": "module"),
CommonJS 的 require() 无法加载 ESM 模块会抛 ERR_REQUIRE_ESM。
改用 dynamic import() 在 async handler 中加载。

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

* fix: fig spec 加载用 pathToFileURL 绕过 package.json exports 限制

@withfig/autocomplete 的 exports 字段只允许 import "." 和 "./dynamic",
Node.js 严格遵守 exports map 拒绝解析 build/git.js 等子路径。

改为手动拼接文件绝对路径 + pathToFileURL 转换为 file:// URL 后 import,
完全绕过 Node.js 的 package exports 限制。

同时修复 promptDetector 不再 trim 尾部空格(用 cursorX 确定实际输入长度),
确保 "git commit " 的尾部空格被保留,触发空 token 显示选项列表。

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

* feat: 补全菜单添加详情面板 + 清理调试日志

- 选中或悬停补全项时,右侧显示详情面板(类似 VS Code IntelliSense)
  - 显示完整命令名、来源类型标签(Option/Subcommand/History 等)
  - 显示完整的描述文本(不再截断)
- source 标记移到左侧,与描述分离,更易读
- 悬停和键盘选中都能触发详情面板
- 向上展开时详情面板也正确对齐
- 清理所有临时调试 console.log

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

* chore: 清理全部调试日志

移除 autocomplete 模块中所有临时 console.log 调试语句,
仅保留 figSpecLoader 中的 console.warn 用于真实错误报告。

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

* fix: 三审问题修复 — 移除多余 prop、过滤子路径 spec、防路径遍历

1. 移除 Terminal.tsx 传给 AutocompletePopup 的多余 onClose prop
2. getCommandNameSuggestions 过滤含 / 的 spec 名(aws/s3 等不是直接命令)
3. figspec:load IPC handler 添加 .. 路径遍历检查

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

* fix: Codex review 5 个问题全部修复

1. [P1] fuzzy 匹配建议不以 userInput 开头时,用 Ctrl+U 清行再写入完整命令,
   避免 substring 截断产生损坏的命令行
2. [P2] Ghost addon 初始化改用 polling 等待 termRef,解决首次挂载时
   termRef.current 为 null 导致 ghost text 永远不激活的问题
3. [P2] popup overlay 改为 pointer-events-none 透传,仅 popup 自身设
   pointer-events: auto,不再阻止终端区域的鼠标交互
4. [P2] getCompletions 异步返回后重新 detectPrompt 校验输入是否已变,
   丢弃过时的补全结果避免覆盖新状态
5. [P2] prompt 检测支持折行:当 line.isWrapped 时向上回溯查找 prompt 行,
   拼接多行内容作为完整 userInput

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

* fix: Codex review 第二轮 3 个问题修复

1. [P2] broadcast 模式下 autocomplete 插入也触发广播 —
   onAcceptText 回调中调用 onBroadcastInputRef 通知其他 session
2. [P2] 支持无尾随空格的 prompt(如 cmd.exe C:\path>)—
   prompt 字符后允许直接是行尾,boundary 为 i+1
3. [P2] 光标移动 escape 序列(Left/Home/End)清除过时建议 —
   不再静默忽略,改为 clearState() 清除 popup 和 ghost text

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

* fix: Codex review 第三轮 3 个问题修复

1. [P2] commandBufferRef 处理 Ctrl+U 清行 — fuzzy 匹配发送 \x15 时
   重置 buffer,避免 onCommandExecuted 记录错误的拼接命令
2. [P2] fetchVersionRef 递增计数器废弃过时异步结果 — clearState/Escape
   关闭 popup 时 bump version,getCompletions 返回后检查 version 匹配,
   防止已关闭的 popup 被旧请求重新打开
3. [P2] prompt scanLimit 从 80 提高到 200 — 支持包含 git branch、
   kube context、长路径的 prompt,超过 80 列不再失效

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

* fix: Codex review 第四轮 3 个问题修复

1. [P1] 拒绝绝对路径 — figspec:load IPC handler 检查 commandName
   不以 / 或 \ 开头,防止 path.join 丢弃前缀导致任意 JS 执行
2. [P1] cmd.exe prompt > 后不要求空格 — 对 > ❯ ➜ › 等 prompt 字符
   不强制要求后跟空格,支持 C:\src>dir 格式
3. [P2] serial line mode 下 autocomplete 走 serialLineBufferRef —
   在串口 lineMode 时不直接 writeToSession,而是缓冲到 line buffer
   并处理 local echo,与正常按键输入行为一致

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

* fix: Codex review 第五轮 — translateToString(false) 保留尾部空格

translateToString(true) 会 trim 行尾空格,导致 cursorX 截取的
userInput 与实际行内容不一致。改为 translateToString(false) 保留
原始空格,确保 "git commit " 的尾部空格被正确保留用于触发选项补全。

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

* feat: 设置页添加自动补全开关(启用/Ghost Text/弹出菜单)

在终端设置页末尾新增「自动补全」区域,包含三个开关:
- 启用自动补全:总开关
- 行内建议(Ghost Text):光标后灰色建议文本
- 弹出菜单:浮动补全列表

子开关在总开关关闭时 disabled。中英文 i18n 翻译齐全。

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

* fix: Codex review 第六轮 3 个问题修复

1. [P1] 光标不在行尾时禁止补全 — 检测 cursorX 后方是否有字符,
   有则 clearState 不显示建议,避免 mid-line 插入导致文本重复
2. [P2] Enter 录入历史改为先尝试实时 detectPrompt,失败则 fallback
   到 lastPromptRef 缓存,应对高延迟 SSH 下 buffer 未回显的情况
3. [P2] fuzzy 替换在 Windows host 上用退格清行而非 Ctrl+U —
   cmd.exe/PowerShell 不支持 Ctrl+U,改为发送 \b 退格序列

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

* fix: Codex review 第七轮 — commandBuffer 退格处理 + 接受后历史记录

1. [P2] commandBufferRef 处理 \b 退格 — Windows fuzzy 替换用退格
   清行时正确移除 buffer 末尾字符,避免记录拼接错误的命令
2. [P3] lastAcceptedCommandRef 追踪接受的补全文本 — Tab/→ 接受后
   立即 Enter 时用追踪值录入历史,不依赖可能未回显的 buffer

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

* fix: Codex review 第八轮 — 历史记录准确性 + 设置同步

1. [P2] 用户继续编辑后清除 lastAcceptedCommandRef — Tab 接受
   "git status" 后追加 " --short" 再 Enter 时记录完整编辑后的命令
2. [P2] Ghost text →/Tab 接受路径也设置 lastAcceptedCommandRef —
   确保所有接受路径在快速 Enter 时都能准确记录命令
3. [P2] autocomplete 设置加入 SYNCABLE_TERMINAL_KEYS —
   跨设备同步时保留自动补全偏好

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

* fix: Codex review 第九轮 — REPL 误识别 + 本地终端 OS 检测

1. [P1] local terminal 的 hostOs 改用 navigator.platform 检测实际 OS,
   避免 Windows 上 fallback 到 "linux" 导致 Ctrl+U 清行失败
2. [P2] 回退 > 无条件接受改动,恢复要求 > 后跟空格或行尾 —
   避免 python >>>、mysql>、sqlite> 等 REPL 被误识别为 shell prompt
3. 新增 REPL NON_PROMPT_PATTERNS:>>>(python)和 word>(mysql/redis)

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

* fix: Codex review 第十轮 4 个问题修复

1. [P1] cmd.exe prompt C:\path> — 对 > 特判:前面是 \ 或 / 时允许无空格,
   避免误匹配 REPL(python>>>、mysql>)的同时支持 Windows cmd prompt
2. [P2] serial lineMode autocomplete 不再 early return — fall through 到
   共享的 commandBuffer/broadcast 更新逻辑
3. [P2] serial 字符模式 + localEcho 时 autocomplete 插入文本也本地回显
4. [P3] 运行时关闭 autocomplete 时调用 clearState() 清除已显示的 popup

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

* fix: Codex review 第十一轮 — option args、PS2 误识别、bridge 缓存

1. [P2] resolveSpecContext 返回 option 的 args — 当光标在 option 参数
   位置时(如 git archive --format |),返回该 option 的 args 而非
   subcommand 的 args,使 tar/zip 等枚举值能正确补全
2. [P2] 排除 bare > 作为 shell prompt — bash PS2 续行提示 > 加入
   NON_PROMPT_PATTERNS,避免在多行命令续行和 REPL 中误触发补全
3. [P3] bridge 不存在时不缓存 null — preload 时 bridge 可能未就绪,
   缓存 null 会永久禁用该命令的 spec 补全

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

* fix: Codex review 第十二轮 — prompt 检测取最后一个分隔符

Starship/Powerlevel10k 等 prompt 包含多个 prompt 字符
(如 ➜  repo git:(main) $),之前在第一个 ➜ 就停了,
把后续 prompt 文本当成用户输入。

改为收集所有候选 prompt 边界,返回最后一个。确保
"➜  repo git:(main) $ ls" 中 userInput 正确为 "ls"。

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

* fix: Codex review 第十三轮 — prompt 搜索范围限制 + cmd.exe 路径

1. [P2] prompt 扫描限制在行前 60% — 避免 "echo foo > bar" 中的
   重定向符 > 被当作 prompt 结束(prompt 不会出现在行尾部分)
2. [P3] cmd.exe 路径检测扩展 — 除了 \ / 前缀,也检测行首是否有
   驱动器号 (X:) 模式,支持 C:\Users\me> 等标准 Windows prompt

P1 (高延迟 SSH buffer 滞后) 和 P2 (Enter 时 stale prompt) 属于
prompt 检测方案的固有局限,根本解决需要 OSC 133 Shell Integration,
不在本 PR 范围内。已有 lastAcceptedCommandRef fallback 缓解。

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-03-26 19:45:34 +08:00
陈大猫 34f9d2a663 chore: 死代码清理与架构分层修复 (#524)
* chore: 移除死代码并修复架构分层违规

- 删除未使用的 ACP 模块 (infrastructure/ai/acp/)
- 删除未使用的 AI 组件 (ExecutionPlan, PermissionDialog)
- 将 syncPayload.ts 从 domain 移至 application 层,修复分层违规
- 移除未使用的导出 (useSecurityState, useProviderStatus, GitHubAuthState,
  getAgentCommandLabel, ImageAttachment, HotkeyActions)
- 收窄 Electron bridge module.exports,移除未使用的导出函数
- 将仅内部使用的函数/类型取消导出 (isSupportedLocale, SyncDashboard)

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

* chore: 二次审查清理 — 移除更多死代码和架构违规

- 移除未使用的 ConversationEmptyState 组件和类型
- 移除未使用的 PromptInputSelect 系列组件 (5 个导出)
- 移除 global.d.ts 中残留的 SMBConfig 类型和 cloudSyncSmb* 方法声明
- 移除 useAutoSync.ts 中未使用的 toast 导入 (同时修复 application→components 反向依赖)
- 清理因删除而产生的多余 import

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

* chore: 消除直接 localStorage 访问,提取 safeSend 共享工具

localStorage 集中化:
- 新增 storageKeys 常量: SIDE_PANEL_WIDTH, PF_RECONNECT_CANCEL, DEBUG_HOTKEYS, DEBUG_UPDATE_DEMO
- TerminalLayer/SettingsApplicationTab/App.tsx/useUpdateCheck 改用 localStorageAdapter
- CloudSyncManager 内部方法改用 localStorageAdapter
- portForwardingService 改用 localStorageAdapter + 集中 key

safeSend 去重:
- 新增 electron/bridges/ipcUtils.cjs 共享模块
- sshBridge/sftpBridge/portForwardingBridge/sshAuthHelper/aiBridge 统一引用

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

* chore: 终审清理 — 移除未使用的 require 和废弃类型别名

- 移除 sftpBridge.cjs 中未使用的 require("node:net")
- 移除 aiBridge.cjs 中未使用的 require("node:path")
- 移除 types.ts 中已废弃的 ChatMessageImage 类型别名

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

* fix: 修复 ESLint 错误 — 组件不再直接导入 infrastructure

- 新增 useStoredNumber hook,TerminalLayer 通过 hook 访问侧边栏宽度
- SettingsApplicationTab 的 isUpdateDemoMode 改为从 useUpdateCheck hook 传入
- 移除 useCloudSync.ts 中未使用的 CloudSyncManager 导入和 GitHubAuthState 接口

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

* chore: 提取 notification port,消除 application 层对 components 的依赖

将 toast 通知抽象为 application/notification.ts 端口,
UI 层通过 setNotify 注入实现,useAutoSync 改用 notify 接口。

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-03-26 14:14:37 +08:00