refactor(ax): align WebSocket and window naming
Some checks failed
Security Scan / security (push) Failing after 30s
Test / test (push) Successful in 1m16s

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-03-31 07:23:22 +00:00
parent 7deb91ac0b
commit f854b65720
16 changed files with 154 additions and 122 deletions

View file

@ -46,7 +46,7 @@ All Wails application APIs are abstracted behind interfaces in `interfaces.go` (
| `window.go` | `WindowOption` functional options pattern, `Window` type alias for `application.WebviewWindowOptions` |
| `window_state.go` | `WindowStateManager` — persists window position/size across restarts |
| `layout.go` | `LayoutManager` — save/restore named window arrangements |
| `events.go` | `WSEventManager` — WebSocket pub/sub for window/theme/screen events |
| `events.go` | `WebSocketEventManager` — WebSocket pub/sub for window/theme/screen events |
| `interfaces.go` | Abstract interfaces + Wails adapter implementations |
| `actions.go` | `ActionOpenWindow` IPC message type |
| `menu.go` | Application menu construction |
@ -61,7 +61,7 @@ All Wails application APIs are abstracted behind interfaces in `interfaces.go` (
- **Functional options**: `WindowOption` functions (`WindowName()`, `WindowTitle()`, `WindowWidth()`, etc.) configure `application.WebviewWindowOptions`
- **Type alias**: `Window = application.WebviewWindowOptions` — direct alias, not a wrapper
- **Event broadcasting**: `WSEventManager` uses gorilla/websocket with a buffered channel (`eventBuffer`) and per-client subscription filtering (supports `"*"` wildcard)
- **Event broadcasting**: `WebSocketEventManager` uses gorilla/websocket with a buffered channel (`eventBuffer`) and per-client subscription filtering (supports `"*"` wildcard)
- **Window lookup by name**: Most Service methods iterate `s.app.Window().GetAll()` and type-assert to `*application.WebviewWindow`, then match by `Name()`
## Testing

View file

@ -5,7 +5,7 @@ Complete API reference for the Display service (`pkg/display`).
## Service Creation
```go
func NewService(c *core.Core) (any, error)
func NewService() (*Service, error)
```
## Window Management
@ -346,7 +346,7 @@ type Theme struct {
## Events
```go
func (s *Service) GetEventManager() *EventManager
func (s *Service) GetWebSocketEventManager() *WebSocketEventManager
```
The EventManager handles WebSocket connections for real-time events.
The WebSocketEventManager handles WebSocket connections for real-time events.

View file

@ -42,11 +42,11 @@ type Service struct {
app App
configData map[string]map[string]any
configFile *config.Config // config instance for file persistence
events *WSEventManager
events *WebSocketEventManager
}
// New is the constructor for the display service.
func New() (*Service, error) {
// NewService constructs the display service.
func NewService() (*Service, error) {
return &Service{
configData: map[string]map[string]any{
"window": {},
@ -56,11 +56,16 @@ func New() (*Service, error) {
}, nil
}
// New is kept for backward compatibility with older callers.
func New() (*Service, error) {
return NewService()
}
// Register creates a factory closure that captures the Wails app.
// Pass nil for testing without a Wails runtime.
func Register(wailsApp *application.App) func(*core.Core) (any, error) {
return func(c *core.Core) (any, error) {
s, err := New()
s, err := NewService()
if err != nil {
return nil, err
}
@ -83,7 +88,7 @@ func (s *Service) OnStartup(ctx context.Context) error {
// Initialise Wails wrappers if app is available (nil in tests)
if s.wailsApp != nil {
s.app = newWailsApp(s.wailsApp)
s.events = NewWSEventManager()
s.events = NewWebSocketEventManager()
}
return nil
@ -230,23 +235,26 @@ func (s *Service) HandleIPCEvents(c *core.Core, msg core.Message) error {
return nil
}
// WSMessage represents a command received from a WebSocket client.
type WSMessage struct {
// WebSocketMessage represents a command received from a WebSocket client.
type WebSocketMessage struct {
Action string `json:"action"`
Data map[string]any `json:"data,omitempty"`
}
// wsRequire extracts a string field from WS data and returns an error if it is empty.
func wsRequire(data map[string]any, key string) (string, error) {
// WSMessage is kept for backward compatibility with older callers.
type WSMessage = WebSocketMessage
// requireWebSocketField extracts a string field from WebSocket data and returns an error if it is empty.
func requireWebSocketField(data map[string]any, key string) (string, error) {
v, _ := data[key].(string)
if v == "" {
return "", coreerr.E("display.wsRequire", "missing required field \""+key+"\"", nil)
return "", coreerr.E("display.requireWebSocketField", "missing required field \""+key+"\"", nil)
}
return v, nil
}
// handleWSMessage bridges WebSocket commands to IPC calls.
func (s *Service) handleWSMessage(msg WSMessage) (any, bool, error) {
// handleWebSocketMessage bridges WebSocket commands to IPC calls.
func (s *Service) handleWebSocketMessage(msg WebSocketMessage) (any, bool, error) {
var result any
var handled bool
var err error
@ -299,51 +307,51 @@ func (s *Service) handleWSMessage(msg WSMessage) (any, bool, error) {
case "contextmenu:list":
result, handled, err = s.Core().QUERY(contextmenu.QueryList{})
case "webview:eval":
w, e := wsRequire(msg.Data, "window")
w, e := requireWebSocketField(msg.Data, "window")
if e != nil {
return nil, false, e
}
script, _ := msg.Data["script"].(string)
result, handled, err = s.Core().PERFORM(webview.TaskEvaluate{Window: w, Script: script})
case "webview:click":
w, e := wsRequire(msg.Data, "window")
w, e := requireWebSocketField(msg.Data, "window")
if e != nil {
return nil, false, e
}
sel, e := wsRequire(msg.Data, "selector")
sel, e := requireWebSocketField(msg.Data, "selector")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(webview.TaskClick{Window: w, Selector: sel})
case "webview:type":
w, e := wsRequire(msg.Data, "window")
w, e := requireWebSocketField(msg.Data, "window")
if e != nil {
return nil, false, e
}
sel, e := wsRequire(msg.Data, "selector")
sel, e := requireWebSocketField(msg.Data, "selector")
if e != nil {
return nil, false, e
}
text, _ := msg.Data["text"].(string)
result, handled, err = s.Core().PERFORM(webview.TaskType{Window: w, Selector: sel, Text: text})
case "webview:navigate":
w, e := wsRequire(msg.Data, "window")
w, e := requireWebSocketField(msg.Data, "window")
if e != nil {
return nil, false, e
}
url, e := wsRequire(msg.Data, "url")
url, e := requireWebSocketField(msg.Data, "url")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(webview.TaskNavigate{Window: w, URL: url})
case "webview:screenshot":
w, e := wsRequire(msg.Data, "window")
w, e := requireWebSocketField(msg.Data, "window")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(webview.TaskScreenshot{Window: w})
case "webview:scroll":
w, e := wsRequire(msg.Data, "window")
w, e := requireWebSocketField(msg.Data, "window")
if e != nil {
return nil, false, e
}
@ -351,43 +359,43 @@ func (s *Service) handleWSMessage(msg WSMessage) (any, bool, error) {
y, _ := msg.Data["y"].(float64)
result, handled, err = s.Core().PERFORM(webview.TaskScroll{Window: w, X: int(x), Y: int(y)})
case "webview:hover":
w, e := wsRequire(msg.Data, "window")
w, e := requireWebSocketField(msg.Data, "window")
if e != nil {
return nil, false, e
}
sel, e := wsRequire(msg.Data, "selector")
sel, e := requireWebSocketField(msg.Data, "selector")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(webview.TaskHover{Window: w, Selector: sel})
case "webview:select":
w, e := wsRequire(msg.Data, "window")
w, e := requireWebSocketField(msg.Data, "window")
if e != nil {
return nil, false, e
}
sel, e := wsRequire(msg.Data, "selector")
sel, e := requireWebSocketField(msg.Data, "selector")
if e != nil {
return nil, false, e
}
val, _ := msg.Data["value"].(string)
result, handled, err = s.Core().PERFORM(webview.TaskSelect{Window: w, Selector: sel, Value: val})
case "webview:check":
w, e := wsRequire(msg.Data, "window")
w, e := requireWebSocketField(msg.Data, "window")
if e != nil {
return nil, false, e
}
sel, e := wsRequire(msg.Data, "selector")
sel, e := requireWebSocketField(msg.Data, "selector")
if e != nil {
return nil, false, e
}
checked, _ := msg.Data["checked"].(bool)
result, handled, err = s.Core().PERFORM(webview.TaskCheck{Window: w, Selector: sel, Checked: checked})
case "webview:upload":
w, e := wsRequire(msg.Data, "window")
w, e := requireWebSocketField(msg.Data, "window")
if e != nil {
return nil, false, e
}
sel, e := wsRequire(msg.Data, "selector")
sel, e := requireWebSocketField(msg.Data, "selector")
if e != nil {
return nil, false, e
}
@ -400,7 +408,7 @@ func (s *Service) handleWSMessage(msg WSMessage) (any, bool, error) {
}
result, handled, err = s.Core().PERFORM(webview.TaskUploadFile{Window: w, Selector: sel, Paths: paths})
case "webview:viewport":
w, e := wsRequire(msg.Data, "window")
w, e := requireWebSocketField(msg.Data, "window")
if e != nil {
return nil, false, e
}
@ -408,13 +416,13 @@ func (s *Service) handleWSMessage(msg WSMessage) (any, bool, error) {
height, _ := msg.Data["height"].(float64)
result, handled, err = s.Core().PERFORM(webview.TaskSetViewport{Window: w, Width: int(width), Height: int(height)})
case "webview:clear-console":
w, e := wsRequire(msg.Data, "window")
w, e := requireWebSocketField(msg.Data, "window")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().PERFORM(webview.TaskClearConsole{Window: w})
case "webview:console":
w, e := wsRequire(msg.Data, "window")
w, e := requireWebSocketField(msg.Data, "window")
if e != nil {
return nil, false, e
}
@ -425,40 +433,40 @@ func (s *Service) handleWSMessage(msg WSMessage) (any, bool, error) {
}
result, handled, err = s.Core().QUERY(webview.QueryConsole{Window: w, Level: level, Limit: limit})
case "webview:query":
w, e := wsRequire(msg.Data, "window")
w, e := requireWebSocketField(msg.Data, "window")
if e != nil {
return nil, false, e
}
sel, e := wsRequire(msg.Data, "selector")
sel, e := requireWebSocketField(msg.Data, "selector")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().QUERY(webview.QuerySelector{Window: w, Selector: sel})
case "webview:query-all":
w, e := wsRequire(msg.Data, "window")
w, e := requireWebSocketField(msg.Data, "window")
if e != nil {
return nil, false, e
}
sel, e := wsRequire(msg.Data, "selector")
sel, e := requireWebSocketField(msg.Data, "selector")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().QUERY(webview.QuerySelectorAll{Window: w, Selector: sel})
case "webview:dom-tree":
w, e := wsRequire(msg.Data, "window")
w, e := requireWebSocketField(msg.Data, "window")
if e != nil {
return nil, false, e
}
sel, _ := msg.Data["selector"].(string) // selector optional for dom-tree (defaults to root)
result, handled, err = s.Core().QUERY(webview.QueryDOMTree{Window: w, Selector: sel})
case "webview:url":
w, e := wsRequire(msg.Data, "window")
w, e := requireWebSocketField(msg.Data, "window")
if e != nil {
return nil, false, e
}
result, handled, err = s.Core().QUERY(webview.QueryURL{Window: w})
case "webview:title":
w, e := wsRequire(msg.Data, "window")
w, e := requireWebSocketField(msg.Data, "window")
if e != nil {
return nil, false, e
}
@ -643,13 +651,13 @@ func (s *Service) SetWindowBounds(name string, x, y, width, height int) error {
// MaximizeWindow maximizes a window via IPC.
func (s *Service) MaximizeWindow(name string) error {
_, err := s.performWindowTask("display.MaximizeWindow", window.TaskMaximise{Name: name})
_, err := s.performWindowTask("display.MaximizeWindow", window.TaskMaximize{Name: name})
return err
}
// MinimizeWindow minimizes a window via IPC.
func (s *Service) MinimizeWindow(name string) error {
_, err := s.performWindowTask("display.MinimizeWindow", window.TaskMinimise{Name: name})
_, err := s.performWindowTask("display.MinimizeWindow", window.TaskMinimize{Name: name})
return err
}
@ -835,11 +843,16 @@ func (s *Service) ApplyWorkflowLayout(workflow window.WorkflowLayout) error {
return err
}
// GetEventManager returns the event manager for WebSocket event subscriptions.
func (s *Service) GetEventManager() *WSEventManager {
// GetWebSocketEventManager returns the event manager for WebSocket event subscriptions.
func (s *Service) GetWebSocketEventManager() *WebSocketEventManager {
return s.events
}
// GetEventManager is kept for backward compatibility with older callers.
func (s *Service) GetEventManager() *WebSocketEventManager {
return s.GetWebSocketEventManager()
}
// --- Menu (handlers stay in display, structure delegated via IPC) ---
func (s *Service) buildMenu() {

View file

@ -46,19 +46,19 @@ func newTestConclave(t *testing.T) *core.Core {
// --- Tests ---
func TestNew(t *testing.T) {
func TestNewService(t *testing.T) {
t.Run("creates service successfully", func(t *testing.T) {
service, err := New()
service, err := NewService()
assert.NoError(t, err)
assert.NotNil(t, service, "New() should return a non-nil service instance")
assert.NotNil(t, service, "NewService() should return a non-nil service instance")
})
t.Run("returns independent instances", func(t *testing.T) {
service1, err1 := New()
service2, err2 := New()
service1, err1 := NewService()
service2, err2 := NewService()
assert.NoError(t, err1)
assert.NoError(t, err2)
assert.NotSame(t, service1, service2, "New() should return different instances")
assert.NotSame(t, service1, service2, "NewService() should return different instances")
})
}
@ -431,7 +431,7 @@ func TestHandleIPCEvents_WindowOpened_Good(t *testing.T) {
c := newTestConclave(t)
// Open a window — this should trigger ActionWindowOpened
// which HandleIPCEvents should convert to a WS event
// which HandleIPCEvents should convert to a WebSocket event
result, handled, err := c.PERFORM(window.TaskOpenWindow{
Window: window.Window{Name: "test"},
})
@ -451,8 +451,8 @@ func TestHandleListWorkspaces_Good(t *testing.T) {
})
}
func TestWSEventManager_Good(t *testing.T) {
em := NewWSEventManager()
func TestWebSocketEventManager_Good(t *testing.T) {
em := NewWebSocketEventManager()
defer em.Close()
assert.NotNil(t, em)
@ -476,7 +476,7 @@ menu:
show_dev_tools: false
`), 0o644))
s, _ := New()
s, _ := NewService()
s.loadConfigFrom(cfgPath)
// Verify configData was populated from file
@ -486,7 +486,7 @@ menu:
}
func TestLoadConfig_Bad_MissingFile(t *testing.T) {
s, _ := New()
s, _ := NewService()
s.loadConfigFrom(filepath.Join(t.TempDir(), "nonexistent.yaml"))
// Should not panic, configData stays at empty defaults
@ -499,7 +499,7 @@ func TestHandleConfigTask_Persists_Good(t *testing.T) {
dir := t.TempDir()
cfgPath := filepath.Join(dir, "config.yaml")
s, _ := New()
s, _ := NewService()
s.loadConfigFrom(cfgPath) // Creates empty config (file doesn't exist yet)
// Simulate a TaskSaveConfig through the handler

View file

@ -8,7 +8,7 @@ The backend is written in Go and uses the `forge.lthn.ai/core/gui/pkg/display` p
The `Service` struct is the main entry point for the display logic.
- **Initialization:**
- `New() (*Service, error)`: Creates a new instance of the service.
- `NewService() (*Service, error)`: Creates a new instance of the service.
- `Register(wailsApp *application.App) func(*core.Core) (any, error)`: Captures the Wails app and registers the service with Core.
- `OnStartup(ctx context.Context) error`: Loads config and registers IPC handlers.

View file

@ -56,8 +56,8 @@ type Subscription struct {
EventTypes []EventType `json:"eventTypes"`
}
// WSEventManager manages WebSocket connections and event subscriptions.
type WSEventManager struct {
// WebSocketEventManager manages WebSocket connections and event subscriptions.
type WebSocketEventManager struct {
upgrader websocket.Upgrader
clients map[*websocket.Conn]*clientState
mu sync.RWMutex
@ -71,9 +71,9 @@ type clientState struct {
mu sync.RWMutex
}
// NewWSEventManager creates a new event manager.
func NewWSEventManager() *WSEventManager {
em := &WSEventManager{
// NewWebSocketEventManager creates a new WebSocket event manager.
func NewWebSocketEventManager() *WebSocketEventManager {
em := &WebSocketEventManager{
upgrader: websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool {
return true // Allow all origins for local dev
@ -91,8 +91,14 @@ func NewWSEventManager() *WSEventManager {
return em
}
// WSEventManager is kept for backward compatibility with older callers.
type WSEventManager = WebSocketEventManager
// NewWSEventManager is kept for backward compatibility with older callers.
func NewWSEventManager() *WebSocketEventManager { return NewWebSocketEventManager() }
// broadcaster sends events to all subscribed clients.
func (em *WSEventManager) broadcaster() {
func (em *WebSocketEventManager) broadcaster() {
for event := range em.eventBuffer {
em.mu.RLock()
for conn, state := range em.clients {
@ -105,7 +111,7 @@ func (em *WSEventManager) broadcaster() {
}
// clientSubscribed checks if a client is subscribed to an event type.
func (em *WSEventManager) clientSubscribed(state *clientState, eventType EventType) bool {
func (em *WebSocketEventManager) clientSubscribed(state *clientState, eventType EventType) bool {
state.mu.RLock()
defer state.mu.RUnlock()
@ -120,7 +126,7 @@ func (em *WSEventManager) clientSubscribed(state *clientState, eventType EventTy
}
// sendEvent sends an event to a specific client.
func (em *WSEventManager) sendEvent(conn *websocket.Conn, event Event) {
func (em *WebSocketEventManager) sendEvent(conn *websocket.Conn, event Event) {
em.mu.RLock()
_, exists := em.clients[conn]
em.mu.RUnlock()
@ -141,7 +147,7 @@ func (em *WSEventManager) sendEvent(conn *websocket.Conn, event Event) {
}
// HandleWebSocket handles WebSocket upgrade and connection.
func (em *WSEventManager) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
func (em *WebSocketEventManager) HandleWebSocket(w http.ResponseWriter, r *http.Request) {
conn, err := em.upgrader.Upgrade(w, r, nil)
if err != nil {
return
@ -158,7 +164,7 @@ func (em *WSEventManager) HandleWebSocket(w http.ResponseWriter, r *http.Request
}
// handleMessages processes incoming WebSocket messages.
func (em *WSEventManager) handleMessages(conn *websocket.Conn) {
func (em *WebSocketEventManager) handleMessages(conn *websocket.Conn) {
defer em.removeClient(conn)
for {
@ -189,7 +195,7 @@ func (em *WSEventManager) handleMessages(conn *websocket.Conn) {
}
// subscribe adds a subscription for a client.
func (em *WSEventManager) subscribe(conn *websocket.Conn, id string, eventTypes []EventType) {
func (em *WebSocketEventManager) subscribe(conn *websocket.Conn, id string, eventTypes []EventType) {
em.mu.RLock()
state, exists := em.clients[conn]
em.mu.RUnlock()
@ -224,7 +230,7 @@ func (em *WSEventManager) subscribe(conn *websocket.Conn, id string, eventTypes
}
// unsubscribe removes a subscription for a client.
func (em *WSEventManager) unsubscribe(conn *websocket.Conn, id string) {
func (em *WebSocketEventManager) unsubscribe(conn *websocket.Conn, id string) {
em.mu.RLock()
state, exists := em.clients[conn]
em.mu.RUnlock()
@ -247,7 +253,7 @@ func (em *WSEventManager) unsubscribe(conn *websocket.Conn, id string) {
}
// listSubscriptions sends a list of active subscriptions to a client.
func (em *WSEventManager) listSubscriptions(conn *websocket.Conn) {
func (em *WebSocketEventManager) listSubscriptions(conn *websocket.Conn) {
em.mu.RLock()
state, exists := em.clients[conn]
em.mu.RUnlock()
@ -272,7 +278,7 @@ func (em *WSEventManager) listSubscriptions(conn *websocket.Conn) {
}
// removeClient removes a client and its subscriptions.
func (em *WSEventManager) removeClient(conn *websocket.Conn) {
func (em *WebSocketEventManager) removeClient(conn *websocket.Conn) {
em.mu.Lock()
delete(em.clients, conn)
em.mu.Unlock()
@ -280,7 +286,7 @@ func (em *WSEventManager) removeClient(conn *websocket.Conn) {
}
// Emit sends an event to all subscribed clients.
func (em *WSEventManager) Emit(event Event) {
func (em *WebSocketEventManager) Emit(event Event) {
event.Timestamp = time.Now().UnixMilli()
select {
case em.eventBuffer <- event:
@ -290,7 +296,7 @@ func (em *WSEventManager) Emit(event Event) {
}
// EmitWindowEvent is a helper to emit window-related events.
func (em *WSEventManager) EmitWindowEvent(eventType EventType, windowName string, data map[string]any) {
func (em *WebSocketEventManager) EmitWindowEvent(eventType EventType, windowName string, data map[string]any) {
em.Emit(Event{
Type: eventType,
Window: windowName,
@ -299,14 +305,14 @@ func (em *WSEventManager) EmitWindowEvent(eventType EventType, windowName string
}
// ConnectedClients returns the number of connected WebSocket clients.
func (em *WSEventManager) ConnectedClients() int {
func (em *WebSocketEventManager) ConnectedClients() int {
em.mu.RLock()
defer em.mu.RUnlock()
return len(em.clients)
}
// Close shuts down the event manager.
func (em *WSEventManager) Close() {
func (em *WebSocketEventManager) Close() {
em.mu.Lock()
for conn := range em.clients {
conn.Close()
@ -318,7 +324,7 @@ func (em *WSEventManager) Close() {
// AttachWindowListeners attaches event listeners to a specific window.
// Accepts window.PlatformWindow instead of *application.WebviewWindow.
func (em *WSEventManager) AttachWindowListeners(pw window.PlatformWindow) {
func (em *WebSocketEventManager) AttachWindowListeners(pw window.PlatformWindow) {
if pw == nil {
return
}

View file

@ -8,5 +8,5 @@ type ActionIDECommand struct {
Command string `json:"command"` // "save", "run", "build"
}
// EventIDECommand is the WS event type for IDE commands.
// EventIDECommand is the WebSocket event type for IDE commands.
const EventIDECommand EventType = "ide.command"

View file

@ -14,13 +14,13 @@ import (
func TestSubsystem_Good_Name(t *testing.T) {
c, _ := core.New(core.WithServiceLock())
sub := New(c)
sub := NewService(c)
assert.Equal(t, "display", sub.Name())
}
func TestSubsystem_Good_RegisterTools(t *testing.T) {
c, _ := core.New(core.WithServiceLock())
sub := New(c)
sub := NewService(c)
// RegisterTools should not panic with a real mcp.Server
server := mcp.NewServer(&mcp.Implementation{Name: "test", Version: "0.1.0"}, nil)
assert.NotPanics(t, func() { sub.RegisterTools(server) })
@ -34,7 +34,7 @@ type mockClipPlatform struct {
}
func (m *mockClipPlatform) Text() (string, bool) { return m.text, m.ok }
func (m *mockClipPlatform) SetText(t string) bool { m.text = t; m.ok = t != ""; return true }
func (m *mockClipPlatform) SetText(t string) bool { m.text = t; m.ok = t != ""; return true }
func TestMCP_Good_ClipboardRoundTrip(t *testing.T) {
c, err := core.New(

View file

@ -12,11 +12,16 @@ type Subsystem struct {
core *core.Core
}
// New creates a display MCP subsystem backed by the given Core instance.
func New(c *core.Core) *Subsystem {
// NewService creates a display MCP subsystem backed by the given Core instance.
func NewService(c *core.Core) *Subsystem {
return &Subsystem{core: c}
}
// New is kept for backward compatibility with older callers.
func New(c *core.Core) *Subsystem {
return NewService(c)
}
// Name returns the subsystem identifier.
func (s *Subsystem) Name() string { return "display" }

View file

@ -183,7 +183,7 @@ type WindowMaximizeOutput struct {
}
func (s *Subsystem) windowMaximize(_ context.Context, _ *mcp.CallToolRequest, input WindowMaximizeInput) (*mcp.CallToolResult, WindowMaximizeOutput, error) {
_, _, err := s.core.PERFORM(window.TaskMaximise{Name: input.Name})
_, _, err := s.core.PERFORM(window.TaskMaximize{Name: input.Name})
if err != nil {
return nil, WindowMaximizeOutput{}, err
}
@ -200,7 +200,7 @@ type WindowMinimizeOutput struct {
}
func (s *Subsystem) windowMinimize(_ context.Context, _ *mcp.CallToolRequest, input WindowMinimizeInput) (*mcp.CallToolResult, WindowMinimizeOutput, error) {
_, _, err := s.core.PERFORM(window.TaskMinimise{Name: input.Name})
_, _, err := s.core.PERFORM(window.TaskMinimize{Name: input.Name})
if err != nil {
return nil, WindowMinimizeOutput{}, err
}
@ -368,9 +368,9 @@ func (s *Subsystem) registerWindowTools(server *mcp.Server) {
mcp.AddTool(server, &mcp.Tool{Name: "window_position", Description: "Set the position of a window"}, s.windowPosition)
mcp.AddTool(server, &mcp.Tool{Name: "window_size", Description: "Set the size of a window"}, s.windowSize)
mcp.AddTool(server, &mcp.Tool{Name: "window_bounds", Description: "Set both position and size of a window"}, s.windowBounds)
mcp.AddTool(server, &mcp.Tool{Name: "window_maximize", Description: "Maximise a window"}, s.windowMaximize)
mcp.AddTool(server, &mcp.Tool{Name: "window_minimize", Description: "Minimise a window"}, s.windowMinimize)
mcp.AddTool(server, &mcp.Tool{Name: "window_restore", Description: "Restore a maximised or minimised window"}, s.windowRestore)
mcp.AddTool(server, &mcp.Tool{Name: "window_maximize", Description: "Maximize a window"}, s.windowMaximize)
mcp.AddTool(server, &mcp.Tool{Name: "window_minimize", Description: "Minimize a window"}, s.windowMinimize)
mcp.AddTool(server, &mcp.Tool{Name: "window_restore", Description: "Restore a maximized or minimized window"}, s.windowRestore)
mcp.AddTool(server, &mcp.Tool{Name: "window_focus", Description: "Bring a window to the front"}, s.windowFocus)
mcp.AddTool(server, &mcp.Tool{Name: "window_title", Description: "Set the title of a window"}, s.windowTitle)
mcp.AddTool(server, &mcp.Tool{Name: "window_title_get", Description: "Get the title of a window"}, s.windowTitleGet)

View file

@ -84,23 +84,23 @@ func defaultNewConn(options Options) func(string, string) (connector, error) {
if err != nil {
return nil, err
}
var wsURL string
var webSocketURL string
for _, t := range targets {
if t.Type == "page" && (bytes.Contains([]byte(t.Title), []byte(windowName)) || bytes.Contains([]byte(t.URL), []byte(windowName))) {
wsURL = t.WebSocketDebuggerURL
webSocketURL = t.WebSocketDebuggerURL
break
}
}
// Fallback: first page target
if wsURL == "" {
if webSocketURL == "" {
for _, t := range targets {
if t.Type == "page" {
wsURL = t.WebSocketDebuggerURL
webSocketURL = t.WebSocketDebuggerURL
break
}
}
}
if wsURL == "" {
if webSocketURL == "" {
return nil, core.E("webview.connect", "no page target found", nil)
}
wv, err := gowebview.New(
@ -123,8 +123,8 @@ func (s *Service) defaultWatcherSetup(conn connector, windowName string) {
return // test mocks don't need watchers
}
cw := gowebview.NewConsoleWatcher(rc.wv)
cw.AddHandler(func(msg gowebview.ConsoleMessage) {
consoleWatcher := gowebview.NewConsoleWatcher(rc.wv)
consoleWatcher.AddHandler(func(msg gowebview.ConsoleMessage) {
_ = s.Core().ACTION(ActionConsoleMessage{
Window: windowName,
Message: ConsoleMessage{
@ -138,8 +138,8 @@ func (s *Service) defaultWatcherSetup(conn connector, windowName string) {
})
})
ew := gowebview.NewExceptionWatcher(rc.wv)
ew.AddHandler(func(exc gowebview.ExceptionInfo) {
exceptionWatcher := gowebview.NewExceptionWatcher(rc.wv)
exceptionWatcher.AddHandler(func(exc gowebview.ExceptionInfo) {
_ = s.Core().ACTION(ActionException{
Window: windowName,
Exception: ExceptionInfo{

View file

@ -35,9 +35,17 @@ type TaskSetSize struct {
Width, Height int
}
type TaskMaximise struct{ Name string }
// TaskMaximize maximizes a named window.
type TaskMaximize struct{ Name string }
type TaskMinimise struct{ Name string }
// TaskMinimize minimizes a named window.
type TaskMinimize struct{ Name string }
// TaskMaximise is kept for backward compatibility with older callers.
type TaskMaximise = TaskMaximize
// TaskMinimise is kept for backward compatibility with older callers.
type TaskMinimise = TaskMinimize
type TaskFocus struct{ Name string }

View file

@ -136,10 +136,10 @@ func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) {
return nil, true, s.taskSetPosition(t.Name, t.X, t.Y)
case TaskSetSize:
return nil, true, s.taskSetSize(t.Name, t.Width, t.Height)
case TaskMaximise:
return nil, true, s.taskMaximise(t.Name)
case TaskMinimise:
return nil, true, s.taskMinimise(t.Name)
case TaskMaximize:
return nil, true, s.taskMaximize(t.Name)
case TaskMinimize:
return nil, true, s.taskMinimize(t.Name)
case TaskFocus:
return nil, true, s.taskFocus(t.Name)
case TaskRestore:
@ -299,8 +299,8 @@ func (s *Service) taskSetSize(name string, width, height int) error {
return nil
}
func (s *Service) taskMaximise(name string) error {
platformWindow, err := s.requireWindow(name, "window.Service.taskMaximise")
func (s *Service) taskMaximize(name string) error {
platformWindow, err := s.requireWindow(name, "window.Service.taskMaximize")
if err != nil {
return err
}
@ -309,8 +309,8 @@ func (s *Service) taskMaximise(name string) error {
return nil
}
func (s *Service) taskMinimise(name string) error {
platformWindow, err := s.requireWindow(name, "window.Service.taskMinimise")
func (s *Service) taskMinimize(name string) error {
platformWindow, err := s.requireWindow(name, "window.Service.taskMinimize")
if err != nil {
return err
}

View file

@ -166,11 +166,11 @@ func TestTaskSetSize_Good(t *testing.T) {
assert.Equal(t, 600, info.Height)
}
func TestTaskMaximise_Good(t *testing.T) {
func TestTaskMaximize_Good(t *testing.T) {
_, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "test"}})
_, handled, err := c.PERFORM(TaskMaximise{Name: "test"})
_, handled, err := c.PERFORM(TaskMaximize{Name: "test"})
require.NoError(t, err)
assert.True(t, handled)
@ -215,13 +215,13 @@ func TestFileDrop_Good(t *testing.T) {
mu.Unlock()
}
// --- TaskMinimise ---
// --- TaskMinimize ---
func TestTaskMinimise_Good(t *testing.T) {
func TestTaskMinimize_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "test"}})
_, handled, err := c.PERFORM(TaskMinimise{Name: "test"})
_, handled, err := c.PERFORM(TaskMinimize{Name: "test"})
require.NoError(t, err)
assert.True(t, handled)
@ -231,9 +231,9 @@ func TestTaskMinimise_Good(t *testing.T) {
assert.True(t, mw.minimised)
}
func TestTaskMinimise_Bad(t *testing.T) {
func TestTaskMinimize_Bad(t *testing.T) {
_, c := newTestWindowService(t)
_, handled, err := c.PERFORM(TaskMinimise{Name: "nonexistent"})
_, handled, err := c.PERFORM(TaskMinimize{Name: "nonexistent"})
assert.True(t, handled)
assert.Error(t, err)
}
@ -267,8 +267,8 @@ func TestTaskRestore_Good(t *testing.T) {
svc, c := newTestWindowService(t)
_, _, _ = c.PERFORM(TaskOpenWindow{Window: Window{Name: "test"}})
// First maximise, then restore
_, _, _ = c.PERFORM(TaskMaximise{Name: "test"})
// First maximize, then restore
_, _, _ = c.PERFORM(TaskMaximize{Name: "test"})
_, handled, err := c.PERFORM(TaskRestore{Name: "test"})
require.NoError(t, err)

View file

@ -60,7 +60,7 @@ type WorkflowLayout int
const (
WorkflowCoding WorkflowLayout = iota // 70/30 split
WorkflowDebugging // 60/40 split
WorkflowPresenting // maximised
WorkflowPresenting // maximized
WorkflowSideBySide // 50/50 split
)
@ -276,7 +276,7 @@ func (m *Manager) ApplyWorkflow(workflow WorkflowLayout, names []string, screenW
}
}
case WorkflowPresenting:
// Maximise first window
// Maximize first window
if pw, ok := m.Get(names[0]); ok {
pw.SetPosition(originX, originY)
pw.SetSize(screenW, screenH)

View file

@ -303,7 +303,7 @@ func TestApplyWorkflow_AllLayouts_Good(t *testing.T) {
{
"Presenting",
WorkflowPresenting,
0, 0, screenW, screenH, // maximised
0, 0, screenW, screenH, // maximized
0, 0, 800, 600, // second window untouched
},
{