* feat: auto-poll Docker capabilities while Docker tab is active
When the Docker tab is visible and hasDocker is not yet true,
poll refreshCapabilities() at the process refresh interval.
Stop polling once hasDocker becomes true, or when switching
to a different tab.
* fix: use resolvedTab instead of activeTab for Docker auto-poll condition
The auto-poll useEffect condition used activeTab, which stays stale
when Docker becomes unavailable. Changed to resolvedTab which reflects
the actual displayed tab. Also updated the dep array.
* fix: replace setInterval with setTimeout recursion in Docker tab probe
Replace setInterval-based polling with setTimeout recursion in the Docker
tab capability probe effect. This ensures the next probe only starts after
the previous one finishes, avoiding overlapping probes when SSH timeout
exceeds the polling interval.
- Add dockerPollTimerRef to track the timeout handle
- Use async pollOnce() that awaits refreshCapabilities() before scheduling next
- Use cancelled flag in cleanup to prevent scheduling after unmount
- Keep same dependency array for correctness
* fix: stabilize docker poll timer by using useRef for refreshCapabilities
refreshCapabilities() can return a new reference on every render, causing
the useEffect to re-run on every render — cleanup cancels the polling timer,
then the effect immediately calls pollOnce(), effectively bypassing the
configured timeout interval.
Fix: store refreshCapabilities in a useRef (refreshRef), use
refreshRef.current() inside pollOnce(), and replace refreshCapabilities
with refreshRef in the useEffect dependency array.
Closes #PR1456 Codex P2 review item.
* fix: delay auto-poll first probe by one interval to avoid overlap with tab-switch probe
When switching to the Docker tab, two mechanisms were triggering probes:
1. tab-switch effect (line 67-76): immediate probe via refreshCapabilities()
2. auto-poll effect: pollOnce() executing immediately on mount
This caused duplicate probes that waste SSH channel resources.
Fix: pollOnce() no longer fires on mount. Instead, the effect schedules the
first probe with setTimeout(pollOnce, capabilitiesTtlMs), so the first probe
happens after one full interval. Subsequent probes continue at interval pace
via the setTimeout recursion in pollOnce itself.
The tab-switch effect still fires the immediate probe (the correct one),
so responsiveness on tab switch is preserved.
* fix: reset cancelledRef in effect body to prevent permanent stalling of Docker polling
The cancelledRef was set to true in the cleanup function when dependencies
changed, but never reset when the effect re-ran. This caused pollOnce to
always early-return on subsequent timer ticks, permanently halting
Docker capability probing after the first dependency change.
* fix(system-manager): replace cancelledRef with closure variables for per-effect cancellation
Each effect generation now has its own and closure
variables instead of shared / . This
prevents stale probes from surviving cleanup when the panel hides and
re-shows (Codex P2 review).
* fix: wrap refreshCapabilities in try/catch to keep polling on exception
If refreshCapabilities throws (instead of returning {success: false}),
the await would exit pollOnce without scheduling the next setTimeout,
silently killing Docker auto-detection polling.
* fix: add in-flight probe guard to prevent tab-switch and auto-poll concurrent probes
Add probingRef to track whether a capabilities probe is already in-flight.
- Tab-switch effect for Docker branch checks probingRef before starting a new probe
- Auto-poll pollOnce checks probingRef at entry and sets/clears it around the actual probe
- Tmux branch left unchanged as it has no auto-poll overlap risk
* fix: re-schedule next poll timer when probe is in-flight
When probingRef.current is true (tab-switch probe still running),
pollOnce was returning early without scheduling the next timer,
causing auto-poll to stop permanently afterward.
Now it schedules the next poll within the interval and returns,
so the polling loop keeps running until a slot where no probe is
active.
* fix: convert comments to ASCII-only English
- Line 105: translate Chinese comment to 'probe is in-flight, reschedule for next cycle'
- Line 113: replace em dash (U+2014) with ASCII dash
* feat: session inline rename, closeSession shortcut, pane zoom
* fix: sidebar inline rename with local state
* fix: add sessionDisplayName to terminalPropsAreEqual comparator
The Terminal component is wrapped with React.memo(…, terminalPropsAreEqual),
but the comparator was missing a check for sessionDisplayName. After renaming
a session, the pane title bar would show the old name until some other prop
changed and triggered a re-render.
Add prev.sessionDisplayName === next.sessionDisplayName to the comparator
so that display name changes cause the Terminal to re-render immediately.
* fix: add onStartSessionRename to TerminalLayerWorkspaceSection ctx destructuring and TerminalPanesHost props
* fix: add toggleWorkspaceViewMode to executeHotkeyActionImpl destructuring
The togglePaneZoom handler calls toggleWorkspaceViewMode() but it
wasn't destructured from getCtx(), causing a ReferenceError at runtime.
* fix: restore truncated ctx object in TerminalView render call
The TerminalView ctx object literal on line 1265 was truncated to
'showSele...' due to an editing tool truncation bug, causing
Parsing error: ',' expected on npm run lint / tsc --noEmit.
Restored the missing fields from the base commit:
showSelectionAIAction, snippets, status, statusDotTone, sudoHintRef,
sudoHintText: t("terminal.sudoHint.pressEnter"), t, termRef,
terminalBackend, terminalContextActions, terminalCwdTracker,
terminalPreviewVars, terminalSettings, timeLeft, toast, zmodem
Kept the PR's new additions (isVisible, onRename, sessionDisplayName)
intact.
* fix: add toggleWorkspaceViewMode to executeHotkeyAction context and add terminal.menu.rename translations
- Add toggleWorkspaceViewMode to the context getter in executeHotkeyAction (App.tsx)
- Add terminal.menu.rename translation for en (Rename), zh-CN (重命名), ru (Переименовать)
* fix: validate focusedSessionId before closing in closeSession hotkey
When closeSession hotkey fires, workspace.focusedSessionId may reference
a session that was already closed by another trigger (e.g., mouse click
on tab close button). Collect alive session IDs from the workspace root
and fall back to the first living pane if the stored focusedSessionId
is stale.
* fix(auto-poll): check useSessionCapabilities probing state in pollOnce
When auto-poll timer fires before the initial probe (from
useSessionCapabilities) completes, probingRef.current is still false
because the initial probe doesn't set it — causing a second overlapping
probe.
Add check so that any in-flight probe from any path
(initial/auto-poll/tab-switch) prevents auto-poll overlap.
PR #1459
* fix: address remaining Codex review issues
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: add detach session from workspace with toolbar button and context menu
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: use customName in pane header display name for renamed sessions
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix: refine workspace terminal detach interactions
* fix: preserve workspace detach tab ordering
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Align window controls with utility icons, extend hover to the full title bar height, restore red close-button hover, flush close to the right edge, and use neutral gray hover for top-bar utility buttons.
Co-authored-by: Cursor <cursoragent@cursor.com>
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>
* perf(terminal): smooth layout drags and faster tab switching
Defer xterm refit during split, sidebar, and host-tree drags while keeping pane containers in sync with live layout measurements. Refactor TerminalLayer into focused sections with TabBridge/memo optimizations and add the terminal host tree sidebar.
Co-authored-by: Cursor <cursoragent@cursor.com>
* fix(terminal): keep side panels alive and guard session attach races
Prevent terminal boot unmount from leaking backend sessions, keep SFTP/scripts/theme/AI state when switching side tabs, and defer heavy SFTP UI mount so first entry stays responsive.
Co-authored-by: Cursor <cursoragent@cursor.com>
* perf(terminal): reduce tab switch jank
---------
Co-authored-by: Cursor <cursoragent@cursor.com>
Expose data-section selectors for SFTP/side panel, split panes, and resizers
so custom CSS can target the correct regions. Clarify docs that
terminal-workspace-sidebar is focus-mode only.
Fixes#1301
Co-authored-by: Cursor <cursoragent@cursor.com>
* 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>
* Bundle Symbols Nerd Font Mono as terminal icon fallback
PR #845 added "Symbols Nerd Font Mono" to the terminal fontFamily
fallback chain so PUA glyphs (powerline / devicons / etc.) resolve
even when the user's primary font lacks them. That only worked if the
user had separately installed the symbol font; ship it ourselves so
icons render out of the box regardless of the chosen base font.
- Drop SymbolsNerdFontMono-Regular.ttf into public/fonts (~2.5 MB);
Vite copies it to dist/fonts and the existing app:// protocol
handler already knows the font/ttf MIME type.
- Register an @font-face in index.css pointing at the bundled file.
font-display: block prevents tofu while the (instantly-available
bundled) face loads, only affecting PUA glyphs since the base font
is listed earlier in the fallback chain.
- Include the upstream LICENSE next to the font.
Source: ryanoasis/nerd-fonts NerdFontsSymbolsOnly v3.4.0 (MIT).
Refs #843
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Reference bundled font by absolute path so prod build resolves
Address Codex P2 on PR #846: the relative `./fonts/...` URL was emitted
verbatim into dist/assets/index-*.css, where the browser resolved it
against the CSS file's location and 404'd on
dist/assets/fonts/SymbolsNerdFontMono-Regular.ttf — the actual file
lives in dist/fonts/, so the icon fallback never loaded in packaged
builds and Nerd Font glyphs still rendered as tofu.
Switch the @font-face url() to `/fonts/...`. Vite's `base: "./"`
config rewrites that to the correct dist-relative form during build
(`../fonts/SymbolsNerdFontMono-Regular.ttf` from dist/assets/), and in
dev the same path is served by the Vite dev server out of public/.
Verified by re-running `vite build` and grepping the produced CSS.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Fixed 8% brightness causes compositers to have severe rendering issues. (Only effected on the Midnight color scheme) 10% seems to be okay.
- Reduced backdrop-blur as it's expensive CSS.
- Removed radial-gradient backgrounds (they don't show up)
* Replace app logo across window icon, tray, splash, and in-app brand
- public/logo.svg: new netcatty mark
- public/icon.png: regenerated 1024x1024 from new SVG (source for
electron-builder — .icns/.ico rebuilt automatically at pack time)
- public/dmg-fix-icon.png: regenerated 1024x1024
- public/tray-icon{,@2x}.png: regenerated color 16/32px for Linux/Windows
- public/tray-iconTemplate{,@2x}.png: regenerated monochrome silhouette
for macOS menu bar (background stripped, foreground flattened to
black on transparent so template-image rendering produces a clean
mask)
- components/AppLogo.tsx: render the new logo as a static <img>. The
old hand-coded inline SVG bound fills to the accent CSS variable;
the new mark has a fixed palette, so callers keep their sizing /
rounding classes via className while the asset itself is a single
file served from /public.
- index.html: splash screen now uses the same /logo.svg via <img>,
with border-radius for the rounded-square frame.
* Polish logo: theme the in-app mark, gloss the OS icon, shrink cat
- components/AppLogo.tsx: back to an inline SVG. Background rect fills
with hsl(var(--primary)) so the in-app brand follows the theme
accent (was fixed navy when imported as <img>). Cat scaled to 68%
of the frame and centred so it doesn't crowd the edges at small
sidebar sizes.
- public/logo.svg + regenerated PNGs: polished OS icon variant with a
large rounded-square clip (rx 224 on 1024), top-left spotlight
radial gradient, subtle top sheen + bottom darkening, and an inner
edge vignette for a slight chamfer. The cat is shrunk to the same
68% as the in-app logo for visual consistency.
- Monochrome tray template (macOS menu bar) is rebuilt from the
shrunk-cat path set with all fills flattened to black; keeps a
clean silhouette instead of a filled rounded square.
* Smooth paws, richer gloss on app icon
- Drop the dark toe/claw detail paths from the source illustration
(indices 22-25, 30, 35, 37, 39 — the ones tracing vertical claw
dividers inside the paws). At small sizes those read as teeth/
claws; paws now render as clean rounded blobs.
- public/logo.svg (OS icon source): richer depth pass —
* two-tone navy vertical gradient (lighter top, deeper bottom)
* brighter upper-left spotlight for glassy highlight
* top sheen + bottom darkening for sheen-across-curve effect
* soft elliptical ground shadow beneath the cat to anchor it
* 2% inner edge stroke to crisp the rounded-square chamfer
- components/AppLogo.tsx: regenerated with the same cleaned cat set,
still themed via hsl(var(--primary)). The in-app mark stays flat
(no gloss) because the effect adds nothing at 20-40px sidebar
sizes and would fight theme accents.
- All raster variants (icon.png, dmg-fix-icon.png, tray color + tray
macOS template) rebuilt from the cleaned sources.
* Respect Apple icon safe area; drop gloss, add thin border
macOS icon was rendering to the full 1024x1024 canvas, so it looked
noticeably larger than neighbour apps (VS Code, Ghostty, Zed) in the
Dock. Apple's Big Sur+ convention puts the artwork body inside an
~824x824 safe area centred in a 1024 canvas, which is how those apps
are sized.
- public/logo.svg: artwork body is now 824x824 centred with ~100px
transparent padding. Corner radius 185 (close enough to the macOS
squircle at Dock scale). Cat rescaled so it keeps the same 68%
proportion within the smaller body.
- Gloss layers (spotlight / sheen / ground shadow / vignette) removed
per request — went for a Ghostty-style clean look instead.
- Thin white inner border (stroke 3px, 22% opacity) outlines the
rounded square for definition.
- Tray PNGs for Linux/Windows keep the full-bleed variant (tray slots
expect the icon to fill the space, unlike the Dock safe area).
- components/AppLogo.tsx unchanged conceptually — it still fills its
own bounding box via hsl(var(--primary)); the Apple safe-area rule
is Dock-specific, not relevant to in-app rendering.
* AppLogo: tighten corner radius to match previous (rx 18.75%)
Previous AppLogo used rx=12 on a 64 viewBox (18.75%). The inline
replacement had rx=224 on a 1024 viewBox (21.9%), which combined
with the caller's rounded-xl class read noticeably rounder in the
sidebar. Drop to rx=192 on 1024 viewBox so the in-app mark matches
the old proportions.
* Beef up icon border so it survives Dock downscaling
3 px at 22% opacity disappeared when rasterised down to ~128 px Dock /
Launchpad size. Bumped stroke-width to 8 px and opacity to 40% so the
inner highlight reads as ~1 px at Dock scale. Stroke is inset by
stroke-width/2 so it sits fully inside the rounded-square body (no
anti-alias bleed outside the safe area). Same treatment applied to the
full-bleed tray variant.
* Enlarge cat inside icon tile (68% -> 85% of body)
Dock render had too much navy margin around the mark. Bump the cat's
scale so it fills 85% of the Apple safe-area body while keeping a
visible bezel to the rounded corners and the inner border. Tray color
variant and macOS template (scale 0.9, no border) follow the same
scale-up.
* Add ripple effect on sidebar nav and tidy logo in vault header
- Add RippleButton wrapper + ripple keyframe; use it for the six vault
sidebar nav entries (Hosts, Keychain, Port Forwarding, Snippets,
Known Hosts, Logs) so clicks get a subtle material-style ripple.
- Shrink vault sidebar AppLogo to h-8 w-8 and drop the outer rounded-xl
so the visible corner comes from the SVG's own rx instead of the
container clip.
- Relax AppLogo tile rx/ry to 144 for a more moderate corner radius.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* AppLogo: bump tile corner radius back up to rx 18.75%
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Unify manager toolbars, tighten tabs and vault sidebar title
- Manager toolbars (Keychain, KnownHosts, PortForwarding, Snippets)
normalised to h-14 / h-10 controls with bg-secondary/80 backdrop-blur
and the shared bg-foreground/5 secondary button treatment, so Hosts /
Keychain / Known Hosts / Port Forwarding / Snippets headers size and
tint identically.
- Keychain filter tabs: drop primary tint and cert-count pill; reuse
the same foreground/5 vs foreground/10 active states as other
managers. Search input grown to h-10 to match.
- Known Hosts: removed the leftover text-xs on Scan System / Import
File so they inherit Button's text-sm like every other action.
- TopTabs: drop the 2px active-accent top line and add rounded-t-md +
overflow-hidden so active tabs read as a clean soft tab shape rather
than a banner.
- VaultView sidebar: wordmark grown to text-xl font-black italic with
tightened tracking; logo gap trimmed from 3 to 2.5; outer bg dropped
from secondary/80 to flat secondary to sit flush against the
toolbars.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(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>
* feat: add workspace focus indicator style setting (dim vs border)
Users can now choose between two focus indicator styles for split
terminal panes:
- Dim: reduces opacity of unfocused panes (current default)
- Border: shows a colored border on the focused pane (old style)
The setting is in Settings > Terminal > Workspace Focus Indicator.
Implementation uses a CSS data attribute on documentElement to
toggle between the two styles, avoiding prop threading.
Closes#556
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: sync workspace focus style across windows
Add cross-window notification handling for the workspace focus style
setting so changes in the Settings window take effect in the main
terminal window immediately.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: preserve AI chat history across reconnects
* fix: retarget restored AI sessions on reconnect
* feat: format tool call results with proper line breaks
Extract stdout/stderr from structured results and unescape \n/\t
so command output displays with real line breaks like terminal output.
Supports both JSON object {stdout,stderr} and executor text formats.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: restrict unescape to stdout/stderr fields only
Plain strings may contain legitimate backslash sequences (file paths,
regex patterns) that should not be converted. Only apply unescape to
stdout/stderr fields extracted from command execution results.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address review findings for AI chat reconnect
1. Add explicit activeTerminalTargetIds guard in shouldRetargetActiveSession
to prevent retargeting sessions owned by other terminals, making the
invariant locally verifiable.
2. Only preserve orphaned terminal sessions with hostIds — workspace,
local, and serial sessions generate fresh IDs and would be permanently
unreachable, wasting MAX_STORED_SESSIONS quota.
3. Clear stale streaming state when restoring a session whose ACP handle
was already cleaned up (e.g., reconnect during mid-response), so the
user can send new messages.
4. Restore overflow-hidden on user message bubbles to prevent content
bleeding past rounded border corners.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address round 2 review findings
1. Fix streaming state clear: only clear for sessions whose targetId
doesn't match current scope (restored from different terminal),
not for built-in Catty chats that never set externalSessionId.
2. Exclude local/serial sessions from preservation: their synthetic
hostIds (local-*/serial-*) change on every open and can never be
matched back.
3. Preserve non-zero exitCode in formatted tool results so failed
commands show a visible failure signal.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: only clear streaming state during retarget, not for all restored sessions
The previous condition (targetId !== scopeTargetId) also fired for
built-in Catty sessions during normal operation, killing active streams.
Now streaming is only cleared when shouldRetargetActiveSession is true,
meaning the session came from a disconnected terminal where any
in-flight response is guaranteed to be dead.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: address round 3 review findings
1. Clear externalSessionId during retarget to prevent stale ACP handle
from surviving if retarget runs before orphan cleanup.
2. Only retarget in visible AI panels — hidden/background panels should
not race to claim orphaned sessions.
3. Remove unescapeTerminalOutput — data flow trace confirms real newline
characters arrive at the component. The unescape was corrupting
legitimate backslash sequences in paths and patterns.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: only ACP-cleanup deleted sessions, not preserved ones
Preserved sessions may be reused on reconnect. Running aiAcpCleanup
on them asynchronously could race with a newly started ACP conversation
on the same session ID, tearing down the fresh provider.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: abort in-flight streams during retarget and restore ACP cleanup
1. Abort the active request's AbortController when retargeting a session
with stale streaming state. Prevents late chunks from the old run
appending into the restored chat.
2. Restore ACP cleanup for all orphaned sessions (not just deleted ones).
Preserved sessions get a new externalSessionId on next use, so
cleaning the old one prevents subprocess leaks without affecting
future conversations.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: guard hidden panels from session ownership and skip null map entries
1. Only assign restored sessions in visible panels — hidden panels
should not race to own sessions via setActiveSessionId, preventing
MCP/tool calls from being bound to the wrong terminal.
2. Skip null entries in activeSessionIdMap when building
activeTerminalTargetIds — deleted chats should not block same-host
history matching on other terminals.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: guard MCP sync behind visibility and cancel exec/approvals on retarget
1. Only sync MCP session metadata from visible panels to prevent
hidden panels from overwriting the scope mapping.
2. Cancel pending approvals and in-flight exec (Catty + ACP) during
retarget, matching handleStop behavior. Prevents stale tool results
and approval prompts from reappearing after session retarget.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: restore MCP sync for hidden panels
MCP scope is keyed by chatSessionId so hidden panels don't overwrite
visible panels' mappings. The isVisible guard was breaking background
chats that need updated terminal session metadata after reconnects
or workspace changes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: remove unused deletedIds variable
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: replace workspace pane border with text dimming for unfocused panes
Replace the 2px primary-color border and Tailwind ring with a subtler
focus indicator: unfocused panes reduce xterm canvas opacity to 70%,
making text slightly dimmer without adding visual clutter.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: use visibility:hidden for terminal caching and restore focus on tab switch
- Replace display:none with visibility:hidden for TerminalLayer and
workspace panes to preserve xterm canvas state across tab switches
- Restore focus to the correct pane when terminal layer becomes visible
again, preventing opacity flash from :focus-within CSS
- Reduce autocomplete popup box-shadow intensity
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* Add session activity indicator and store
Introduce a SessionActivityStore (useSyncExternalStore) to track which tabs/workspaces have unread terminal activity. TerminalLayer now strips terminal control sequences, listens for session data, and marks tabs as active when not focused; it also clears activity on focus change and prunes stale IDs. TopTabs consumes the activity map to render a breathing activity dot on session/workspace tabs and adjusts the workspace tab layout to show the dot next to the pane count. Add CSS animation for the activity indicator.
* fix: buffer incomplete escape sequences across data chunks
Add ChunkedEscapeFilter to carry partial ANSI/OSC escape-sequence
tails between successive data chunks, preventing false-positive
activity badges from split control sequences on busy sessions.
Also fix missing trailing newline in sessionActivity.ts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: remove 256-byte cap on pending escape sequence tails
Long OSC sequences (e.g. clipboard/title payloads) can exceed 256
bytes. Removing the cap ensures they are fully buffered across
chunks instead of being misclassified as printable output.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix: buffer OSC tails that end on bare ESC awaiting backslash
OSC sequences terminated with ESC\ can split at the ESC boundary.
Extend the incomplete tail regex to also match an in-progress OSC
sequence ending with ESC (awaiting the closing backslash).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: bincxz <16399091+binaricat@users.noreply.github.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Integrate Claude Agent SDK for direct streaming chat, add Codex login/logout
flow with OAuth support in settings, improve AI chat panel UI and agent
discovery, and update build config for new dependencies.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add auto-discovery of CLI agents (Claude Code, Codex, Gemini) from system PATH
- Integrate ACP (Agent Client Protocol) for real-time streaming with codex-acp
- Bundle @zed-industries/codex-acp binary for reliable agent spawning
- Add ThinkingBlock component with shimmer animation and auto-collapse
- Refactor chat UI: no avatars, bordered user bubbles, plain assistant text
- Support {prompt} placeholder in agent args for flexible invocation
- Add persistent ACP sessions with proper cleanup on app exit
- Detect auth errors and show actionable messages to users
- Fallback to raw process spawn for agents without ACP support
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Refactors UI font handling to support dynamic font loading and improves selection in settings.
Implements "Load More" pagination for connection logs, enhancing performance and user experience with large datasets. Adds lazy loading for the Connection Logs Manager.
Addresses an issue in `usePortForwardingState` to prevent duplicate backend sync calls in React StrictMode. Improves IPC communication robustness by checking window destruction status. Refines the visual presentation of the snippets empty state.
Replaces the Ghostty Web terminal backend with xterm.js, updating references, dependencies, and styles accordingly. Removes Ghostty-specific initialization, CSS, and dependencies for improved compatibility and maintainability. Updates terminal and SFTP mounting to use React lazy loading for better performance. Improves Electron window startup to avoid initial white flash. Cleans up cloud adapter dynamic imports for more efficient loading.
Updates documentation to reflect the terminal engine change.
Implements a flexible system for configuring and persisting keyboard shortcuts across the app, enabling users to select hotkey schemes (Mac/PC), edit, disable, or reset shortcuts, and apply them globally and in terminal contexts.
Improves accessibility and workflow by allowing tailored key bindings, storing preferences in local storage, and integrating shortcut management into settings UI.
Refines styling for active UI elements, spinners, and borders for better theme consistency.
Provides immediate feedback when customizing terminal appearance by applying theme, font, and font size changes in real time within the customization modal. Adds cancellation logic to revert unsaved changes, prevents duplicate IPC handler registration, and makes minor UI refinements for improved user experience.
Standardizes import statement spacing and adjusts whitespace for better readability in the component file. No logic changes are introduced.
Improves code style and import formatting consistency
Standardizes spacing in import statements and introduces helper constants for drag region styles, enhancing readability and maintainability. No logic changes are made.
Refines app drag and no-drag regions for better window movement and interaction, especially in Electron environments. Adds support for double-click-to-maximize on titlebars (Windows/Linux), ensures controls/buttons remain non-draggable, and tunes visual padding for consistency. Enhances usability when UI is crowded with tabs, and improves cross-platform window management behavior.
Updates all code references, IPC channels, CSS classes, and storage keys
from "nebula" to "netcatty" to align with new branding and application identity.
Ensures consistency across frontend, Electron backend, global types,
and package metadata.
Refactors code for consistent indentation, spacing, and style.
Enhances readability and maintainability by aligning formatting.
No logic changes; only visual and stylistic improvements applied.
Switches to Tailwind CSS v4 with theme configuration via CSS, removing inline CDN config for better maintainability and security.
Adds explicit eslint-disable comments in hooks to clarify dependency intent, aiding future development and preventing unnecessary warnings.
Renames props and variables reserved for future features to underscore-prefixed names, improving clarity and reducing confusion.
Removes unused catch parameters for cleaner error handling.
References to future UI and logic enhancements are annotated for maintainability and roadmap visibility.
Refactors card components to use consistent "soft-card" and "elevate" styles,
harmonizes sizing and padding, and standardizes grid gaps for a uniform look
across managers and views. Updates hover effects and background transitions for
better visual feedback. Reduces box-shadow intensity and simplifies elevation
transitions for a cleaner UI.
Switches SFTP file manager from a side panel to a modal to prevent terminal width issues and streamline the UI. Updates terminal status indicators for clearer error and connection states. Adds CSS tweaks for better terminal canvas fit and scrollbar appearance, enhancing visual consistency and usability.
- Implemented TrafficDiagram component to visualize local, remote, and dynamic port forwarding.
- Added SVG icons for cloud, firewall, and server.
- Created animated line components for visual representation of connections.
- Integrated AppLogo and icons into the diagram for better UX.
- Included SVG assets for cloud, firewall, and server in the public folder.
- Added helper functions to format transfer bytes and speed for better UI representation.
- Improved TransferItem component to display bytes transferred and remaining time.
- Implemented streaming transfer with real-time progress updates and cancellation capability in the Electron main process.
- Introduced IPC handlers for file operations including delete, rename, and stat for both SFTP and local filesystems.
- Updated preload script to handle transfer progress, completion, and error events.
- Enhanced global type definitions to support new streaming transfer API.
- Modified CSS animations for a smoother progress indication.