A desktop AI coding assistant application built with Wails (Go + Svelte/TypeScript). CCUI provides a unified interface for interacting with AI agents through ACP (Agent Client Protocol) or directly via the Anthropic API.
CCUI is a multi-session AI coding assistant with the following key features:
- Multi-session chat: Create and switch between multiple concurrent AI sessions
- Real-time tool execution: Visualize file reads, edits, bash commands, and other tools as they execute
- Permission system: Configurable permission layer for tool execution (auto-allow reads, ask for writes)
- Review workflow: Review and comment on AI-generated file changes before applying
- Integrated terminal: Built-in terminal with PTY support
- Session modes: Support for different agent modes (architect, code, debug, etc.)
- Split-pane UI: Configurable panel layout with chat, review, and terminal views
- Language: Go 1.23+
- Framework: Wails v2 - Desktop app framework
- Key Dependencies:
github.com/creack/pty- PTY support for terminalgithub.com/mark3labs/mcp-go- MCP (Model Context Protocol) server/clientgithub.com/wailsapp/wails/v2- Wails runtime
- Language: TypeScript
- Framework: Svelte 3.x
- Build Tool: Vite 3.x
- Styling: Tailwind CSS v4 with custom "paper" theme
- Testing: Vitest with jsdom
- Key Dependencies:
xterm- Terminal emulatormarked- Markdown parsingdiff- Diff generation
.
├── main.go # Application entry point
├── app.go # Main app logic, Wails bindings, session management
├── mcpserver.go # MCP server for user question tool
├── pty.go # PTY/terminal session management
├── go.mod # Go module definition
├── wails.json # Wails configuration
│
├── backend/ # Backend packages
│ ├── interface.go # AgentBackend and Session interfaces
│ ├── types.go # Shared types (ToolState, FileChange, etc.)
│ ├── acp/ # ACP (Agent Client Protocol) implementation
│ │ ├── client.go # ACP client for claude-code-acp
│ │ ├── transport.go # JSON-RPC over stdio transport
│ │ ├── adapters.go # Tool event adapters (claude-code, opencode)
│ │ └── types.go # ACP protocol types
│ ├── anthropic/ # Direct Anthropic API backend
│ │ ├── backend.go # AnthropicBackend implementation
│ │ ├── session.go # Session management for direct API
│ │ ├── stream.go # SSE streaming for API responses
│ │ └── tools.go # Tool definitions for Anthropic
│ └── tools/ # Tool executor for direct API backend
│ ├── executor.go # Tool registry and execution interface
│ ├── read.go # Read tool implementation
│ ├── write.go # Write tool implementation
│ ├── edit.go # Edit tool implementation
│ ├── bash.go # Bash tool implementation
│ ├── grep.go # Grep tool implementation
│ └── glob.go # Glob tool implementation
│
├── permission/ # Permission layer
│ ├── layer.go # Permission layer with request/response flow
│ └── rules.go # Permission rules (Allow/Ask/Deny)
│
├── frontend/ # Frontend application
│ ├── package.json # Node.js dependencies (use bun, not npm)
│ ├── vite.config.ts # Vite configuration
│ ├── tsconfig.json # TypeScript configuration
│ └── src/
│ ├── main.ts # Entry point
│ ├── App.svelte # Main application component
│ ├── style.css # Global styles with Tailwind
│ ├── lib/
│ │ ├── shared.ts # Shared types and utilities
│ │ ├── ChatContent.svelte # Chat message display
│ │ ├── ToolCard.svelte # Tool execution visualization
│ │ ├── ReviewPanel.svelte # Code review interface
│ │ ├── Terminal.svelte # Terminal component
│ │ ├── SplitPane.svelte # Resizable split pane
│ │ ├── CommandPalette.svelte # Panel selector
│ │ ├── SessionSelector.svelte # Session tabs
│ │ └── ModeSelector.svelte # Agent mode selector
│ └── assets/ # Static assets
│
├── specs/ # Architecture specifications
│ └── unified-backend/ # Backend architecture design docs
│
├── acp/ # ACP protocol documentation
├── build/ # Build assets and configurations
└── agent-os/ # Agent OS related files
# Run in live development mode with hot reload
wails dev
# Frontend development server only (accessible at http://localhost:34115)
cd frontend && bun run dev# Build production binary
wails build
# Build for specific platform
wails build -platform darwin/universal
wails build -platform windows/amd64# Run all Go tests
go test ./...
# Run with verbose output
go test -v ./...
# Run specific package tests
go test ./backend/acp/...
go test ./backend/tools/...
go test ./permission/...cd frontend
# Run tests once
bun run test
# Run tests in watch mode
bun run test:watch
# Run with coverage
bunx vitest run --coverage- Follow standard Go conventions (
go fmt) - Use meaningful variable names, avoid single-letter except for common cases (i, err, ctx)
- Error handling: wrap errors with context using
fmt.Errorf("...: %w", err) - Interface naming: prefer
-ersuffix (e.g.,EventEmitter,ToolExecutor) - Use
sync.RWMutexfor concurrent access to shared state
- Use TypeScript strict mode
- Prefer
constandletovervar - Use Svelte's reactive statements (
$:) judiciously - Event naming: use camelCase with descriptive names
Important: Use bun for all frontend operations, not npm/npx.
# Correct
bun install
bun run dev
bunx vitest
# Incorrect
cd frontend && npm install # Don't use npmThe backend follows a layered architecture designed to support multiple AI backends:
Frontend (Svelte)
│
▼ Wails Events
Permission Layer
│
Tool Executor (fs, bash, grep, etc.) ────┐
│ │
AgentBackend Interface │
│ │
├── ACP Backend (claude-code-acp) ────┤ (external execution)
│
└── Direct API Backend (Anthropic) ───┘ (local execution)
-
Permission Layer (
permission/)- Deterministic rules:
Read/Glob/Grep= Allow,Write/Edit/Bash= Ask - Blocks on user permission requests
- Emits events to frontend for user interaction
- Deterministic rules:
-
Tool Executor (
backend/tools/)- Local execution for direct API backend
- Registry pattern for tool registration
- Results include content, diffs, and file changes
-
ACP Client (
backend/acp/)- Communicates with
claude-code-acpvia stdio JSON-RPC - Handles tool events with adapters for different ACP implementations
- File change tracking for review workflow
- Communicates with
-
Session Management (
app.go)- Multi-session support with session switching
- Event bridging between backend events and Wails events
- Session-scoped event channels (
session:{id}:{event})
- User sends message →
EventsEmit('send_message', text) app.goroutes to active session's ACP client- ACP client streams responses via
eventChan bridgeEventsforwards to Wails events with session prefix- Frontend subscribes to
session:{id}:chat_chunk,session:{id}:tool_state, etc.
Set environment variable to select backend:
# ACP backend (default)
CCUI_BACKEND=acp ./ccui
# Anthropic direct API backend (planned)
CCUI_BACKEND=anthropic ./ccui- Go: Table-driven tests for tools, permission layer, and ACP adapters
- Frontend: Component tests using Vitest and Testing Library
- Go:
*_test.goalongside source files - Frontend:
*.test.tsinsrc/__tests__/or alongside components
# Go tests (run from project root)
go test ./... -v
# Frontend tests (run from frontend/)
cd frontend && bun run test| Variable | Description | Default |
|---|---|---|
ANTHROPIC_API_KEY |
API key for Anthropic | Required for direct API |
CCUI_BACKEND |
Backend type (acp or anthropic) |
acp |
SHELL |
Shell for PTY sessions | /bin/bash |
The application requires claude-code-acp to be installed and available in PATH for the ACP backend:
# Install claude-code-acp
npm install -g @anthropics/claude-code-acp- Permission System: All write operations and bash commands require explicit user permission
- Auto-allow list: Only read operations are auto-allowed (
Read,Glob,Grep,WebSearch) - API Key Handling: API keys are read from environment, never stored in code
- MCP Server: Local-only SSE server binding to
127.0.0.1:0(random port) - Bash Timeout: Commands have configurable timeout (default 2min, max 10min)
- Implement
Toolinterface inbackend/tools/ - Add to registry in executor setup
- Add permission rule in
permission/rules.goif needed
- Implement
AgentBackendandSessioninterfaces frombackend/interface.go - Add backend type constant in
app.go - Initialize in
NewApp()based onCCUI_BACKENDenv var
- Backend emits via
runtime.EventsEmit(ctx, "event_name", data) - Frontend subscribes via
EventsOn('event_name', callback) - For session-scoped events: use
session:{sessionId}:{event}prefix
Use bun/bunx not npm/npx
The Wails runtime provides a unified events system, where events can be emitted or received by either Go or JavaScript. Optionally, data may be passed with the events. Listeners will receive the data in the local data types. EventsOn
This method sets up a listener for the given event name. When an event of type eventName is emitted, the callback is triggered. Any additional data sent with the emitted event will be passed to the callback. It returns a function to cancel the listener.
Go: EventsOn(ctx context.Context, eventName string, callback func(optionalData ...interface{})) func() JS: EventsOn(eventName string, callback function(optionalData?: any)): () => void EventsOff
This method unregisters the listener for the given event name, optionally multiple listeners can be unregistered via additionalEventNames.
Go: EventsOff(ctx context.Context, eventName string, additionalEventNames ...string) JS: EventsOff(eventName string, ...additionalEventNames) EventsOnce
This method sets up a listener for the given event name, but will only trigger once. It returns a function to cancel the listener.
Go: EventsOnce(ctx context.Context, eventName string, callback func(optionalData ...interface{})) func() JS: EventsOnce(eventName string, callback function(optionalData?: any)): () => void EventsOnMultiple
This method sets up a listener for the given event name, but will only trigger a maximum of counter times. It returns a function to cancel the listener.
Go: EventsOnMultiple(ctx context.Context, eventName string, callback func(optionalData ...interface{}), counter int) func() JS: EventsOnMultiple(eventName string, callback function(optionalData?: any), counter int): () => void EventsEmit
This method emits the given event. Optional data may be passed with the event. This will trigger any event listeners.
Go: EventsEmit(ctx context.Context, eventName string, optionalData ...interface{}) JS: EventsEmit(eventName: string, ...optionalData: any)
This part of the runtime provides access to native dialogs, such as File Selectors and Message boxes. JavaScript
Dialog is currently unsupported in the JS runtime. OpenDirectoryDialog
Opens a dialog that prompts the user to select a directory. Can be customised using OpenDialogOptions.
Go: OpenDirectoryDialog(ctx context.Context, dialogOptions OpenDialogOptions) (string, error)
Returns: Selected directory (blank if the user cancelled) or an error OpenFileDialog
Opens a dialog that prompts the user to select a file. Can be customised using OpenDialogOptions.
Go: OpenFileDialog(ctx context.Context, dialogOptions OpenDialogOptions) (string, error)
Returns: Selected file (blank if the user cancelled) or an error OpenMultipleFilesDialog
Opens a dialog that prompts the user to select multiple files. Can be customised using OpenDialogOptions.
Go: OpenMultipleFilesDialog(ctx context.Context, dialogOptions OpenDialogOptions) ([]string, error)
Returns: Selected files (nil if the user cancelled) or an error SaveFileDialog
Opens a dialog that prompts the user to select a filename for the purposes of saving. Can be customised using SaveDialogOptions.
Go: SaveFileDialog(ctx context.Context, dialogOptions SaveDialogOptions) (string, error)
Returns: The selected file (blank if the user cancelled) or an error MessageDialog
Displays a message using a message dialog. Can be customised using MessageDialogOptions.
Go: MessageDialog(ctx context.Context, dialogOptions MessageDialogOptions) (string, error)
Returns: The text of the selected button or an error Options OpenDialogOptions
type OpenDialogOptions struct {
DefaultDirectory string
DefaultFilename string
Title string
Filters []FileFilter
ShowHiddenFiles bool
CanCreateDirectories bool
ResolvesAliases bool
TreatPackagesAsDirectories bool
}These methods provide information about the currently connected screens. ScreenGetAll
Returns a list of currently connected screens.
Go: ScreenGetAll(ctx context.Context) []screen JS: ScreenGetAll() Screen
Go struct:
type Screen struct {
IsCurrent bool
IsPrimary bool
Width int
Height int
}Typescript interface:
interface Screen {
isCurrent: boolean;
isPrimary: boolean;
width : number
height : number
}These methods are related to the application menu. JavaScript
Menu is currently unsupported in the JS runtime. MenuSetApplicationMenu
Sets the application menu to the given menu.
Go: MenuSetApplicationMenu(ctx context.Context, menu *menu.Menu) MenuUpdateApplicationMenu
Updates the application menu, picking up any changes to the menu passed to MenuSetApplicationMenu.
Go: MenuUpdateApplicationMenu(ctx context.Context)