From cf284e995488376b9df7de6e6e980040d2bf8e4d Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 19:21:28 +0000 Subject: [PATCH] feat(gui): add event info and layout query fixes --- pkg/display/display.go | 27 +++++++++++++++++++++++++-- pkg/display/display_test.go | 22 ++++++++++++++++++++++ pkg/display/events.go | 24 ++++++++++++++++++++++++ pkg/window/messages.go | 6 ++++-- pkg/window/service.go | 10 ++++++++-- 5 files changed, 83 insertions(+), 6 deletions(-) diff --git a/pkg/display/display.go b/pkg/display/display.go index eb33e94..54c78a5 100644 --- a/pkg/display/display.go +++ b/pkg/display/display.go @@ -573,6 +573,16 @@ func (s *Service) handleWSMessage(msg WSMessage) (any, bool, error) { return nil, false, e } result, handled, err = s.Core().PERFORM(webview.TaskCloseDevTools{Window: w}) + case "webview:errors": + w, e := wsRequire(msg.Data, "window") + if e != nil { + return nil, false, e + } + limit := 0 + if l, ok := msg.Data["limit"].(float64); ok { + limit = int(l) + } + result, handled, err = s.Core().QUERY(webview.QueryExceptions{Window: w, Limit: limit}) case "layout:beside-editor": editor, _ := msg.Data["editor"].(string) windowName, _ := msg.Data["window"].(string) @@ -663,9 +673,12 @@ func (s *Service) handleWSMessage(msg WSMessage) (any, bool, error) { if h, ok := msg.Data["height"].(float64); ok { height = int(h) } + screenWidth, screenHeight := s.primaryScreenSize() result, handled, err = s.Core().QUERY(window.QueryFindSpace{ - Width: width, - Height: height, + Width: width, + Height: height, + ScreenWidth: screenWidth, + ScreenHeight: screenHeight, }) case "screen:list": result, handled, err = s.Core().QUERY(screen.QueryAll{}) @@ -862,6 +875,8 @@ func (s *Service) handleWSMessage(msg WSMessage) (any, bool, error) { result, handled, err = s.Core().PERFORM(systray.TaskSetTrayMenu{Items: items}) case "tray:info": result, handled, err = s.GetTrayInfo(), true, nil + case "event:info": + result, handled, err = s.GetEventInfo(), true, nil case "theme:get": result, handled, err = s.GetTheme(), true, nil case "theme:system": @@ -1964,6 +1979,14 @@ func (s *Service) GetEventManager() *WSEventManager { return s.events } +// GetEventInfo returns a summary of the live WebSocket event server state. +func (s *Service) GetEventInfo() EventServerInfo { + if s.events == nil { + return EventServerInfo{} + } + return s.events.Info() +} + // --- 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 67d7787..56490b4 100644 --- a/pkg/display/display_test.go +++ b/pkg/display/display_test.go @@ -1040,6 +1040,18 @@ func TestHandleWSMessage_Extended_Good(t *testing.T) { assert.True(t, handled) }) + t.Run("webview errors", func(t *testing.T) { + result, handled, err := svc.handleWSMessage(WSMessage{ + Action: "webview:errors", + Data: map[string]any{"window": "editor", "limit": float64(10)}, + }) + require.NoError(t, err) + assert.True(t, handled) + errors, ok := result.([]webview.ExceptionInfo) + require.True(t, ok) + assert.Len(t, errors, 0) + }) + t.Run("tray message", func(t *testing.T) { _, handled, err := svc.handleWSMessage(WSMessage{ Action: "tray:show-message", @@ -1084,4 +1096,14 @@ func TestHandleWSMessage_Extended_Good(t *testing.T) { assert.True(t, handled) assert.Equal(t, "OK", result) }) + + t.Run("event info", func(t *testing.T) { + result, handled, err := svc.handleWSMessage(WSMessage{Action: "event:info"}) + require.NoError(t, err) + assert.True(t, handled) + info, ok := result.(EventServerInfo) + require.True(t, ok) + assert.Equal(t, 0, info.ConnectedClients) + assert.Equal(t, 0, info.Subscriptions) + }) } diff --git a/pkg/display/events.go b/pkg/display/events.go index cee4a31..5eba607 100644 --- a/pkg/display/events.go +++ b/pkg/display/events.go @@ -57,6 +57,13 @@ type Subscription struct { EventTypes []EventType `json:"eventTypes"` } +// EventServerInfo summarises the live WebSocket event server state. +type EventServerInfo struct { + ConnectedClients int `json:"connectedClients"` + Subscriptions int `json:"subscriptions"` + BufferedEvents int `json:"bufferedEvents"` +} + // WSEventManager manages WebSocket connections and event subscriptions. type WSEventManager struct { upgrader websocket.Upgrader @@ -307,6 +314,23 @@ func (em *WSEventManager) ConnectedClients() int { return len(em.clients) } +// Info returns a snapshot of the WebSocket event server state. +func (em *WSEventManager) Info() EventServerInfo { + em.mu.RLock() + defer em.mu.RUnlock() + + info := EventServerInfo{ + ConnectedClients: len(em.clients), + BufferedEvents: len(em.eventBuffer), + } + for _, state := range em.clients { + state.mu.RLock() + info.Subscriptions += len(state.subscriptions) + state.mu.RUnlock() + } + return info +} + // Close shuts down the event manager. func (em *WSEventManager) Close() { em.mu.Lock() diff --git a/pkg/window/messages.go b/pkg/window/messages.go index 7ea8526..8482db8 100644 --- a/pkg/window/messages.go +++ b/pkg/window/messages.go @@ -40,8 +40,10 @@ type QueryWindowBounds struct{ Name string } // QueryFindSpace returns a suggested free placement for a new window. type QueryFindSpace struct { - Width int - Height int + Width int + Height int + ScreenWidth int + ScreenHeight int } // QueryLayoutSuggestion returns a layout recommendation for the current screen. diff --git a/pkg/window/service.go b/pkg/window/service.go index bd3077f..6446395 100644 --- a/pkg/window/service.go +++ b/pkg/window/service.go @@ -86,10 +86,16 @@ func (s *Service) handleQuery(c *core.Core, q core.Query) (any, bool, error) { } return &l, true, nil case QueryFindSpace: - screenW, screenH := s.primaryScreenSize() + screenW, screenH := q.ScreenWidth, q.ScreenHeight + if screenW <= 0 || screenH <= 0 { + screenW, screenH = s.primaryScreenSize() + } return s.manager.FindSpace(screenW, screenH, q.Width, q.Height), true, nil case QueryLayoutSuggestion: - screenW, screenH := s.primaryScreenSize() + screenW, screenH := q.ScreenWidth, q.ScreenHeight + if screenW <= 0 || screenH <= 0 { + screenW, screenH = s.primaryScreenSize() + } return s.manager.SuggestLayout(screenW, screenH, q.WindowCount), true, nil default: return nil, false, nil