A provider is how aimux learns about a type of AI coding agent. Each provider implements the provider.Provider interface, which gives aimux the ability to:
- Discover running agent processes and recent sessions automatically
- Display agents in the dashboard with status, model, cost, and metadata
- Resume sessions by zooming into an embedded PTY or jumping to an external terminal
- Show traces by resolving session/conversation log files
- Launch new agent sessions from the
:newlauncher with model and mode selection - Track costs via token-based pricing in the cost dashboard
Once you implement the interface and register your provider, all of these features work without touching any view or TUI code.
Copy internal/provider/gemini.go, rename it, and adjust. This gets you a compilable stub that shows up in the launcher.
1. Create the file:
// internal/provider/aider.go
package provider
import (
"os/exec"
"github.com/zanetworker/aimux/internal/agent"
)
type Aider struct{}
func (a *Aider) Name() string { return "aider" }
func (a *Aider) Discover() ([]agent.Agent, error) { return nil, nil }
func (a *Aider) ResumeCommand(ag agent.Agent) *exec.Cmd { return nil }
func (a *Aider) CanEmbed() bool { return false }
func (a *Aider) FindSessionFile(ag agent.Agent) string { return "" }
func (a *Aider) RecentDirs(max int) []RecentDir { return nil }
func (a *Aider) SpawnCommand(dir, model, mode string) *exec.Cmd {
bin := findBinary("aider")
cmd := exec.Command(bin)
cmd.Dir = dir
return cmd
}
func (a *Aider) SpawnArgs() SpawnArgs {
return SpawnArgs{
Models: []string{"default"},
Modes: []string{"default"},
}
}2. Register in internal/tui/app.go -- add to the allProviders slice in NewApp():
allProviders := []provider.Provider{
&provider.Claude{},
&provider.Codex{},
&provider.Gemini{},
&provider.Aider{}, // <-- add this
}3. Add to internal/config/config.go -- add to Default() providers map:
Providers: map[string]ProviderConfig{
"claude": {Enabled: true},
"codex": {Enabled: true},
"gemini": {Enabled: true},
"aider": {Enabled: true}, // <-- add this
},4. Add tests (see Testing Checklist below).
5. Build and run:
go build -o aimux ./cmd/aimux
go test ./... -timeout 30sYour provider now appears in the :new launcher. Flesh out methods as you learn how the agent stores its data.
Defined in internal/provider/provider.go:
type Provider interface {
Name() string
Discover() ([]agent.Agent, error)
ResumeCommand(a agent.Agent) *exec.Cmd
CanEmbed() bool
FindSessionFile(a agent.Agent) string
RecentDirs(max int) []RecentDir
SpawnCommand(dir, model, mode string) *exec.Cmd
SpawnArgs() SpawnArgs
}Returns a unique lowercase identifier for this provider. Used as a key everywhere: config lookup, provider matching, display labels.
When called: On every discovery cycle (every 2 seconds), during provider registration, and when resolving which provider owns an agent.
Rules:
- Must be unique across all providers
- Must be lowercase, no spaces
- Must match the key you add to
config.goDefault()
func (a *Aider) Name() string { return "aider" }Scans for running agent processes and recent session files. Returns a slice of agent.Agent structs. This is the core discovery mechanism -- it runs every 2 seconds via the orchestrator.
When called: Every tick (2 seconds) by discovery.Orchestrator.Discover(), which calls all providers in parallel.
What to do:
- Scan
ps auxoutput for processes matching your agent's binary name - Resolve each process's working directory (via
lsofor/proc) - Find and parse session/trace files for metadata (model, tokens, status)
- Optionally discover recent idle sessions (no running process but recent trace files)
- Set
ProviderNameon every returned agent
Return nil, nil for a stub. That is valid and means "no agents found."
Reference: See claude.go Discover() for the full pattern (process scan + session enrichment + idle session discovery + deduplication). See codex.go Discover() for a mid-complexity version.
func (a *Aider) Discover() ([]agent.Agent, error) {
// Minimal: scan ps for "aider" processes
out, err := exec.Command("ps", "aux").Output()
if err != nil {
return nil, fmt.Errorf("ps aux: %w", err)
}
var agents []agent.Agent
for _, line := range strings.Split(string(out), "\n") {
if !strings.Contains(line, "aider") {
continue
}
// Parse PID, build agent.Agent, etc.
// ...
}
return agents, nil
}Key agent.Agent fields to populate:
| Field | Required | Description |
|---|---|---|
PID |
Yes (for running) | Process ID. Use 0 for idle/session-only entries. |
ProviderName |
Yes | Must equal your Name() return value. |
Status |
Yes | agent.StatusActive, StatusIdle, or StatusUnknown. |
Source |
Yes | agent.SourceCLI, SourceVSCode, or SourceSDK. |
WorkingDir |
Recommended | Absolute path to the project directory. |
SessionID |
If available | Unique session identifier for resumption. |
SessionFile |
If available | Path to the conversation trace file. |
Model |
If available | Model name string (e.g. "gpt-4o", "claude-opus-4-6"). |
Name |
Recommended | Display name -- usually filepath.Base(WorkingDir). |
TokensIn / TokensOut |
If available | For cost calculation. |
EstCostUSD |
If available | Pre-calculated via cost.Calculate(). |
GroupCount |
Default 1 | Set to 1 for single agents. |
GroupPIDs |
Default []int{} |
List of PIDs if grouping multiple processes. |
Builds the exec.Cmd that resumes an existing session. The TUI runs this command inside a PTY when the user presses Enter on an agent.
When called: When the user selects an agent and presses Enter (zoom in) or J (jump out).
Return nil if the agent cannot be resumed (no session ID, no working directory). The TUI handles nil gracefully by showing a status hint.
func (a *Aider) ResumeCommand(ag agent.Agent) *exec.Cmd {
bin := findBinary("aider")
if ag.WorkingDir == "" {
return nil
}
cmd := exec.Command(bin)
cmd.Dir = ag.WorkingDir
return cmd
}Reference: Claude uses --resume <sessionID> or --continue. Codex uses resume --no-alt-screen <sessionID>.
Reports whether this agent's TUI can run inside aimux's embedded PTY. If true, pressing Enter opens a split view (trace on left, live session on right). If false, pressing Enter opens the trace-only view, and the user presses J to jump out to a tmux or iTerm split pane.
When called: When the user presses Enter on an agent, to decide the layout mode.
Guidelines:
- Return
trueif the agent uses a standard terminal (no alternate screen buffer fighting, no raw mode conflicts). Claude works. - Return
falseif the agent has its own TUI framework that conflicts with Bubble Tea's alternate screen. Codex and Gemini return false. - When in doubt, return
false. It is the safer option.
func (a *Aider) CanEmbed() bool { return true }Resolves the path to the session's trace/conversation file. Each provider knows its own storage layout (Claude uses ~/.claude/projects/, Codex uses ~/.codex/sessions/).
When called: When opening the trace viewer (l key or Enter on a non-embeddable provider), and during preview pane updates.
Return "" if no session file exists. The TUI shows "No trace data yet" to the user.
func (a *Aider) FindSessionFile(ag agent.Agent) string {
if ag.WorkingDir == "" {
return ""
}
home, err := os.UserHomeDir()
if err != nil {
return ""
}
// Aider stores chat history in .aider.chat.history.md in the project dir
candidate := filepath.Join(ag.WorkingDir, ".aider.chat.history.md")
if _, err := os.Stat(candidate); err == nil {
return candidate
}
return ""
}Returns recently-used project directories from this provider's session history, sorted most-recent first, capped at max. These populate the directory picker in the :new launcher.
When called: When the user opens the launcher (:new command). All providers' recent dirs are merged and deduplicated.
Return nil if you have no session history to scan. The launcher still works -- it just won't show directories from your provider.
type RecentDir struct {
Path string
LastUsed time.Time
}func (a *Aider) RecentDirs(max int) []RecentDir {
home, err := os.UserHomeDir()
if err != nil {
return nil
}
// Scan ~/.aider/ for recent session dirs
// ...
return nil // stub
}Builds the exec.Cmd to launch a brand-new agent session. Called from the launcher overlay after the user picks a directory, model, and mode.
When called: When the user completes the launcher flow (:new -> pick dir -> pick provider -> pick model -> pick mode).
Parameters:
dir-- absolute path to the project directory. Always setcmd.Dir = dir.model-- the model string fromSpawnArgs().Models. May be""or"default"-- skip adding a--modelflag in that case.mode-- the mode string fromSpawnArgs().Modes. May be""or"default".
func (a *Aider) SpawnCommand(dir, model, mode string) *exec.Cmd {
bin := findBinary("aider")
var args []string
if model != "" && model != "default" {
args = append(args, "--model", model)
}
switch mode {
case "architect":
args = append(args, "--architect")
case "ask":
args = append(args, "--ask")
}
cmd := exec.Command(bin, args...)
cmd.Dir = dir
return cmd
}Returns the available models and modes for the launcher UI. These populate the model and mode selection dropdowns.
When called: When building the launcher overlay. The first entry in each slice is the default selection.
type SpawnArgs struct {
Models []string // e.g., ["default", "gpt-4o", "claude-sonnet"]
Modes []string // e.g., ["default", "architect", "ask"]
}func (a *Aider) SpawnArgs() SpawnArgs {
return SpawnArgs{
Models: []string{"default", "gpt-4o", "claude-3.5-sonnet", "deepseek-r1"},
Modes: []string{"default", "architect", "ask"},
}
}Two files need a one-line change each:
Add your provider to the allProviders slice:
allProviders := []provider.Provider{
&provider.Claude{},
&provider.Codex{},
&provider.Gemini{},
&provider.Aider{}, // <-- add here
}The orchestrator and all views pick it up automatically from this single registration point.
Add your provider to the default config so it is enabled out of the box:
Providers: map[string]ProviderConfig{
"claude": {Enabled: true},
"codex": {Enabled: true},
"gemini": {Enabled: true},
"aider": {Enabled: true}, // <-- add here
},Users can disable it in ~/.aimux/config.yaml:
providers:
aider:
enabled: falseNote: providers not listed in the config map are enabled by default (IsProviderEnabled returns true for unknown names), so the config entry is not strictly required for functionality. But adding it makes the provider visible in the default config and documents its existence.
To show cost estimates for your provider's agents, add model pricing to internal/cost/tracker.go.
var pricing = map[string]ModelPricing{
// ... existing entries ...
// Aider-supported models (example)
"gpt-4o": {
Input: 2.50,
Output: 10.00,
},
"deepseek-r1": {
Input: 0.55,
Output: 2.19,
},
}var aliases = map[string]string{
// ... existing entries ...
"4o": "gpt-4o",
}How it works: During Discover(), when you set agent.TokensIn and agent.TokensOut, call cost.Calculate() to populate agent.EstCostUSD:
import "github.com/zanetworker/aimux/internal/cost"
a.EstCostUSD = cost.Calculate(
a.Model, // e.g. "gpt-4o"
info.tokensIn,
info.tokensOut,
info.cacheRead, // 0 if not applicable
info.cacheWrite, // 0 if not applicable
)The pricing map uses per-million-token rates in USD. Calculate() normalizes model names (strips version suffixes, resolves aliases) before lookup. Unknown models return $0.00.
Every provider needs these tests. See internal/provider/stubs_test.go and internal/provider/claude_test.go for the patterns.
1. Compile-time interface check (in your test file or stubs_test.go):
var _ Provider = (*Aider)(nil)This fails at compile time if Aider is missing any interface methods. No runtime cost.
2. Name returns the correct string:
func TestAiderName(t *testing.T) {
a := &Aider{}
if got := a.Name(); got != "aider" {
t.Errorf("Aider.Name() = %q, want %q", got, "aider")
}
}3. CanEmbed returns the expected value:
func TestAiderCanEmbed(t *testing.T) {
a := &Aider{}
if !a.CanEmbed() {
t.Error("Aider.CanEmbed() = false, want true")
}
}4. SpawnArgs returns valid, non-empty slices:
func TestAiderSpawnArgs(t *testing.T) {
a := &Aider{}
sa := a.SpawnArgs()
if len(sa.Models) == 0 {
t.Fatal("SpawnArgs.Models is empty")
}
if sa.Models[0] != "default" {
t.Errorf("SpawnArgs.Models[0] = %q, want %q", sa.Models[0], "default")
}
if len(sa.Modes) == 0 {
t.Fatal("SpawnArgs.Modes is empty")
}
if sa.Modes[0] != "default" {
t.Errorf("SpawnArgs.Modes[0] = %q, want %q", sa.Modes[0], "default")
}
}5. SpawnCommand with various model/mode combinations:
func TestAiderSpawnCommand_Default(t *testing.T) {
a := &Aider{}
cmd := a.SpawnCommand("/tmp/myproject", "", "")
if cmd == nil {
t.Fatal("SpawnCommand returned nil")
}
if cmd.Dir != "/tmp/myproject" {
t.Errorf("cmd.Dir = %q, want %q", cmd.Dir, "/tmp/myproject")
}
}
func TestAiderSpawnCommand_WithModel(t *testing.T) {
a := &Aider{}
cmd := a.SpawnCommand("/tmp/myproject", "gpt-4o", "")
if cmd == nil {
t.Fatal("SpawnCommand returned nil")
}
assertArgsContain(t, cmd.Args, "--model", "gpt-4o")
}
func TestAiderSpawnCommand_DefaultModelSkipped(t *testing.T) {
a := &Aider{}
cmd := a.SpawnCommand("/tmp/myproject", "default", "")
if cmd == nil {
t.Fatal("SpawnCommand returned nil")
}
assertArgAbsent(t, cmd.Args, "--model")
}
func TestAiderSpawnCommand_ArchitectMode(t *testing.T) {
a := &Aider{}
cmd := a.SpawnCommand("/tmp/myproject", "", "architect")
if cmd == nil {
t.Fatal("SpawnCommand returned nil")
}
assertArgPresent(t, cmd.Args, "--architect")
}6. FindSessionFile with empty agent does not panic:
func TestAiderFindSessionFile_Empty(t *testing.T) {
a := &Aider{}
if got := a.FindSessionFile(agent.Agent{}); got != "" {
t.Errorf("FindSessionFile(empty) = %q, want empty", got)
}
}7. RecentDirs does not panic:
func TestAiderRecentDirs(t *testing.T) {
a := &Aider{}
_ = a.RecentDirs(5) // must not panic
}8. Discover does not error on a clean system:
func TestAiderDiscover(t *testing.T) {
a := &Aider{}
_, err := a.Discover()
if err != nil {
t.Errorf("Aider.Discover() error = %v, want nil", err)
}
}The test helpers assertArgPresent, assertArgAbsent, and assertArgsContain are defined in internal/provider/claude_test.go and available to all test files in the package.
go test ./internal/provider/ -v -timeout 30s
go test ./... -timeout 30sThis walkthrough adds a hypothetical aider provider with process discovery, session file resolution, and cost tracking.
package provider
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
"github.com/zanetworker/aimux/internal/agent"
"github.com/zanetworker/aimux/internal/cost"
)
// Aider is a Provider implementation for the aider AI pair programming tool.
type Aider struct{}
func (a *Aider) Name() string { return "aider" }
// Discover finds running aider processes.
func (a *Aider) Discover() ([]agent.Agent, error) {
out, err := exec.Command("ps", "aux").Output()
if err != nil {
return nil, fmt.Errorf("ps aux: %w", err)
}
var agents []agent.Agent
for _, line := range strings.Split(string(out), "\n") {
line = strings.TrimSpace(line)
if line == "" {
continue
}
if !a.isAiderProcess(line) {
continue
}
fields := strings.Fields(line)
if len(fields) < 11 {
continue
}
pid, err := strconv.Atoi(fields[1])
if err != nil {
continue
}
ag := agent.Agent{
PID: pid,
ProviderName: "aider",
Status: agent.StatusUnknown,
Source: agent.SourceCLI,
LastActivity: time.Now(),
GroupCount: 1,
GroupPIDs: []int{pid},
}
// Resolve working directory
if cwd, err := exec.Command("lsof", "-a", "-p",
strconv.Itoa(pid), "-d", "cwd", "-Fn").Output(); err == nil {
for _, l := range strings.Split(string(cwd), "\n") {
if strings.HasPrefix(l, "n/") {
ag.WorkingDir = l[1:]
break
}
}
}
if ag.WorkingDir != "" {
ag.Name = filepath.Base(ag.WorkingDir)
} else {
ag.Name = fmt.Sprintf("aider-%d", pid)
}
// Extract model from command line
cmd := strings.Join(fields[10:], " ")
if m := extractFlag(cmd, "--model"); m != "" {
ag.Model = m
}
agents = append(agents, ag)
}
return agents, nil
}
func (a *Aider) isAiderProcess(line string) bool {
fields := strings.Fields(line)
if len(fields) < 11 {
return false
}
binary := fields[10]
cmd := strings.Join(fields[10:], " ")
if strings.Contains(cmd, "grep") {
return false
}
if strings.Contains(cmd, "aimux") {
return false
}
return strings.HasSuffix(binary, "/aider") || binary == "aider" ||
(strings.Contains(binary, "python") && strings.Contains(cmd, "aider"))
}
func (a *Aider) ResumeCommand(ag agent.Agent) *exec.Cmd {
bin := findBinary("aider")
if ag.WorkingDir == "" {
return nil
}
// aider auto-resumes from .aider.chat.history.md in the project dir
cmd := exec.Command(bin)
cmd.Dir = ag.WorkingDir
return cmd
}
// CanEmbed returns true because aider uses a standard terminal interface.
func (a *Aider) CanEmbed() bool { return true }
// FindSessionFile looks for aider's chat history in the project directory.
func (a *Aider) FindSessionFile(ag agent.Agent) string {
if ag.WorkingDir == "" {
return ""
}
candidate := filepath.Join(ag.WorkingDir, ".aider.chat.history.md")
if _, err := os.Stat(candidate); err == nil {
return candidate
}
return ""
}
// RecentDirs scans for directories containing .aider.chat.history.md files.
func (a *Aider) RecentDirs(max int) []RecentDir {
home, err := os.UserHomeDir()
if err != nil {
return nil
}
// Check common project locations
searchDirs := []string{
filepath.Join(home, "projects"),
filepath.Join(home, "src"),
filepath.Join(home, "go", "src"),
}
seen := make(map[string]bool)
var dirs []RecentDir
for _, searchDir := range searchDirs {
_ = filepath.Walk(searchDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return filepath.SkipDir
}
if info.IsDir() {
return nil
}
if info.Name() != ".aider.chat.history.md" {
return nil
}
dir := filepath.Dir(path)
if seen[dir] {
return nil
}
seen[dir] = true
dirs = append(dirs, RecentDir{
Path: dir,
LastUsed: info.ModTime(),
})
return nil
})
}
sort.Slice(dirs, func(i, j int) bool {
return dirs[i].LastUsed.After(dirs[j].LastUsed)
})
if max > 0 && len(dirs) > max {
dirs = dirs[:max]
}
return dirs
}
// SpawnCommand builds the exec.Cmd to launch a new aider session.
func (a *Aider) SpawnCommand(dir, model, mode string) *exec.Cmd {
bin := findBinary("aider")
var args []string
if model != "" && model != "default" {
args = append(args, "--model", model)
}
switch mode {
case "architect":
args = append(args, "--architect")
case "ask":
args = append(args, "--ask")
}
cmd := exec.Command(bin, args...)
cmd.Dir = dir
return cmd
}
// SpawnArgs returns available models and modes for the launcher.
func (a *Aider) SpawnArgs() SpawnArgs {
return SpawnArgs{
Models: []string{"default", "gpt-4o", "claude-3.5-sonnet", "deepseek-r1"},
Modes: []string{"default", "architect", "ask"},
}
}
// extractFlag extracts the value following a CLI flag from a command string.
func extractFlag(args, flag string) string {
fields := strings.Fields(args)
for i, f := range fields {
if f == flag && i+1 < len(fields) {
return fields[i+1]
}
}
return ""
}In NewApp(), add to the allProviders slice:
allProviders := []provider.Provider{
&provider.Claude{},
&provider.Codex{},
&provider.Gemini{},
&provider.Aider{},
}In Default(), add to the providers map:
"aider": {Enabled: true},Add any models aider uses that are not already in the pricing map:
// In the pricing map:
"gpt-4o": {
Input: 2.50,
Output: 10.00,
},
"deepseek-r1": {
Input: 0.55,
Output: 2.19,
},
// In the aliases map:
"4o": "gpt-4o",package provider
import (
"path/filepath"
"testing"
"github.com/zanetworker/aimux/internal/agent"
)
// Compile-time interface check.
var _ Provider = (*Aider)(nil)
func TestAiderName(t *testing.T) {
a := &Aider{}
if got := a.Name(); got != "aider" {
t.Errorf("Aider.Name() = %q, want %q", got, "aider")
}
}
func TestAiderCanEmbed(t *testing.T) {
a := &Aider{}
if !a.CanEmbed() {
t.Error("Aider.CanEmbed() = false, want true")
}
}
func TestAiderDiscover(t *testing.T) {
a := &Aider{}
_, err := a.Discover()
if err != nil {
t.Errorf("Aider.Discover() error = %v, want nil", err)
}
}
func TestAiderResumeCommand_NoWorkingDir(t *testing.T) {
a := &Aider{}
cmd := a.ResumeCommand(agent.Agent{})
if cmd != nil {
t.Errorf("ResumeCommand(empty) = %v, want nil", cmd)
}
}
func TestAiderResumeCommand_WithWorkingDir(t *testing.T) {
a := &Aider{}
cmd := a.ResumeCommand(agent.Agent{WorkingDir: "/tmp/project"})
if cmd == nil {
t.Skip("aider binary not found")
}
if cmd.Dir != "/tmp/project" {
t.Errorf("cmd.Dir = %q, want %q", cmd.Dir, "/tmp/project")
}
}
func TestAiderFindSessionFile_Empty(t *testing.T) {
a := &Aider{}
if got := a.FindSessionFile(agent.Agent{}); got != "" {
t.Errorf("FindSessionFile(empty) = %q, want empty", got)
}
}
func TestAiderRecentDirs(t *testing.T) {
a := &Aider{}
_ = a.RecentDirs(5) // must not panic
}
func TestAiderSpawnCommand_Default(t *testing.T) {
a := &Aider{}
cmd := a.SpawnCommand("/tmp/myproject", "", "")
if cmd == nil {
t.Fatal("SpawnCommand returned nil")
}
if cmd.Dir != "/tmp/myproject" {
t.Errorf("cmd.Dir = %q, want %q", cmd.Dir, "/tmp/myproject")
}
if base := filepath.Base(cmd.Args[0]); base != "aider" {
t.Errorf("binary = %q, want %q", base, "aider")
}
}
func TestAiderSpawnCommand_DefaultModelSkipped(t *testing.T) {
a := &Aider{}
cmd := a.SpawnCommand("/tmp/myproject", "default", "")
if cmd == nil {
t.Fatal("SpawnCommand returned nil")
}
assertArgAbsent(t, cmd.Args, "--model")
}
func TestAiderSpawnCommand_WithModel(t *testing.T) {
a := &Aider{}
cmd := a.SpawnCommand("/tmp/myproject", "gpt-4o", "")
if cmd == nil {
t.Fatal("SpawnCommand returned nil")
}
assertArgsContain(t, cmd.Args, "--model", "gpt-4o")
}
func TestAiderSpawnCommand_ArchitectMode(t *testing.T) {
a := &Aider{}
cmd := a.SpawnCommand("/tmp/myproject", "", "architect")
if cmd == nil {
t.Fatal("SpawnCommand returned nil")
}
assertArgPresent(t, cmd.Args, "--architect")
}
func TestAiderSpawnCommand_AskMode(t *testing.T) {
a := &Aider{}
cmd := a.SpawnCommand("/tmp/myproject", "", "ask")
if cmd == nil {
t.Fatal("SpawnCommand returned nil")
}
assertArgPresent(t, cmd.Args, "--ask")
}
func TestAiderSpawnCommand_ModelAndMode(t *testing.T) {
a := &Aider{}
cmd := a.SpawnCommand("/tmp/myproject", "gpt-4o", "architect")
if cmd == nil {
t.Fatal("SpawnCommand returned nil")
}
assertArgsContain(t, cmd.Args, "--model", "gpt-4o")
assertArgPresent(t, cmd.Args, "--architect")
}
func TestAiderSpawnArgs(t *testing.T) {
a := &Aider{}
sa := a.SpawnArgs()
if len(sa.Models) == 0 {
t.Fatal("SpawnArgs.Models is empty")
}
if sa.Models[0] != "default" {
t.Errorf("SpawnArgs.Models[0] = %q, want %q", sa.Models[0], "default")
}
if len(sa.Modes) == 0 {
t.Fatal("SpawnArgs.Modes is empty")
}
if sa.Modes[0] != "default" {
t.Errorf("SpawnArgs.Modes[0] = %q, want %q", sa.Modes[0], "default")
}
}go test ./internal/provider/ -v -timeout 30s
go test ./... -timeout 30s
go build -o aimux ./cmd/aimux
./aimux # verify aider appears in :new launcherThat is everything. The agent list, preview pane, trace viewer, cost dashboard, and launcher all pick up your provider automatically from the interface contract and registration.