desktop

package
v0.2.0-alpha.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 29, 2026 License: Apache-2.0 Imports: 30 Imported by: 0

Documentation

Overview

Package desktop implements the BuildMax desktop app (Wails) and is used by cmd/buildmax-desktop.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Run

func Run(assets fs.FS) error

Run starts the Wails desktop application. assets is the embedded frontend filesystem (e.g. from //go:embed all:frontend in the main package). It creates an App, wires lifecycle hooks, and calls wails.Run.

Types

type App

type App struct {
	// contains filtered or unexported fields
}

App holds desktop application state and implements Wails lifecycle hooks. Each project gets its own AgentApp and ApprovalHandler instance, created lazily on first use.

func NewApp

func NewApp() *App

NewApp returns a new App instance.

func (*App) CancelRun

func (a *App) CancelRun(projectID string) error

CancelRun cancels the in-flight run for the given project, if any. Cancellation is cooperative: the agent loop returns the partial assistant reply produced so far and emits desktop/stream-done as a normal completion. Calling CancelRun when no run is in flight is a no-op.

Stopping also discards anything queued behind the run. Those prompts were written for work the user has just called off; delivering them afterwards would restart it in their name.

func (*App) ClearProjectSessions

func (a *App) ClearProjectSessions(projectID string) ([]string, error)

func (*App) CreateProject

func (a *App) CreateProject(name, folderPath string) (*Project, error)

CreateProject creates a new project with the given name and folder.

func (*App) DeleteProject

func (a *App) DeleteProject(id string) error

DeleteProject removes a project and closes its AgentApp if one was running.

func (*App) DeleteSession

func (a *App) DeleteSession(sessionID string) error

func (*App) DeliverNextJobEvent

func (a *App) DeliverNextJobEvent(projectID, sessionID string) (bool, error)

DeliverNextJobEvent runs one parked background event for the given session as its own streaming turn, mirroring SendMessageStream's serialization. It returns false when nothing was started: no parked event, or a run already in flight — the parked event then simply waits for the next trigger. The frontend calls it when the session is on screen and idle.

func (*App) DoLogin

func (a *App) DoLogin(serverURL, email, otp string) (*AuthStatus, error)

DoLogin authenticates with a single-use login code and saves credentials on success. It is the recovery path: claiming a new account, or getting back in after a forgotten password.

func (*App) DoLoginWithPassword

func (a *App) DoLoginWithPassword(serverURL, email, password string) (*AuthStatus, error)

DoLoginWithPassword authenticates with a password and saves credentials on success. This is the everyday path.

func (*App) ForkSession

func (a *App) ForkSession(projectID, sessionID, itemID string) (HistoryMoveResult, error)

ForkSession copies this session's history through itemID into a new session and returns it, leaving the original exactly as it was.

func (*App) GetAuthStatus

func (a *App) GetAuthStatus() (*AuthStatus, error)

GetAuthStatus reports the stored login, if there is one, and whether it still works. The check runs here rather than on the first prompt so the answer arrives while the user is still deciding what to do.

func (*App) GetDefaultServerURL

func (a *App) GetDefaultServerURL() string

GetDefaultServerURL is what the sign-in form starts with. It reads the same settings.yaml key `buildmax login` does, so the two entry points offer the same address instead of the Desktop insisting on a local server someone has already configured away from.

func (*App) GetGitBranch

func (a *App) GetGitBranch(projectID string) (string, error)

GetGitBranch returns the current git branch for the given project's folder, or an empty string if the folder is not a git repository.

func (*App) GetHistoryPoints

func (a *App) GetHistoryPoints(sessionID string) (HistoryPointsResult, error)

GetHistoryPoints lists what this session can be rewound to and forked from, each newest first.

It reads without the writer lock, so the picker still opens while a run is in flight. Only the move itself is refused then.

func (*App) GetJobOutput

func (a *App) GetJobOutput(projectID, jobID, stream string, cursor uint64) (JobOutputPayload, error)

GetJobOutput reads one job's captured output incrementally. stream is "stdout" (default) or "stderr"; pass the previous next_cursor to continue.

func (*App) GetPlugins

func (a *App) GetPlugins(projectID string) (PluginsResult, error)

GetPlugins returns what this project's runtime loaded.

It reads the runtime's snapshot rather than scanning again, so what is shown is what the running agent has — an install completed a moment ago appears once the runtime is rebuilt, which is the same rule every other surface has.

func (*App) GetRunStatus

func (a *App) GetRunStatus(projectID, sessionID string) (RunStatusPayload, error)

func (*App) GetSession

func (a *App) GetSession(sessionID string) (SessionDetail, error)

GetSession loads one session by ID and returns it for display.

func (*App) GetSlashAgents

func (a *App) GetSlashAgents(projectID string) (SlashAgentsResult, error)

GetSlashAgents returns all agent types (builtin + user-defined) for the project.

func (*App) GetSlashMCP

func (a *App) GetSlashMCP(projectID string) (SlashMCPResult, error)

GetSlashMCP returns the MCP server status for the given project.

func (*App) GetSlashModels

func (a *App) GetSlashModels(projectID string) (SlashModelsResult, error)

GetSlashModels returns configured models and the active model for a project.

func (*App) GetSlashSkills

func (a *App) GetSlashSkills(projectID string) (SlashSkillsResult, error)

GetSlashSkills returns all skills discovered for the given project.

func (*App) GetWorkspaceDiff

func (a *App) GetWorkspaceDiff(projectID string) (git.WorkspaceDiff, error)

GetWorkspaceDiff returns the current git-backed changed-file view for a project.

func (*App) InstallPlugin

func (a *App) InstallPlugin(name, version string, update bool) (InstallPluginPlan, error)

InstallPlugin downloads and installs a release.

It resolves again rather than taking the plan the app was shown: a release published between the two is a different install, and acting on a stale plan would put bytes on disk that nobody was shown.

func (*App) ListJobs

func (a *App) ListJobs(projectID string) ([]JobPayload, error)

ListJobs returns the project's background jobs in creation order.

func (*App) ListProjects

func (a *App) ListProjects() ([]Project, error)

ListProjects returns all saved projects.

func (*App) ListSessions

func (a *App) ListSessions() ([]session.ItemSummary, error)

ListSessions returns all sessions across all projects.

func (*App) Logout

func (a *App) Logout() error

Logout clears stored credentials and revokes the session on the server.

A server that cannot be reached is not a failed logout: the credentials are gone from this machine either way, and returning an error would leave the UI showing someone as signed in when they are not. Signing out is what returns the app to local mode: the credentials are the mode, so removing them is the whole switch.

func (*App) OpenFolderDialog

func (a *App) OpenFolderDialog() (string, error)

OpenFolderDialog opens a native directory picker and returns the selected path.

func (*App) PendingJobDeliveries

func (a *App) PendingJobDeliveries(projectID, sessionID string) int

PendingJobDeliveries reports how many parked deliveries wait for a session.

func (*App) PlanPluginInstall

func (a *App) PlanPluginInstall(name, version string, update bool) (InstallPluginPlan, error)

PlanPluginInstall resolves which release an install would take.

func (*App) QueuedMessages

func (a *App) QueuedMessages(projectID string) []string

QueuedMessages returns the prompts waiting behind the project's in-flight run, oldest first. The frontend reads it when it switches back to a project whose queue events it was not mounted for.

func (*App) RenameProject

func (a *App) RenameProject(id, newName string) error

RenameProject updates the name of a project.

func (*App) RenameSession

func (a *App) RenameSession(sessionID, title string) error

func (*App) RequestOTP

func (a *App) RequestOTP(serverURL, email, intent string) error

RequestOTP calls the server's OTP endpoint.

func (*App) RespondApproval

func (a *App) RespondApproval(projectID string, decision string)

RespondApproval is called by the frontend when the user answers a tool approval prompt. projectID must match the project that triggered the desktop/approval-request event. decision is "once", "session", or "deny"; anything else denies, so a frontend that falls out of step fails closed.

func (*App) RewindSession

func (a *App) RewindSession(projectID, sessionID, itemID string) (HistoryMoveResult, error)

RewindSession removes itemID and everything after it, and returns the prompt for the composer to take back.

No session lifecycle hook fires. Nothing is starting or ending here — the user is editing history — and the transient open this needs is an artifact of Desktop not holding sessions, not an event a hook should be told about. The TUI, which rewinds a session it already has open, fires none either.

func (*App) SendMessageStream

func (a *App) SendMessageStream(projectID, sessionID, prompt string) (int, error)

SendMessageStream runs a prompt in the given project and session with streaming. It returns immediately and emits desktop/stream-delta, then desktop/stream-done or desktop/stream-error. sessionID may be empty to start a new session.

At most one run per project may be in flight. A prompt submitted while one is active is queued and runs as its own turn once the current one finishes; the return value is that prompt's 1-based position in the queue, and 0 when the prompt started a run of its own. The position comes back as a return value rather than an event because it answers the caller's own call.

func (*App) SetPluginDisabled

func (a *App) SetPluginDisabled(name string, disabled bool) error

SetPluginDisabled stops a plugin loading, or lets it load again.

func (*App) SetProjectModel

func (a *App) SetProjectModel(projectID, modelName string) error

SetProjectModel switches the active model for a project's agent.

func (*App) SetSessionPinned

func (a *App) SetSessionPinned(sessionID string, pinned bool) error

func (*App) Shutdown

func (a *App) Shutdown(_ context.Context)

Shutdown closes all per-project AgentApp instances and cancels any in-flight runs.

func (*App) Startup

func (a *App) Startup(ctx context.Context)

Startup is called by Wails when the app is starting. AgentApp instances are created lazily on first use per project.

func (*App) StopJob

func (a *App) StopJob(projectID, jobID string) error

StopJob requests termination of one background job.

func (*App) UninstallPlugin

func (a *App) UninstallPlugin(name string, force bool) error

UninstallPlugin removes an installed plugin.

type ApprovalRequestPayload

type ApprovalRequestPayload struct {
	ProjectID string         `json:"project_id"`
	ToolName  string         `json:"tool_name"`
	Args      map[string]any `json:"args"`
}

ApprovalRequestPayload is emitted to the frontend when a tool call needs approval.

type AuthStatus

type AuthStatus struct {
	LoggedIn  bool   `json:"logged_in"`
	ServerURL string `json:"server_url,omitempty"`
	UserID    string `json:"user_id,omitempty"`
	Email     string `json:"email,omitempty"`
	Name      string `json:"name,omitempty"`
	// Expired means the stored login no longer works. The app stays in managed
	// mode and refuses to run rather than quietly using local models, which
	// would send prompts somewhere the user did not choose. Signing in again or
	// signing out are the two ways out — see docs/design/client-modes.md
	// section 8.
	Expired bool `json:"expired,omitempty"`
	// ExpiredDetail is what to tell the user, set only when Expired.
	ExpiredDetail string `json:"expired_detail,omitempty"`
}

AuthStatus is who is signed in, and therefore which mode the app is in.

There is no mode field. A login is the mode: with one the app is managed and its models come from that server, without one it is local and they come from settings.yaml. Anything remembered alongside the credentials would be a second source of truth for one fact. See docs/design/client-modes.md section 3.

type DesktopApprovalHandler

type DesktopApprovalHandler struct {
	// contains filtered or unexported fields
}

DesktopApprovalHandler implements agent.ApprovalHandler for the Wails desktop app. It emits a Wails event to the frontend and blocks until RespondApproval is called.

func (*DesktopApprovalHandler) RequestApproval

func (h *DesktopApprovalHandler) RequestApproval(ctx context.Context, name string, args map[string]any) agent.ApprovalDecision

RequestApproval emits an approval-request event to the frontend and blocks until the user responds via RespondApproval. Denies if the app context is not ready.

type HistoryMoveResult

type HistoryMoveResult struct {
	// SessionID is the session to display next: the same one after a rewind,
	// the new child after a fork.
	SessionID string              `json:"session_id"`
	Messages  int                 `json:"messages"`
	Tools     []HistoryToolEffect `json:"tools,omitempty"`
	// Prompt is the rewound message, for the composer to take back. Empty
	// after a fork, which removes nothing.
	Prompt string `json:"prompt,omitempty"`
	// Attachments counts images the rewound message carried and the composer
	// cannot restore.
	Attachments int `json:"attachments,omitempty"`
}

HistoryMoveResult is what happened, for the report shown afterwards.

type HistoryPoint

type HistoryPoint struct {
	ItemID   string              `json:"item_id"`
	Role     string              `json:"role"`
	Content  string              `json:"content"`
	Messages int                 `json:"messages"`
	Tools    []HistoryToolEffect `json:"tools,omitempty"`
}

HistoryPoint is one message a picker offers.

Messages and Tools describe the span choosing it affects, and the two operations mean opposite things by it. A rewind removes that span from this conversation and leaves the tools' effects on disk; a fork removes nothing — the original keeps all of it — but the copy begins without knowing that work happened. The surface chooses the reading; the binding reports the span once.

type HistoryPointsResult

type HistoryPointsResult struct {
	SessionID string         `json:"session_id"`
	Rewind    []HistoryPoint `json:"rewind"`
	Fork      []HistoryPoint `json:"fork"`
}

HistoryPointsResult is the picker's whole input, consequences included.

The two lists are computed here rather than filtered in the frontend, because which messages an operation may be pointed at is a rule about the journal, not a presentation choice: rewind offers the prompts it can hand back, fork offers every message a turn ended on, and the head is a fork point but never a rewind one. Every point carries what choosing it would affect, rather than the surface asking again each time the selection moves: the computation is in-memory over a branch that is already loaded, and a round trip per keystroke would be the expensive half of an otherwise free question.

type HistoryToolEffect

type HistoryToolEffect struct {
	Name string `json:"name"`
	// Interrupted marks a call that entered its tool and never reported back.
	// It is the worse of the two: it may have changed as much as one that
	// finished, and nothing recorded what.
	Interrupted bool `json:"interrupted"`
}

HistoryToolEffect is one tool call on the far side of a chosen point.

type InstallPluginPlan

type InstallPluginPlan struct {
	Name             string   `json:"name"`
	Version          string   `json:"version"`
	Digest           string   `json:"digest"`
	SizeBytes        int64    `json:"size_bytes"`
	PublishedBy      string   `json:"published_by,omitempty"`
	AlreadyInstalled bool     `json:"already_installed"`
	Skills           []string `json:"skills,omitempty"`
	Subagents        []string `json:"subagents,omitempty"`
	MCP              []string `json:"mcp,omitempty"`
	Hooks            []string `json:"hooks,omitempty"`
	// MissingEnv names the variables this release reads that are not set here,
	// which is the usual reason a plugin looks installed and does nothing.
	MissingEnv []string `json:"missing_env,omitempty"`
	// DirtySource says the release was packed from a working tree that was not
	// the commit it names.
	DirtySource bool `json:"dirty_source,omitempty"`
}

InstallPluginPlan is what an install would do, resolved before anything is downloaded so the app can show it while the decision is still open.

type JobDeliveryPayload

type JobDeliveryPayload struct {
	ProjectID string `json:"project_id"`
	SessionID string `json:"session_id"`
	JobID     string `json:"job_id"`
	Source    string `json:"source"`
	Title     string `json:"title"`
}

JobDeliveryPayload describes the delivery turn that just started.

type JobDeliveryPendingPayload

type JobDeliveryPendingPayload struct {
	ProjectID string `json:"project_id"`
	SessionID string `json:"session_id"`
	Pending   int    `json:"pending"`
}

JobDeliveryPendingPayload reports parked deliveries for one session.

type JobOutputPayload

type JobOutputPayload struct {
	Data       string `json:"data"`
	NextCursor uint64 `json:"next_cursor"`
	Dropped    uint64 `json:"dropped,omitempty"`
	Running    bool   `json:"running"`
	State      string `json:"state"`
}

JobOutputPayload is one incremental output read.

type JobPayload

type JobPayload struct {
	ProjectID  string `json:"project_id"`
	ID         string `json:"id"`
	Kind       string `json:"kind"`
	State      string `json:"state"`
	StopReason string `json:"stop_reason,omitempty"`
	ExitCode   int    `json:"exit_code,omitempty"`
	Error      string `json:"error,omitempty"`
	Command    string `json:"command"`
	SessionID  string `json:"session_id,omitempty"`
	Running    bool   `json:"running"`
	CreatedAt  string `json:"created_at"`
	EndedAt    string `json:"ended_at,omitempty"`
}

JobPayload is one background job as the frontend sees it.

type MessageBlockedPayload

type MessageBlockedPayload struct {
	Prompt string   `json:"prompt"`
	Reason string   `json:"reason"`
	Queued []string `json:"queued"`
}

MessageBlockedPayload is emitted when a hook refuses a queued prompt (event desktop/message-blocked). It reports one message, not the end of the run — the run carries on with what it already had.

type MessageDequeuedPayload

type MessageDequeuedPayload struct {
	Prompt string   `json:"prompt"`
	Queued []string `json:"queued"`
}

MessageDequeuedPayload is emitted just before a queued prompt starts its own turn (event desktop/message-dequeued), so the transcript can show it as sent.

type PluginEntry

type PluginEntry struct {
	Name        string `json:"name"`
	DisplayName string `json:"display_name"`
	Description string `json:"description,omitempty"`
	Path        string `json:"path"`
	Source      string `json:"source"`
	// State is active, disabled, refused, or error — a decision and a defect
	// are different things and a reader should not have to tell them apart
	// from a message.
	State string `json:"state"`

	Version string `json:"version,omitempty"`
	Digest  string `json:"digest,omitempty"`

	RemoteURL string `json:"remote_url,omitempty"`
	Branch    string `json:"branch,omitempty"`
	Commit    string `json:"commit,omitempty"`
	Dirty     bool   `json:"dirty,omitempty"`

	Skills    []string       `json:"skills,omitempty"`
	Subagents []string       `json:"subagents,omitempty"`
	MCP       []string       `json:"mcp,omitempty"`
	Hooks     []string       `json:"hooks,omitempty"`
	Env       []PluginEnvVar `json:"env,omitempty"`
	// Shadowed names what a higher layer overrode, so a plugin does not read
	// as fully active while part of it never loads.
	Shadowed []string `json:"shadowed,omitempty"`
	Problems []string `json:"problems,omitempty"`
}

PluginEntry is one installed plugin as the app shows it.

type PluginEnvVar

type PluginEnvVar struct {
	Name        string `json:"name"`
	Description string `json:"description,omitempty"`
	Required    bool   `json:"required"`
	Set         bool   `json:"set"`
}

PluginEnvVar is one declared environment variable and whether it is set here.

type PluginsResult

type PluginsResult struct {
	Dir     string        `json:"dir"`
	Plugins []PluginEntry `json:"plugins"`
	// AllowedSources is the operator's restriction, empty when there is none.
	AllowedSources []string `json:"allowed_sources,omitempty"`
	// Notes are directory-level findings: a stray directory, unreadable state.
	Notes []string `json:"notes,omitempty"`
}

PluginsResult is the project's plugin inventory.

type Project

type Project struct {
	ID         string `json:"id"`
	Name       string `json:"name"`
	FolderPath string `json:"folder_path"`
	CreatedAt  string `json:"created_at"`
	LastUsedAt string `json:"last_used_at"`
}

Project is a named local folder that groups desktop sessions.

type ReplyPayload

type ReplyPayload struct {
	Reply                 string `json:"reply"`
	SessionID             string `json:"session_id"`
	ContextTokens         int    `json:"context_tokens"`
	ContextWindow         int    `json:"context_window"`
	PromptTokens          int    `json:"prompt_tokens"`
	CompletionTokens      int    `json:"completion_tokens"`
	TotalPromptTokens     int    `json:"total_prompt_tokens"`
	TotalCompletionTokens int    `json:"total_completion_tokens"`
	// Cache counts are the cached parts of the prompt counts above, not extra
	// tokens to add to them.
	CacheReadTokens       int `json:"cache_read_tokens"`
	CacheWriteTokens      int `json:"cache_write_tokens"`
	TotalCacheReadTokens  int `json:"total_cache_read_tokens"`
	TotalCacheWriteTokens int `json:"total_cache_write_tokens"`
}

ReplyPayload is returned when a desktop prompt completes successfully.

type RunStatusPayload

type RunStatusPayload struct {
	ContextTokens         int `json:"context_tokens"`
	ContextWindow         int `json:"context_window"`
	PromptTokens          int `json:"prompt_tokens"`
	CompletionTokens      int `json:"completion_tokens"`
	TotalPromptTokens     int `json:"total_prompt_tokens"`
	TotalCompletionTokens int `json:"total_completion_tokens"`
	CacheReadTokens       int `json:"cache_read_tokens"`
	CacheWriteTokens      int `json:"cache_write_tokens"`
	TotalCacheReadTokens  int `json:"total_cache_read_tokens"`
	TotalCacheWriteTokens int `json:"total_cache_write_tokens"`
}

type SessionDetail

type SessionDetail struct {
	ID        string        `json:"id"`
	Title     string        `json:"title,omitempty"`
	CreatedAt string        `json:"created_at"`
	Messages  []llm.Message `json:"messages,omitempty"`
}

SessionDetail is the session payload returned to the frontend for display.

type SlashAgentEntry

type SlashAgentEntry struct {
	Name        string `json:"name"`
	Description string `json:"description"`
	IsBuiltin   bool   `json:"is_builtin"`
}

type SlashAgentsResult

type SlashAgentsResult struct {
	Agents []SlashAgentEntry `json:"agents"`
}

type SlashMCPResult

type SlashMCPResult struct {
	LoadError string           `json:"load_error,omitempty"`
	Servers   []SlashMCPServer `json:"servers"`
}

type SlashMCPServer

type SlashMCPServer struct {
	ID        string `json:"id"`
	Type      string `json:"type"`
	OK        bool   `json:"ok"`
	ToolCount int    `json:"tool_count"`
	Error     string `json:"error,omitempty"`
}

type SlashModelEntry

type SlashModelEntry struct {
	Name          string `json:"name"`
	ProviderModel string `json:"provider_model,omitempty"`
	IsCurrent     bool   `json:"is_current"`
	// Destination is the provider endpoint a local model is called at. Empty in
	// managed mode, where the deployment on the result says where every prompt
	// goes and no model has an endpoint of its own.
	Destination string `json:"destination,omitempty"`
}

type SlashModelsResult

type SlashModelsResult struct {
	Current string `json:"current"`
	// Managed and ServerURL say where every prompt in this session goes. It is
	// the app's mode, not a property of one model, so the picker states it once
	// above the list. See docs/design/client-modes.md.
	Managed   bool              `json:"managed"`
	ServerURL string            `json:"server_url,omitempty"`
	Models    []SlashModelEntry `json:"models"`
}

type SlashSkillEntry

type SlashSkillEntry struct {
	Name        string `json:"name"`
	Description string `json:"description"`
	Path        string `json:"path"`
}

type SlashSkillsResult

type SlashSkillsResult struct {
	Skills []SlashSkillEntry `json:"skills"`
}

type StreamErrorPayload

type StreamErrorPayload struct {
	Message string `json:"message"`
}

StreamErrorPayload is emitted when streaming fails (event desktop/stream-error).

type ToolEndPayload

type ToolEndPayload struct {
	ToolCallID string `json:"tool_call_id"`
	ToolName   string `json:"tool_name"`
	DurationMs int64  `json:"duration_ms,omitempty"`
	IsError    bool   `json:"is_error,omitempty"`
	Denied     bool   `json:"denied,omitempty"`
	Reason     string `json:"reason,omitempty"`
}

ToolEndPayload is emitted when a tool call finishes or is denied.

type ToolStartPayload

type ToolStartPayload struct {
	ToolCallID string `json:"tool_call_id"`
	ToolName   string `json:"tool_name"`
	Args       string `json:"args"`
}

ToolStartPayload is emitted when a tool call begins executing.

type TurnDigestPayload

type TurnDigestPayload struct {
	// Recap is a short account of what the turn did, or "" when it earned none.
	Recap string `json:"recap"`
	// Suggestion is the answer the user is likely about to give, or "" when the
	// turn did not end by asking them anything.
	Suggestion string `json:"suggestion"`
}

TurnDigestPayload is what the finished turn is worth telling the user, and nothing the model will read again (event desktop/turn-digest). It is emitted once per turn rather than with stream-done, because a run that drains a queue runs several turns and each one's recap describes only itself.

It deliberately does not travel in the message list: stream-done reloads the thread from the session, and neither of these is in the session.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL