diff --git a/CLAUDE.md b/CLAUDE.md index 594d4c0..ca734bc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/docs/framework/display.md b/docs/framework/display.md index 96f01b8..0784f57 100644 --- a/docs/framework/display.md +++ b/docs/framework/display.md @@ -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. diff --git a/pkg/display/display.go b/pkg/display/display.go index 66b18bc..f646eeb 100644 --- a/pkg/display/display.go +++ b/pkg/display/display.go @@ -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() { diff --git a/pkg/display/display_test.go b/pkg/display/display_test.go index 59601ae..ac7efe8 100644 --- a/pkg/display/display_test.go +++ b/pkg/display/display_test.go @@ -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 diff --git a/pkg/display/docs/backend.md b/pkg/display/docs/backend.md index 853ec7b..fce152c 100644 --- a/pkg/display/docs/backend.md +++ b/pkg/display/docs/backend.md @@ -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. diff --git a/pkg/display/events.go b/pkg/display/events.go index 6333c3c..4db69a8 100644 --- a/pkg/display/events.go +++ b/pkg/display/events.go @@ -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 } diff --git a/pkg/display/messages.go b/pkg/display/messages.go index 43d4e3f..077f7cf 100644 --- a/pkg/display/messages.go +++ b/pkg/display/messages.go @@ -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" diff --git a/pkg/mcp/mcp_test.go b/pkg/mcp/mcp_test.go index d3a3453..a06c280 100644 --- a/pkg/mcp/mcp_test.go +++ b/pkg/mcp/mcp_test.go @@ -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( diff --git a/pkg/mcp/subsystem.go b/pkg/mcp/subsystem.go index b567d1b..0648617 100644 --- a/pkg/mcp/subsystem.go +++ b/pkg/mcp/subsystem.go @@ -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" } diff --git a/pkg/mcp/tools_window.go b/pkg/mcp/tools_window.go index ccf4ff3..1d44ae0 100644 --- a/pkg/mcp/tools_window.go +++ b/pkg/mcp/tools_window.go @@ -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) diff --git a/pkg/webview/service.go b/pkg/webview/service.go index b6e468b..7be174d 100644 --- a/pkg/webview/service.go +++ b/pkg/webview/service.go @@ -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{ diff --git a/pkg/window/messages.go b/pkg/window/messages.go index bdb3791..8960f8c 100644 --- a/pkg/window/messages.go +++ b/pkg/window/messages.go @@ -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 } diff --git a/pkg/window/service.go b/pkg/window/service.go index 27550fc..87b4f71 100644 --- a/pkg/window/service.go +++ b/pkg/window/service.go @@ -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 } diff --git a/pkg/window/service_test.go b/pkg/window/service_test.go index d39bde7..806371b 100644 --- a/pkg/window/service_test.go +++ b/pkg/window/service_test.go @@ -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) diff --git a/pkg/window/tiling.go b/pkg/window/tiling.go index 6137e89..9e3a3e2 100644 --- a/pkg/window/tiling.go +++ b/pkg/window/tiling.go @@ -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) diff --git a/pkg/window/window_test.go b/pkg/window/window_test.go index d504e4f..442d8a6 100644 --- a/pkg/window/window_test.go +++ b/pkg/window/window_test.go @@ -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 }, {