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.
Every figure traces to a git command or a file path.
| Metric | Value | Evidence |
|---|---|---|
| Commit count | 5 | git rev-list --count HEAD |
| First commit | 2026-04-14 16:05:09+02:00 | git log --reverse --date=iso-strict |
| Last commit | 2026-04-14 17:50:32+02:00 | git log --date=iso-strict |
| Commit span | ~1h45m, single day | first/last commit timestamps |
| Authors | 1 | git log --format='%an <%ae>' | sort -u |
| Swift LOC | 1,870 across 10 files | find Sources -name '*.swift' | xargs wc -l |
| Bash LOC | 118 | wc -l build-dist.sh |
| Swift files indexed | 11 | git ls-files '*.swift'; jcodemunch get_repo_outline |
| AST symbol count | 282 (31 classes, 55 methods, 193 properties/constants, 3 functions) | jcodemunch get_repo_outline |
| Test files | 0 | find . -iname "*test*" (excl. .build, .git) |
| Published packages | 0 (app, not library) | Package.swift:11-15, one .executableTarget |
| Direct dependency | SwiftTerm 1.12.0 declared, 1.13.0 resolved | Package.swift:7; Package.resolved |
| Transitive dependency | swift-argument-parser 1.7.1 | Package.resolved |
| Git tags / releases | 0 | git tag |
| DB / SQLite files | 0 | find . -iname "*.db" -o -iname "*.sqlite*" |
| Deploy target / live URL | none, manual .pkg/.zip | README.md Installation; no CI/deploy config |
| Monaco language mappings | 35 | grep -c 'return "' MonacoEditorView.swift |
| File-icon mappings | 50 | grep -c 'return .init' FileIcons.swift |
| Preset pane/tab colors | 12 | TabStore.swift:42-47 |
| Entitlements file | none | find . -iname "*.entitlements" |
| SSH / remote-terminal code | none | grep -rin "ssh|citadel" Sources/ |
| Local build output (untracked) | .pkg 2,369,653 bytes; .zip 2,366,421 bytes | ls -la dist/, local evidence only |
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.
Nine tradeoffs visible in the code, the commits, and the docs.
Single AppKit canvas, not per-pane SwiftUI views
architectureSwiftUI'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
fixCommit 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
tradeoffMonacoEditorView.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 defectapplyContent (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
persistencegrep -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
shellsharedZdotdir (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
securityNo .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
signingbuild-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
testingPackage.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.
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.
| Phase | Commit | Description |
|---|---|---|
| 1 | 0d84a0f | The 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. |
| 2 | d9e72fa | README rewritten to its contributor-facing form; MIT LICENSE added. |
| 3 | 935b303 | App icon asset added and wired into build-dist.sh. |
| 4 | c26e35f | Pure README reformat. |
| 5 | c8c50e8 | The only post-launch functional fix: the focus-tracking change above, plus a screenshot added to the README. |
What Multi-Term is not.
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.