docs/superpowers/plans/2026-04-15-go-native-launcher-runtime.md
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Replace the Flutter launcher window with a single-process Go-native launcher runtime while keeping the Flutter settings window as a separate frontend.
Architecture: Keep wox.core as the product root, but split startup into an explicit core bootstrap plus a native launcher runtime that lives on the locked UI thread. Keep launcher state, layout, and preview orchestration in Go, but draw fixed launcher chrome through platform-native drawing backends on each OS, host IME and WebView with platform-native child hosts, and preserve existing launcher semantics by moving current WebSocket/HTTP launcher behavior behind in-process LauncherCoreAPI and LauncherEventBus adapters. Land the work Windows-first with a real wox.core query spike before broad cross-platform rollout.
Tech Stack: Go, cgo, Win32 + Direct2D/DirectWrite + WebView2, AppKit/CoreText + WKWebView, GTK + Cairo/Pango + WebKitGTK, existing wox.core services, Flutter settings frontend
Operator rule: Do not create commits while executing this plan unless the operator explicitly asks for one in that session.
Shared Go runtime
wox.core/app/bootstrap.gowox.core/app/core_services.gowox.core/launcher/runtime.gowox.core/launcher/contracts.gowox.core/launcher/events.gowox.core/launcher/store/state.gowox.core/launcher/store/store.gowox.core/launcher/store/reducer.gowox.core/launcher/layout/layout.gowox.core/launcher/layout/query_box.gowox.core/launcher/layout/result_list.gowox.core/launcher/scene/frame.gowox.core/launcher/scene/diff.gowox.core/launcher/theme/paint_theme.gowox.core/launcher/theme/mapper.gowox.core/launcher/input/router.gowox.core/launcher/input/query_editor.gowox.core/launcher/input/shortcuts.gowox.core/launcher/result/reconciler.gowox.core/launcher/result/selection.gowox.core/launcher/preview/models.gowox.core/launcher/preview/resolver.gowox.core/launcher/preview/host.gowox.core/launcher/preview/plain_text_renderer.gowox.core/launcher/preview/markdown_renderer.gowox.core/launcher/preview/image_renderer.gowox.core/launcher/preview/file_renderer.gowox.core/launcher/preview/webview_renderer.gowox.core/launcher/webview/session.gowox.core/launcher/webview/pool.gowox.core/launcher/webview/profile.gowox.core/launcher/debug/automation.goPlatform-native host layer
wox.core/launcher/platform/host.gowox.core/launcher/platform/windows/dispatcher_windows.gowox.core/launcher/platform/windows/host_windows.gowox.core/launcher/platform/windows/window_windows.hwox.core/launcher/platform/windows/window_windows.ccwox.core/launcher/platform/windows/text_input_host_windows.hwox.core/launcher/platform/windows/text_input_host_windows.ccwox.core/launcher/platform/windows/webview_host_windows.hwox.core/launcher/platform/windows/webview_host_windows.ccwox.core/launcher/platform/darwin/dispatcher_darwin.gowox.core/launcher/platform/darwin/host_darwin.gowox.core/launcher/platform/darwin/app_host_darwin.mmwox.core/launcher/platform/darwin/text_input_host_darwin.mmwox.core/launcher/platform/darwin/webview_host_darwin.mmwox.core/launcher/platform/linux/dispatcher_linux.gowox.core/launcher/platform/linux/host_linux.gowox.core/launcher/platform/linux/app_host_linux.cwox.core/launcher/platform/linux/text_input_host_linux.cwox.core/launcher/platform/linux/webview_host_linux.cRenderer bridge and assets
wox.core/launcher/render/contracts.gowox.core/launcher/render/text_metrics.gowox.core/launcher/platform/windows/render_host_windows.hwox.core/launcher/platform/windows/render_host_windows.ccwox.core/launcher/platform/darwin/render_host_darwin.mmwox.core/launcher/platform/linux/render_host_linux.cwox.core/resource/ui/native_launcher/markdown/index.htmlwox.core/resource/ui/native_launcher/markdown/main.csswox.core/resource/ui/native_launcher/markdown/main.jsBridges, settings, diagnostics, and smoke coverage
wox.core/ui/launcher_bridge.gowox.core/ui/settings_transport.gowox.core/test/native_launcher_test_helper.gowox.core/test/native_launcher_startup_smoke_test.gowox.core/test/native_launcher_query_preview_smoke_test.gowox.core/test/native_launcher_webview_smoke_test.godocs/superpowers/audits/2026-04-15-launcher-thread-audit.mdwox.ui.flutter/wox/lib/settings_main.dartExisting files that must be refactored instead of replaced wholesale
wox.core/main.gowox.core/Makefilewox.core/common/ui.gowox.core/common/theme.gowox.core/common/image.gowox.core/resource/resource.gowox.core/ui/manager.gowox.core/ui/ui_impl.gowox.core/ui/http.gowox.core/ui/router.gowox.core/util/mainthread/mainthread.gowox.core/util/mainthread/mainthread_darwin.gowox.ui.flutter/wox/lib/main.dartExecute this plan as minimum working vertical slices, not as a pure infrastructure-first rewrite. Every slice must end in a launcher build that can run and be smoke-tested on the target platform.
Target:
Primary tasks:
Exit signal:
Target:
Primary tasks:
Exit signal:
Esc, focus return, and query-box show/hide behavior workTarget:
wox.corePrimary tasks:
LauncherCoreAPI and LauncherEventBus work from Task 3Exit signal:
wox.core query path works end to endTarget:
text, markdown, image, and file previewPrimary tasks:
Exit signal:
Target:
webview preview works with native embedded browserEsc fallback workPrimary tasks:
Exit signal:
Target:
Primary tasks:
Exit signal:
Target:
Primary tasks:
Target:
WebKitGTKPrimary tasks:
Target:
Primary tasks:
Planning rule:
Files:
Create: wox.core/app/bootstrap.go
Create: wox.core/app/core_services.go
Create: wox.core/launcher/runtime.go
Create: wox.core/launcher/contracts.go
Create: wox.core/launcher/events.go
Create: wox.core/ui/launcher_bridge.go
Modify: wox.core/main.go
Modify: wox.core/common/ui.go
Modify: wox.core/ui/manager.go
Step 1: Extract the current startup path into a reusable core bootstrap package
Create wox.core/app/core_services.go and wox.core/app/bootstrap.go so main.go no longer owns the full startup sequence directly.
package app
type CoreServices struct {
UIManager *ui.Manager
SettingManager *setting.Manager
PluginManager *plugin.Manager
}
func StartCoreServices(ctx context.Context, serverPort int) (*CoreServices, error) {
return &CoreServices{
UIManager: ui.GetUIManager(),
SettingManager: setting.GetSettingManager(),
PluginManager: plugin.GetPluginManager(),
}, nil
}
Rules for this extraction:
keep the current initialization order from wox.core/main.go
do not change deeplink, telemetry, tray, updater, or hotkey behavior yet
return an explicit CoreServices struct so the launcher runtime can depend on stable handles instead of package globals
Step 2: Define the launcher runtime contracts before adding any host code
Create wox.core/launcher/contracts.go, wox.core/launcher/events.go, and wox.core/launcher/runtime.go with the minimum contract surface the native launcher will implement.
package launcher
type CoreAPI interface {
Query(ctx context.Context, req QueryRequest) error
QueryMRU(ctx context.Context, req QueryMRURequest) ([]plugin.QueryResultUI, error)
Action(ctx context.Context, req ActionRequest) error
FormAction(ctx context.Context, req FormActionRequest) error
ToolbarMsgAction(ctx context.Context, req ToolbarMsgActionRequest) error
ResolvePreview(ctx context.Context, req ResolvePreviewRequest) (plugin.WoxPreview, error)
GetQueryMetadata(ctx context.Context, req QueryMetadataRequest) (QueryMetadata, error)
GetPluginDetail(ctx context.Context, pluginID string) (PluginDetailPayload, error)
TerminalSubscribe(ctx context.Context, req TerminalSubscribeRequest) error
TerminalUnsubscribe(ctx context.Context, req TerminalUnsubscribeRequest) error
TerminalSearch(ctx context.Context, req TerminalSearchRequest) error
}
type EventBus interface {
Publish(event Event)
Subscribe(handler func(Event))
}
type UIThreadDispatcher interface {
Invoke(fn func()) error
}
Rules:
keep names aligned with the approved spec: LauncherCoreAPI, LauncherEventBus, UIThreadDispatcher
keep request and event structs in Go, not C headers
do not add paint or node-level methods to these contracts
Step 3: Introduce a launcher bridge instead of rewriting common.UI call sites
Create wox.core/ui/launcher_bridge.go and keep common.UI as the stable backend-facing façade.
type launcherBridge struct {
runtime launcher.Runtime
}
func (b *launcherBridge) ShowApp(ctx context.Context, showContext common.ShowContext) {
b.runtime.Show(ctx, showContext)
}
func (b *launcherBridge) HideApp(ctx context.Context) {
b.runtime.Hide(ctx)
}
Rules:
move launcher-specific behavior behind the bridge
keep common.UI call sites in plugins and ui.Manager unchanged in this task
do not route settings-only operations through the native launcher
Step 4: Shrink main.go to orchestration only
Update wox.core/main.go so it becomes:
func main() {
mainthread.Init(run)
}
func run() {
ctx := util.NewTraceContext()
serverPort, err := resolveServerPort(ctx)
if err != nil {
util.GetLogger().Error(ctx, err.Error())
return
}
coreServices, err := app.StartCoreServices(ctx, serverPort)
if err != nil {
util.GetLogger().Error(ctx, err.Error())
return
}
_ = coreServices
}
Rules:
keep mainthread.Init(run) for now
do not start the native launcher yet
make this compile with a no-op runtime stub so later tasks can land incrementally
Step 5: Build-check the seam extraction before touching native code
Run:
cd /mnt/c/dev/Wox/wox.core
make build
Expected:
wox.core query pathFiles:
Create: wox.core/launcher/platform/host.go
Create: wox.core/launcher/platform/windows/dispatcher_windows.go
Create: wox.core/launcher/platform/windows/host_windows.go
Create: wox.core/launcher/platform/windows/window_windows.h
Create: wox.core/launcher/platform/windows/window_windows.cc
Create: wox.core/launcher/render/contracts.go
Create: wox.core/launcher/render/text_metrics.go
Create: wox.core/launcher/platform/windows/render_host_windows.h
Create: wox.core/launcher/platform/windows/render_host_windows.cc
Create: wox.core/launcher/debug/automation.go
Modify: wox.core/Makefile
Modify: wox.core/util/mainthread/mainthread.go
Modify: wox.core/main.go
Step 1: Define the native render contract for the Windows spike
Create wox.core/launcher/render/contracts.go, wox.core/launcher/render/text_metrics.go, and wox.core/launcher/platform/windows/render_host_windows.*.
type Renderer interface {
MeasureText(ctx context.Context, req []TextMeasureRequest) ([]TextMeasureResult, error)
PresentFrame(ctx context.Context, frame scene.Frame) error
}
Rules:
keep the render contract platform-neutral from the Go side
use Windows-native drawing primitives behind render_host_windows.*
do not introduce a shared third-party rendering runtime in this task
Step 2: Implement a Windows UI-thread dispatcher around PostMessage(...)
Create wox.core/launcher/platform/host.go and wox.core/launcher/platform/windows/dispatcher_windows.go.
type Host interface {
Start(ctx context.Context) error
Stop(ctx context.Context) error
Dispatcher() launcher.UIThreadDispatcher
}
type windowsDispatcher struct {
hwnd windows.HWND
queue chan func()
msgID uint32
}
Rules:
the dispatcher must queue callbacks from goroutines and execute them only on the Win32 UI thread
panic recovery must happen around callback execution
do not make launcher.Runtime call Win32 APIs directly from background goroutines
Step 3: Create a transparent Win32 host window and a Direct2D/DirectWrite-backed test frame
Create wox.core/launcher/platform/windows/host_windows.go, window_windows.h, window_windows.cc, and render_host_windows.*.
type WindowsHost struct {
dispatcher *windowsDispatcher
renderer render.Renderer
}
func (h *WindowsHost) Start(ctx context.Context) error {
if err := h.createWindow(ctx); err != nil {
return err
}
if err := h.renderer.Initialize(ctx, h.hwnd); err != nil {
return err
}
return h.renderer.Present(ctx, h.bootstrapFrame)
}
Rules:
create one top-level launcher window with transparency and rounded corner support
draw launcher chrome through Direct2D and text through DirectWrite
present one hard-coded frame containing query box, result list area, and preview area
reserve one child-host rectangle for future WebView attachment
Step 4: Feed the spike from the real backend query path, not mock data
Create wox.core/launcher/debug/automation.go and use it from main.go on a development-only path.
type Automation struct {
Runtime launcher.Runtime
}
func (a *Automation) SubmitQuery(ctx context.Context, text string) error {
return a.Runtime.CoreAPI().Query(ctx, launcher.QueryRequest{
QueryID: uuid.NewString(),
QueryType: plugin.QueryTypeInput,
QueryText: text,
})
}
Rules:
the spike must submit a real launcher query through the new LauncherCoreAPI
results must come from plugin.GetPluginManager() and existing backend lifecycle code
do not hard-code fake result rows in the final spike verification path
Step 5: Verify the Windows spike before continuing
Run on Windows:
cd /mnt/c/dev/Wox/wox.core
make build
Manual verification:
wox.core populates the native result listLauncherCoreAPI and LauncherEventBus from the current WebSocket handlersFiles:
Create: wox.core/launcher/coreapi/service.go
Create: wox.core/launcher/coreapi/query_service.go
Create: wox.core/launcher/coreapi/action_service.go
Create: wox.core/launcher/coreapi/preview_service.go
Create: wox.core/launcher/eventbus/eventbus.go
Create: wox.core/launcher/eventbus/coalescer.go
Create: docs/superpowers/audits/2026-04-15-launcher-thread-audit.md
Modify: wox.core/ui/ui_impl.go
Modify: wox.core/ui/http.go
Modify: wox.core/ui/router.go
Step 1: Move launcher request handling out of ui_impl.go into explicit services
Move the business logic currently sitting in handleWebsocketQuery, handleWebsocketAction, handleWebsocketFormAction, handleWebsocketToolbarMsgAction, handleWebsocketTerminalSubscribe, handleWebsocketTerminalUnsubscribe, and handleWebsocketTerminalSearch into wox.core/launcher/coreapi/*.go.
type Service struct {
plugins *plugin.Manager
ui *ui.Manager
}
func (s *Service) Query(ctx context.Context, req launcher.QueryRequest) error {
query, queryPlugin, err := plugin.GetPluginManager().NewQuery(ctx, common.PlainQuery{
QueryId: req.QueryID,
QueryType: req.QueryType,
QueryText: req.QueryText,
})
if err != nil {
return err
}
plugin.GetPluginManager().HandleQueryLifecycle(ctx, query, queryPlugin)
return nil
}
Rules:
copy behavior first, refactor later
keep existing plugin.GetPluginManager() semantics intact
return typed Go values instead of WebSocket payload maps
Step 2: Implement the event bus with explicit backpressure and revision coalescing
Create wox.core/launcher/eventbus/eventbus.go and coalescer.go.
type Bus struct {
subscribers []func(launcher.Event)
results map[string]launcher.ResultSnapshotEvent
}
func (b *Bus) Publish(event launcher.Event) {
if snapshot, ok := event.(launcher.ResultSnapshotEvent); ok {
b.results[snapshot.QueryRevision] = snapshot
return
}
for _, subscriber := range b.subscribers {
subscriber(event)
}
}
Rules:
coalesce result snapshots by query revision
drop stale incremental updates once a new revision supersedes them
bound terminal-chunk queues instead of blocking the core
Step 3: Audit every launcher-facing backend path for UI-thread safety
Create docs/superpowers/audits/2026-04-15-launcher-thread-audit.md and list each current launcher-facing backend entrypoint with one explicit conclusion.
Required rows:
ShowAppHideAppToggleAppChangeQueryRefreshQueryUpdateResultPushResultsShowToolbarMsgClearToolbarMsgOpenSettingWindowPickFilesRules:
call out whether the path currently holds locks
record whether it waits for a response
record which paths must become async-only
Step 4: Leave settings transport alive while cutting launcher transport over
Refactor wox.core/ui/http.go and wox.core/ui/router.go so settings HTTP/WS routes remain available, but launcher semantics are now served by LauncherCoreAPI and LauncherEventBus.
var keepSettingsRoutes = []string{
"/setting/wox",
"/setting/plugin/update",
"/on/setting",
}
var removeLauncherTransport = []string{
"Query",
"Action",
"FormAction",
"ShowApp",
"HideApp",
}
Rules:
do not break settings window startup in this task
keep any still-needed diagnostics routes behind dev-only guards if necessary
stop adding new launcher behavior to the WebSocket path
Step 5: Build-check and verify the thread audit is complete
Run:
cd /mnt/c/dev/Wox/wox.core
make build
Expected:
Files:
Create: wox.core/launcher/store/state.go
Create: wox.core/launcher/store/store.go
Create: wox.core/launcher/store/reducer.go
Create: wox.core/launcher/layout/layout.go
Create: wox.core/launcher/layout/query_box.go
Create: wox.core/launcher/layout/result_list.go
Create: wox.core/launcher/scene/frame.go
Create: wox.core/launcher/scene/diff.go
Create: wox.core/launcher/theme/paint_theme.go
Create: wox.core/launcher/theme/mapper.go
Modify: wox.core/common/theme.go
Step 1: Define the launcher store state exactly once
Create wox.core/launcher/store/state.go.
type State struct {
Visibility VisibilityState
Mode ModeState
FocusTarget FocusTarget
QuerySession QuerySession
PreviewSession PreviewSession
Results []plugin.QueryResultUI
Theme theme.PaintTheme
}
Rules:
include the versioned QuerySession and PreviewSession fields from the spec
keep current selection and preview identity in state
do not hide focus or mode flags inside renderer-private structs
Step 2: Implement a reducer instead of ad-hoc store mutation
Create wox.core/launcher/store/reducer.go.
func Reduce(state State, action Action) State {
switch action := action.(type) {
case ShowAppAction:
case QueryChangedAction:
case ResultsFlushedAction:
case SelectionChangedAction:
case PreviewResolvedAction:
}
return next
}
Rules:
every legal transition from the spec must have one explicit action
illegal transitions must be rejected or normalized in one place
freeze preview churn when overlays are open
Step 3: Create the scene graph and diff layer
Create wox.core/launcher/scene/frame.go and diff.go.
type Frame struct {
Nodes []Node
Interactive []Region
ChildHosts []ChildHostReservation
}
Rules:
stable node IDs are mandatory
the diff layer must emit dirty-node or dirty-region updates
text blocks must be measurable without one cgo call per visual node
Step 4: Map existing Wox themes into renderer-facing paint tokens
Create wox.core/launcher/theme/paint_theme.go and mapper.go.
type PaintTheme struct {
AppBackground color.NRGBA
QueryBox QueryBoxTheme
ResultRow ResultRowTheme
Preview PreviewTheme
Toolbar ToolbarTheme
}
Rules:
load from current backend theme JSON, not a new schema
parse once and cache
cover all launcher-visible fields before any renderer tries to read raw theme JSON
Step 5: Render a real query box and result list from store state
Update the Windows spike so the frame is no longer hard-coded.
Run on Windows:
cd /mnt/c/dev/Wox/wox.core
make build
Manual verification:
Files:
Create: wox.core/launcher/input/router.go
Create: wox.core/launcher/input/query_editor.go
Create: wox.core/launcher/input/shortcuts.go
Create: wox.core/launcher/result/reconciler.go
Create: wox.core/launcher/result/selection.go
Create: wox.core/launcher/platform/windows/text_input_host_windows.h
Create: wox.core/launcher/platform/windows/text_input_host_windows.cc
Modify: wox.core/launcher/platform/windows/host_windows.go
Create: wox.core/test/native_launcher_test_helper.go
Create: wox.core/test/native_launcher_startup_smoke_test.go
Create: wox.core/test/native_launcher_query_preview_smoke_test.go
Step 1: Reconcile results by identity instead of replacing the whole list
Create wox.core/launcher/result/reconciler.go.
func ReconcileResults(current []plugin.QueryResultUI, next []plugin.QueryResultUI, selectedID string) ([]plugin.QueryResultUI, string) {
nextSelectedID := ""
if selectedID != "" {
for _, item := range next {
if item.Id == selectedID {
nextSelectedID = selectedID
break
}
}
}
if nextSelectedID == "" && len(next) > 0 {
nextSelectedID = next[0].Id
}
return next, nextSelectedID
}
Rules:
preserve selection by resultId
fall back to the first row only when the previous selection disappears
keep quick-select numbering derived from visible order
Step 2: Implement the input-priority chain in one router
Create wox.core/launcher/input/router.go.
func (r *Router) HandleKey(event KeyEvent) bool {
return r.overlay.Handle(event) ||
r.webview.Handle(event) ||
r.preview.Handle(event) ||
r.resultList.Handle(event) ||
r.queryEditor.Handle(event) ||
r.window.Handle(event)
}
Rules:
follow the exact priority chain from the spec
keep Esc, Tab, arrows, and Enter behavior centralized
do not let preview-specific code reach back into QueryEditor directly
Step 3: Add a native text-input host instead of reimplementing IME in Go
Create wox.core/launcher/platform/windows/text_input_host_windows.* and wox.core/launcher/input/query_editor.go.
type QueryEditor struct {
Text string
Selection selection.Selection
Composition CompositionRange
CaretRect image.Rectangle
}
Rules:
native text input owns composition and candidate windows
the Go side owns visible query state and caret geometry
expose caret rect lookup for candidate-window positioning
Step 4: Add native launcher smoke coverage for startup and query flows
Create wox.core/test/native_launcher_test_helper.go, native_launcher_startup_smoke_test.go, and native_launcher_query_preview_smoke_test.go.
func launchNativeLauncherForTest(t *testing.T) *launcherdebug.Automation {
ctx := util.NewTraceContext()
services, err := app.StartCoreServices(ctx, 0)
require.NoError(t, err)
runtime := launcher.NewForTest(services)
require.NoError(t, runtime.Start(ctx))
return launcherdebug.NewAutomation(runtime)
}
Smoke assertions:
launcher shows and hides cleanly
one query updates result rows
rapid selection changes do not show stale preview state
Step 5: Run the first native smoke suite
Run:
cd /mnt/c/dev/Wox/wox.core
go test ./test -run 'TestNativeLauncher(Startup|QueryPreview)' -count=1
make build
Expected:
Files:
Create: wox.core/launcher/preview/models.go
Create: wox.core/launcher/preview/resolver.go
Create: wox.core/launcher/preview/host.go
Create: wox.core/launcher/preview/plain_text_renderer.go
Create: wox.core/launcher/preview/markdown_renderer.go
Create: wox.core/launcher/preview/image_renderer.go
Create: wox.core/launcher/preview/file_renderer.go
Create: wox.core/resource/ui/native_launcher/markdown/index.html
Create: wox.core/resource/ui/native_launcher/markdown/main.css
Create: wox.core/resource/ui/native_launcher/markdown/main.js
Modify: wox.core/resource/resource.go
Modify: wox.core/common/image.go
Create: wox.core/test/native_launcher_query_preview_smoke_test.go
Step 1: Normalize WoxPreview into typed runtime preview models
Create wox.core/launcher/preview/models.go and resolver.go.
type ResolvedPreview interface {
Identity() string
Type() PreviewType
}
type MarkdownPreview struct {
HTML string
}
Rules:
resolve remote previews before renderer selection
keep cancellation tokens on every async resolve path
derive preview identity from query ID, result ID, preview type, and preview hash
Step 2: Implement plain text, image, and file routing without WebView reuse yet
Create plain_text_renderer.go, image_renderer.go, and file_renderer.go.
func SelectFileRenderer(path string) RendererKind {
switch strings.ToLower(filepath.Ext(path)) {
case ".md":
return RendererMarkdown
case ".pdf":
return RendererPDF
}
return RendererText
}
Rules:
implement the exact v1 file-extension matrix from the spec
degrade unsupported file types to an explicit unsupported-preview state
normalize relative and file-icon image references before painting
Step 3: Render markdown through a sandboxed document template
Create wox.core/resource/ui/native_launcher/markdown/index.html, main.css, main.js, and markdown_renderer.go.
func RenderMarkdown(markdown string) (string, error) {
var buf bytes.Buffer
if err := goldmark.Convert([]byte(markdown), &buf); err != nil {
return "", err
}
return buf.String(), nil
}
Rules:
use goldmark for Markdown-to-HTML conversion
keep links external
use a transient document profile, not a browsing session profile
Step 4: Extend resource extraction to include native launcher assets
Update wox.core/resource/resource.go so the markdown template assets are extracted alongside the existing resources.
Rules:
keep resource paths deterministic
do not overwrite user data
log extraction failures with the current logging pattern
Step 5: Re-run the preview smoke test against real preview content
Run:
cd /mnt/c/dev/Wox/wox.core
go test ./test -run TestNativeLauncherQueryPreview -count=1
make build
Expected:
WebViewHost, session pooling, and crash recoveryFiles:
Create: wox.core/launcher/webview/session.go
Create: wox.core/launcher/webview/pool.go
Create: wox.core/launcher/webview/profile.go
Create: wox.core/launcher/preview/webview_renderer.go
Create: wox.core/launcher/platform/windows/webview_host_windows.h
Create: wox.core/launcher/platform/windows/webview_host_windows.cc
Modify: wox.core/launcher/input/router.go
Modify: wox.core/launcher/platform/windows/host_windows.go
Create: wox.core/test/native_launcher_webview_smoke_test.go
Step 1: Implement explicit session objects and pooling rules
Create wox.core/launcher/webview/session.go, pool.go, and profile.go.
type Session struct {
ID string
CacheKey string
Profile ProfileKind
Navigation NavigationState
AttachedHost string
CacheDisabled bool
}
Rules:
cached and transient sessions must be explicit types
cacheDisabled=true must always bypass reuse
markdown document sessions must not share the general browsing profile
Step 2: Implement attach, activate, detach, and release on Windows
Create wox.core/launcher/platform/windows/webview_host_windows.* and preview/webview_renderer.go.
func (h *Host) Attach(sessionID string, rect image.Rectangle) error
func (h *Host) Activate(sessionID string) error
func (h *Host) Detach(sessionID string) error
func (h *Host) Release(sessionID string) error
Rules:
use WebView2
keep Esc, refresh, back, and forward routed through the active session
preserve cached sessions across hide and show
Step 3: Add runtime fault recovery for WebView and renderer failures
Extend the Windows host and renderer bridge so failure paths stay local.
Required handling:
WebView child-process exit attempts session recreation
renderer device or drawing-context loss attempts local renderer reinitialization
dispatcher callback panic is recovered and logged
Step 4: Add smoke coverage for warm-cache webview reuse
Create wox.core/test/native_launcher_webview_smoke_test.go.
func TestNativeLauncherWebViewWarmCache(t *testing.T) {
automation := launchNativeLauncherForTest(t)
automation.SelectResultWithWebViewPreview(t)
firstSessionID := automation.ActiveWebViewSessionID(t)
automation.HideLauncher(t)
automation.ShowLauncher(t)
automation.SelectResultWithWebViewPreview(t)
require.Equal(t, firstSessionID, automation.ActiveWebViewSessionID(t))
}
Smoke assertions:
cached session survives hide/show
Esc returns focus to the launcher
a simulated WebView crash leaves the process alive
Step 5: Run the Windows native smoke suite
Run on Windows:
cd /mnt/c/dev/Wox/wox.core
go test ./test -run 'TestNativeLauncher(WebView|Startup|QueryPreview)' -count=1
make build
Expected:
Files:
Create: wox.ui.flutter/wox/lib/settings_main.dart
Modify: wox.ui.flutter/wox/lib/main.dart
Modify: wox.core/ui/manager.go
Modify: wox.core/ui/ui_impl.go
Modify: wox.core/ui/http.go
Modify: wox.core/ui/router.go
Step 1: Add a settings-only Flutter entrypoint
Create wox.ui.flutter/wox/lib/settings_main.dart.
Future<void> main(List<String> arguments) async {
await initialServices(arguments);
runApp(const WoxSettingsApp());
}
Rules:
do not register WoxLauncherController in the settings-only entrypoint
keep WoxSettingController and language/theme loading
settings startup must not depend on launcher WebSocket methods
Step 2: Stop launching the Flutter app as the main launcher window
Update wox.core/ui/manager.go so StartUIApp is no longer part of launcher startup.
func (m *Manager) OpenSettingWindow(ctx context.Context, windowContext common.SettingWindowContext) error {
appPath := util.GetLocation().GetUIAppPath()
_, err := shell.Run(appPath, "--settings", fmt.Sprintf("%d", m.serverPort), windowContext.Path, windowContext.Param)
return err
}
Rules:
the packaged product must not start the Flutter launcher at boot
OpenSettingWindow must launch or focus the settings app
settings process exit must not terminate the main wox process
Step 3: Remove launcher WebSocket request-response dependence from ui_impl.go
Refactor wox.core/ui/ui_impl.go so launcher-facing methods delegate to the native launcher bridge, while settings transport lives in settings_transport.go.
Rules:
ShowApp, HideApp, ToggleApp, ChangeQuery, RefreshQuery, PushResults, UpdateResult, PickFiles, and launcher chat hooks must no longer depend on invokeWebsocketMethod
keep settings-specific transport behavior isolated
Step 4: Verify settings still open and launcher no longer depends on Flutter
Run:
cd /mnt/c/dev/Wox/wox.core
make build
Manual verification:
WKWebViewFiles:
Create: wox.core/launcher/platform/darwin/dispatcher_darwin.go
Create: wox.core/launcher/platform/darwin/host_darwin.go
Create: wox.core/launcher/platform/darwin/app_host_darwin.mm
Create: wox.core/launcher/platform/darwin/text_input_host_darwin.mm
Create: wox.core/launcher/platform/darwin/webview_host_darwin.mm
Create: wox.core/launcher/platform/darwin/render_host_darwin.mm
Create: wox.core/test/native_launcher_platform_darwin_test.go
Modify: wox.core/Makefile
Modify: wox.core/util/mainthread/mainthread_darwin.go
Step 1: Wire the macOS native drawing backend into the existing cgo build
Create wox.core/launcher/platform/darwin/render_host_darwin.mm and extend wox.core/Makefile only as needed for the AppKit/CoreText build flags already required by the native host.
Create dispatcher_darwin.go, host_darwin.go, and app_host_darwin.mm.
func (h *DarwinHost) Start(ctx context.Context) error {
if err := h.createWindow(ctx); err != nil {
return err
}
return h.renderer.Initialize(ctx, h.window)
}
Rules:
AppKit and WKWebView must remain on the main thread
reuse the same store, scene, layout, and preview code from Windows
Step 3: Add native text input and WKWebView integration
Create text_input_host_darwin.mm and webview_host_darwin.mm.
Rules:
implement NSTextInputClient-compatible query input bridging
use WKUserContentController for bridge messages
use evaluateJavaScript only for imperative commands like refresh and navigation
Step 4: Add a macOS platform smoke test
Create wox.core/test/native_launcher_platform_darwin_test.go with build tags as needed.
Assertions:
launcher shows
one query returns results
markdown and webview preview both attach
Esc returns focus to the query box
Step 5: Build and run the macOS smoke path
Run on macOS:
cd /mnt/c/dev/Wox/wox.core
go test ./test -run TestNativeLauncherDarwin -count=1
make build
Expected:
WebKitGTKFiles:
Create: wox.core/launcher/platform/linux/dispatcher_linux.go
Create: wox.core/launcher/platform/linux/host_linux.go
Create: wox.core/launcher/platform/linux/app_host_linux.c
Create: wox.core/launcher/platform/linux/text_input_host_linux.c
Create: wox.core/launcher/platform/linux/webview_host_linux.c
Create: wox.core/launcher/platform/linux/render_host_linux.c
Create: wox.core/test/native_launcher_platform_linux_test.go
Modify: wox.core/Makefile
Step 1: Wire the Linux native drawing backend into the GTK build
Create wox.core/launcher/platform/linux/render_host_linux.c and document the required GTK, Pango, Cairo, and WebKitGTK development packages in the build notes or Makefile comments.
g_idle_add() dispatcherCreate dispatcher_linux.go, host_linux.go, and app_host_linux.c.
func (h *LinuxHost) Start(ctx context.Context) error {
if err := h.createWindow(ctx); err != nil {
return err
}
return h.renderer.Initialize(ctx, h.window)
}
Rules:
use g_idle_add() for cross-thread UI dispatch
keep a CPU fallback for unsupported GPU environments
Step 3: Add GTK IME and WebKitGTK bridges
Create text_input_host_linux.c and webview_host_linux.c.
Rules:
use gtk_im_context for query composition
keep compositor differences visible in logs
degrade a failing webview instance to “open in browser” without freezing the launcher
Step 4: Add Linux smoke coverage for X11 and Wayland targets
Create wox.core/test/native_launcher_platform_linux_test.go.
Assertions:
launcher starts
query and preview work
a failed embedded webview does not kill the process
Step 5: Build and run the Linux smoke path
Run on Linux:
cd /mnt/c/dev/Wox/wox.core
go test ./test -run TestNativeLauncherLinux -count=1
make build
Expected:
Files:
Modify: wox.core/test/native_launcher_test_helper.go
Modify: wox.core/test/native_launcher_startup_smoke_test.go
Modify: wox.core/test/native_launcher_query_preview_smoke_test.go
Modify: wox.core/test/native_launcher_webview_smoke_test.go
Modify: wox.core/Makefile
Modify: docs/superpowers/specs/2026-04-15-launcher-runtime-design.md
Step 1: Add smoke helpers for repeated query, hide/show, and crash-recovery loops
Extend wox.core/test/native_launcher_test_helper.go with helpers that:
submit repeated queries
switch selection across mixed preview types
hide and re-show the launcher
simulate renderer or webview fault conditions
Step 2: Cover the spec’s stress scenarios in the smoke suite
Update the smoke tests so they validate:
100 selection changes across mixed preview types
50 hide/show cycles with a cached webview preview
20 rapid query revisions while results flush incrementally
Step 3: Make make build fail when native smoke prerequisites are missing
Update wox.core/Makefile so missing native dependencies or missing packaged assets fail fast.
build:
@test -n "$(PLATFORM)"
# existing platform build commands
Update docs/superpowers/specs/2026-04-15-launcher-runtime-design.md only if the implementation forced a contract change.
Rules:
do not change the spec for convenience
only record real implementation-driven deltas
Step 5: Run the final verification matrix
Run on each target platform:
cd /mnt/c/dev/Wox/wox.core
go test ./test -run TestNativeLauncher -count=1
make build
Expected:
LauncherCoreAPI, LauncherEventBus, and backpressure: Task 3TODO, TBD, or “similar to” placeholders remain.LauncherCoreAPI, LauncherEventBus, and UIThreadDispatcher are used consistently across tasks.common.UI as the stable backend-facing façade while moving launcher behavior behind a new bridge.Plan complete and saved to docs/superpowers/plans/2026-04-15-go-native-launcher-runtime.md. Two execution options:
Which approach?