* fix(ai): keep dropped AI chats recoverable with dangling marking and /rebind
Closing a member terminal or dissolving a workspace used to clear the
chat's active pointer, making it invisible (scope rank 0) and
unrecoverable from the UI even though the record survived in storage.
- orphan cleanup now marks dropped chats `scopeDangling: true` and keeps
record + active pointer intact; the flag clears when the target
returns or the chat is rebound
- `/chats` lists dangling chats with a numbered inline affordance and
hint line
- new `/rebind <n>` panel action rewrites `scope` to the panel's current
scope, clears the marker and restores the active pointer
- i18n keys added for en / zh-CN / zh-TW / es / ru
Stacked on #4018. Refs #4015 (R2).
* fix(workspace): reuse existing workspace id when merging tabs with a workspace
createWorkspaceFromSessions silently no-opped when either merged tab
already belonged to a workspace (e.g. dragging a workspace pane onto a
standalone terminal), and the older dissolve->recreate behavior
regenerated the id that workspace-scoped AI chats bind to.
When one of the merged tabs has a workspace, the orphan tab now joins
that existing workspace (pane inserted next to the anchor session, tab
slot collapsed), so workspace-scoped chat bindings survive. Merging two
standalone tabs still creates a fresh workspace.
Stacked on #4018. Refs #4015 (R3).
* fix(ai): persist cleaned active map on dangling chat rebind
rebindSessionScope was updating React state to drop stale old-scope
pointers, then rebuilding localStorage from the pre-rebind snapshot and
resurrecting them. Persist the same cleaned map via rebindActiveSessionMap.
* fix(ai): persist rebind map inside setState; wire backup actions on terminal panel
- Write cleaned active-session map from inside the rebind updater so a
deferred React setState cannot skip snapshot/localStorage persistence.
- Pass exportSessionsBackup/importSessionsBackup through TerminalLayerSupport
and include them in AI_CHAT_SIDE_PANEL_AI_STATE_KEYS so history-drawer
backup buttons work on the normal terminal path.
* fix(ai): guard backup restore races and use app-wide live targets for /chats
- Skip restore when in-memory sessions already exist; apply snapshot,
storage write, and AI state emit atomically inside the empty-state path
so peer useAIState instances see the restore.
- Thread validAIScopeTargetIds into AIChatSidePanel as liveScopeTargetIds
so /chats does not mark chats bound to other open terminals as dangling.
* fix(ai): open rebound chat in panel view and refresh /chats listing
- rebindSessionScope also sets the panel view to the rebound session so an
existing draft/other chat does not keep hiding the recovery.
- After /rebind, rewrite the stored listing instead of rebuilding from the
stale render so the chat is no longer offered as dangling.
* fix(workspace): invert join split when dragged pane owns workspace; refresh /chats titles
- When merging a workspace-owned dragged pane onto a standalone tab, the
orphan is inserted — invert the drop position so the previewed side matches.
- Compare workspaces in AIChatSidePanel memo so /chats picks up renames.
* fix(ai): validate live sessions array on boot; mark imported orphans dangling
- Reject parseable non-array store values (e.g. {}) via readLiveSessionsArray
so boot restore is not blocked by a corrupt-but-truthy value.
- After manual import, re-run orphan cleanup with the last known live target
set so chats bound to closed scopes become dangling and /rebind-able.
* fix(ai): merge imported sessions before orphan cleanup
Apply import merge to the snapshot/ref synchronously, then run dangling
marking, so a deferred setState cannot let cleanup rewrite over the import.
* fix(ai): mark restored/imported orphans dangling even with no live targets
Restore and import do not change live terminals/workspaces, so the orphan
cleanup effect will not re-run. Always call cleanup against the last known
live target set after those paths — an empty set is a valid observed state
(nothing mounted), not an unavailable snapshot — so chats bound to missing
scopes become dangling and /chats can offer /rebind.
* fix(ai): apply backup restore synchronously before dangling cleanup
Do not gate restore + orphan marking on a setState updater having run.
A deferred updater left didRestore false, skipped cleanup, and could leave
restored chats with missing scopes unmarked and unrebindable from /chats.
* fix(ai): complete /rebind from a partial slash picker selection
Selecting Rebind after typing `/re` left the composer unchanged, so a
following number became `/re 1` and could be sent to the agent. Replace the
slash query with `/rebind ` the same way Rename does.
* fix(ai): replace hostIds on rebind; skip non-array backup generations
- Rebind builds a fresh destination scope (including hostIds from the
panel) instead of spreading the old scope, so history matching follows
the new terminal/workspace host.
- readLatestGeneration treats valid JSON that is not a session array as
unusable and continues to older generations, matching corrupt skips.
* fix(ai): revalidate dangling chat before /rebind from stale /chats listing
Skip rebind when the cached entry is deleted or already bound to a live
scope; drop stale listing entries so a second panel cannot steal the chat.
* fix(ai): reject malformed /rebind numbers
Require the entire trimmed argument to be digits so /rebind 1foo and
/rebind 1.5 show usage instead of silently targeting index 1.
---------
Co-authored-by: binaricat <binaricat@users.noreply.github.com>
Co-authored-by: 陈大猫 <16399091+binaricat@users.noreply.github.com>
* feat(ai): add /rename and /chats slash commands plus a session durability layer
Panel slash commands (#4015 feedback):
- /rename <title> renames the current chat via updateSessionTitle; the
command is intercepted in ChatInput's system-command branch and never
reaches the agent. Empty title shows a usage hint. The history drawer
gains an inline rename affordance.
- /chats renders a dismissible system-style note listing the panel's
visible chats (title / agent backend / workspace-or-terminal scope with
a dangling marker / relative last-active time; current chat marked).
Deliberately not named /resume: session.resume already means
flow-recovery-after-cancel. Listing logic is pure (aiChatsListing.ts)
so a future rebind action can attach per-entry actions.
- i18n keys added for en/es/ru/zh-CN/zh-TW.
Durability layer (additive; cleanup/eviction semantics untouched):
- Every successful store write schedules a debounced (~2s) mirror of the
exact persisted JSON to {userData}/ai-sessions-backup/ through a new
aiSessionsBackupBridge: atomic temp+rename writes, 5 rolling
generations, timestamped names.
- On boot, a missing/empty live store is restored from the newest
parseable backup generation. The asymmetry is deliberate: empty ->
restore, non-empty -> leave alone, so restored data can never
resurrect explicitly deleted chats.
- Manual export/import in the history drawer via real file dialogs
(fixed-path fallback where dialogs are unavailable); import merges by
id with newer updatedAt winning.
Tests: bridge rotation/corruption/rejection, restore decisions, merge
semantics, debounce throttle, mirror-on-write, slash interception and
/chats listing seams. eslint clean; tsc unchanged vs baseline.
* fix(ai): repair sessions backup IPC and restore/import guards
- Forward write/export payloads through preload unchanged so the main
bridge receives a string json (was double-wrapped → invalid-json).
- Restore from disk only when the live store is missing or corrupt;
treat a valid [] as an intentional clear. Flush the debounced mirror
on beforeunload so deletions land before quit.
- Require id + messages[] + finite updatedAt when parsing backup/
import entries so malformed objects cannot reach pruneSessionsForStorage.
Co-authored-by: Netcatty Maintainer <binaricat@users.noreply.github.com>
* fix(ai): complete /rename from partial slash and harden import shape
- Selecting Rename from the slash picker replaces the active slash token
with `/rename ` so a following title is intercepted, not sent to the agent.
- parseBackupSessions also requires title, agentId, and a scope object with
a string type before accepting an imported/backup entry.
Co-authored-by: Netcatty Maintainer <binaricat@users.noreply.github.com>
* fix(ai): validate backup messages and skip prune on synced restore
- Drop backup/import sessions whose messages lack id/role/timestamp.
- When cloud AI-session sync is enabled, restore parsed sessions without
pruneSessionsForStorage so local retention cannot truncate synced history.
Co-authored-by: Netcatty Maintainer <binaricat@users.noreply.github.com>
* fix(ai): export prepareRestoredSessions helper
Missed in the previous commit; required by useAIState boot restore and tests.
Co-authored-by: Netcatty Maintainer <binaricat@users.noreply.github.com>
* fix(ai): require message content on backup import; raise disk cap to 64MB
* fix(ai): normalize corrupt sessions store to [] before boot restore
* fix(ai): skip non-array backup generations when reading latest
* fix(ai): emit sessions change on backup restore; skip unusable gens
* fix(ai): treat empty backup gen as wipe; align gen usability with parser
* fix(ai): seed backup on boot for live store; report durable import count
* fix(ai): emit sessions change after backup import
* docs(ai): clarify sessions backup recovers in-profile store only
---------
Co-authored-by: binaricat <binaricat@users.noreply.github.com>
Merge global Agent host runtime + sandbox Chat sample on top of #4011.
Electron plugin smoke PASS at fb691318c (S0–S4; S5 BLOCKED-FIXTURE). Codex CLEAN. No release.
* fix(sync): block convergent AI session shrink after merge
Related #4017 — convergent smartMerge could drop many AI sessions
post-merge (e.g. 46→7) and still report success; classic syncAll
already guarded this. Gate upload/apply with detectSuspiciousShrink.
* fix(sync): drop explicit any in convergent shrink override test
* Fix blocked chat shrink advancing sync baseline
Preserve the original PR #3885 commits and integrate latest main.
Resolve local shell defaults, acknowledge committed workspace insertion, and preserve background readiness codes.
The list-models pipeline already served LKG on failure; this wires the
rest of the LKG-first, manual-refresh policy end to end.
Main: the list-models IPC accepts a force flag — manual refresh always
skips the in-memory cache and the persisted entry and goes live, and a
forced success overwrites the persisted catalog.
Renderer: the panel passes force only from the retry banner and the
picker's refresh button; panel-visibility and send-preflight loads stay
non-force and replay the renderer cache, so with a persisted catalog
main answers them without spending the live-fetch budget.
Picker: a footer refresh icon-button next to the status chip, visible
whenever any fetched catalog (live or last-known-good) or the degraded
warning is present; the spinner row remains the in-flight feedback.
Tooltip localized as ai.chat.refreshModelList in en/es/ru/zh-CN/zh-TW.
Fresh installs are unchanged (no persisted entry → live fetch as
before); existing users stop seeing jitter warnings on panel open and
pick up new CLI models via the refresh button.
* perf(vault): store connection-log replay blobs in IndexedDB
Move unsaved connection-log terminal replay out of the multi-megabyte
localStorage blob into one IndexedDB row per log id. Persist only the
changed ids, keep the newest 50, and signal other windows with a tiny
revision key.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): keep connection-log replay across boot races and IndexedDB failure
Re-read connection-log metadata after the IndexedDB load before pruning,
return the capture promise through TerminalLayer, and write the unpruned
log blob when the side store rejects the commit.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): keep unmigrated connection-log replays until IndexedDB commits
Copy legacy replay ids that are missing from IndexedDB before deleting
the old blob, and do not treat an uncommitted legacy map as the diff
baseline.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): migrate legacy replays without clearing newer IndexedDB rows
Insert missing legacy ids in one transaction instead of replaceAll, so a
replay committed after the empty read is not wiped.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): rebase the IndexedDB fallback onto the latest connection logs
When the side store rejects a close, attach this window's replay to the
connection-log blob currently on disk instead of writing the stale snapshot.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): rebase queued connection-log writes onto the latest logs
A persist task can run after another window changes the log list. Write
the rebased list instead of the snapshot captured when the task was queued.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): keep queued connection logs after a remote snapshot
A storage event can replace memory with another window's logs before the
queued write runs. Keep ids from the queued snapshot unless this snapshot
deleted them.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): restore only locally changed queued logs
A queued persist was putting back every id from the pre-queue snapshot,
which undid another window's delete of a log this recipe never touched.
Track the ids the recipe added or replaced, and reload IndexedDB during
boot when the revision key moves across the migrate await so that peer
replay is hydrated before this window marks the revision seen.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): rebase connection logs after the side-store await
A non-pruned write stored the pre-await snapshot and could replace logs
another window saved during the IndexedDB round trip. Rebase immediately
before that write, shrink the quota fallback until the newest replay fits,
and leave the revision unconsumed when IndexedDB cannot be read.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): ignore same-window connection-log echoes
The deferred localStorage notification replays this window's own metadata
write. Applying it replaced connectionLogsRef with an older blob and dropped
logs that were added or finalized while that write was still queued.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): do not restore a queued log deleted from memory
A queued add or update was written back when the id was already gone from
memory, which let another window keep that row after a later local delete.
Rebase now applies a changed id only while memory still has it.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* feat: open saved session logs from terminal toolbar
Add one-click access to the current automatic or manual session log, flush before opening, and reuse application selection with a remembered log preference. Preserve recording controls and current-session ownership across worker routing and reconnects.
Refs #3807
* fix(logging): preserve saved logs on failed reconnect and localize failures
Address both Codex review findings with generation- and identity-checked replacement cleanup and localized user-facing error handling.
* fix(logging): guard successful main-process session replacement
Add shared inactive-log cleanup to fallback start handlers with regression coverage. Registration activation follows separately.
* fix(logging): enable log lifecycle cleanup for main-process fallback
* fix(terminal): keep log cleanup and tuck open-log into More actions
Always end a raw session-log write stream on finalize, including when a
flush already disabled it. Keep immediate snapshot retries, and pause
them only while prepare is waiting so a disk error can be reported.
Show open, reveal, and open-with in the toolbar overflow menu when
logging is active, instead of a default toolbar button.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(logging): back off failed snapshot writes and drop retired log actions
Persistent snapshot failures retry on a growing delay instead of a tight
loop, and still flush immediately when new output arrives. Hide open-log
actions after a successful reconnect retires a stopped manual log.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): keep open-log actions after automatic logging is turned off
The current automatic log keeps recording until reconnect, so the menu
stays available for that connection and clears on the next successful
connect.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): derive log menu availability from saved session logs
* fix(terminal): nest session log actions in one More actions submenu
Collapse Open, Show in folder, and the two open-with actions into a Session log submenu that follows the Terminal Encoding menu, and drop the remembered-application hint.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): round-trip jump host chains in CSV import/export
CSV export dropped hostChain, so a vault imported on another machine
lost every jump host. Persist the ordered chain as an OpenSSH
ProxyJump column and bind each hop on import.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): drop inline hops from a rejected ProxyJump chain
A self-referential ProxyJump list created unresolved hops before the
chain was skipped, so the import kept those hosts. Discard inline hosts
created for that rejected chain.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): prefer a same-group hop when binding CSV ProxyJump
When an imported host and an existing vault host both match a hop
endpoint, pick the candidate in the target host's group before falling
back to the imported match.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): prefer a same-group label when binding CSV ProxyJump
Combine imported and existing label matches before pickCsvJumpCandidate so a cross-group imported label cannot beat an existing hop in the target group.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(vault): reject CSV ProxyJump chains that contain a malformed hop
A multi-hop ProxyJump cell with any invalid hop now warns and leaves the
host chain unset, instead of saving the valid hops as a shortened route.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Share opt-in, reversible privacy masking through main-owned policy and conversation maps across Catty, integrated agents, and External MCP. Restore managed arguments before validation, approval, and execution; fail closed on retired policy state. Add shared settings, documentation, boundary and lifecycle regressions.
Validated with deterministic regressions, production desktop smoke, clean Codex review and passing cross-platform CI.
Closes#3620
* feat(ai): support OpenCode 2.x while keeping the 1.x driver
Detect the installed opencode major version. 1.x stays on the bundled SDK and the old startup banner. 2.x uses the new serve banner, HTTP Basic auth, and the session API so model lists and chat turns work.
Fixes#3799
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ai): stop the OpenCode 2 event stream when a prompt is rejected
A rejected prompt does not emit a terminal SSE event. Abort that stream and close the server before waiting, and keep HTTP "not found" responses from being reported as a missing CLI.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ai): select OpenCode 2 catalog ids and keep context optional
Aliases expose provider/catalog id rather than the provider API model id. A rejected instruction entry warns and still sends the user prompt. The entry value stays the system-prompt string, which is valid JSON for that endpoint.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ai): post OpenCode 2 prompts after the event stream is open
The live /api/event stream does not replay. Wait until the server accepts the subscriber and start reading it before sending the user prompt.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ai): refresh OpenCode version and catalog after startup races
Key the version cache by the resolved PATH binary and its mtime, so an in-place upgrade is not stuck on the previous major. While listing 2.x models, refetch after model.updated or provider.updated until the snapshot stays quiet.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ai): refetch the OpenCode catalog when it changes during the first read
An update that arrives while the initial model list is in flight belongs to a newer snapshot. Record the event count before that request and fetch again when it moves.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ai): refetch the OpenCode catalog when the settlement budget elapses during a read
A catalog read that outlasts the quiet-window budget still has to fetch the snapshot announced by an in-flight model or provider update.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ai): localize OpenCode 2 failures when the server dies mid-turn
A killed event stream was surfacing undici's raw "terminated" text. Map that disconnect, a closed socket, and an SSE stream that ends before the turn finishes to the existing session-failed message.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ai): ignore Bun version banners and hide undici transport errors
A crashed Bun binary was cached as OpenCode 1.x, and fetch or JSON-body failures still showed raw undici text. Skip Bun and Node.js runtime versions, and map those transport errors to the existing unavailable message.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ai): keep the OpenCode 2 config model as a qualified string
The selected provider/model stays the original string in OPENCODE_CONFIG_CONTENT. Session create still sends Model.Ref with id and providerID.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ai): keep caller OpenCode config and hide transport session errors
A profile's OPENCODE_CONFIG_CONTENT is merged under Netcatty's generated settings, and a server-reported fetch failure no longer reaches the chat as raw transport text.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ai): drop undici transport text from OpenCode 2 details
HTTP error bodies and tool failures share the same sanitizer as session errors, so fetch failed, terminated, and other side closed never reach the chat.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ai): keep OpenCode transport mapping to mid-stream failures
Server-authored tool and session messages are no longer rewritten just because they match a transport string. A dead reply stream still reports the localized session-failed key.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ai): restore the OpenCode 2 server username
The transport-sanitizer edit replaced the opencode username constant, so a real 2.x spawn threw ReferenceError before Basic auth. Put the constant back and lock the default client username.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* feat(system-manager): add container runtime switcher and kubectl pods
Keep the existing Docker tab and select docker, podman, nerdctl, or
Kubernetes from runtimes detected on the host. Kubernetes replaces the
switcher with context and namespace selectors in the same panel.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): address container runtime review findings
Move the runtime adapter into the domain layer, keep a context namespace
when namespace listing is forbidden, and emit PowerShell or cmd startup
commands for Kubernetes shell, logs, and describe.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): cover Windows container popups and stale namespaces
Run container shell and logs through the host shell's inspect wrapper on
PowerShell and cmd, and reset the namespace when a remembered Kubernetes
context is no longer present.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): parse kubectl JSON after banners and scope inspect cache
Skip bracketed banners before kubectl JSON, and include the container
runtime in image and container inspect cache keys.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): parse Podman image lists emitted as JSON arrays
Podman applies {{json .}} to the whole image slice. Accept that array,
including Names and byte sizes, and keep Docker's JSON lines.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): parse Podman stats arrays
Accept Podman's stats JSON array and its id/cpu_percent fields, and keep
Docker's one-stat-per-line output.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): list every Podman image tag
Expand each name in a Podman image's Names array into its own row so
extra tags stay visible and searchable.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): quote kubectl list and delete for the session shell
Namespace, pod, and delete commands follow PowerShell or cmd quoting when
the session shell is not POSIX, so apostrophes in a context name survive
local PowerShell.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): drop renamed runtime launch commands from shell history
Container popups now probe with _nc_rt_err and also cover podman, nerdctl,
and kubectl. History sanitizers still required _nc_docker_err, so those
launches stayed in persisted history. Confirm-dialog resets also follow the
selected runtime.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): sum Podman interface stats and quote kubectl for the executor
Podman {{json .}} stats keep per-interface counters on Network, so the
details panel now sums RxBytes and TxBytes. Background kubectl runs in
powershell.exe on local Windows even when the terminal is cmd, so those
commands are quoted for that executor instead of the interactive shell.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): remove a selected image tag instead of the shared id
Podman rows that share one image id were all deleted with rmi <id>, which
either fails while other tags remain or drops every name. Tagged rows now
run rmi repository:tag; dangling images still use the id.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): reject cmd kubectl arguments that contain percent signs
Interactive cmd.exe expands %VAR% inside double quotes, so a context such
as prod-%USERNAME% would be rewritten before kubectl sees it. Cmd popups
now refuse that argument. PowerShell quoting still preserves the percent.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* refactor(system-manager): move runtime selection into an application hook
Persisted source, context, and namespace state plus the kubectl discovery
effects now live in useContainerRuntimeSelection. DockerManagerTab only
renders the switcher and panels.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): offer kubectl shell only for running containers
Succeeded and failed pods have no running container, and a completed init
container cannot accept exec. Shell targets now come from container
statuses that are running; logs still include init containers.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): keep Windows popup commands out of shell history
PowerShell and cmd container popups inspect then run, and kubectl popups
are typed directly. History filtering only matched the POSIX sh -c wrapper,
so those launches were stored as user commands.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): mark Windows popups instead of matching kubectl syntax
Filtering kubectl --context logs and describe also dropped commands the
user typed. Windows popups now carry __NC_POPUP__, and ordinary kubectl
lines stay in history.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): reject cmd arguments that contain delayed-expansion marks
cmd.exe with /V:ON expands !VAR! inside double quotes. Contexts such as
prod!USER! are refused for cmd popups, same as percent signs.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): hide raw nerdctl and unreachable cluster errors
Only offer nerdctl after a short info probe succeeds, or when the failure is a socket permission error that sudo can still recover. Map dead containerd and unreachable kube API errors to short localized messages and keep the CLI text in the log.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): show terminating pods and ephemeral containers
A deletion timestamp is Terminating even while the phase is still Running. Ephemeral containers are included in the logs list, and a running one is a shell target.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): keep the runtime select on Kubernetes and avoid covering it
Kubernetes uses the same compact source menu as Docker, with context and namespace beside it. Shell popups open in the free part of the window so they no longer sit on top of the pod badges.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): restore the saved engine when Kubernetes disappears
A host that was on Podman before Kubernetes keeps Podman if kubectl is gone, instead of falling back to Docker and overwriting that choice.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): avoid the visible system panel when others are parked
A hidden parking-host panel can be the first system-manager match and
has a zero-size rect. Scan every match and pass the first panel that
actually has size so the shell popup still sits beside the open panel.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): keep a runtime choice across probe gaps and show podman ports
A failed nerdctl or kubectl probe was saved as the fallback engine, so the
previous choice never returned. Reconciliation now restores the stored source
when it is detected again, and automatic fallbacks are not written back.
Podman ps port mappings and exposed ports are formatted into the same display
string the container detail strip already shows.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): show a pod's Evicted reason
Pod status.reason is the eviction signal, while the phase stays Failed and
the container often reports Error. Use that reason after a container waiting
state and before the generic phase so the badge stays Evicted.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): keep an avoided shell popup inside the work area
When the source window sits partly off-screen, clamping a slot back onto
the display could cover the system panel, and the fallback then used the
raw slot origin. Place the popup in the on-screen part of the slot instead.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(system-manager): left-align the runtime selector row
The Kubernetes context, namespace, and runtime selects sat on the right of
the header, so they did not line up with the search field. Keep the same
padding, let the context select fill the remaining width, and leave the
runtime and namespace selects at their natural width.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* feat(ssh): add per-host TOTP with QR scan and MFA auto-fill
Store an optional otpauth TOTP secret on the host, encrypt and sync it
like a password, and auto-submit it once when a keyboard-interactive
prompt matches JumpServer or PAM TOTP text.
Closes#3323
Refs #3415
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): address Codex TOTP review findings
Reset authenticator editor state when the host changes, match custom
prompts with RE2, offer keyboard-interactive during connection tests
when TOTP is configured, and drop the secret when a host becomes a plugin.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): fill TOTP from the active prompt and ship re2js
Attach the connection's otpauth URI to the keyboard-interactive dialog
so Fill current code uses that attempt, including unsaved hosts. Move
re2js into production dependencies so packaged builds can match custom prompts.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): ship the shared TOTP runtime in packaged builds
electron-builder allowlists domain CommonJS modules individually, and
sshAuthHelper requires domain/totp.shared.cjs at load.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): save a pasted TOTP secret and ignore stale QR imports
Host save waits for the authenticator draft to validate before writing
the host. QR import completions from the previous host no longer change
busy state or errors on the newly selected host.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* test(host): ignore TOTP copy in the proxy command summary check
The assertion was matching the word secret in the authenticator UI.
It now checks that the ProxyCommand token itself is not rendered.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* feat(ssh): simplify host TOTP setup and type codes into the terminal
Move authenticator setup into a dialog so the host form keeps a single setting row, show keyboard-interactive prompts as field labels, and add a terminal context-menu action that types the current code without Enter.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): keep TOTP fill inside the auth dialog
Remove the terminal context-menu action. Offer the verification-code button on each non-password prompt, highlight OTP prompts, and never write the code into the password field.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): keep MFA instructions and scan every screen
Show keyboard-interactive name and instructions as plain text when they are not already the field label, and scan each unmatched desktop source once instead of repeating the first display.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): keep multiline MFA prompt directions
Show lines before the last keyboard-interactive prompt as plain-text guidance, and keep that last line as the field label.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): scan TOTP QR codes at each display's full size
Request a thumbnail as wide as the widest display and as tall as the tallest, so a portrait screen is not scaled into a landscape frame.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(ssh): keep TOTP fill off menu prompts
Judge the fill action from the last prompt line and the shared menu, SMS, and confirmation exclusion list. Use a secondary button on the label row, and simplify the host authenticator row.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* feat: add optional Streamable HTTP for External MCP
Serve the existing catalog tools and approval path on a loopback HTTP
endpoint so clients that cannot spawn the stdio launcher, such as Codex
in WSL, can connect with a bearer token. The listener stays off until
the user enables it.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: close HTTP sockets before stop and export the Codex bearer
Destroy tracked connections before awaiting server.close so disable does not hang on a half-open request. Prefix the Codex snippet with export so the child process receives the token.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: advertise a real client host and return 413 intact
Wildcard binds no longer copy 127.0.0.1 into client configs; the URL stays empty until a reachable client address is set. Oversized POSTs receive a JSON-RPC 413 before the socket is closed.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: recheck the bearer token after the HTTP body arrives
A half-open POST already passed auth before readBody. Recheck the live token before dispatch so a rotation during that wait cannot still run a tool.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: expire idle Streamable HTTP sessions
JSON responses do not keep a socket open, so a client that never sends DELETE used to pin an MCP session until the listener stopped. Sweep sessions that have been idle for 15 minutes, and skip any session with a request still in flight.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: return 404 when an HTTP session id is unknown
GET and DELETE with a nonempty session id that is no longer active now return 404, matching POST and the Streamable HTTP session contract. A missing session id stays 400.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: accept bracketed IPv6 origins
Node reports URL.hostname as [::1], so an otherwise valid loopback or wildcard Origin was rejected. Compare the address with the brackets removed.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: release rejected HTTP handshakes that never open a session
A 406 or 415 from the SDK returns normally before onsessioninitialized. Detach the approval client and close the MCP server when that happens so rejected initializes do not accumulate.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): migrate the saved Shift+Enter newline once
Existing installs kept the pre-#3770 "\\n" default, so Shift+Enter still
submitted in TUI CLIs. Rewrite only that stored value to ESC+CR, and let
xterm's native CSI u own the chord when an app has opted in.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): keep the Shift+Enter migration local
The one-time rewrite of a saved "\n" does not need a sync flag or a
three-way merge. An imported or synced old newline can stay as stored.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix(terminal): keep native kitty Shift+Enter on broadcast peers
When a target has negotiated xterm kitty keyboard and the custom
protocol setting is off, resolve the broadcast chord as CSI 13;2u
instead of the ESC+CR fallback.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* feat: add a global default proxy with an explicit direct override
Resolve the proxy as the host setting, then the nearest group default,
then one optional saved profile from synced settings, then a direct
connection. A host can choose direct and skip both inherited defaults.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: keep global default proxy persistence across settings instances
Unregister only the setter owned by the unmounting useSettingsState instance so closing Settings still writes the cleared default through the remaining AppLockGate setter.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: round-trip explicit direct mode in vault CSV
Write the Proxy cell as direct when a host sets directProxy, and import that literal back as an explicit direct connection. An empty Proxy cell still inherits.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: keep CSV proxy export from embedding the global default
Grouped hosts ran through applyGroupDefaults and picked up the ambient
global proxy, while ungrouped hosts exported an empty inheriting cell.
Both paths now opt out of that default so inheritance stays inheritance.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: follow global default proxy changes in peer session windows
Session windows disable settings IPC sync, which also skipped the
storage listener and left the resolver singleton at its mount-time id.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* refactor: clear a deleted global default proxy at the call site
Drop the pass-through field on removeProxyProfileReferences. The profile
manager already knows the active id and clears it directly.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* test: share the global default proxy settings harness
The peer-window case and the multi-instance cleanup case were mounting
the same JSDOM fixture twice.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* feat: alert when an external MCP approval is waiting
Show a system notification and request taskbar or Dock attention so confirm-mode approvals are not missed before the existing timeout.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
* fix: stop MCP approval attention when a later window focuses
Watch app browser-window-focus while a flash or dock bounce is active so a
window created after the alert starts can clear it.
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: 陈大猫 <binaricat@users.noreply.github.com>
Typed request compression ran before every Catty request (routine pre-turn and every step) and replaced image/file parts with a text placeholder, so attached images never reached the model.
preserveAttachments now keeps image attachments on the routine pre-turn and step paths. Non-image files stay placeholders (chat providers reject e.g. text/plain; the model reads them via list_attachments/read_attachment). The explicit 413 retry still trades attachments for placeholders.
Verified in the real Electron app: image reaches the provider as image_url (fake OpenAI-compatible server and DeepSeek deepseek-flash, which described the test image correctly); text-file turns still succeed via attachment tools; request prefix stability across turns unchanged vs main (DeepSeek cache hits ~98% on follow-up turns).
Fixes#3764
Co-authored-by: yangfan <1413737839@qq.com>
* Codex worktree snapshot: startup-cleanup
Co-authored-by: Codex
* fix(browser): close malformed proxy connections without a reply
* feat(browser): embed host browsing in terminal side panels
* style(browser): blend compact browser chrome into terminal panels
* feat(browser): open terminal links internally with a system browser preference
* fix(browser): retain the first terminal link during panel mount replay
* fix(browser): release contexts when their split pane closes
* style(browser): remove the downloads panel shadow
* fix(browser): ignore late downloads after panel closure
* fix(browser): mount browser content on terminal auto-open
* fix(browser): preserve empty-vault recovery with bookmarks
* fix(browser): expose actions for supported terminal types
* fix(browser): retain open requests before the panel mounts
* fix(browser): align vault actions with supported hosts
* fix(browser): share download controls across host contexts
* fix(browser): keep vault browser requests on visible sessions
* fix(browser): release contexts after workspace detach
* fix(browser): enforce WebRTC routing on page contents
* fix(browser): skip unsupported sessions in open requests
* style(browser): encode glyphs in ASCII-safe source