Files
Termind/Memory/ARCHITECTURE.md
T

70 KiB

Termind architecture

Termind is one Xcode project with macOS (TFM) and iOS/iPadOS (TFI) apps, shared static frameworks, platform test targets, and a macOS-only MCP bridge. The agent runtime is the separately versioned AgentKit package.

Modules and dependency direction

  • TermindDomain owns platform-independent values and rules. It may use Foundation, CryptoKit, and AgentKit values, but never UI or concrete I/O.
  • TermindApplication owns repository ports, feature models, connection policy, SSH preparation/import previews, remote-file-system interfaces, sync/outbox coordination, and other use cases. It depends on Domain and AgentKit.
  • TermindInfrastructure owns in-memory repositories, metrics parsing, StoreKit, and the shared Vault/Settings/Metrics CloudKit coordinators. It depends on Application, Domain, and Persistence.
  • TermindRemoteInfrastructure owns SSH/SFTP, Keychain, key material, remote sampling, forwarding, transfers, and scoped SSH import reads. Its C libraries remain outside the portable framework-test graph. It depends on Infrastructure, Application, and Domain.
  • TermindPresentation owns reusable SwiftUI content, presentation policy, Ghostty themes, and the TerminalPreferences bridge. It depends only on Application and Domain; native surfaces, transports, and lifetimes stay in apps.
  • TermindPersistence exclusively owns every SwiftData model, frozen V1-V18 schema, migration plan, and container factory. It never depends on an app target.
  • TFM and TFI own platform UI, adapters, and lifecycle orchestration. Both use App, Application, Design, DesignSystem, Features, Infrastructure, Platform, and UI; frameworks never import either app.

Frameworks expose explicit public APIs and do not re-export imports. Apps import canonical owner modules directly, without forwarding aliases or Shared type names. There is no generic Core module.

Persistence preserves entity identities, fields, defaults, and historical schema semantics even when code moves. Original-module V1/V14 fixtures guard migration. makeContainer fixes the complete schema and migration plan; each process shares one container across its scenes. Store failures never silently recreate data. macOS retains the existing Termind store location; mobile uses Application Support and an in-memory store for tests. Both apps use the same CloudKit container and bundle-derived Keychain access group, with target-specific signing.

Shared localized resources are copied into each app bundle. Platform icons and metadata remain app-owned. Framework, macOS, and mobile tests stay separate; architecture tests fail closed when an expected production directory is absent or empty. Source-based checks are limited to module dependencies, native-list import allowlists, AgentKit type ownership, and SwiftData model ownership. They do not pin UI copy, source layout, call-site spellings, retired paths, or shared-component wiring; those concerns rely on behavioral tests or explicit visual review.

State, navigation, and ownership

Single-line UI fields share Presentation's newline-to-space editing policy and horizontal layout. Binding reads preserve stored values; only edits are normalized. App-owned native rename editors apply the same policy without interrupting marked text. Platforms inject app-owned single-line paste adapters so normalization happens before native controls flatten individual line breaks. Multiline content and secrets are excluded. Existing validation is unchanged. No persistence migration or bulk cleanup is performed.

  • Repository snapshots and preference models are application-wide. A window owns its shell, quick-open, search, navigation, and context-panel state.
  • Desktop main scenes each own a distinct WindowModel. The process-owned main window coordinator routes UUID-scoped scene requests and weakly tracks native windows. Creating a window starts on Welcome; Dock reopening preserves the existing destination, while the Metrics entry explicitly selects Metrics. The Dock context menu's New Window action always creates an independent Welcome window through the same scene factory and activates the app, including when hidden or without main windows. It is disabled until the factory is ready. Restoring a scene never restores or reconnects live Workspaces.
  • Desktop new-window requests snapshot their source's ordinary content size; repeated registration cannot reapply it. The native registration bridge sets the full-size content style before sizing, so titlebar height never accumulates across copies. Command-N opens Connect in a fullscreen source; explicit Quick Open New Window still creates a window using its pre-fullscreen size. The last-used main window's ordinary size is device-local UserDefaults state, excluded from sync. Fullscreen transitions and auxiliary windows cannot replace it, and termination freezes it before window teardown. A cold scene uses this default only when macOS has no individual restored frame; restoring multiple scenes never applies the shared size over their frames. Tests and screenshot demos use an in-memory size store. Window positions remain system-owned.
  • Desktop Command-W closes the selected Workspace's focused pane first. Closing the last pane removes its Workspace but keeps the window open. Without a selected Workspace it requests native window dismissal. All user window-close paths share SwiftUI's dismissal confirmation whenever that window owns any Workspaces, including disconnected or hidden ones; cancellation preserves them. Closing a different window never participates in that decision. Shift-Command-W remains direct window dismissal, and the existing last-window background policy applies. Main-window registration suppresses the native document-edited decoration that SwiftUI couples to dismissal confirmation. Workspaces are not unsaved documents; the confirmation condition and native dismissal delegate remain unchanged. The observation follows the registration view's window and ends on detachment.
  • A Workspace can move intact to a new window through commands. Navigator drags use SwiftUI's original UUID string provider for in-window ordering and pane composition; releasing a drag outside the window never creates a window. The destination must register before the MainActor transfers ownership; moves preserve native terminal surfaces, transports, split geometry and session state, and do not enter close history. Empty source windows remain on Welcome. Native window closure explicitly disconnects only its owned sessions, cancels pending moves, and unregisters it. Independent transfers and application-owned services retain their existing lifetimes. Mounting a session cannot restart an attempted connection or authentication; explicit retry/reopen still can.
  • Paywall presentation is pinned to one main window until dismissed. Speech input ownership belongs to a composer instance, not a shared conversation ID, so closing another view of that conversation cannot cancel the recording.
  • Desktop navigation starts with Vault and Assistant, places Metrics above Vault libraries, appends dynamic SSH/SFTP Workspaces, and ends with Settings. Vault owns Hosts, Keychain, Snippets, Known Hosts, Welcome, and Host Editor routes.
  • Desktop main pages share a 360-point detail minimum, independent of the Assistant's 840-point reading maximum. Opening the Navigator grows the window when necessary. Vault overlays its existing editor below 721 points and keeps the covered list mounted at its last visible size. Below 700 points Metrics uses a titlebar Status/Processes picker with the toolbar title hidden; at larger widths both panels and the title are visible. Covered Metrics panels retain their last visible size, table identity, sorting, and scroll position. Page selection and measured detail width are window-local; selecting another server resets to Status. Focusing search or narrowing while search is focused reveals Processes. Compact actions group Pause/Resume into More, and macOS 15 presents its search field in a toolbar popover. Resizing changes layout, never route, editor identity, Workspace composition, or monitoring ownership.
  • Assistant conversations are owned by AssistantLibraryModel; they are not Workspace panes. A Workspace owns SSH/SFTP/serial panes and any session-scoped conversation. A session owns live transports and terminal/browser state.
  • Assistant list ordering and pinning are metadata-only operations, independent of running, approval-waiting, or compaction state. Desktop drag and pin commands preserve the live conversation instance and commit ordering before publishing it. Archived chats cannot move, rename editors cannot drag, and archive/delete retain their active-run and compaction guards.
  • Settings data is app-wide but its navigation is window-scoped. Opening Settings remembers and later restores the replaced destination.
  • Feature-specific Application models expose narrow intent surfaces even when they share one Vault snapshot. VaultStore owns the transaction; sessions use RemoteSessionHostStore, never the full mutation API.
  • Runtime connection state is never written onto domain records. Saved repository data is persistent truth; device-local enabled forwarding intent is UserDefaults state and is excluded from Vault/Metrics/Settings sync.
  • Serial sessions are temporary and local. IOKit/termios objects remain in Infrastructure; serial paths and parameters are neither persisted nor exposed as Agent hosts.
  • Workspace templates are Vault-synced domain values. They preserve a named SSH/SFTP/empty-pane split tree and divider fractions, while TFM alone maps those values to live Workspace sessions and UI. TFI carries the records for sync but exposes no template surface. Missing saved hosts restore as empty panes so the stored layout is never collapsed.

Known Hosts is independent of Host records: editing or deleting a Host does not change trust. Plain and OpenSSH |1| hashed endpoints share one matcher; hashed records keep their wire hostname, and their separate port field is ignored.

SSH imports recursively classify files by content and stage only selectable public metadata in a window-owned model. Hosts, identities, and keys form an indexed preview: an imported identity requires its selected key, while a host uses an exact existing or selected imported identity and otherwise remains unbound. Selecting a host never selects credentials. The actor reader retains scoped source descriptors and content digests, never private-key text or passphrases; it rereads selected keys transiently at unlock/commit and fails closed if a source changed. Picking and drag-and-drop use the same scoped validation; config parsing never executes commands. Commit writes new credentials before one Vault save, cleans them up on failure, never overwrites existing records, and never clears an existing trust mismatch.

App appearance is a global UserDefaults preference applied by the macOS app delegate through NSApplication.appearance; following System clears the override. Opening at login reflects SMAppService on every Settings visit rather than a mirrored preference. A quiet login hides the app without suppressing its window. macOS owns an explicit in-app language preference. Mobile delegates language selection to Apple's per-app Settings and never stores or injects its own locale override.

Remote sessions, terminal, and files

SSH, SFTP, mobile Workspaces, and Metrics share connection preparation and recovery policy. Interactive host-key approval remains inside the actual transport attempt: the same socket/libssh2 session continues from handshake through approval and authentication; callers never perform a separate discovery connection. Generation tokens prevent callbacks, credential results, and retry timers from a retired connection mutating its replacement.

SSH keyboard-interactive challenges use the same authenticated-opening path for PTY, exec, SFTP, forwarding, key installation, and each jump host. The C callback waits only on its transport queue while an async, cancellable application responder presents the server's literal name, instructions, prompts and echo flags. Rounds and connections have distinct request IDs; answers are ephemeral and never autofilled from saved passwords, cached, logged, or added to Agent/MCP history. Empty answers are valid; zero-question rounds complete without requiring a visible form. User waiting time does not consume libssh2's network timeout. Cancellation wakes the callback, cancels its responder, and interrupts the pending transport.

User connections own their challenge queues. Agent runs reuse their application interaction queue; macOS background operations and MCP share the native authentication window with independently cancellable origins. Stopping MCP preserves app-owned requests. Mobile background operations use an app-owned queue presented by one active scene. Terminal file drops and all additional transfer connections forbid Termind credential, passphrase, MFA and host-key forms, including fallback responders on jump hosts. System key authorization such as Touch ID remains allowed. Username-only and SSH none authentication remain eligible; actual connection success decides availability. No upload-specific mobile sheet exists; the app-owned MFA sheet serves forwarding, while Metrics owns its challenges inside Connection Journey on both platforms. Cancellation interrupts opening, and temporary-file cleanup must also remain silent. Cleanup requiring new interaction retains the existing failure report. Forwarding rules sharing a host session share authentication; a new connection must answer fresh challenges. An Identity may contain only a username: preparation permits an empty credential list, SSH tries none, then advertised methods. Missing configured secrets and existing authentication failure/retry behavior stay unchanged.

An Identity's device-local lastUsedAt means its saved password or private key was successfully read for connection setup, not that remote authentication succeeded. Connection-specific resolvers batch those accesses across destinations, jump hosts, and authenticated proxies; missing secrets and ordinary Keychain availability or editing reads never update usage. The first access for each Identity in an app process is written directly to its SwiftData row without entering the Vault journal or repository refresh path. Later retries are deduplicated, and Vault sync strips this local metadata while preserving it when applying remote Identity configuration.

The SSH worker's serial queue owns PTY dimensions. It retains the latest valid viewport during authentication and uses it for the initial PTY request, including pixel dimensions. Nonblocking window-change requests retain their original size through EAGAIN; the pump finishes that packet before sending the newest viewport. Other channel I/O continues so an earlier blocked write can finish. Disconnect and connection failure clear resize state, and a retired authentication attempt cannot install a PTY or clear its replacement's pending dimensions. Ghostty remains the source of grid measurements; platform layout callbacks never compute remote rows or columns. Window-change failures are diagnostic logs, not connection failures or UI alerts; the failed packet is discarded while newer or subsequently requested sizes can proceed.

RemoteFile is a Domain value, RemoteFileSystem the Application port, and the libssh2 SFTPClientProtocol its Remote Infrastructure implementation. File symbols and localized kind names belong to Presentation. MobileSFTPSessionModel owns path, listing, navigation history, filters, mutations, and stale-request state.

TerminalCommandDraftController owns draft requests, timeout, cancellation, and bracketed-paste payloads. CommandGenerationCoordinator adds model timeout, snapshot freshness, and presentation state. Platforms provide live context, validity, and insertion only; generated commands are inserted but never submitted. TerminalFindController is shared by desktop SSH/Serial and mobile SSH.

Terminal typography is device-local on both platforms. Font family, size, and size-follow-theme live only in UserDefaults and never enter TerminalPreferences, the Settings journal, or outgoing CloudKit payloads. Before sync can materialize a remote snapshot, missing local size keys are seeded from the pre-upgrade local preferences blob (existing mirrors win), or from the defaults. Legacy remote font fields are ignored. New terminal payloads omit them; older apps that require those fields must upgrade to read the new payloads. Theme/custom configuration text keeps its existing sync behavior, including fonts explicitly authored in that text. An empty local family means Follow Theme; unavailable installed fonts fall back to the theme without deleting the selection. Platform font catalogs refresh on Settings entry and foregrounding. Native font discovery stays in apps, while Presentation owns the local SwiftUI bindings and final Ghostty configuration. Explicit family and size overrides follow theme/custom commands; the family list is reset before applying the selected family. Changes reconfigure existing surfaces in place.

macOS SSH and serial terminals share two default-off mouse preferences: copy a completed mouse selection and paste on right-click. They use the existing terminal Settings sync record and UserDefaults mirror; missing fields in older records decode as false. Mobile preserves the values without exposing or applying them. Preference changes update the persistent native view without rebuilding its Ghostty surface. Only a local mouse selection completion triggers automatic copying. A right-click chooses its route on press and pastes once on release through Ghostty's clipboard binding inside interactive input capture, preserving bracketed paste and broadcast routing. Remote mouse capture and Option-right-click retain Ghostty's original handling, and mouse reports never enter the broadcast input capture scope. Empty clipboard pastes do not arm deferred input capture. A captured bracketed paste retains its classification across callback chunks until its closing marker; an interrupted sequence expires with the existing short-lived input ticket window.

Mobile SSH sends shell-integration and transport-exit callbacks through one ordered, session-scoped MainActor consumer. Close, reconnect, or background expiration ends it. Authenticated installation and command completion establish prompt readiness; following prompt markers are idempotent. Bash DEBUG fallback passes BASH_COMMAND and ignores Termind's __termind_* helpers so draft protocol traffic never creates History rows or marks the prompt busy.

Command History begins only after an authenticated start event. Completion may add remote exit status and duration; both remain optional and duration is never inferred from client receive time. Explicit backslash continuations are stored with each boundary normalized to one space; other line breaks remain part of the command while desktop and mobile History rows show only the first line with an ellipsis. History is capped at 1,000 rows per host.

SFTP History is device-local persisted truth shared across apps. Directories are recorded only after a fresh successful listing and files only after download. Failed, cancelled, stale, missing, or type-changed operations do not update it. Transfers retain generation-scoped SFTPTransferSession ownership independently of browser panes. The first additional connection probes the complete SFTP route silently; success supplies a worker and enables concurrent silent opens within the existing limits. Failure selects serial reuse of the authenticated browser connection. Later silent failures degrade pending work without replaying active file operations. Probe results are in-memory, shared within that generation, and cancellation does not cache a negative result. Terminal uploads use their successful silent preflight connection for the first transfer. Durable host trust is refreshed after initial authentication, but once-only approval is not promoted to trust for additional connections. No credential presence or persistence flag gates transfer eligibility or parallelism. Closing a pane releases only its ownership. Queued jobs retain the primary connection until completion/cancellation; paused jobs retain their configuration/checkpoint but release their connection ownership. Retired browser generations never replace transfer snapshots. Copies whose endpoints share one connection alternate bounded reads and writes on that worker, checking source size/metadata and EOF before the existing atomic temporary-file commit. Terminal preflight and batch preparation are cancellable and scoped to the terminal generation; late results cannot enqueue files after invalidation. Upload/download checkpoints, file references, acknowledged offsets and incremental digests live only in memory. Pause and connection loss retain the destination partial file and release the connection; explicit resume verifies the source and partial prefix before seeking. Ordinary transfers add no source staging, bookmark persistence, or restart recovery. Cancellation and host deletion remove partial files, reporting a path if cleanup fails. Mobile background expiration pauses uploads/downloads and cancels remote copies. macOS termination gives asynchronous cleanup at most three seconds, so an unreachable server cannot hold up quitting. Nothing is silently replayed. Desktop and mobile upload/download and remote-copy preparation and enqueueing use Remote Infrastructure planners. Download batches retain their destination independently of file jobs, so directory-only selections still create their empty directory trees. Remote-copy enqueueing reports directory mutations so desktop refreshes also cover copies without file jobs. App sessions retain pending-batch presentation, cancellation, error handling, and completion refresh; interactive browser deletion stays app-owned. Upload and remote-copy conflict resolution share one Remote Infrastructure resolver over RemoteFileSystem. Upload retains strict-descendant preflight counting while copy also suppresses the conflicting root itself. Both use the same subtree mapping, Keep Both naming, cancellation checkpoints, and recursive child-name/path validation; created-directory accounting remains available to awaited copy outcomes.

macOS SFTP file-row context menus are AppKit snapshots built on the context click, after reconciling selection. Their targets and native menu items survive SwiftUI row reconfiguration from hover, focus, selection, and directory refreshes. Menu presentation never mutates selection from a SwiftUI appearance callback.

SFTP external edits have one process-local Application model per app, independent of browser/workspace lifetime. Remote Infrastructure owns managed local copies, streamed SHA-256 checks, file/directory observation (including atomic-save replacement), and immutable upload snapshots. Snapshots are an edit-specific exception to ordinary transfers' no-staging rule; paused uploads retain them and terminal completion removes them. Editing never automatically uploads. Only changed records appear in the floating bottom overlay; clean records remain internal so repeated opens reuse their local copy. The edit card overlays the full-height file viewport with native Liquid Glass on OS 26 and later, and native material on older systems. Its row container stays transparent and the rounded glass is a separate backdrop, so file rows remain visible around and through the floating card. Only the scroll content gets a bottom inset, so files scroll behind the glass while the last row can still clear it; no blank safe-area region is reserved for the card. Discard stops tracking and preserves the local copy for any external editor; the next remote open downloads a fresh copy. iOS exposes Discard through native trailing swipe actions, and both platforms expose it in the row menu. Running/preparing uploads cannot be discarded; paused uploads are cancelled and cleaned up before their record is removed. Hosts own activation, background, termination and native preview/open adapters. The macOS edit menu uses ordinary menu actions; Keep Local Copy delegates to an app-owned save panel and coordinated file copy instead of embedding ShareLink in the native menu. macOS checks edits before quitting; iOS cannot promise a quit warning or recovery after process termination. Background expiration still pauses uploads, with explicit resume.

Edit upload policy validates the original destination's kind, size, modification time and mode both before writing and immediately before remote rename, preserving its permission bits on the temporary file. Overwriting commits use the OpenSSH POSIX rename extension; standard rename is used only when the extension is unsupported. Ordinary non-overwriting renames never use that extension, and failures never unlink the original as a replacement workaround. This is a best-effort metadata guard, not atomic compare-and-swap against unrelated remote writers. Missing metadata or changed/missing files require explicit confirmation; nonregular destinations fail closed. Successful commit acknowledges only the snapshot digest, so later saves remain dirty. Host removal or route changes invalidate old records without deleting their editable local copies. There are no extension allowlists: SHA-256 compares all regular-file contents. The iOS Quick Look delegate enables native editing where supported, receives in-place updates and takes ownership of temporary edited copies. Different-format copies are exported separately instead of overwriting the original remote format. Shared Presentation owns the dirty-file card; AppKit/UIKit and their lifecycle adapters stay in their apps.

Open With uses the same managed edit copy as ordinary Open. macOS discovers applications by UTType and launches the one selected for that invocation without changing defaults. iOS retains Quick Look as its primary action and uses a workspace-owned document interaction controller for Open With; retired/background presentation requests cannot surface a late download in another workspace. The controller survives scene deactivation until an in-progress document handoff finishes. Receiving apps may edit an independent copy, so mobile offers explicit same-format edited-copy import into an existing record. Import preserves the original remote identity and baseline, rejects invalidated/busy records, and only marks local changes; the existing explicit upload/conflict flow commits them.

sftp_copy is Termind-owned and acting-only in Assistant, with Ask/sequential policy. It copies regular files and directory trees into an existing directory; unsupported descendants fail before writes. Its awaited operation shares browser manifest and conflict helpers, including directory merging and destructive type replacement. Source data is never deleted. Batches are not atomic: failures retain completed items and disclose the failing path and completed counts. Two dedicated SFTP leases use the same authenticated connection opener as other Agent tools, but are neither pooled nor idle-swept. Their shared cancellation token is cancelled by host deletion, transport shutdown, or caller cancellation. Leases remain tracked while opening and are always disconnected after use. Credential retries apply only to acquisition, never to an operation that has started. Socket-relayed copies require successful source EOF and an exact byte count before the remote temporary file is committed. Cancellation shuts down the relay to unblock socket I/O. No scratch staging, local temporary file, or model-visible payload is used.

The macOS MCP Server exports the same sftp_copy definition without a scratch workspace. Its existing Bearer token/tool switches and Full access execution remain the authorization boundary; SSH credentials and host keys still use external authentication. The tool is annotated as state-changing, potentially destructive, non-idempotent, and open-world. Session termination/server shutdown cancels and awaits active copies. Audit consumes byte progress and final counts only, never file bodies; the summary records both endpoints and the conflict policy. The stdio bridge remains a protocol-only forwarder. Mobile exposes Assistant tools but no local MCP listener.

The macOS MCP server also owns a separate HTTP-to-SFTP byte channel. sftp_upload issues a ten-minute, session-bound 256-bit capability bound to its MCP session, host, full remote path, size, SHA-256, and overwrite intent. sftp_upload_status reads only that session's bounded in-memory state. Upload PUTs authenticate their own capability before consuming the body; ordinary MCP retains its 4 MiB body limit and long-lived Bearer authentication. Both routes remain loopback-only. Capabilities are headers, never query parameters, durable audit data, or long-lived credentials.

HTTP bodies are consumed in bounded chunks with socket-relay backpressure and no local file staging or scratch access. Remote Infrastructure owns the HTTP-agnostic stream operation and dedicated SFTP lease. Exact byte count and incrementally verified SHA-256 authorize source EOF; only then may the remote temporary file be closed and committed. Existing directories cannot be overwritten. Failed cleanup reports the potentially remaining remote temporary path. Interrupted writes retain an in-memory checkpoint and a remote partial file. Status exposes upload_offset, counting only acknowledged remote writes. A sequential retry must send Upload-Offset equal to it and a Content-Length covering the remaining suffix; parallel claims are rejected. The full-file digest includes the verified prefix. Completed writes are never replayed. Session retirement, host deletion, tool disablement, server shutdown, and token rotation cancel live work; active uploads prevent idle session expiry. A completed remote commit stays successful even when its HTTP response is lost. Retained upload completions also keep an otherwise idle session queryable for thirty minutes; terminal records are globally bounded to 256 entries. Metadata-only HTTP upload audit rows are independent of ticket-creation audit rows. No sandbox entitlement, persistence schema, or AgentKit dependency change is needed.

sftp_download issues the reverse resumable HTTP capability, bound to the session, host, full remote path, and optional expected SHA-256. Ticket creation performs no remote I/O. GET authenticates and atomically claims before connecting; it supports regular files only, with no query credentials, request bodies or HEAD. A single open-ended Range: bytes=N- returns 206 and Content-Range, while an out-of-bounds offset returns 416 without consuming the ticket. The client chooses N from its actually saved bytes, not server send progress. A bounded prefix reread verifies previously read bytes before response headers and reconstructs the full-file digest; source metadata changes or prefix mismatches reject resumption. A dedicated SFTP lease opens a checked SFTPDownloadStream; the worker owns its handle IDs, native pointers, and queue. FSTAT on the opened handle fixes the reliable declared size, and EOF verifies exact byte count plus the final size and available modification time. This is not an atomic remote-file snapshot.

The HTTP file-response body is pull-based, reading at most 64 KiB only after the previous network send completes; it never uses the SSE AsyncStream or local staging. SHA-256 covers actual transmitted bytes. A terminal success requires source validation, full HTTP sending, checked handle closure, and dedicated lease release. Pre-header failures return safe JSON; post-header failures close the connection without appending JSON or changing the announced status. Clients must poll sftp_download_status, compare local size/hash, and finalize their own file; HTTP 200 and Content-Length alone are not proof of success or local persistence.

Upload and download share one four-active-transfer budget. Each direction retains at most 64 unfinished capabilities (including paused attempts) and 256 terminal records for thirty minutes. HTTP disconnect and recoverable SSH failure pause a transfer; explicit revocation still cancels it. Retries require the original active session and unexpired capability, and neither checkpoints nor capabilities survive process exit. Generation IDs prevent old transport callbacks affecting a new attempt. Both directions share session/host retirement, server-stop, token-rotation, and activity boundaries; disabling one direction revokes it immediately. Capacity is released only after cleanup. HTTP download audit rows are independent of MCP ticket calls and retain only endpoint metadata, sent byte counts, and terminal state, never file bytes or capability tokens. The existing upload and stdio bridge remain intact.

Callback isolation

Speech services and coordinator state are MainActor-isolated, but authorization callbacks may arrive on any queue. A nonisolated factory creates an explicitly Sendable continuation callback; dispatching from an actor-inherited closure would trap before its body runs. The coordinator rechecks its recording-session token after authorization so a late result cannot revive a cancelled session.

iOS Core Audio taps follow the same rule: a nonisolated factory creates the realtime tap block, which calls only Sendable buffer/level handlers. Reconnect timers and metrics workers also enter explicit MainActor methods rather than relying on inherited task isolation; remote I/O stays behind asynchronous/detached boundaries.

Agent runtime and security

Model-provider credentials are optional on macOS and mobile. An empty credential reference means anonymous requests; a nonempty reference whose secret is missing, empty, or unreadable remains an error. AIConfigurationModel.modelSecret(for:) shares this distinction across discovery, probes, Chat, Guardian, titles, compact, Command Generator, and Memory review. Anonymous resolution never reads secret storage or inherits another provider's storage error. Vertex still requires its service account and a valid project/location. AgentKit omits empty authentication headers for ordinary providers.

Model-provider forms show protocol-specific Name and URL hints, not prefilled input. A shared Domain policy materializes blank/whitespace fields at the Application save boundary: Automatic stores OpenAI's full Responses URL; explicit protocols store OpenAI/Anthropic v1 or Google v1beta bases. Nonempty input never falls back or changes on a type switch. Runtime reads never infer public destinations for legacy empty rows. Desktop keeps page-owned input buffers while saving concrete defaults immediately; mobile previews a normalized copy and normalizes again after conflict merging at Save. Reopening shows saved values. Merely viewing or editing a form does not contact a provider.

Explicit generateContent offers a Vertex AI toggle. Project, location (blank saves as global), and service-account identity are independent of the retained ordinary URL/API key. Leaving that type disables Vertex without deleting its configuration. Missing Vertex credentials never fall back to an API key or anonymous access; Vertex models are manually added. AgentKit owns address derivation and OAuth token exchange. File/paste import validates bounded service-account JSON and trusted Google token URLs; plaintext stays in transient drafts and AISecretStore. Two-reference credential edits compare their original values and roll back all changed secrets on metadata failure. Deletion removes both references; disabling Vertex removes neither. V19 adds independent PersistentModelProviderVertexConfiguration backing without changing V1-V18 entities. Settings sync carries only metadata/references, preserves existing Vertex metadata when an older payload omits it, and respects explicit clears and deletes. The existing Keychain migration covers both references; no private key enters CloudKit configuration. Vertex data already discarded by V12 is not reconstructed.

Both apps accept user-selected HTTP and HTTPS model endpoints. Their ATS arbitrary loads exception supports unknown user-configured domains, including local servers; HTTPS keeps default certificate trust evaluation. No local-network ATS exception may override that setting. Local-network usage descriptions are localized. App Store review notes must explain that users configure arbitrary self-hosted model servers, including HTTP endpoints which the app does not own and cannot upgrade to HTTPS.

AgentKit owns the provider adapters, loop, approval gate, MCP client, and built-in host-agnostic tools. Termind owns everything that reaches its machines or people: SSH/SFTP tools, connection pools, Vault resolution, persistence, policy, and UI. The additive seams are:

  • AgentToolCatalog.registry(builtIn:additional:): AgentKit groups first, then TermindAgentTools; Termind and remote MCP tools cannot shadow built-ins.
  • ChatSlashCommandRegistry: one registration owns a command's platforms, availability, options, current-value presentation, and execution. Desktop and mobile composers inject only host navigation, compaction, send, and access actions.
  • AgentToolServices: typed host services, including AgentSSHInteraction, keep credentials and host-key prompts outside AgentKit events and durable history.
  • TermindAgentPolicy: Termind's prompt, plan contract, and ssh_exec decision; stable prompt prefixes preserve provider caching.
  • TermindAgentPresentation: Termind-only card values whose stable raw values keep persisted cards decodable.

AgentRuntime is an actor. AgentContextPipeline shapes only model requests; stored history remains complete. AgentTurnDriver streams a turn, AgentToolScheduler runs its batch, AgentToolExecutor performs schema validation, preflight, card creation, hooks, authorization, then execution, and AgentEventChannel serializes UI and durable events. Provider adapters include OpenAI Responses/Chat Completions, Anthropic Messages, and Gemini. Models without tool capability receive no tools; only recognized chat-purpose models appear in conversation pickers, and unknown purpose values safely default to chat. Ordinary planning and acting runs have no turn or tool-call count ceiling; they end when the model finishes, the user stops, a hook terminates, or an error occurs. Guardian remains separately bounded so unavailable or inconclusive review fails closed. Compact, conversation titles, and Command Generator use AgentKit's buffered-result interface over streaming transport, not stream: false requests. They collect validated results before applying them and never expose intermediate fragments. Buffered SSE responses require a terminal marker and are capped at 16 MiB of raw response bytes; cancellation, malformed data, and premature EOF discard partial results. Ordinary agent streaming retains its existing execution policy, and providers returning complete JSON remain supported. Each ordinary model turn retries only transient provider transport/service failures five times at 1/2/4/8/16 seconds. A retry reuses the logical message and request, abandons its partial presentation, and cannot replay a tool because tool dispatch begins only after a complete model response. Retry progress is ephemeral; final run failures remain durable and render as error rows rather than assistant-authored text. AgentKit's callable fetch and scratch_fetch tools are .ask because each contacts a caller-selected URL. Explicitly enabled web_search remains pre-approved; a provider-native search card is a post-hoc record of a search the selected model provider already ran.

Guardian owns Auto-reviewing as an isolated .reviewing agent posture. Ordinary tools default only to .planning and .acting. The registry supports disjoint same-name registrations with independent schema, preflight, and execution for each run mode, but Termind currently registers no .reviewing tools, so every Guardian request is tool-free. Its in-memory per-conversation channels serialize reviews and retain a bounded full-to-delta transcript, while only direct authored user text and locally captured request_user_input answers may establish authorization. OpenAI-shaped requests use guardian:<conversation-id> as the stable prompt cache key; fixed policy, schema, and tool prefixes remain byte-identical. Structured Guardian decisions use provider-native JSON schema when the selected model declares support and strict local validation otherwise. Any unavailable, timed-out, malformed, recursively gated, or policy-inconsistent result fails closed before the pending tool executes. An unset Guardian role is Automatic: it prefers codex-auto-review, then the highest numeric version in the gpt-*-luna, claude-sonnet-*, gemini-*-flash, deepseek-v*-flash, and glm-*-flash families, in that order, before falling back to the resolved Chat model. Matching is case-insensitive and compares numeric version components (0.11 is newer than 0.8). An explicit Guardian assignment always wins.

User messages carry immutable, hidden AgentTurnContextSnapshot metadata. Session facts, skill catalogs, plan contracts, and surface-tool availability are captured on submission, including steering and edits. Live surface state still belongs to the window. Replay emits an initial baseline and then only changed blocks at their original message boundaries; missing legacy snapshots are unknown, never reconstructed from the current terminal. Leaving a session or mode emits an explicit clearing transition. Retries retain their snapshot, forks copy it, and compaction retains context facts while establishing a new baseline. V16 adds independent PersistentAgentMessageReplay rows; V1-V15 message definitions remain unchanged. Deletion and truncation remove the backing.

Tool model output is projected once before toolFinished, including a bounded preview and a scratch capture reference for oversized results. Result metadata preserves the projection across crash recovery; message backing preserves it across replay. Earlier results are never shortened merely because another tool finishes. Paged reads retain their structured continuation fields. sftp_read streams UTF-8 line windows; it and scratch_read report only complete returned lines and a truthful next offset.

Terminal suggestion definitions remain stable across surfaces. The executor checks availability from the most recently delivered user snapshot before preflight; merely changing focus does not change the active run's capabilities. Termind's stable system prompt explains that surface availability is a partial override, not a tool catalog; terminal suggestion availability does not restrict independent SSH/SFTP tools. The standalone Assistant need not attach session context: no active session means no default target, not an inability to execute. Discover targets with list_hosts and ask only if the user's intended target remains ambiguous; no sidebar or terminal connection is a prerequisite. SSH/SFTP login is handled by the host configuration and app authentication UI. request_user_secret is only for command secret input, not a prerequisite for SSH; ssh_exec needs only host_id and command for ordinary commands. A terminal suggestion creates a user-operated card and never constitutes command execution. Provider tool/schema JSON is deterministic. Responses and Chat Completions requests use a conversation cache key without hostname restrictions; Anthropic uses short-lived system/message cache breakpoints, and Gemini retains implicit caching. Cache read/write usage is normalized without adding cached tokens twice to input totals.

A batch is concurrent only if every call is parallel. Preflight may tighten this: ssh_exec is parallel only when local command classification proves it safe; an MCP destructive hint makes a call sequential. Results always enter the transcript in model order, and every call gets an answer even after cancellation or refusal. Approvals/questions are queued one at a time; state derives from outstanding work. Authorization is a structural executor stage and cannot be removed by hooks.

Command classification recursively unwraps supported shell wrappers and static nested commands, then applies argument- and subcommand-aware rules. Only explicitly proven read forms are locally read-only; unknown tools, options, embedded programs, excessive complexity, and unsupported shell syntax fail closed to a state-changing assessment.

Every call has an approval policy before the run's permission mode is consulted:

  • Deny refuses every permission mode while keeping the tool registered and visible to the model; Approve runs without another approval step.
  • Ask for approval sends Ask calls to the user. Approve for me sends them to Security Review; missing credentials, timeout, transport failure, or malformed review fails closed and is recorded.
  • Full access pre-approves Ask calls but retains host-key, secret, schema, path, fetch, sandbox, and capability boundaries.
  • A genuinely new MCP tool starts at Approve only when its remote readOnlyHint is true and its destructiveHint is not true; every other new tool starts at Ask. Rediscovery preserves the user's stored policy, including an explicit Approve choice for a destructive tool.
  • MCP tools follow Deny, Ask, or Approve in both acting and planning runs.

Skills are global, app-managed packages. Settings imports and approved skill_manage transactions use the same user library and Settings sync journal. Skill Creator and Skill Installer are immutable application instructions merged into runtime snapshots; they appear in the catalog, /skill, and read tools, but never in Settings, persistence, or sync. Their reserved names cannot be shadowed or mutated. Existing user collisions remain visible and paused until renamed. There is no starter seeding or Skills Reset. Previously installed starter skills remain ordinary user-owned records, including unchanged or disabled copies. Their identities, contents, resources, and sync behavior are preserved without migration or special recognition; new installations start with an empty user library. The old starter definitions and fixtures are not shipped.

skill_install is acting-only and uses the existing Ask/Guardian/Full Access boundary. AgentKit resolves public GitHub skill directories to one commit, bounds and verifies all downloaded bytes, and rejects redirects, symlinks, submodules and incomplete packages. A run-owned in-memory staging area binds reviewed source, files and installation name to the invocation; execution commits only those bytes against the reviewed library revision. Termind alone persists the complete package and Settings sync intent in one transaction. Installation executes no scripts, never overwrites an existing name, and becomes available on the next run. skills_list snapshots inventory, and skill_read_file reads bounded SKILL.md instructions or paged supporting resources. Only SKILL.md is an instructional result; supporting files remain reference data and no frontmatter grants permission. Mutations are acting-only, Ask, atomic, and bound to the library revision reviewed at preflight. Running agents retain their starting package snapshots; edits become available on the next run. The retired load_skill name has historical presentation/replay support only. V18 adds independent package backing while V1-V17 models remain frozen. Complete packages sync with existing skills; legacy records without package fields cannot erase supporting files.

Hermes Memory (持久化记忆 / 持久化記憶 in Chinese) is a device-local, default-off feature. AgentKit owns generic memory values, content screening, tools, learning, and revocable prompt snapshots; Application owns lifecycle and UI state; Persistence alone imports GRDB. Its dedicated Application Support database stores bounded global memory (2200 Unicode scalars) and user (1375 scalars), frozen per-conversation snapshots, learning checkpoints, and the FTS5 history index. SwiftData remains the original conversation source. Forks retain the snapshot reference, not a second copy of memory in SwiftData. Tool writes are atomic; background reviews compare their base state transactionally before committing. Skills always retain their separate approval boundary. Memory and credentials never sync.

Disabling memory revokes live access and prompt snapshots and cancels learning/indexing, but preserves storage. Destroy is user-only: persist disablement and an unfinished-destroy marker, revoke all leases, cancel and join work, close GRDB, then remove its entire owned directory. Failed destruction remains disabled and is retried at launch. No stale task can write through an old generation. Skills, original messages/tool results, and exported files survive. Explicit re-enablement creates a new memory store and permits full history backfill; old chats may therefore teach the agent the same facts again. Disabled startup does not create or open a database. Planning can search history but cannot write memory.

Tools never receive the interactive PTY. SSH exec uses a dedicated channel and file operations use run-scoped SFTP. User-entered secrets are single-use handles bound to run/tool/host/purpose; plaintext never enters model input, reviews, events, logs, or SwiftData. A live approval is the deliberate user-review boundary: while it is pending, the local UI projects the invocation's original arguments through the registered tool presenter so the person can inspect exactly what will run. That projection is neither recorded nor reused after the decision. Fetch permits proven public HTTP(S) reads, revalidates DNS on every redirect, reauthorizes entry into a restricted origin, follows explicit headers like curl -H -L, and redacts sensitive values. Remote MCP metadata can tighten but never relax local safety.

Each conversation has a bounded AgentScratchWorkspace under app-owned Application Support. It rejects absolute escape, .., and symlinks, proves resolved containment, and serializes mutations per path. Pull/push move remote bytes without putting them in the transcript; these host-touching endpoints remain Termind-owned. Edit returns a diff and search bounded previews. Push approval is bound to size/digest and includes a redacted head, so changed content is refused.

Agent conversations, runs, visible messages, tool calls/results, and reviews are stored after truncation/redaction. Launch marks unfinished runs interrupted and never replays remote actions. Restore materializes completed results missing from the last snapshot; remaining unanswered calls receive an explicit unknown-outcome repair so the model cannot assume an action was never dispatched.

Pro access and review invitations

ProAccessController owns application-wide StoreKit state. Permanent Pro is a Family Sharing non-consumable; annual Pro is a Family Sharing auto-renewable subscription whose introductory-offer eligibility, free period, billing period, localized price, expiration, and renewal state come from StoreKit. Permanent access wins over subscription access. Subscribed, introductory-trial, and verified grace-period states grant Pro; billing retry without grace, expiration, refund, and revocation do not. Product metadata failure never replaces verified access. ProOfferPresentation derives purchase labels, disclosures, and period text from value inputs, never from a StoreKit controller. Native paywalls retain purchase and navigation state. macOS supplies explicit app-language lookup; mobile retains Apple's bundle lookup, independently of the locale used for duration formatting.

The retired standalone trial product and its historical transactions are not recognized. Entitlement expiration is checked on activation and at most minutely while active. Workspace composition is free on macOS, including splitting, merging, rearranging, duplicating, broadcasting input, batch opening, multi-host snippets, and local templates. It never depends on StoreKit or retains a launch-scoped Pro grant. Template iCloud sync and all other Pro features use the current entitlement immediately.

One app-wide shared AppReviewController stores device-local first-use/active-day/version/ attempt history, never syncs it, and never claims a review was shown. Desktop windows and mobile scenes use the same surface-based policy; their platform adapters alone own native focus, input, pointer, lifecycle, and modal observation. Automatic requests require seven elapsed days, three active dates, and five foreground workspace minutes, then a three-second quiet transition to a Vault list or Settings index/App page. Input, navigation, focus loss, sleep, or modal presentation cancels the attempt. Attempts are persisted before StoreKit, limited to once per marketing version and a 120-day cooldown; manual review restarts the cooldown. Tests and screenshot launches disable live requests.

Server metrics

ServerMetricsModel is application-owned and shares one serial SSH sampler per saved host across windows, menu bar, and mobile. Rows exist before secrets/networking and represent connecting, collecting, live, paused, sleeping, or actionable failure. Workers run detached and enter short, synchronous MainActor-isolated state steps; no long-lived async method owns observable or host-store state. Credential lookup and remote I/O stay off actor, and cancellation is rechecked after lookup. Secrets are cleared from memory on pause/removal/termination. Known-host and credential decisions remain interactive and scoped to the active monitor.

Screenshot launches inject a frozen, in-memory metrics model into the production WindowGroup with restoration disabled. It never reads saved monitor configuration, starts samplers, or performs host-key discovery.

Infrastructure reads bounded Linux procfs and optional hardened nvidia-smi XML. Base metrics survive GPU/process/filesystem failure; invalid deltas remain unknown. Rates use remote uptime, process identity uses PID plus start time, RSS uses remote page size, and no process arguments/environment are collected. Five-minute samples are in memory; ordered monitors, pause, and device selection persist. Filesystem capacity refreshes only while details are observed. Network defaults prefer physical interfaces, with explicit selection overriding aggregation.

get_metrics exposes an existing monitor's Sendable snapshot to Agent and the authenticated MCP bridge. It cannot add hosts, sample, observe details, or access auth state. It is read-only, parallel, planning-safe, validates host IDs, preserves section timestamps/age/staleness, uses null for unknown/non-finite values, and limits processes to 20. Monitoring itself does not depend on AgentKit.

Metrics order syncs independently of Vault order; local layout, process sorting, search, detail navigation, and menu-bar visibility do not. Detail observation alone enables process/filesystem collection. Authentication opens the shared Connection Journey while the metrics coordinator retains transport ownership. Each monitor owns its keyboard-interactive queue, including challenges from its jump hosts. Metrics always supplies its own responder, never falling back to an app-owned sheet or external authentication window. Cards indicate required authentication without changing navigation; opening a card presents its Journey. Leaving details retains the challenge, while explicit authentication cancellation pauses the monitor and returns that window to the list. Retiring an attempt cancels both queued and not-yet-enqueued responders; generation and request IDs prevent stale answers or cleanup from affecting another attempt. Temporary trust is cleared on pause/removal/configuration change; rejection pauses the monitor. Free access admits at most three cards (paused included), atomically and without deleting restored cards after downgrade; Pro/trial access is unlimited.

The macOS status item/popover is process-owned and receives its model explicitly. Closing the last window keeps the process alive while the menu item or monitors need it; Quit cancels monitoring. System sleep and an inactive login session independently suspend monitoring; collection resumes only after the system is awake and the session is active, when the Data Protection Keychain is available again. Native lifecycle, window reopening, input modality, and screenshot fixture wiring remain in TFM/Platform.

Settings replication

iCloud has one master switch plus independent Vault, Metrics, and Settings choices. Vault/Metrics default on, Settings off; disabling the master preserves selections, and upgrades do not re-enable it. Pro gates three independent coordinators: TermindVaultV1, TermindMetricsV1, and the compatibility-named TermindAIConfigurationV1.

V13 Settings entity names/defaults are frozen; V14 only adds PersistentTerminalPreferences; V15 adds the independent Metrics journal/account/ quarantine. Settings persistence atomically commits data and upload intent. Separate records cover providers/models, MCP clients, skills, roles, Web Search, and terminal preferences/custom text. The most recently selected conversation model remains device-local. Remote commits refresh models without re-entering local mutation. Active Agent runs keep their starting snapshot. Every model role has a live synced record: a nil Command Generator assignment means Same as Chat and a nil Guardian assignment means Automatic, while Chat's explicit selection is synced normally. Resolved fallback models remain local. The legacy securityReview record ID is accepted only at the CloudKit read boundary, migrated to guardian without reusing its record system fields, and all new writes use the canonical ID.

Conversations, approval mode, local MCP listener settings, and unrelated defaults do not sync. Listener enablement, port, and tool selection remain UserDefaults; its token uses a this-device-only Data Protection Keychain. Explicit provider/MCP credentials use AISecretStore and migrate only after account verification, verified cloud copy, and a durable local marker. Reads do not cache secrets; deletion markers prevent late Keychain delivery from reviving cleared values. Custom MCP headers and terminal text are ordinary synchronized settings, but explicit credentials never enter CloudKit.

Journals retain tombstones and unresolved references across partial fetches. Baseline defaults yield to cloud records. Application skills stay outside synchronized records; previously installed starters remain user-owned, and user deletions survive migration. Duplicate skill names/MCP namespaces retain data, but only the lowest UUID executes until resolved. Metrics keeps one versioned record per host; records whose Vault host is absent remain hidden, and samples, prompts, secrets, and UI preferences never enter its zone.

Mobile boundaries

Each mobile window owns one native sidebar-adaptable TabView with five stable feature stacks, ordered Vault, Metrics, Workspaces, Assistant, Settings. The system adapts the tab bar and sidebar without replacing the root navigation tree. Width changes retain the selected feature, child pages, searches, scroll state, sheets and unsaved forms. Metrics has one independent stack in all widths; Vault search shortcuts select that destination rather than constructing another Metrics page. Resizing never restarts live connections, transfers or Agent runs, or creates a new conversation.

Metrics detail and Assistant use the actual tab-bar placement to hide their toolbar titles only beside the top tab bar. Assistant retains its conversation switcher as an icon menu there; sidebar and bottom-bar layouts retain the original titles and title menus. Assistant library pages retain their titles in every placement.

Editor saves do not gate layout changes. Their tasks, duplicate-submit protection, unsaved-dismiss confirmation and error feedback survive width changes. Sheets follow the presenting window's width using stable presentation modifiers. Cards use container width and one column at accessibility text sizes; SFTP breadcrumbs fit three or two trailing components to the available space. Device idiom is not a production layout input. Navigation and draft states remain memory-only and independent per window.

Neither layout shows Welcome. Each Workspace owns exactly one SSH or SFTP session; mobile never composes panes, even with Pro. Assistant is independent. The title menu switches open peers without reconnecting existing sessions; selecting a Recent Workspace creates and connects it on demand. More owns Rename/Close and cross-opening SSH/SFTP when a live path exists. Explicit close never retries.

Initial connection failure and recovery stay in Connection Journey. Background-task expiration closes transports but retains intent; foreground resumes existing workspaces. Mobile saves the full ordered list of device-local Workspace descriptors (stable ID, Host ID, kind, title and duplicate index) under mobile.workspaces.v1. Cold launch loads descriptors into Recent Workspaces after loading Hosts, leaving workspaces empty: no sessions, connections or history events are created. One MainActor open-by-ID entry point reuses an existing instance or registers and connects only the requested descriptor, preserving its ID and recording history once across scenes. Failures require explicit retry. New and restored instances share the same ordered saved list; saving current instances alone must never discard unopened entries. Commands, credentials, paths, terminal contents and transfers are excluded.

The store reads both legacy bare arrays and the version-1 object envelope. Legacy records may lack duplicate indices; migration assigns the lowest free index per Host and kind in array order, reserving explicit indices, and immediately writes the envelope, including for an empty array. Successful loads permit later saves. Missing Hosts are pruned; unknown versions and corrupt snapshots retain their bytes and block writes until a successful load. Close, normal SSH exit and local or remote Host deletion remove current and unopened saved entries. Close All also clears Recent Workspaces, while retaining connection history. Recent Workspaces is a separate list from connection history, whose actions still create new sessions. The history section appears only when both current and Recent Workspaces are empty. Titles reserve indices across current and unopened entries; manual names retain the allocation, and closing releases it without renumbering peers. Host deletion runs the shared outbox saga and cancels sessions, transfers, forwarding, and local history.

One serialized Live Activity represents only current Workspaces with counts and up to six distribution icons. Validated links open a Workspace by ID or navigate to the list without restoring sessions, and carry no secrets or commands. Cold launch ends leftover activities; their URLs can still restore a saved Workspace. Explicit opening retains a start intent until foreground conditions allow a request. Closing the last current Workspace clears that intent and ends the activity even when recent entries remain. Ordinary foreground updates cannot recreate a dismissed activity after its start intent has been consumed. Immutable activity snapshots are separate from observable Workspace/Vault state, and starts, updates and ends share one serial queue. An activity cannot keep SSH or Agent runs alive. A stale link with no matching entry opens the list with an unavailable explanation. Restoration does not distinguish system termination from a user force-quit.

Mobile Settings exposes App, Terminal, SFTP, AI, and Sync. The macOS window-title preference stays synchronized but has no mobile control; there is no Live Activities settings page. AI lists Models, Skills, MCP, then Hermes Memory; user skills and MCP use explicit-save drafts. Network tests/discovery use ephemeral snapshots, cancel on editor/config changes, and cannot repopulate discarded drafts. Secrets remain in memory until atomic save and roll back on metadata failure. Mobile does not start the local MCP listener.

Each mobile scene owns one navigation/command model and one Assistant library model, including its selected conversation and New Chat draft. The app-wide Assistant runtime and conversation store remain shared so active runs are never duplicated. The scene model is published as a focused value, so hardware-keyboard commands never operate another iPad window. Home Screen quick actions enter through a per-window scene delegate, are consumed once by that scene's navigation model, and never target another iPad window. Connect is static; up to three dynamic history actions derive from the shared event log. Workspace History and those actions keep the newest display row per Host and connection kind without deleting duplicate persisted events. Mobile uses the macOS shortcuts for capabilities that exist on both platforms: navigation, connection/creation, terminal Find and Command Generator, Workspace close/switch, and Assistant send/stop. Workspace switching never reconnects the session or claims terminal focus. The UIKit terminal surface owns F3/Shift-F3 and replaces Ghostty's bare/Shift-Escape commands only while Find is presented; otherwise Escape continues to the terminal unchanged.

Mobile Agent runs belong to the app, not the visible page. Conversations retain independent drafts/runs; archiving persists metadata before navigation and becomes read-only until restored. Attachment parsing and speech are injected services; speech appends without sending and cancels on interruption, conversation change, or background. Mobile shares desktop-independent tool registration and permission/ secret interaction, but finite background expiration cancels runs, drains events, closes Agent transports, and never replays or auto-approves work on foreground.

ListViewKit is an app-owned implementation detail: macOS uses native bridges for the streaming transcript, large SFTP lists, conversation library, and tool sidebars; iOS uses one UIKit bridge for transcript and large SFTP lists. Bounded navigation and archive pages remain SwiftUI List. Shared frameworks never import ListViewKit, and architecture tests own its allowlist.

Mobile workspace file drops use one upload policy for native routing and payload loading. The visible workspace marks its bounds for a window drop interaction; the terminal adapter forwards file drops to that destination instead of Ghostty's local-path paste, while text and web links keep Ghostty's behavior. Leaving the destination restores the original interaction and clears targeting. File-provider representations are copied within their callbacks into app-owned staging before upload preparation; queued transfers retain those files after navigation away.

Mobile top-level destinations are registered in MobileFeatureRegistry. A registration owns stable identity, label, symbol and its destination factory; registration order is shared by the tab bar and sidebar. The root shell renders that catalog and does not maintain separate tab, sidebar, and destination switches. Registries are injected values rather than global singletons, and duplicate feature IDs fail immediately.

Mobile Workspace descriptors use Domain's ConnectionHistoryEntry.Kind as the single SSH/SFTP kind vocabulary. Host Picker and Snippet operation state live in the mobile Application layer rather than beside their views. Live Metrics samplers enter through MobileMetricsServices, while both app shells obtain Agent host resolution, authentication, connections, and background commands from the shared Remote Infrastructure AgentHostServices composition.

Application derives flattened Host and Snippet sections once for both platforms. Presentation owns the shared command-history subtitle, Vault sync status label, SFTP transfer status labels, and the exported SFTP drag type. Transfer labels distinguish upload/download while running and upload/download/copy on completion; other states reuse the generic Domain label. App targets retain native card/list composition, selection, menus, and clipboard behavior.

Assistant transcript presentation parity (2026-09-12)

TermindPresentation owns tool summaries, batch status semantics, bounded result previews, timestamp formatting, Markdown sanitization/soft-break policy and the shared disclosure/deadline state. Hosts inject a resolved AgentToolDetailPresenter from TermindRemoteInfrastructure; Presentation never imports the registry's transport module. Tool protocol results and persisted schemas are unchanged.

TFM retains native AppKit rows, Markdown caches and message editing state. TFI retains SwiftUI content inside its UIKit ListViewKit bridge. A row's presentation revision can change without its data changing: the mobile bridge reconfigures that row explicitly, invalidates only its height, and retains its UIHostingConfiguration content view. Display/measurement purpose, Reduce Motion and scene activity cross that hosting boundary. Only displayed active content starts timers or scanning; the shared scan parameters and three-second reasoning deadlines cannot restart merely because a row is recycled. Work completion follows the shared default only after the whole row leaves the viewport, unless the reader has already chosen its disclosure state.

The direct approval callback stages a tool card idempotently before accepting a response. Queued tool-proposed and approval-requested notifications update that identity without erasing an already recorded denial. This keeps UI state independent of whether the event channel or the direct interaction reaches MainActor first.

Skill and memory tools own their UI-neutral result and approval projections in AgentKit. Presentation repairs their historical summary semantics only for recognized local tool/presenter identities. Their compact result preview shares a 600-character/five-row budget across the entire card and a scaled 180-point viewport. Full-detail expansion is independent, window-owned transcript state and survives row reuse; Copy retains the full already-sanitized projection. Unknown presenter IDs and MCP tools never opt into this policy by name. Long approvals instead retain all live arguments in a scaled 240-point scroll region with actions outside it. Neither preview path reads live skill/memory storage or changes the original protocol result, authorization, or persisted data.

Desktop transcript width changes reconfigure mounted rows without replacing the cached list or following the tail. Reused row constraints and Markdown geometry track the current content width even when message identity and text are unchanged; parsed Markdown remains reusable independently of width-dependent measurements.