This document provides context for AI agents working on the Skwad codebase.
Skwad is a macOS SwiftUI application that manages multiple AI coding agents, each running in an embedded terminal. It supports two terminal engines (Ghostty and SwiftTerm), agent-to-agent communication via MCP, and git worktree integration.
- Language: Swift 5.9+
- UI Framework: SwiftUI (macOS 26+)
- Terminal Engines:
- Ghostty (libghostty) - GPU-accelerated, default
- SwiftTerm - fallback option
- MCP Server: Hummingbird HTTP framework
- Persistence: @AppStorage (UserDefaults) + Codable JSON
- Build: Xcode project + Swift Package Manager
Skwad/
├── Models/
│ ├── Agent.swift # Agent data model with runtime state
│ ├── AgentManager.swift # Central agent lifecycle management
│ └── AppSettings.swift # App settings with @AppStorage
├── Views/
│ ├── ContentView.swift # Main layout with sidebar + terminal
│ ├── Sidebar/
│ │ ├── SidebarView.swift # Agent list with drag-drop
│ │ └── AgentSheet.swift # New/edit agent dialog
│ ├── Terminal/
│ │ ├── AgentTerminalView.swift # Terminal wrapper
│ │ ├── GhosttyHostView.swift # Ghostty NSViewRepresentable
│ │ └── TerminalHostView.swift # SwiftTerm NSViewRepresentable
│ ├── Git/
│ │ ├── GitPanelView.swift # Sliding git status panel
│ │ ├── DiffView.swift # Syntax-highlighted diff display
│ │ └── CommitSheet.swift # Commit dialog
│ └── Settings/
│ └── SettingsView.swift # Settings window
├── Git/
│ ├── GitCLI.swift # Low-level git command runner (with timeout)
│ ├── GitRepository.swift # High-level git operations
│ ├── GitWorktreeManager.swift # Worktree discovery and creation
│ ├── GitFileWatcher.swift # FSEvents file monitoring
│ └── GitTypes.swift # FileStatus, DiffLine, etc.
├── MCP/
│ ├── AgentCoordinator.swift # Actor managing messages and agent data
│ ├── MCPServer.swift # Hummingbird HTTP server + hook event handler
│ ├── MCPSessionManager.swift # MCP session tracking
│ ├── MCPToolHandler.swift # Tool execution
│ └── MCPTypes.swift # Message, AgentInfo structs
├── Services/
│ ├── NotificationService.swift # macOS desktop notifications
│ ├── RepoDiscoveryService.swift # Background repo discovery
│ ├── TerminalAdapter.swift # Protocol + Ghostty/SwiftTerm adapters
│ ├── TerminalCommandBuilder.swift # Agent command construction
│ └── TerminalSessionController.swift # Terminal session lifecycle + status state machine
├── GhosttyTerminal/ # Ghostty integration (libghostty wrappers)
└── SkwadApp.swift # App entry point
- All terminals are kept alive in a ZStack with opacity toggle (not recreated on switch)
- This preserves terminal state/history when switching between agents
restartTokenon Agent model forces terminal recreation on restart while keeping same ID- Focus is managed via
window?.makeFirstResponder()in updateNSView
- Ghostty (default): Uses libghostty C API via Swift wrappers
GhosttyAppManager- singleton managing Ghostty app instanceGhosttyHostView- NSViewRepresentable wrapper- Reads user's
~/.config/ghostty/configfor styling
- SwiftTerm: Fallback option
TerminalHostView- NSViewRepresentable wrapperActivityDetectingTerminalView- subclass for activity detection
TerminalSessionControllerowns the status state machine per agentActivityTrackingbitfield controls which sources trigger status changes:.all(default): terminal output + user input drive running/idle.userInput: hook-managed agents — only user input is tracked locally, hooks handle running/idle.none: shell agents — no status tracking
- When hooks are active (
sessionIdset + agent supports hooks), terminal output is ignored as a status source - Status colors: orange=Working, green=Idle, red=Blocked, red=Error
- Blocked status: set via hook when agent needs user attention (e.g. permission prompt). Unblocked by Return (→ running) or Escape (→ idle) keypress only
- Input protection: user keypresses activate a 10s guard that blocks automatic text injection (
injectText), preventing message delivery while user is typing. Messages stay in MCP queue and are delivered on next idle or when protection expires onUserInputcallback passesUInt16keyCode (macOS keyCode from Ghostty, mapped from raw bytes for SwiftTerm)- When idle, checks for unread MCP messages
AgentCoordinator(actor) manages message queue and agent queriesAgentDataProviderprotocol bridges MainActor-isolated AgentManager safely- Server starts AFTER AgentManager is set to avoid race conditions
- Registration prompt injected ~3s after terminal starts (if MCP enabled)
- Communication tools:
register-agent,list-agents,send-message,check-messages,broadcast-message - Management tools:
list-repos,list-worktrees,create-agent
GitCLI- Low-level command runner with 30s timeoutGitRepository- High-level operations (status, diff, stage, commit)GitWorktreeManager- Repo discovery and worktree operationsGitFileWatcher- FSEvents monitoring with debounce for auto-refreshGitPanelView- Sliding panel UI with VSplitView layout
- User creates agent via AgentSheet (picks folder/worktree, name, avatar)
- AgentManager.addAgent() creates Agent, saves to settings
- Terminal view spawns shell, sends
cd <folder> && <agent-command> - If MCP enabled, registration prompt injected after ~3s
- Agent marked as registered when
register-agenttool is called - On restart, same ID is kept but
restartTokenchanges to force terminal recreation
AppSettings.sharedsingleton with @AppStorage properties- Simple values: @AppStorage directly
- Complex types (savedAgents, recentRepos): Codable + JSON Data
- Source folder auto-detected on first launch (~/src, ~/source, ~/sources)
AgentCoordinatoris an actor for thread-safe message handlingAgentDataProviderprotocol bridges MainActor ↔ actor boundaries safely- Never use
nonisolated(unsafe)- use proper async boundaries instead - Terminal callbacks dispatch to MainActor when updating UI state
- Terminal dictionary in AgentManager uses weak refs to avoid retain cycles
- Coordinator classes use
[weak self]in closures and timers
- GitCLI has 30s timeout to prevent hung processes
- Git operations return Result<T, GitError> for proper error propagation
- Add @AppStorage property to AppSettings.swift
- Add UI control in appropriate SettingsView section
- Use the setting where needed via AppSettings.shared
- Add tool name to
MCPToolNameenum in MCPTypes.swift - Add response struct in MCPTypes.swift if needed
- Add tool definition in
MCPToolHandler.listTools()in MCPTools.swift - Add switch case in
MCPToolHandler.callTool()in MCPTools.swift - Implement handler method in MCPTools.swift
- Add service method in AgentCoordinator.swift if needed
- If accessing AgentManager, extend
AgentDataProviderprotocol andAgentManagerWrapper
- Ghostty: GhosttyHostView.swift callbacks (onReady, onActivity, etc.)
- SwiftTerm: TerminalHostView.swift and ActivityDetectingTerminalView
- Both check
settings.mcpServerEnabledbefore MCP operations
- Add low-level command in GitCLI if needed
- Add high-level operation in GitRepository
- Update GitPanelView UI to expose the feature
NEVER duplicate production logic in test helpers. Tests must call production code directly. If production logic is private, extract it to a static method or a utility enum so tests can access it via @testable import.
- DO:
XCTAssertEqual(VoiceAudioUtils.easeOut(0.5), 0.75) - DON'T: Copy the easeOut formula into a
private funcin the test file and test that copy instead
Why: Mirror helpers give false confidence — tests pass even if production code breaks. This anti-pattern was cleaned up across the entire test suite; do not reintroduce it.
When a function is too deeply embedded in a view or @MainActor class to test directly:
- Extract the pure logic to a utility enum (e.g.,
AvatarUtils,VoiceAudioUtils,PathUtils) - Have the view/class call the utility
- Test the utility directly
Delete tests that only verify hardcoded constants, trivial math (max/min), string interpolation, or enum raw values — they add zero value.
make testThis runs all tests and always produces explicit output: prints ALL TESTS PASSED on success or TESTS FAILED with error details on failure. The exit code is 0 on success, 1 on failure.
- Build and run (Cmd+R)
- Create agent from repo picker, verify terminal launches
- Create agent with new worktree
- Switch agents, verify state preserved
- Open git panel, stage/unstage/commit
- Test agent communication (send message between agents)
- Restart agent, verify same ID kept
- Change settings, verify applied
- Quit and relaunch, verify restore works
- Drag and drop to reorder agents
To bump the marketing version, use the Makefile target:
make set-version VERSION=x.y.zThis updates MARKETING_VERSION in all build configurations in the Xcode project file.
- Terminal colors set by claude/shell may override app colors (Ghostty respects config)
- Single window only (no multi-window support)
- MCP messages are in-memory only (lost on app restart)
- Split pane view for multiple agents
- Agent templates/presets
- GitHub PR integration
- Voice input