diff --git a/pkg/display/display.go b/pkg/display/display.go index c166886c..70475147 100644 --- a/pkg/display/display.go +++ b/pkg/display/display.go @@ -393,16 +393,21 @@ type WSMessage struct { 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) { +// requireStringField extracts a string field from WebSocket data and fails when it is missing. +func requireStringField(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.requireStringField", "missing required field \""+key+"\"", nil) } return v, nil } -func wsOptions(data map[string]any) core.Options { +// wsRequire is kept for backward compatibility inside the display package. +func wsRequire(data map[string]any, key string) (string, error) { + return requireStringField(data, key) +} + +func optionsFromMap(data map[string]any) core.Options { items := make([]core.Option, 0, len(data)) for key, value := range data { items = append(items, core.Option{Key: key, Value: value}) @@ -410,6 +415,11 @@ func wsOptions(data map[string]any) core.Options { return core.NewOptions(items...) } +// wsOptions is kept for backward compatibility inside the display package. +func wsOptions(data map[string]any) core.Options { + return optionsFromMap(data) +} + // handleWSMessage bridges WebSocket commands to IPC calls. func (s *Service) handleWSMessage(msg WSMessage) core.Result { ctx := context.Background() @@ -998,13 +1008,13 @@ func (s *Service) SetWindowFullscreen(name string, fullscreen bool) error { // SetWindowBackgroundColour sets the background colour of a window. func (s *Service) SetWindowBackgroundColour(name string, r, g, b, a uint8) error { - res := s.Core().Action("window.setBackgroundColour").Run(context.Background(), core.NewOptions( + result := s.Core().Action("window.setBackgroundColour").Run(context.Background(), core.NewOptions( core.Option{Key: "task", Value: window.TaskSetBackgroundColour{ Name: name, Red: r, Green: g, Blue: b, Alpha: a, }}, )) - if !res.OK { - if e, ok := res.Value.(error); ok { + if !result.OK { + if e, ok := result.Value.(error); ok { return e } } @@ -1221,10 +1231,10 @@ func (s *Service) GetEventManager() *WSEventManager { func (s *Service) buildMenu() { items := []menu.MenuItem{ - {Role: ptr(menu.RoleAppMenu)}, - {Role: ptr(menu.RoleFileMenu)}, - {Role: ptr(menu.RoleViewMenu)}, - {Role: ptr(menu.RoleEditMenu)}, + {Role: pointerTo(menu.RoleAppMenu)}, + {Role: pointerTo(menu.RoleFileMenu)}, + {Role: pointerTo(menu.RoleViewMenu)}, + {Role: pointerTo(menu.RoleEditMenu)}, {Label: "Workspace", Children: []menu.MenuItem{ {Label: "New...", OnClick: s.handleNewWorkspace}, {Label: "List", OnClick: s.handleListWorkspaces}, @@ -1240,8 +1250,8 @@ func (s *Service) buildMenu() { {Label: "Run", Accelerator: "CmdOrCtrl+R", OnClick: s.handleRun}, {Label: "Build", Accelerator: "CmdOrCtrl+B", OnClick: s.handleBuild}, }}, - {Role: ptr(menu.RoleWindowMenu)}, - {Role: ptr(menu.RoleHelpMenu)}, + {Role: pointerTo(menu.RoleWindowMenu)}, + {Role: pointerTo(menu.RoleHelpMenu)}, } // On non-macOS, remove the AppMenu role @@ -1254,7 +1264,8 @@ func (s *Service) buildMenu() { )) } -func ptr[T any](v T) *T { return &v } +// pointerTo returns a pointer to value. +func pointerTo[T any](value T) *T { return &value } // --- Menu handler methods --- diff --git a/pkg/display/display_test.go b/pkg/display/display_test.go index 6afd85b6..229ade5e 100644 --- a/pkg/display/display_test.go +++ b/pkg/display/display_test.go @@ -111,6 +111,23 @@ func TestConfigTask_Good(t *testing.T) { assert.Equal(t, 800, cfg["default_width"]) } +func TestResolveScheme_StoreRoute_Good(t *testing.T) { + svc, _ := newTestDisplayService(t) + + result := svc.ResolveScheme(context.Background(), "core://store?q=alpha") + require.True(t, result.OK) + + payload, ok := result.Value.(map[string]any) + require.True(t, ok) + assert.Equal(t, "text/html", payload["content_type"]) + + body, ok := payload["body"].(string) + require.True(t, ok) + assert.Contains(t, body, "core://store") + assert.Contains(t, body, "storage scopes") + assert.Contains(t, body, "Search the in-memory storage scopes") +} + // --- Conclave integration tests --- func TestServiceConclave_Good(t *testing.T) { diff --git a/pkg/display/scheme.go b/pkg/display/scheme.go index 29c01b42..cee93e9e 100644 --- a/pkg/display/scheme.go +++ b/pkg/display/scheme.go @@ -40,6 +40,17 @@ func (s *Service) registerDefaultSchemes() { )) } return s.Core().Action("gui.chat.conversations.list").Run(ctx, core.NewOptions()) + case "store": + return core.Result{ + Value: map[string]any{ + "content_type": "text/html", + "body": s.renderStoreSearchPage(query.Get("q")), + "route": route, + "url": "core://store", + "query": query, + }, + OK: true, + } default: return core.Result{ Value: map[string]any{ @@ -71,6 +82,14 @@ func (s *Service) ResolveScheme(ctx context.Context, rawURL string) core.Result return resolved } + if payload, ok := resolved.Value.(map[string]any); ok { + if contentType, _ := payload["content_type"].(string); strings.EqualFold(contentType, "text/html") { + if body, ok := payload["body"].(string); ok && strings.TrimSpace(body) != "" { + return core.Result{Value: payload, OK: true} + } + } + } + body := s.renderSchemeBody(route, resolved.Value) return core.Result{ Value: map[string]any{ @@ -95,6 +114,15 @@ func (s *Service) renderSchemeBody(route string, value any) string { "" } +func (s *Service) renderStoreSearchPage(query string) string { + safeQuery := html.EscapeString(query) + return "core://store
core://store
Search the in-memory storage scopes exposed by the preload shim. Query: " + + safeQuery + + "
Loading results...
" +} + func coalesce(values ...string) string { for _, value := range values { if strings.TrimSpace(value) != "" {