30 KiB
type, title, description, tags, sources, generated, verified
| type | title | description | tags | sources | generated | verified | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| architecture map | Module and Dependency Map | Maps Termind's native products, six shared modules, composition roots, dependency direction, and the remote package inputs actually selected by Xcode. |
|
|
|
|
Module and Dependency Map
Termind has two native apps and six shared frameworks, not one framework per product feature. A feature such as SFTP crosses domain values, application policy, transport implementation, presentation, and native session ownership. Directory names describe responsibilities inside those layers; they are not necessarily independent build targets.
Reading this map
Start with the owner and dependency direction, not the folder with the closest feature name. The module roots are Frameworks/Termind…; Frameworks/Resources is shared input rather than another module. Native Application and Infrastructure directories are app-local policy and adapters, not additional shared framework targets.
Use System Boundaries for contracts, Engineering Invariants for safe-change rules, and Platform Runtime for detailed scene, session, restoration, and teardown lifetimes. Dependency update and release procedures belong in Build, Test, and Release.
Module owners
| Module or directory | Primary responsibility | Important distinction |
|---|---|---|
Frameworks/TermindDomain |
Portable records, value types, parsing, and domain rules. | A Host or transfer record is not a live connection. |
Frameworks/TermindApplication |
Ports, use cases, observable feature models, connection policy, and Assistant coordination. | Defines/injects capabilities rather than constructing concrete storage or transports. |
Frameworks/TermindPersistence |
SwiftData models, historical schemas, migrations, stores, container factory, and GRDB-backed memory. | Owns stored representation, not a window or scene. |
Frameworks/TermindInfrastructure |
General concrete services: CloudKit, commerce, speech, logging, importers, and metrics parsing. | Separate from remote transport mechanics. |
Frameworks/TermindRemoteInfrastructure |
SSH/SFTP/Telnet, remote credentials, transfers, forwarding, sampling, and host-aware Agent tools. | Owns protocol machinery, not native interaction surfaces. |
Frameworks/TermindPresentation |
Reusable SwiftUI content, forms, formatting, transcript policy, and terminal appearance. | Not a persistence or native-app owner; some shared views use conditional AppKit/UIKit for platform-specific rendering. |
TFM / TFI |
Native composition, interaction, navigation, adapters, and runtime lifetimes. | Desktop windows own their workspaces; on mobile each persistent UIKit scene session owns a workspace collection through an app-managed MobileWorkspaceSessionModel. |
Frameworks/Resources |
Shared bundle assets, localization, model catalogs, and privacy metadata. | A resource group, not a seventh framework. |
| AgentKit remote package | Separately versioned, host-neutral agent runtime and built-in tools. | Not a repository-local module or maintenance submodule; Termind owns host-specific tool registration. |
| Supporting products | MCP stdio bridge, Live Activity extension, and standalone forwarding benchmark. | Different executable/extension boundaries, not app feature folders. |
Host artwork is a concrete example of shared Presentation ownership. HostIconCatalog loads HostIcons.json from the main bundle (falling back to an empty catalog on load/decode failure), resolves an explicit iconOverrideID before detected distribution matching, and otherwise matches the distribution ID before its likeIDs, falling back to the generic Linux entry when neither matches. An unknown explicit override returns no catalog entry rather than falling through to detection. HostDistributionIcon composes that choice with HostSystemIcon; tint precedence is the host's color tag, then HostIconBrandColor, then the accent color. HostIconBrandColor owns the conditional AppKit/UIKit semantic-color adaptation, adjusting brand foregrounds toward the label color for contrast on the tinted icon surface. TFMTests/HostIconPresentationTests.swift checks bundled assets, override priority, and light/dark contrast. These are shared rendering responsibilities, not connection or app-lifecycle ownership.
Shared-framework build dependencies
Solid arrows mean “the source target directly depends on the destination target.” This diagram shows the six shared Xcode targets only; external package products and app embedding are separate below. It is a build graph, not request order or data flow.
flowchart TB
Presentation["TermindPresentation"] --> Application["TermindApplication"]
Presentation --> Domain["TermindDomain"]
Remote["TermindRemoteInfrastructure"] --> Infrastructure["TermindInfrastructure"]
Remote --> Application
Remote --> Domain
Infrastructure --> Persistence["TermindPersistence"]
Infrastructure --> Application
Infrastructure --> Domain
Persistence --> Application
Persistence --> Domain
Application --> Domain
Direct dependencies among the six shared framework targets.
Both TFM and TFI directly depend on all six frameworks; TermindDomain has no shared-target dependency. These are explicit PBXTargetDependency edges, not inferred from directory proximity or imports. The diagram also explains why Persistence is not interchangeable with general Infrastructure: Infrastructure can use Persistence, while Persistence must not depend on Infrastructure or Remote Infrastructure. Shared frameworks never depend on an app target. FrameworkTests depends on all six shared frameworks, while TFMTests and TFITests depend on their respective app plus selected shared frameworks; the benchmark does not depend on either app or a test target.
Product and package boundaries
Arrows here are labeled relationships, not a complete link graph. The shared-framework group expands into the graph above. The remote AgentKit package supplies AgentKit to Domain, Application, Persistence, both apps, and tests; AgentKitScrubber is linked by Remote Infrastructure and desktop composition. The single package edge below groups those consumers rather than implying one exclusive entry point. TermindPresentation instead directly links the Ghostty and MarkdownView package products, and TermindRemoteInfrastructure links CSSH2, OpenSSL, TailscaleKit, and the swift-mosh / swift-et bootstrap and engine products (MoshBootstrap, MoshCore, ETBootstrap, ETSession). These remote packages implement transports behind the shared Remote Infrastructure boundary; they do not add shared Xcode framework targets. Check target packageProductDependencies for the exact package linkage before relocating code.
flowchart LR
Desktop["TFM / macOS"] -->|depends on| Shared["Six shared frameworks"]
Mobile["TFI / iPhone and iPad"] -->|depends on| Shared
Desktop -->|embeds executable| Bridge["termind-mcp"]
Mobile -->|embeds extension| Widget["TermindWidgets"]
Widget -->|depends on| Presentation["TermindPresentation"]
Activity["WorkspaceActivitySupport"] -.->|source compiled into both| Mobile
Activity -.->|source compiled into both| Widget
Package["Resolved AgentKit package products"] -.->|linked by owning consumers| Shared
Benchmark["Port-forwarding benchmark"] -->|depends on| Remote["Remote Infrastructure + Application + Domain"]
Native products, supporting executables and extensions, and remote package consumption boundaries.
The MCP bridge translates stdio requests into authenticated HTTP calls to the desktop server; it is not the server itself. TFM builds and embeds this separate tool. TFI builds and embeds the TermindWidgets extension, which directly depends on Presentation; WorkspaceActivitySupport is source-compiled in both the app and extension (and in TFITests), not a seventh framework. The widget renders a bounded workspace summary; it does not establish SSH sessions. The benchmark is a standalone executable linked to Remote Infrastructure, Application, and Domain, not app startup logic or a test-host dependency.
AgentKit version boundary: AgentKit is a remote package, not a repository-root maintenance submodule or local Xcode package path. The project declares a remote upToNextMinorVersion requirement starting at 4.0.0, and Package.resolved pins version 4.0.0 at revision 3481866cd47e83f6bf56b80c432c7824f61130b9. Change the package requirement/resolution to change the build input; do not look for a local AgentKit maintenance tree. AgentKit owns the generic agent loop and tools; Termind registers host-specific SSH/SFTP tools in Remote Infrastructure. TermindAgentTools builds the host tool list from narrowly injected services, registers the general SSH run/stdin tools and mutating SFTP tools for acting, with separate planning SSH run/stdin registrations, and combines built-in and Termind presenter registrations so persisted transcript cards can still be rendered. Optional pane and scratch capabilities omit their tools when unavailable. Presenter lookup requires a presentation ID and rejects MCP descriptors; there is no legacy tool-name fallback in TermindAgentTools. Both native dependency assemblies layer SSHExecToolPresentation onto the shared presenter registry, keeping native SSH card presentation separate from host-tool registration. See Agent Runtime and Assistant.
Remote dependency boundaries: Xcode's declared requirement and its selected revision are separate checks:
| Remote input | Current Xcode requirement | Selected version and revision |
|---|---|---|
AkinoKaede/libssh2-spm |
upToNextMinorVersion from 2.0.3-termind-4 |
2.0.3-termind-4, 7ca39b8bcaf408351e8b6e4f449d2a39959bab49 |
Lakr233/libghostty-spm |
upToNextMinorVersion from 2.2.2026101502 |
2.2.2026101502, 42bacd1339f323a693ddbca779295f4f77e5a697 |
AkinoKaede/libtailscale-spm |
upToNextMinorVersion from 1.0.3 |
1.0.3, b58ed45fb22e53b472ccf6febde1f4a3ab08e295 |
.gitmodules now lists only com.akinokaede.termind; AgentKit, libssh2, libtailscale, and Ghostty have no declared maintenance submodules. Treat the SSH and Tailscale packages as remote inputs, not repository maintenance trees. TailscaleKit links directly into Remote Infrastructure and is not a shared framework target. For any package, project.pbxproj defines the allowed remote input and Package.resolved records the selected revision. Resolved-only verification uses -onlyUsePackageVersionsFromResolvedFile; update and release procedures are in Build, Test, and Release.
How responsibilities meet
| Task | Policy / data owner | Concrete mechanism | Native owner |
|---|---|---|---|
| Connect a terminal | Domain host/session values; Application route, trust, and retry policy. | Remote Infrastructure SSH or Telnet; desktop serial adapter for serial sessions. | Desktop session/pane or mobile workspace; native terminal surface. |
| Browse or transfer files | Domain remote-file values; Application remote-filesystem and edit model. | Remote Infrastructure SFTP channels, transfer scheduler, planners, and edit service. | Native browser, conflict UI, local file access, and Open With presentation. |
| Run an Assistant turn | Application conversation/execution coordination; AgentKit generic runtime. | Termind host tools in Remote Infrastructure; persistence and memory stores. | Native composer, approval/input presentation, transcript mounting, and navigation. |
| Edit and synchronize Vault data | Domain snapshots and mutations; Application library/use-case state. | Persistence local stores and journals; Infrastructure CloudKit coordinators; Keychain secret implementation. | Platform editors and app-level service assembly. |
| Resolve this or another installation's display name | Domain DeviceRecord; Application device-directory port and observable model. |
Persistence-owned SwiftData device records and a device-kind History journal; Infrastructure's DeviceSyncCoordinator writes native DeviceItem fields through its own sync engine, subject to the app's iCloud/Pro sync gate. |
Platform composition supplies the stable installation ID and system display name. |
| Display remote metrics | Domain metrics; Application sampling/snapshot interfaces and model. | Remote SSH sampler plus Infrastructure output parsers. | Native dashboards and desktop menu-bar integration. |
Transport construction and extension seams
RemoteConnectionCoordinator lives in Remote Infrastructure but is supplied by native composition. It constructs PTY, SSH and SFTP clients with shared logging and max-session policy; its one-shot SSH/SFTP connect helpers disconnect the newly created client before rethrowing a connection failure. MobileAppDependencies.live() shares one coordinator across workspace connections, metrics and app assembly. Its testing factory selects in-memory composition and test metrics, but defaults workspace connections to .live: in-memory storage alone is not a guarantee of transport isolation. Inject workspace connection services when a test must not connect.
Resumable terminal protocols extend Remote Infrastructure, not native feature switches over engine classes. The coordinator injects an immutable ResumableTerminalEngineRegistry into PTY channels; its live registrations select Mosh and Eternal Terminal engines. Engines own protocol resources while ResumableTerminalClient owns the shared lifecycle and serialization boundary; it rejects an unregistered transport before wake, route or bootstrap side effects. Persistent remote-session backends use a separate seam: RemoteSessionBackendRegistry.live obtains backend factories from registered multiplexer dialects. Each dialect supplies its backend: tmux uses RemoteTmuxBackend, while socket dialects use RemoteMultiplexerBackend. Application policy consumes backend contracts rather than tmux/Zellij command dialects. Architecture tests explicitly keep engine and dialect implementation names out of Domain, Application, Presentation and both native feature trees, and guard PTY/client code against bypassing registration. FrameworkTests/ResumableTerminalEngineTests.swift exercises injected engines, missing-registration failure before side effects, restore without startup replay, and recovery retry on the existing engine.
The Assistant is a useful change-placement example: AssistantExecutionController accepts an injected registry factory, authorization callback, run repository, and title generator; app-specific host services provide tools without making the Application framework construct SSH machinery. On mobile the app model wires AgentHostServices.live with its connection coordinator and SFTP scheduler. For speech it supplies IOSSpeechAudioInput to the shared transcription service and cancels speech on interruption. The remote-filesystem port similarly separates file policy from the SFTP transfer coordinator; Vault sync reads Persistence-owned journals rather than moving CloudKit policy into the app.
The device directory illustrates why target edges alone are insufficient: native composition supplies the installation ID from VaultSyncJournal and acquires the display name outside the shared model. Desktop TermindApp delegates to the app-owned MacDeviceName, which prefers a trimmed, nonempty kern.hostname and falls back to Device.current.safeDescription when that hostname is unavailable or blank; mobile supplies Device.current.safeDescription directly. MacDeviceNameTests pins hostname priority and nil, empty, and whitespace fallback behavior. DeviceDirectoryModel registers the current installation on initialization, caches lookup results, and reports storage errors. The SwiftData repository refuses to resurrect a remotely deleted device during launch registration, but an explicit rename can upsert it; successful writes notify DeviceSyncCoordinator through History mutations. Remote application of device records invalidates the model's cache on both platforms. The device and connection-history coordinators share a CloudKit zone but keep separate kind-filtered engines and sync state; the device coordinator is independent of the Vault record format and journal, not exempt from the app's Pro/iCloud start/pause policy. See System Boundaries for the sync contract.
This split is also a placement rule: shared behavior belongs with its existing owner; native lifetime or platform API usage stays in the app. Similar names such as Agent, Transfers, Terminal, or Metrics in different modules do not imply duplicated responsibilities. Application owns TerminalFindController policy and state; native session/workspace owners inject binding actions and focus callbacks. Dismissal retains the query and can suppress focus restoration, while native resize can suspend engine search and later resume only a presented nonempty query. MobileWorkspaceSearchModifier owns the feature-level SSH/Mosh/Eternal Terminal/Telnet versus SFTP binding choice; the reusable UIKit search bridge does not. TerminalFindControllerTests covers retention, empty-query navigation, focus-free dismissal and resize suspension; native shortcut tests pin F3/Escape routing.
Composition and state boundaries
The composition boundary is directional: app entrypoints choose concrete adapters and inject them into shared feature models. A scene or window route is not a durable store or a transport. Both platforms keep window-local workspace collections, but mobile session models are retained by the shared app model and keyed to persistent UIKit scene IDs. Initialization failures have different product behavior: desktop presents a database-in-use error and exits when another process holds the store, or aborts launch for other database-open errors; mobile renders unavailable content instead. Test composition uses in-memory stores and omits live sync, so an in-memory passing test does not verify CloudKit transport.
flowchart LR
DesktopEntry["TermindApp"] --> DesktopApp["AppModel and shared stores"]
DesktopEntry --> DesktopWindow["WindowModel per main window"]
DesktopWindow --> DesktopWorkspaces["Window workspaces"]
MobileEntry["TermindIOSApp"] --> MobileApp["MobileAppModel and shared services"]
MobileEntry --> MobileRoot["MobileStartupView and MobileRootView"]
MobileRoot --> MobileScene["MobileSceneModel per scene"]
MobileScene -->|"binds persistent session ID"| MobileApp
MobileApp --> MobileSession["MobileWorkspaceSessionModel per UIKit session"]
MobileScene -->|"selects in bound session"| MobileSession
MobileSession --> MobileWorkspaces["Session workspaces"]
Composition and navigation lifetimes: desktop windows own workspaces; mobile scene navigation binds an app-retained workspace session for its persistent UIKit window ID.
TFM/App/TermindApp.swift is the desktop @main entrypoint. It opens the Persistence container, wires stores, Keychain-backed secrets, sync and connection coordinators into an app-wide AppModel, and injects that model and container into each WindowGroup root. Each main window receives its own WindowModel; a separate authentication window uses the shared app environment. A database-in-use failure calls DatabaseStartupGuard.presentDatabaseInUseAndExit(); other initialization errors end launch with fatalError. On mobile, TFI/App/TermindIOSApp.swift is the actual @main, not MobileAppModel.swift: it chooses live versus test dependencies and constructs one MobileAppModel. MobileStartupView installs its container for the bound scene root or displays unavailable content if initialization throws. MobileAppModel assembles SwiftData, Keychain or in-memory secrets, remote services, sync coordinators and Assistant; it retains the workspace sessions and a flattened view of their live workspaces. In the in-memory/test path it omits live sync and disables Live Activities by default. Each MobileSceneModel binds to the MobileWorkspaceSessionModel for its persistent UIKit session ID. That session owns live workspaces, ordered restoration descriptors and open/close operations; scene navigation selects from its own session, not from all app workspaces. Cross-window workspace links activate the owning native session rather than stealing its connection.
The ownership seam is process-wide services → window or persistent UIKit-session workspace owner → live workspace runtime. Durable storage holds metadata and restoration intent, not live transports. Detailed cold-launch consolidation, orphan recovery, discard, cross-window activity routing, and background behavior are covered in Platform Runtime, rather than being additional module dependencies.
For focused ownership regression coverage, use TFITests/MobileWorkspaceSessionTests.swift and MobileWorkspaceWindowIntegrationTests.swift; native-window integration is opt-in with TERMIND_MOBILE_UI_TESTS=1 and parallel testing disabled. For change-oriented navigation and verification, return to the Engineering Quickstart.
Verifying a boundary change
Read the project metadata as four separate checks; none substitutes for the others:
- Build ordering: follow each native target's
dependenciesIDs to thePBXTargetDependencyobjects and theirtargetfields. This is the graph shown above. - Linkage: inspect
packageProductDependenciesand the correspondingPBXFrameworksBuildPhase. Package products such asAgentKit,TailscaleKit, andGhosttyTerminalare not additional shared-framework target edges. - Compilation and resources: inspect
fileSystemSynchronizedGroups, membership exceptions, and Sources/Resources phases.Frameworks/Resourcesbelongs to both apps,FrameworkTests, and the widget;WorkspaceActivitySupportbelongs toTFI,TFITests, and the widget. These are shared inputs, not independent framework products. Empty explicit phase lists do not imply that synchronized inputs are absent. - Embedding: inspect the app's copy phases independently of its dependency list.
TFMcopiestermind-mcpthrough Embed MCP Bridge;TFIcopiesTermindWidgets.appexthrough Embed Foundation Extensions. Building or linking a dependency is not the same as embedding an executable or extension.
For external input changes, compare the remote requirement in project.pbxproj with the selected version and revision in Package.resolved, not with a maintenance checkout's HEAD.
TFMTests/ArchitectureBoundaryTests.swift supplies source-level guards alongside the Xcode target graph, not a blanket ban on UI or platform APIs everywhere. Its import scan excludes Termind and TermindIOS plus AppKit, UIKit, IOKit, and ServiceManagement from Domain, Application, Infrastructure, Remote Infrastructure, and Persistence; the separate Presentation check excludes app imports, storage and implementation modules, StoreKit, AppKit, and UIKit. However, the import helper checks only exact, unindented import Module lines, and shared Presentation currently uses indented conditional AppKit/UIKit imports in HostIconBrandColor.swift for semantic rendering colors; treat those platform exclusions as test intentions, not proof that every shared view is platform-agnostic. Domain also excludes storage, transport, and UI libraries; Application excludes concrete implementation imports and selected construction tokens; Persistence excludes Infrastructure and Remote Infrastructure imports. Native UI and feature restrictions are scoped: mobile Application excludes UI and storage imports, mobile UI excludes storage and Remote Infrastructure imports, desktop Features excludes selected storage and transport imports, and feature directories are checked for specific direct construction/access tokens. This does not forbid all framework imports in every native feature. The test also rejects declarations of a named subset of AgentKit-owned runtime types in the desktop source tree, not every AgentKit-related type across the repository; Termind still owns host-specific tool registrations. Further checks protect SwiftData model/repository placement and the forwarding benchmark's independence from app/test targets. When moving a capability, inspect both target/package-product edges and the precise test scope. Use the pinned-resolution build/test commands in Engineering Quickstart.