Single SourceStudios Engage SSS
Swift Package Manager · macOS 13+ · single-session build

One AppKit canvas hosting every terminal pane.

Multi-Term is a single-target Swift Package Manager app that hosts multiple independent /bin/zsh --login processes as draggable, resizable panes on one AppKit NSView canvas, with a file explorer that follows the focused pane's directory over OSC 7 and a Monaco code editor running inside a WKWebView. Built in one continuous session, by one author, in under two hours.

1,870 Swift LOC, 10 files
5 commits, ~105 minutes
1 author
0 automated tests
0 persistence, in-memory only
What it is

A single AppKit canvas standing in for every terminal pane.

Multi-Term is a single-target Swift Package Manager executable named MultiTerminal (Package.swift:5-16). It runs multiple independent /bin/zsh --login processes as draggable, resizable panes on one AppKit NSView canvas, where each pane wraps a LocalProcessTerminalView from the SwiftTerm library, declared at a minimum of 1.12.0 and resolved to 1.13.0. Panes group into project tabs managed by an ObservableObject store, each tab carrying its own color and pane list, snapped to a 20-point position and size grid.

A file explorer sidebar tracks the working directory of whichever pane last received a click, driven by OSC 7 escape sequences emitted from a shared per-launch ZDOTDIR. A built-in code editor loads Monaco inside a WKWebView, so files opened from the explorer can be viewed and edited without leaving the app.

All application state, open tabs, panes, positions, open files, lives in memory only. Nothing survives a relaunch.

Hard numbers

Every figure traces to a git command or a file path.

MetricValueEvidence
Commit count5git rev-list --count HEAD
First commit2026-04-14 16:05:09+02:00git log --reverse --date=iso-strict
Last commit2026-04-14 17:50:32+02:00git log --date=iso-strict
Commit span~1h45m, single dayfirst/last commit timestamps
Authors1git log --format='%an <%ae>' | sort -u
Swift LOC1,870 across 10 filesfind Sources -name '*.swift' | xargs wc -l
Bash LOC118wc -l build-dist.sh
Swift files indexed11git ls-files '*.swift'; jcodemunch get_repo_outline
AST symbol count282 (31 classes, 55 methods, 193 properties/constants, 3 functions)jcodemunch get_repo_outline
Test files0find . -iname "*test*" (excl. .build, .git)
Published packages0 (app, not library)Package.swift:11-15, one .executableTarget
Direct dependencySwiftTerm 1.12.0 declared, 1.13.0 resolvedPackage.swift:7; Package.resolved
Transitive dependencyswift-argument-parser 1.7.1Package.resolved
Git tags / releases0git tag
DB / SQLite files0find . -iname "*.db" -o -iname "*.sqlite*"
Deploy target / live URLnone, manual .pkg/.zipREADME.md Installation; no CI/deploy config
Monaco language mappings35grep -c 'return "' MonacoEditorView.swift
File-icon mappings50grep -c 'return .init' FileIcons.swift
Preset pane/tab colors12TabStore.swift:42-47
Entitlements filenonefind . -iname "*.entitlements"
SSH / remote-terminal codenonegrep -rin "ssh|citadel" Sources/
Local build output (untracked).pkg 2,369,653 bytes; .zip 2,366,421 bytesls -la dist/, local evidence only
Architecture

One canvas, two state stores, a web view for editing.

Entry point is MultiTerminalApp.swift:1-25, a @main App that forces NSApp.setActivationPolicy(.regular) on appear, because Swift Package Manager executables launch without a bundle identifier and default to a background process that would not otherwise receive key events. It wraps MainContentView.swift:3-129, a three-pane HStack of file explorer, optional code editor, and terminal canvas, divided by a draggable PaneDivider.

State splits into two ObservableObject models built in MainContentView: TabStore (Models/TabStore.swift) owns project tabs and their pane arrays, id, name, color, cwd, position, size; AppState (Models/AppState.swift) owns the files open in the built-in editor plus save and error state. Both inject as @EnvironmentObject.

The terminal layer is one AppKitTerminalCanvas (Views/TerminalPane.swift:387-402), a single NSViewRepresentable wrapping one CanvasNSView (TerminalPane.swift:339-383) that owns every pane as a plain NSView subview positioned by direct frame assignment, not a per-pane SwiftUI view. CanvasNSView.sync(with:) diffs the store's pane list against existing subviews on every SwiftUI re-render. Each pane composes a title bar, an embedded SwiftTerm LocalProcessTerminalView, and a resize strip, and implements its own mouseDown, mouseDragged, and mouseUp for drag and resize.

The file explorer (Views/FileExplorer.swift) reads the focused pane's current directory and lists contents via FileManager. The code editor (Views/MonacoEditorView.swift) is a WKWebView loading Monaco from a CDN, exchanging content with Swift through WKScriptMessageHandler and base64-encoded calls.

Engineering decisions

Nine tradeoffs visible in the code, the commits, and the docs.

Single AppKit canvas, not per-pane SwiftUI views

architecture

SwiftUI's NSHostingView intercepts every event via hitTest, and .offset() uses a CALayer transform that AppKit's frame-based hitTest ignores, so embedded terminal views never receive mouse or keyboard input (TerminalPane.swift:51-61). The fix hosts every pane as a plain NSView inside one NSViewRepresentable, at the cost of hand-rolled drag, resize, and hit-testing instead of SwiftUI's DragGesture or HSplitView.

Focus tracking widened to every click zone

fix

Commit c8c50e8 moved the setFocus(paneID:) call out of the terminal-only case in mouseDown into an async dispatch that fires for title bar, resize strip, or terminal body clicks, so the file explorer and title-bar highlight track the last-clicked pane, not only the last-typed-in one.

Monaco loaded from a CDN, not bundled

tradeoff

MonacoEditorView.swift:163-167 pulls monaco-editor@0.52.2 from cdn.jsdelivr.net inside the WKWebView's HTML. This keeps the compiled app small and the editor centrally updatable; it fails to load with no internet connection, a case the README and CLAUDE.md both flag.

Base64 content bridge corrupts non-ASCII files on save

known defect

applyContent (MonacoEditorView.swift:97-108) base64-encodes Swift string content for the WKWebView; the JS side decodes with atob(b64) (MonacoEditorView.swift:225), which returns Latin-1 bytes, not UTF-8. AppState.saveFile (AppState.swift:108) then writes the corrupted string back to disk with .utf8 encoding. Accents, emoji, and box-drawing characters mangle in the editor and stay mangled on disk. This is a diagnosed, unfixed defect, not a hypothetical.

No layout, tab, or open-file state survives relaunch

persistence

grep -rn "UserDefaults|Codable|JSONEncoder" Sources/ returns no matches. TabStore and AppState hold everything in @Published in-memory arrays; every relaunch starts from a hardcoded single "Development" tab with one "Main" pane (TabStore.swift:36-38).

Per-launch shared ZDOTDIR instead of editing the user's shell config

shell

sharedZdotdir (TerminalPane.swift:13-40) writes stub .zshenv, .zprofile, .zshrc, and .zlogin files to a temp directory that source the user's real dotfiles and register an add-zsh-hook chpwd emitting an OSC 7 escape sequence on every directory change, giving the file explorer live cwd tracking without touching ~/.zshrc.

App Sandbox disabled by omission

security

No .entitlements file exists anywhere in the repo. The app relies on Swift Package Manager executables being unsandboxed by default, which SwiftTerm's forkpty()-based process spawning requires.

Ad-hoc signing only, no notarization

signing

build-dist.sh:76 runs codesign --force --deep --sign - "${APP_BUNDLE}", the deprecated --deep flag with a null identity. The README instructs users to right-click, Open to bypass Gatekeeper on first launch.

No test target shipped

testing

Package.swift declares one .executableTarget and no .testTarget. The project's own CLAUDE.md states the omission is deliberate for a roughly 1,900-line codebase, with no repeated multi-step workflow or deterministic guardrail to justify one.

Timeline

Five commits, one author, under two hours.

All five commits land on 2026-04-14 between 16:05 and 17:50 local time, a single session spanning roughly 105 minutes, from one author.

PhaseCommitDescription
10d84a0fThe entire application in one commit: 15 files, 2,639 insertions, every Swift source file, a 520-line pre-implementation research document, and a 107-line README.
2d9e72faREADME rewritten to its contributor-facing form; MIT LICENSE added.
3935b303App icon asset added and wired into build-dist.sh.
4c26e35fPure README reformat.
5c8c50e8The only post-launch functional fix: the focus-tracking change above, plus a screenshot added to the README.
Boundary

What Multi-Term is not.

Boundary

Multi-Term is not cross-platform. It targets macOS 13 and above only, built on AppKit NSView hit-testing and WKWebView, with no Linux or Windows target anywhere in Package.swift.

It is not an SSH or remote-terminal client. No SSH code, library, or dependency exists in the source (grep -rin "ssh|citadel" Sources/ returns nothing), despite a checked-in research document that discusses an SSH approach that was never implemented.

It has no persistence, sync, or multi-device story. All state is in-memory and resets on relaunch.

It has no automated test suite, no CI configuration, and no release or tagging history (git tag returns nothing) to point to as a maturity signal.

Evidence: compiled from the multi-term repository on 2026-08-03. Every number on this page traces to a file path or command output in the source tree. Last updated: 2026-08-03.

Single Source

Every number on the dossier and whitepaper pages traces to a file path or command output in the source tree.

LinkedIn Facebook (c) 2026 Single Source Studios (Pty) Ltd