diff --git a/pkg/display/display.go b/pkg/display/display.go index 91dc69bd..b45cd2c2 100644 --- a/pkg/display/display.go +++ b/pkg/display/display.go @@ -1464,6 +1464,82 @@ func (s *Service) ApplyWorkflowLayout(workflow window.WorkflowLayout) error { return nil } +// LayoutBesideEditor places a window beside a detected editor window. +// +// result, err := svc.LayoutBesideEditor("preview", "code", "right", 0.62) +func (s *Service) LayoutBesideEditor(name, editor, side string, ratio float64) (window.LayoutBesideEditorResult, error) { + r := s.Core().Action("window.layoutBesideEditor").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskLayoutBesideEditor{ + Name: name, Editor: editor, Side: side, Ratio: ratio, + }}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return window.LayoutBesideEditorResult{}, e + } + return window.LayoutBesideEditorResult{}, nil + } + result, _ := r.Value.(window.LayoutBesideEditorResult) + return result, nil +} + +// LayoutSuggest returns a layout recommendation for the current screen. +// +// suggestion, err := svc.LayoutSuggest("", 2) +func (s *Service) LayoutSuggest(screenID string, windowCount int) (window.LayoutSuggestion, error) { + r := s.Core().Action("window.layoutSuggest").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskLayoutSuggest{ + ScreenID: screenID, WindowCount: windowCount, + }}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return window.LayoutSuggestion{}, e + } + return window.LayoutSuggestion{}, nil + } + result, _ := r.Value.(window.LayoutSuggestion) + return result, nil +} + +// FindScreenSpace finds an unused rectangle for a new window. +// +// space, err := svc.FindScreenSpace("", 800, 600, 24) +func (s *Service) FindScreenSpace(screenID string, width, height, padding int) (window.ScreenSpace, error) { + r := s.Core().Action("window.findSpace").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskScreenFindSpace{ + ScreenID: screenID, Width: width, Height: height, Padding: padding, + }}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return window.ScreenSpace{}, e + } + return window.ScreenSpace{}, nil + } + result, _ := r.Value.(window.ScreenSpace) + return result, nil +} + +// ArrangeWindowPair positions two windows in an optimal split. +// +// arrangement, err := svc.ArrangeWindowPair("editor", "preview", "", 0.55) +func (s *Service) ArrangeWindowPair(primary, secondary, screenID string, ratio float64) (window.PairArrangement, error) { + r := s.Core().Action("window.arrangePair").Run(context.Background(), core.NewOptions( + core.Option{Key: "task", Value: window.TaskWindowArrangePair{ + Primary: primary, Secondary: secondary, ScreenID: screenID, Ratio: ratio, + }}, + )) + if !r.OK { + if e, ok := r.Value.(error); ok { + return window.PairArrangement{}, e + } + return window.PairArrangement{}, nil + } + result, _ := r.Value.(window.PairArrangement) + return result, nil +} + // GetEventManager returns the event manager for WebSocket event subscriptions. func (s *Service) GetEventManager() *WSEventManager { return s.events diff --git a/pkg/notification/service.go b/pkg/notification/service.go index 9e443e99..5138bf8d 100644 --- a/pkg/notification/service.go +++ b/pkg/notification/service.go @@ -102,6 +102,7 @@ func (s *Service) send(options NotificationOptions) error { if options.ID == "" { options.ID = "core-" + strconv.FormatInt(time.Now().UnixNano(), 10) } + options = s.applyCategoryActions(options) if err := s.platform.Send(options); err != nil { // Fallback: show as dialog via IPC @@ -115,6 +116,22 @@ func (s *Service) send(options NotificationOptions) error { return nil } +func (s *Service) applyCategoryActions(options NotificationOptions) NotificationOptions { + if options.CategoryID == "" || len(options.Actions) > 0 { + return options + } + + s.mu.Lock() + category, ok := s.categories[options.CategoryID] + s.mu.Unlock() + if !ok || len(category.Actions) == 0 { + return options + } + + options.Actions = append([]NotificationAction(nil), category.Actions...) + return options +} + // fallbackDialog shows a dialog via IPC when native notifications fail. func (s *Service) fallbackDialog(options NotificationOptions) error { // Map severity to dialog type diff --git a/pkg/notification/service_test.go b/pkg/notification/service_test.go index 416bd35e..340cd467 100644 --- a/pkg/notification/service_test.go +++ b/pkg/notification/service_test.go @@ -216,6 +216,30 @@ func TestTaskSend_WithActions_Good(t *testing.T) { assert.Equal(t, 2, len(mock.lastOpts.Actions)) } +func TestTaskSend_RegisteredCategoryActions_Good(t *testing.T) { + mock, c := newTestService(t) + require.True(t, taskRun(c, "notification.registerCategory", TaskRegisterCategory{ + Category: NotificationCategory{ + ID: "message", + Actions: []NotificationAction{ + {ID: "reply", Title: "Reply"}, + {ID: "dismiss", Title: "Dismiss"}, + }, + }, + }).OK) + + r := taskRun(c, "notification.send", TaskSend{ + Options: NotificationOptions{ + Title: "Team Chat", + Message: "New message from Alice", + CategoryID: "message", + }, + }) + require.True(t, r.OK) + assert.Equal(t, 2, len(mock.lastOpts.Actions)) + assert.Equal(t, "reply", mock.lastOpts.Actions[0].ID) +} + func TestTaskClear_Good_Specific(t *testing.T) { mock, c := newTestService(t) require.True(t, taskRun(c, "notification.send", TaskSend{