diff --git a/pkg/display/display.go b/pkg/display/display.go index bfbf847..ba126d7 100644 --- a/pkg/display/display.go +++ b/pkg/display/display.go @@ -1120,6 +1120,11 @@ func (s *Service) FocusWindow(name string) error { return err } +// FocusSet is a compatibility alias for FocusWindow. +func (s *Service) FocusSet(name string) error { + return s.FocusWindow(name) +} + // CloseWindow closes a window via IPC. func (s *Service) CloseWindow(name string) error { _, _, err := s.Core().PERFORM(window.TaskCloseWindow{Name: name}) @@ -1177,6 +1182,12 @@ func (s *Service) SetWindowOpacity(name string, opacity float32) error { return err } +// ClearWebviewConsole clears the captured console buffer for a window. +func (s *Service) ClearWebviewConsole(name string) error { + _, _, err := s.Core().PERFORM(webview.TaskClearConsole{Window: name}) + return err +} + // GetFocusedWindow returns the name of the currently focused window. func (s *Service) GetFocusedWindow() string { infos := s.ListWindowInfos() @@ -1772,6 +1783,27 @@ func (s *Service) PromptDialog(title, message string) (string, bool, error) { return button, button == "OK", nil } +// DialogMessage shows an informational, warning, or error message via the notification pipeline. +func (s *Service) DialogMessage(kind, title, message string) error { + var severity notification.NotificationSeverity + switch kind { + case "warning": + severity = notification.SeverityWarning + case "error": + severity = notification.SeverityError + default: + severity = notification.SeverityInfo + } + _, _, err := s.Core().PERFORM(notification.TaskSend{ + Opts: notification.NotificationOptions{ + Title: title, + Message: message, + Severity: severity, + }, + }) + return err +} + // --- Theme --- // GetTheme returns the current theme state. diff --git a/pkg/display/display_test.go b/pkg/display/display_test.go index 47a07c4..05df901 100644 --- a/pkg/display/display_test.go +++ b/pkg/display/display_test.go @@ -726,6 +726,21 @@ func TestServiceWrappers_Good(t *testing.T) { assert.True(t, fixture.notificationPlatform.clearCalled) }) + t.Run("compatibility aliases", func(t *testing.T) { + _ = svc.OpenWindow(window.WithName("alias-win")) + + require.NoError(t, svc.FocusSet("alias-win")) + info, err := svc.GetWindowInfo("alias-win") + require.NoError(t, err) + require.NotNil(t, info) + assert.True(t, info.Focused) + + require.NoError(t, svc.DialogMessage("warning", "Alias", "Message")) + assert.Equal(t, notification.SeverityWarning, fixture.notificationPlatform.lastOpts.Severity) + assert.Equal(t, "Alias", fixture.notificationPlatform.lastOpts.Title) + assert.Equal(t, "Message", fixture.notificationPlatform.lastOpts.Message) + }) + t.Run("dialog wrappers", func(t *testing.T) { paths, err := svc.OpenFileDialog(dialog.OpenFileOptions{Title: "Pick"}) require.NoError(t, err) diff --git a/pkg/mcp/mcp_test.go b/pkg/mcp/mcp_test.go index d3a3453..9a6d78d 100644 --- a/pkg/mcp/mcp_test.go +++ b/pkg/mcp/mcp_test.go @@ -7,6 +7,7 @@ import ( "forge.lthn.ai/core/go/pkg/core" "forge.lthn.ai/core/gui/pkg/clipboard" + "forge.lthn.ai/core/gui/pkg/notification" "github.com/modelcontextprotocol/go-sdk/mcp" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -34,7 +35,20 @@ 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 } + +type mockNotificationPlatform struct { + sendCalled bool + lastOpts notification.NotificationOptions +} + +func (m *mockNotificationPlatform) Send(opts notification.NotificationOptions) error { + m.sendCalled = true + m.lastOpts = opts + return nil +} +func (m *mockNotificationPlatform) RequestPermission() (bool, error) { return true, nil } +func (m *mockNotificationPlatform) CheckPermission() (bool, error) { return true, nil } func TestMCP_Good_ClipboardRoundTrip(t *testing.T) { c, err := core.New( @@ -53,6 +67,27 @@ func TestMCP_Good_ClipboardRoundTrip(t *testing.T) { assert.Equal(t, "hello", content.Text) } +func TestMCP_Good_DialogMessage(t *testing.T) { + mock := &mockNotificationPlatform{} + c, err := core.New( + core.WithService(notification.Register(mock)), + core.WithServiceLock(), + ) + require.NoError(t, err) + require.NoError(t, c.ServiceStartup(context.Background(), nil)) + + sub := New(c) + _, result, err := sub.dialogMessage(context.Background(), nil, DialogMessageInput{ + Title: "Alias", + Message: "Hello", + Kind: "error", + }) + require.NoError(t, err) + assert.True(t, result.Success) + assert.True(t, mock.sendCalled) + assert.Equal(t, notification.SeverityError, mock.lastOpts.Severity) +} + func TestMCP_Bad_NoServices(t *testing.T) { c, _ := core.New(core.WithServiceLock()) // Without any services, QUERY should return handled=false diff --git a/pkg/mcp/tools_notification.go b/pkg/mcp/tools_notification.go index 82c1161..5683efe 100644 --- a/pkg/mcp/tools_notification.go +++ b/pkg/mcp/tools_notification.go @@ -110,6 +110,38 @@ func (s *Subsystem) notificationClear(_ context.Context, _ *mcp.CallToolRequest, return nil, NotificationClearOutput{Success: true}, nil } +// --- dialog_message --- + +type DialogMessageInput struct { + Title string `json:"title"` + Message string `json:"message"` + Kind string `json:"kind,omitempty"` +} +type DialogMessageOutput struct { + Success bool `json:"success"` +} + +func (s *Subsystem) dialogMessage(_ context.Context, _ *mcp.CallToolRequest, input DialogMessageInput) (*mcp.CallToolResult, DialogMessageOutput, error) { + var severity notification.NotificationSeverity + switch input.Kind { + case "warning": + severity = notification.SeverityWarning + case "error": + severity = notification.SeverityError + default: + severity = notification.SeverityInfo + } + _, _, err := s.core.PERFORM(notification.TaskSend{Opts: notification.NotificationOptions{ + Title: input.Title, + Message: input.Message, + Severity: severity, + }}) + if err != nil { + return nil, DialogMessageOutput{}, err + } + return nil, DialogMessageOutput{Success: true}, nil +} + // --- Registration --- func (s *Subsystem) registerNotificationTools(server *mcp.Server) { @@ -118,4 +150,5 @@ func (s *Subsystem) registerNotificationTools(server *mcp.Server) { mcp.AddTool(server, &mcp.Tool{Name: "notification_permission_request", Description: "Request notification permission"}, s.notificationPermissionRequest) mcp.AddTool(server, &mcp.Tool{Name: "notification_permission_check", Description: "Check notification permission status"}, s.notificationPermissionCheck) mcp.AddTool(server, &mcp.Tool{Name: "notification_clear", Description: "Clear notifications when supported"}, s.notificationClear) + mcp.AddTool(server, &mcp.Tool{Name: "dialog_message", Description: "Show a message dialog using the notification pipeline"}, s.dialogMessage) } diff --git a/pkg/mcp/tools_webview.go b/pkg/mcp/tools_webview.go index 22b8c70..bea4d73 100644 --- a/pkg/mcp/tools_webview.go +++ b/pkg/mcp/tools_webview.go @@ -658,6 +658,7 @@ func (s *Subsystem) registerWebviewTools(server *mcp.Server) { mcp.AddTool(server, &mcp.Tool{Name: "webview_viewport", Description: "Set the webview viewport dimensions"}, s.webviewViewport) mcp.AddTool(server, &mcp.Tool{Name: "webview_console", Description: "Get captured console messages from a webview"}, s.webviewConsole) mcp.AddTool(server, &mcp.Tool{Name: "webview_console_clear", Description: "Clear captured console messages"}, s.webviewConsoleClear) + mcp.AddTool(server, &mcp.Tool{Name: "webview_clear_console", Description: "Alias for webview_console_clear"}, s.webviewConsoleClear) mcp.AddTool(server, &mcp.Tool{Name: "webview_query", Description: "Find a single DOM element by CSS selector"}, s.webviewQuery) mcp.AddTool(server, &mcp.Tool{Name: "webview_element_info", Description: "Get detailed information about a DOM element"}, s.webviewElementInfo) mcp.AddTool(server, &mcp.Tool{Name: "webview_query_all", Description: "Find all DOM elements matching a CSS selector"}, s.webviewQueryAll) diff --git a/pkg/mcp/tools_window.go b/pkg/mcp/tools_window.go index 7604ba2..678536c 100644 --- a/pkg/mcp/tools_window.go +++ b/pkg/mcp/tools_window.go @@ -407,6 +407,7 @@ func (s *Subsystem) registerWindowTools(server *mcp.Server) { 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_focus", Description: "Bring a window to the front"}, s.windowFocus) + mcp.AddTool(server, &mcp.Tool{Name: "focus_set", Description: "Alias for window_focus"}, 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) mcp.AddTool(server, &mcp.Tool{Name: "window_visibility", Description: "Show or hide a window"}, s.windowVisibility)