Source-verified agent skills for Go: Cobra command-line interfaces, Viper configuration, and Wails desktop apps, each backed by tested examples.
99
100%
Does it follow best practices?
Impact
99%
1.02xAverage score across 18 eval scenarios
Passed
No findings from the security scan
Wails v3 runs a Go backend and a web frontend in native windows. You create
the app with application.New, register Go structs as services, open windows
through app.Window, and reach dialogs and events through managers on the
app (app.Dialog, app.Event). The exported methods of each service become
promise-returning functions in generated TypeScript or JavaScript. v3 is a
prerelease, its API changes between betas, and it shares almost no API with
v2, so most bugs come from code written for v2 or for a different beta.
go.mod. If it requires github.com/wailsapp/wails/v2, stop and use
the wails2 skill; without it, read the pinned v2 module source. v2 and
v3 APIs do not mix, so do not migrate between majors, or move to another
v3 beta, unless asked. If the project pins a v3 version other than the one
in Sources, read that version's source before trusting a
detail here: go doc github.com/wailsapp/wails/v3/pkg/application, or the
module cache under $(go env GOMODCACHE)/github.com/wailsapp/wails/v3@<version>.application.New(application.Options{...}), the services, the
windows, the //go:embed asset directory, and how the project builds:
Taskfile.yml with the wails3 CLI, or plain go build from a script.
Keep the existing workflow.| Concern | Wails v3 | Wails v2 (never in v3 code) |
|---|---|---|
| Entry point | application.New(application.Options{...}), then app.Run() | wails.Run(&options.App{...}) |
| Exposing Go | Services: []application.Service{application.NewService(&T{})} | Bind: []any{&T{}} |
| Lifecycle | ServiceStartup(ctx, application.ServiceOptions) error and ServiceShutdown() error on the service; Options.OnShutdown | OnStartup, OnDomReady, OnShutdown, OnBeforeClose in options.App |
| Runtime calls | app.Event.Emit, app.Dialog.SaveFile(), app.Window.NewWithOptions | runtime.EventsEmit(ctx, ...), runtime.SaveFileDialog(ctx, ...) |
| Frontend | frontend/bindings/<import path>/ and @wailsio/runtime | wailsjs/go/... and window.go |
| Tooling | wails3 CLI, Taskfile.yml, build/config.yml | wails CLI, wails.json |
| Linux | gtk4 and webkitgtk-6.0; -tags gtk3 for GTK3 | gtk+-3.0 and webkit2gtk-4.0 or -4.1 |
Imports from github.com/wailsapp/wails/v2/..., a runtime. call that
takes a context, or a wailsjs path in a v3 project is a mixing mistake.
Keep each service thin, its exported methods the whole frontend API, and everything native behind an interface the tests can replace:
// examples/wails3/notes/service.go (trimmed: storage and the other methods omitted)
type Native interface {
SaveFile(defaultName string) (string, error) // "" when cancelled
Emit(event string, data any)
}
type Service struct {
mu sync.Mutex // bound calls can run concurrently
notes []Note
native Native
}
func NewService(native Native) *Service { return &Service{native: native} }
func (s *Service) SaveNote(in NoteInput) (Note, error) {
in, err := validate(in)
if err != nil {
return Note{}, err // rejects the frontend promise
}
return s.store(Note(in)) // locks s.mu and emits EventNotesChanged
}The app wires it up in main:
// examples/wails3/main.go (trimmed: init, window size, Mac options, Emit, and the file filter omitted)
// assets is the //go:embed all:frontend/dist filesystem (see Assets below).
func main() {
app := application.New(application.Options{
Name: "Notes",
Assets: application.AssetOptions{Handler: application.AssetFileServerFS(assets)},
})
app.RegisterService(application.NewService(notes.NewService(desktop{app})))
app.Window.NewWithOptions(application.WebviewWindowOptions{Title: "Notes", URL: "/"})
if err := app.Run(); err != nil {
log.Fatal(err)
}
}
// desktop implements notes.Native; the service's tests use a fake instead.
type desktop struct{ app *application.App }
func (d desktop) SaveFile(name string) (string, error) {
return d.app.Dialog.SaveFile().SetFilename(name).PromptForSingleSelection()
}application.NewService takes a pointer to a named, non-generic struct.
Register one instance per type, in Options.Services or with
app.RegisterService before Run. A service registered after Run starts
is dropped and the error goes to Options.ErrorHandler.ServiceName, ServiceStartup, ServiceShutdown, and ServeHTTP.
v2's exemption for methods passed as lifecycle hooks does not exist, so an
exported Startup or Shutdown method is callable from JavaScript. Keep
helpers unexported and wiring in plain functions.T, error, or (T, error); several non-error results arrive as
an array. A non-nil error rejects the promise with a RuntimeError whose
message is err.Error(). When the error value marshals to JSON, that JSON
becomes the rejection's cause; Options.MarshalError or
ServiceOptions.MarshalError changes it. Arguments that do not decode
reject with a TypeError. A panic rejects that call and the app keeps
running.json
tag; an untagged field keeps its Go name and json:"-" drops it. A nil
slice is null on the wire, so return an empty slice.context.Context is filled in by Wails and left
out of the generated signature. Wails cancels it when the frontend cancels
the promise, and it carries the calling window under
application.WindowKey.ServiceStartup runs in registration order before windows open; its
context is cancelled before shutdown, and a returned error makes Run
return it. ServiceShutdown runs in reverse order.application.New returns the first App on every later call. Build it
once in main.init, by calling
application.RegisterEvent with the payload type T as its type argument
and the event name (see examples/wails3/main.go). The generator then writes TypeScript types for them, and app.Event.Emit
checks the data type exactly: a *T for a T event is reported to
ErrorHandler and cancelled. Events the frontend emits arrive in Go
decoded into T.app.Event.On returns a function that removes the listener. Use method
results for request and response, and events for progress or pushed state.app.Dialog: SaveFile(), OpenFile(), and message
dialogs (Info, Question, Warning, Error). A cancelled file dialog
returns "" (multiple selection: nil) and a nil error; treat that as
"nothing happened".app.Window.NewWithOptions opens a window; call it again for more. Windows
created before Run open when it starts. Give each a Name and find it
later with app.Window.GetByName. Menus and the system tray hang off
app.Menu and app.SystemTray.//go:embed all:frontend/dist with
application.AssetFileServerFS(assets) serves the built frontend; the asset
server finds index.html below the embed root, so fs.Sub is not needed.
Build the frontend before go build. In v3 a missing index.html does not
stop startup: every request fails with a 500 that says "no index.html could
be found", which shows as a blank or error window. The app serves its
JavaScript runtime at /wails/runtime.js.
Before finishing, run go vet ./... and go test ./... for the changed
packages, and check:
wails3 version and build
tags, not edited, and the frontend type checks against them;index.html is in the embedded assets;When a check fails, match the symptom in references/building.md or the sections above, fix it, and rerun every check. Server mode and browser tests do not exercise WebKitGTK, WebKit, WebView2, native dialogs, or window lifecycle; where a desktop session is available, launch the built app, and state in the handoff what native behavior was not exercised.
Verified against Wails v3.0.0-beta.28, a prerelease. Each stated behavior has a test or a source citation in the examples module. When the project's version differs, trust its module source over this skill.