Build the EternalTerminal `et` client in CI the way mosh-client is:
- build-et-binaries.yml builds et for linux x64/arm64, macOS universal
and windows x64, uploads artifacts, and optionally publishes them to a
dedicated binary repository on manual workflow_dispatch.
- scripts/build-et/{linux,macos,windows} compile et from upstream.
- .gitignore excludes the fetched binaries; resources/et/README.md
documents source provenance.
* Bundle mosh-client via CI build pipeline
Add a GitHub Actions workflow that builds a static, distro-portable
mosh-client for linux-x64, linux-arm64, darwin-universal (arm64+x86_64)
from upstream mobile-shell/mosh source, plus a pinned win32-x64 binary
sourced from FluentTerminal (GPL-3.0). Releases attach SHA256SUMS so
scripts/fetch-mosh-binaries.cjs can verify and pull the right binary
into resources/mosh/<platform-arch>/ during npm run pack.
electron-builder.config.cjs gains a moshExtraResources() helper that
adds the binary to extraResources only when present on disk, keeping
local dev packages working without bundled mosh.
terminalBridge.cjs now exports bundledMoshClient() and prefers the
bundled static client over whatever the system mosh wrapper would
resolve via PATH (via the MOSH_CLIENT env var). The Windows branch
throws a clear error pointing at Settings instead of silently falling
back to a literal "mosh.exe" string when no wrapper is installed.
This is Phase 1 — Phase 2 (follow-up) replaces the FluentTerminal
Windows binary with an in-CI Cygwin static build and adds a Node-side
mosh-server bootstrap so Mosh works out-of-the-box on Windows.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 2: Node-side Mosh handshake (no Perl wrapper required)
Reimplement what the upstream Mosh Perl wrapper does in pure Node:
spawn `ssh [user@]host -- mosh-server new`, sniff the byte stream
for `MOSH CONNECT <port> <key>`, then spawn `mosh-client` locally
with MOSH_KEY in the environment.
The new electron/bridges/moshHandshake.cjs module exposes the parser,
sniffer, and command builders as pure functions so they can be unit
tested without spawning real ssh. terminalBridge.startMoshSession now
prefers this path whenever a bare mosh-client (bundled, explicit, or
system) and ssh (in-box OpenSSH on Win10 1809+, system everywhere
else) are both detectable. The legacy path through the system mosh
Perl wrapper is preserved as a fallback so users with custom mosh
setups don't regress.
Auth is delegated to system ssh, so keys, agent, ssh_config, and
known_hosts all keep working. Password / 2FA need a controlling TTY
which the bootstrap doesn't provide; affected users keep the legacy
wrapper path until interactive UI lands.
Tests:
- moshHandshake.test.cjs (20 tests) — parser corner cases, command
builders, sniffer split-chunk handling, ring-buffer trim, exec
resolver
- terminalBridge.bareMoshClient.test.cjs (4 tests) — explicit-path
basename gating
317 → 341 passing tests; lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Phase 3: in-CI Cygwin Windows build + visible PTY handshake
Phase 3a — in-CI Cygwin Windows build
- scripts/build-mosh/build-windows.sh builds mosh-client.exe from
upstream mobile-shell/mosh source inside Cygwin, then walks the
cygcheck import graph to bundle every required Cygwin DLL
(cygwin1.dll, cygcrypto, cygprotobuf, cygncursesw, etc) into a
tar.gz alongside the exe.
- The `build-mosh-binaries` workflow swaps the FluentTerminal-pinned
fetch job for a real Cygwin build (windows-latest + cygwin-install-
action). fetch-windows.sh is preserved as an emergency fallback but
no longer wired into the matrix.
- fetch-mosh-binaries.cjs unpacks the tar.gz into resources/mosh/
win32-x64/ so mosh-client.exe sits next to its DLLs.
- mosh-extra-resources.cjs ships the entire win32-x64/ dir
(exe + DLL bundle) into Resources/mosh/, so the packaged installer
runs on a stock Windows host with no Cygwin install.
Phase 3b — visible PTY handshake (password / 2FA prompts)
- terminalBridge.startMoshSession now spawns ssh inside node-pty so
the user sees and can answer password / 2FA / known-hosts prompts
in their terminal. When `MOSH CONNECT` is sniffed from the byte
stream, session.proc is atomically swapped from the ssh PTY to a
freshly-spawned mosh-client PTY. The MOSH CONNECT line itself is
redacted from the visible output.
- writeToSession / resizeSession read session.proc lazily, so input
arriving after the swap goes to mosh-client without extra wiring.
- The ZMODEM sentry is recreated for the new proc since its
writeToRemote closure captured the previous handle.
- Removes the earlier non-PTY child_process.spawn handshake — the
PTY-based one supersedes it.
Phase 3c — win32-arm64 deferred
- Cygwin's arm64 port has no stable cygwin1.dll release yet, so we
do not attempt an arm64 Windows build. arm64 Windows installs fall
through to the legacy `mosh` wrapper path that the bridge already
handles. Documented in the workflow.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Allow branch/PR pushes to test the mosh-binaries workflow
Mirrors the build-packages workflow change in #868: any push or PR
that touches the mosh build pipeline triggers the matrix (artifacts
only, no release), while only `mosh-bin-*` tag pushes (or an
explicit workflow_dispatch with release_tag) publish a release.
`paths` filter keeps unrelated commits from running this expensive
workflow (~30min for the Cygwin leg). Concurrency group cancels
superseded branch/PR builds; tag builds use a unique group so a
follow-up commit can't kill an in-progress release.
Release job's `if:` enforces the same rule independently — even if
the trigger gets re-broadened, branches/PRs can't leak a release.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Fix mosh binary workflow runners
* Fix Windows mosh workflow invocation
* Keep shell scripts LF in workflow checkouts
* Trigger mosh workflow on attributes changes
* Fix mosh build tool dependencies
* Fix Linux mosh static build
* Fix macOS mosh build tool lookup
* Skip macOS ncurses terminfo install
* Fix mosh PR review findings
* Allow Linux system mosh dependencies
* Fix Windows mosh DLL bundling
* Limit bundled Windows mosh DLLs
* Honor configured PATH for mosh handshake
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: ignore local .worktrees/ directory
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): editorTabStore scaffold with single-tab ops
Implements the EditorTabStore class singleton (matching activeTabStore pattern)
with updateContent, markSaved, setWordWrap, setSavingState, close, and subscribe.
Includes useSyncExternalStore hooks and 6 passing unit tests.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): editorTabStore promoteFromModal with per-session path dedup
* feat(editor): confirmCloseBySession for session teardown
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(sftp): writeTextFileByConnection for pane-agnostic saves
Adds a new `writeTextFileByConnection(connectionId, expectedHostId, filePath, content, filenameEncoding?)` method to `useSftpExternalOperations` that looks up the SFTP pane by connection ID (with a hostId safety check) instead of the left/right-side coupling used by `writeTextFile`. Threads the existing `getPaneByConnectionId` callback through the call site and re-exports the new method via `SftpStateApi`.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(editor): editorSftpBridge singleton for out-of-React saves
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(editor): extract TextEditorPane from TextEditorModal
Lift Monaco editor body + toolbar + theme sync + paste fallback into a
pure TextEditorPane component. Adds sftp.editor.maximize i18n key to
en.ts and zh-CN.ts locale files.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(editor): drop unused getLanguageId import in TextEditorPane
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* refactor(editor): TextEditorModal delegates to TextEditorPane
Replace the monolithic modal (560 lines including full Monaco setup)
with a thin Dialog shell (~150 lines) that owns content/saving/saveError/
languageId state, save orchestration, and dirty-check on close, then
delegates all editor chrome to <TextEditorPane chrome="modal" />.
Exports TextEditorModalSnapshot for the optional onPromoteToTab callback
so callers can later wire tab promotion (Task 12) without breaking the
existing interface — the new prop is optional and existing callers
(SftpOverlays.tsx) are source-compatible with zero changes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(editor): include fileName and wordWrap in TextEditorModalSnapshot
Task 12 will populate the promoted tab with these fields, so the snapshot
must carry them from the modal at maximize time.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): UnsavedChangesDialog three-button confirm
* fix(editor): resolve UnsavedChangesDialog re-entrance and unmount leaks
- Re-entrance: if prompt() is called while a prior prompt is still pending,
cancel the prior one so its caller doesn't hang forever.
- Unmount: resolve any in-flight prompt as "cancel" in the effect cleanup
so awaiters don't leak when the provider unmounts.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): TextEditorTabView tab-form shell
Add TextEditorTabView component that binds an editorTabStore entry to
TextEditorPane, with CSS display:none toggling for inactive tabs so the
Monaco instance persists across tab switches. Also adds setLanguage
public method to EditorTabStore (lands Task 15's intent early — Task 15
can be a no-op).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(editor): read live store state in TextEditorTabView handlers
React state snapshot lags the store by a microtask. Closing over `tab`
meant a keystroke between Monaco's onChange and a Ctrl+S would write
stale content and mark a stale baseline. Read via editorTabStore.getTab
at call time instead.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): dispatch editor:* tab ids in App and activeTabStore
- Add EDITOR_PREFIX, isEditorTabId, toEditorTabId, fromEditorTabId helpers
- Add useIsEditorTabActive hook to activeTabStore
- Update useIsTerminalLayerVisible to exclude editor tabs
- Import useEditorTabs and TextEditorTabView into App.tsx
- Append editor tab ids (editor:<id>) to allTabs in hotkey handler
- Mount TextEditorTabView per editorTab with CSS visibility toggling
- Add editorTabs to executeHotkeyAction useCallback dependency array
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(editor): render editor tabs in TopTabs with icon/dirty/tooltip
- Add `fromEditorTabId`, `isEditorTabId` imports to TopTabs.tsx
- Add `FileCode`, `FileText` icons; use FileCode for code-like extensions
- Extend `TopTabsProps` with `editorTabs`, `onRequestCloseEditorTab`, `hostById`
- Build `editorTabMap` for O(1) lookup; add `editor` branch in `orderedTabItems`
- Render editor tab chrome matching terminal tab style: file icon, dirty dot (●),
filename with disambiguation suffix for duplicate filenames, close button
- In App.tsx: add stub `handleRequestCloseEditorTab`, `orderedTabsWithEditors`,
pass new props to `<TopTabs>`
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* refactor(editor): hoist editor-tab code-extension regex and use onSelectTab
- Move CODE_EXTENSIONS_RE to module scope so it isn't recompiled per render.
- Call onSelectTab(tabId) for consistency with other tab types, instead of
reaching into activeTabStore directly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): maximize modal to tab and dirty-confirm tab close
Wire onPromoteToTab from TextEditorModal through SftpOverlays and
useSftpViewFileOps so clicking the maximize button snapshots editor
state into editorTabStore and activates the new editor tab.
Replace the stub handleRequestCloseEditorTab in App.tsx with a real
dirty-confirm flow using UnsavedChangesProvider render-prop: clean tabs
close immediately, dirty tabs prompt save/discard/cancel, and save
routes through editorSftpBridge with markSaved on success.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* feat(editor): register SFTP bridge and gate session close on dirty editor tabs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(editor): make onDisconnect async so host-picker waits for dirty check
The session-close dirty gate added in Task 13 made onDisconnect async, but
the host-picker in SftpPaneDialogs still called it synchronously before
kicking off onConnect — a fire-and-forget that raced past the dirty prompt
and let unsaved editor tabs slip through. Propagate the Promise return type
through SftpPaneCallbacks / SftpPaneDialogs / useSftpViewPaneActionsResult
and await it at the host-picker call sites.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): block app quit while editor tabs are dirty
Add a before-quit IPC guard that asks the renderer whether any editor
tab has unsaved changes. If dirty tabs exist, preventDefault() blocks
the quit and a warning toast is shown. The app quits normally once
editors are clean.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
* fix(editor): add 5s timeout fallback to quit-guard IPC check
If the renderer crashes or throws before reporting back, the quitGuard
would stay busy forever and the app could not be quit. Fall back to
force-quit after 5 s if no reply arrives.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(editor): quit-guard uses quitConfirmed flag to prevent re-entry loop
The prior flow reset quitGuardChannelBusy before calling app.quit(), which
on macOS re-fires before-quit and re-entered the dirty check with the flag
cleared — creating an infinite IPC loop. Introduce a separate quitConfirmed
flag that commits to quitting before app.quit() fires, so the re-entry takes
the fast path.
Also extract QUIT_GUARD_TIMEOUT_MS and clarify that a concurrent quit while
a check is in flight is swallowed (preventDefault) rather than letting the
second event through.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(editor): use absolute inset-0 for tab panel and add sr-only DialogTitle
Two bugs surfaced during the first dev-server smoke test:
1. Editor tab content was blank because TextEditorTabView used only
className="h-full", while its sibling panels (VaultView, SftpView,
TerminalLayerMount, LogView) all fill their flex-1 parent via
`absolute inset-0`. In normal flow the editor tab collapsed to zero
height. Match the sibling convention.
2. Radix printed an accessibility warning because the Task 7 refactor
pulled the DialogTitle out of DialogContent and into the Pane header
(now a plain span). Add a visually hidden DialogTitle that mirrors the
filename, so screen readers have a title without showing it twice.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(editor): raise tab panel z-index to 20 so it sits above TerminalLayer
TerminalLayer's root is visibility:hidden when the active tab is an editor
tab, but its inner panels set `absolute inset-0 z-10` on their own and those
still paint. Without an explicit z on the editor tab panel, TerminalLayer's
inner bg-background div was covering the Monaco content, producing a blank
screen.
Also add bg-background to the wrapper so the editor tab paints an opaque
surface (matches the pattern VaultViewContainer / TerminalLayer follow).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): show host label and remote path next to filename in tab header
The editor tab form previously only showed the bare filename in its header,
which is ambiguous when the same filename is open against multiple hosts.
Add an optional subtitle prop on TextEditorPane and populate it from the
tab form with `<hostLabel>:<remotePath>` rendered in muted text beside the
filename. The modal keeps its existing filename-only header.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(editor): bridge supports multiple useSftpState instances
useSftpState is instantiated in both the top-level SftpView and the
terminal's SftpSidePanel, each owning its own pane registry. The editor
bridge previously stored only one writer, so maximizing a file opened from
the terminal side panel registered nothing (bridge was owned by SftpView
which may never have mounted) and save failed with "bridge not registered".
Change the bridge to track a Set of writers and dispatch by trying each
until one owns the connectionId (signalled by its specific "connection no
longer available" error). Add registerEditorSftpWriterScoped that returns
an unregister fn so each instance's cleanup removes only its own entry.
Register in both SftpView and SftpSidePanel.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(editor): Cmd+W closes editor tab + terminal close forces tab close
Two behaviors added after user feedback from dev-server smoke-test:
1. Cmd/Ctrl+W (the closeTab hotkey) previously did nothing on editor tabs
because executeHotkeyAction had no branch for editor:* ids. Add one that
reaches into the UnsavedChangesProvider render-prop's close flow via a
ref, routing through the existing dirty-confirm path.
2. Closing a terminal tab unmounts its SftpSidePanel which destroys the
useSftpState instance that owned the connection. Any editor tab promoted
from that panel would then be stuck — bridge gone, save channel dead.
On SftpSidePanel unmount, gather the connection ids it owned and call a
new editorTabStore.forceCloseBySessions to drop matching editor tabs.
Dirty state is dropped because the user closed the terminal knowing the
file was open — there is no save channel left anyway.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(editor): Cmd/Ctrl+W works when focus is inside Monaco
Monaco's internal key-event dispatcher swallows keydown before the
capture-phase handler on the Pane's root div can see it, so the global
hotkey dispatcher never got the chance to close the editor tab when the
editor had focus. Register a Monaco editor command for the close-tab
keybinding and route it through a handleCloseRef — mirrors the same
pattern used for Cmd/Ctrl+S. Also drop the modal-only guard in the
capture-phase handler so the outer-chrome path works in tab mode too.
TextEditorTabView now receives an onRequestClose(tabId) prop that App.tsx
wires via the render-prop-exposed handleRequestCloseEditorTabRef, same
mechanism as the hotkey-dispatcher path.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(editor): fall back to Vaults when forceCloseBySessions removes the active tab
Closing a terminal tab triggers SftpSidePanel unmount which force-closes its
editor tabs. If the editor tab being removed happened to be the active tab
(user maximized → then closed the owning terminal from another path), the
app ended up on a stale activeTabId with no selected tab and blank content.
Inside forceCloseBySessions, if the active tab was one of the removed
editor ids, redirect to 'vault'. Picking a more sophisticated neighbor
would need the full orderedTabs list which isn't reachable from this layer;
Vaults is always valid.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (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>
- Remove unused getSenderWindow() from autoUpdateBridge (replaced by broadcastToAllWindows)
- Fix .gitignore: /CLAUDE.md instead of CLAUDE.md to only match root
- Merge duplicate [Unreleased] sections in CHANGELOG.md
- Correct checkNow description: uses GitHub API, then triggers electron-updater async
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Configures the editor to load Monaco assets from a local directory in production, improving reliability and performance by avoiding CDN usage.
Adds prebuild script to copy Monaco files, and updates ignore rules to exclude copied assets from version control.
Moves Windows biometric SSH key protection from native modules to direct WebAuthn (Windows Hello) in the renderer for more reliable user gesture handling. Integrates a new communication flow between main and renderer processes for credential creation and verification, updates session and permission handling for WebAuthn, and removes the native dependency. Ensures the biometric key flow is robust, cross-platform, and compatible with OS-level security UI requirements.