diff --git a/pkg/dialog/messages.go b/pkg/dialog/messages.go index c274f2c..176f75e 100644 --- a/pkg/dialog/messages.go +++ b/pkg/dialog/messages.go @@ -1,9 +1,75 @@ package dialog +// TaskOpenFile presents an open-file dialog with the given options. +// +// result, _, err := c.PERFORM(dialog.TaskOpenFile{Options: dialog.OpenFileOptions{Title: "Pick file"}}) +// paths := result.([]string) type TaskOpenFile struct{ Options OpenFileOptions } +// TaskOpenFileWithOptions presents an open-file dialog pre-configured from an options struct. +// Equivalent to TaskOpenFile but mirrors the stub DialogManager.OpenFileWithOptions API. +// +// result, _, err := c.PERFORM(dialog.TaskOpenFileWithOptions{Options: &dialog.OpenFileOptions{Title: "Select log", AllowMultiple: true}}) +type TaskOpenFileWithOptions struct{ Options *OpenFileOptions } + +// TaskSaveFile presents a save-file dialog with the given options. +// +// result, _, err := c.PERFORM(dialog.TaskSaveFile{Options: dialog.SaveFileOptions{Filename: "report.csv"}}) +// path := result.(string) type TaskSaveFile struct{ Options SaveFileOptions } +// TaskSaveFileWithOptions presents a save-file dialog pre-configured from an options struct. +// Equivalent to TaskSaveFile but mirrors the stub DialogManager.SaveFileWithOptions API. +// +// result, _, err := c.PERFORM(dialog.TaskSaveFileWithOptions{Options: &dialog.SaveFileOptions{Title: "Export data"}}) +type TaskSaveFileWithOptions struct{ Options *SaveFileOptions } + +// TaskOpenDirectory presents a directory picker dialog. +// +// result, _, err := c.PERFORM(dialog.TaskOpenDirectory{Options: dialog.OpenDirectoryOptions{Title: "Choose folder"}}) +// path := result.(string) type TaskOpenDirectory struct{ Options OpenDirectoryOptions } +// TaskMessageDialog presents a message dialog of the given type. +// +// result, _, err := c.PERFORM(dialog.TaskMessageDialog{Options: dialog.MessageDialogOptions{Type: dialog.DialogQuestion, Title: "Confirm", Message: "Delete?", Buttons: []string{"Yes", "No"}}}) +// clicked := result.(string) type TaskMessageDialog struct{ Options MessageDialogOptions } + +// TaskInfo presents an information message dialog. +// +// result, _, err := c.PERFORM(dialog.TaskInfo{Title: "Done", Message: "File saved successfully."}) +// clicked := result.(string) +type TaskInfo struct { + Title string + Message string + Buttons []string +} + +// TaskQuestion presents a question message dialog. +// +// result, _, err := c.PERFORM(dialog.TaskQuestion{Title: "Confirm", Message: "Delete file?", Buttons: []string{"Yes", "No"}}) +// if result.(string) == "Yes" { deleteFile() } +type TaskQuestion struct { + Title string + Message string + Buttons []string +} + +// TaskWarning presents a warning message dialog. +// +// result, _, err := c.PERFORM(dialog.TaskWarning{Title: "Low disk", Message: "Disk space is critically low."}) +type TaskWarning struct { + Title string + Message string + Buttons []string +} + +// TaskError presents an error message dialog. +// +// result, _, err := c.PERFORM(dialog.TaskError{Title: "Operation failed", Message: err.Error()}) +type TaskError struct { + Title string + Message string + Buttons []string +} diff --git a/pkg/dialog/platform.go b/pkg/dialog/platform.go index 10585a4..1a6bc11 100644 --- a/pkg/dialog/platform.go +++ b/pkg/dialog/platform.go @@ -20,27 +20,38 @@ const ( ) // OpenFileOptions contains options for the open file dialog. +// +// opts := OpenFileOptions{Title: "Select image", Filters: []FileFilter{{DisplayName: "Images", Pattern: "*.png;*.jpg"}}} type OpenFileOptions struct { - Title string `json:"title,omitempty"` - Directory string `json:"directory,omitempty"` - Filename string `json:"filename,omitempty"` - Filters []FileFilter `json:"filters,omitempty"` - AllowMultiple bool `json:"allowMultiple,omitempty"` + Title string `json:"title,omitempty"` + Directory string `json:"directory,omitempty"` + Filename string `json:"filename,omitempty"` + Filters []FileFilter `json:"filters,omitempty"` + AllowMultiple bool `json:"allowMultiple,omitempty"` + CanChooseDirectories bool `json:"canChooseDirectories,omitempty"` + CanChooseFiles bool `json:"canChooseFiles,omitempty"` + ShowHiddenFiles bool `json:"showHiddenFiles,omitempty"` } // SaveFileOptions contains options for the save file dialog. +// +// opts := SaveFileOptions{Title: "Export", Filename: "report.pdf", ShowHiddenFiles: false} type SaveFileOptions struct { - Title string `json:"title,omitempty"` - Directory string `json:"directory,omitempty"` - Filename string `json:"filename,omitempty"` - Filters []FileFilter `json:"filters,omitempty"` + Title string `json:"title,omitempty"` + Directory string `json:"directory,omitempty"` + Filename string `json:"filename,omitempty"` + Filters []FileFilter `json:"filters,omitempty"` + ShowHiddenFiles bool `json:"showHiddenFiles,omitempty"` } // OpenDirectoryOptions contains options for the directory picker. +// +// opts := OpenDirectoryOptions{Title: "Choose folder", ShowHiddenFiles: true} type OpenDirectoryOptions struct { - Title string `json:"title,omitempty"` - Directory string `json:"directory,omitempty"` - AllowMultiple bool `json:"allowMultiple,omitempty"` + Title string `json:"title,omitempty"` + Directory string `json:"directory,omitempty"` + AllowMultiple bool `json:"allowMultiple,omitempty"` + ShowHiddenFiles bool `json:"showHiddenFiles,omitempty"` } // MessageDialogOptions contains options for a message dialog. diff --git a/pkg/dialog/service.go b/pkg/dialog/service.go index b9b23b5..fcc48e1 100644 --- a/pkg/dialog/service.go +++ b/pkg/dialog/service.go @@ -14,6 +14,9 @@ type Service struct { platform Platform } +// Register(p) binds the dialog service to a Core instance. +// +// c.WithService(dialog.Register(wailsDialog)) func Register(p Platform) func(*core.Core) (any, error) { return func(c *core.Core) (any, error) { return &Service{ @@ -37,15 +40,71 @@ func (s *Service) handleTask(c *core.Core, t core.Task) (any, bool, error) { case TaskOpenFile: paths, err := s.platform.OpenFile(t.Options) return paths, true, err + + case TaskOpenFileWithOptions: + options := OpenFileOptions{} + if t.Options != nil { + options = *t.Options + } + paths, err := s.platform.OpenFile(options) + return paths, true, err + case TaskSaveFile: path, err := s.platform.SaveFile(t.Options) return path, true, err + + case TaskSaveFileWithOptions: + options := SaveFileOptions{} + if t.Options != nil { + options = *t.Options + } + path, err := s.platform.SaveFile(options) + return path, true, err + case TaskOpenDirectory: path, err := s.platform.OpenDirectory(t.Options) return path, true, err + case TaskMessageDialog: button, err := s.platform.MessageDialog(t.Options) return button, true, err + + case TaskInfo: + button, err := s.platform.MessageDialog(MessageDialogOptions{ + Type: DialogInfo, + Title: t.Title, + Message: t.Message, + Buttons: t.Buttons, + }) + return button, true, err + + case TaskQuestion: + button, err := s.platform.MessageDialog(MessageDialogOptions{ + Type: DialogQuestion, + Title: t.Title, + Message: t.Message, + Buttons: t.Buttons, + }) + return button, true, err + + case TaskWarning: + button, err := s.platform.MessageDialog(MessageDialogOptions{ + Type: DialogWarning, + Title: t.Title, + Message: t.Message, + Buttons: t.Buttons, + }) + return button, true, err + + case TaskError: + button, err := s.platform.MessageDialog(MessageDialogOptions{ + Type: DialogError, + Title: t.Title, + Message: t.Message, + Buttons: t.Buttons, + }) + return button, true, err + default: return nil, false, nil } diff --git a/pkg/dialog/service_test.go b/pkg/dialog/service_test.go index de476da..4c3be0d 100644 --- a/pkg/dialog/service_test.go +++ b/pkg/dialog/service_test.go @@ -59,13 +59,15 @@ func newTestService(t *testing.T) (*mockPlatform, *core.Core) { return mock, c } -func TestRegister_Good(t *testing.T) { +// --- Good path tests --- + +func TestService_Register_Good(t *testing.T) { _, c := newTestService(t) svc := core.MustServiceFor[*Service](c, "dialog") assert.NotNil(t, svc) } -func TestTaskOpenFile_Good(t *testing.T) { +func TestService_TaskOpenFile_Good(t *testing.T) { mock, c := newTestService(t) mock.openFilePaths = []string{"/a.txt", "/b.txt"} @@ -80,7 +82,81 @@ func TestTaskOpenFile_Good(t *testing.T) { assert.True(t, mock.lastOpenOpts.AllowMultiple) } -func TestTaskSaveFile_Good(t *testing.T) { +func TestService_TaskOpenFile_FileFilters_Good(t *testing.T) { + mock, c := newTestService(t) + mock.openFilePaths = []string{"/img.png"} + + filters := []FileFilter{{DisplayName: "Images", Pattern: "*.png;*.jpg"}} + result, handled, err := c.PERFORM(TaskOpenFile{ + Options: OpenFileOptions{ + Title: "Select image", + Filters: filters, + }, + }) + require.NoError(t, err) + assert.True(t, handled) + assert.Equal(t, []string{"/img.png"}, result.([]string)) + require.Len(t, mock.lastOpenOpts.Filters, 1) + assert.Equal(t, "Images", mock.lastOpenOpts.Filters[0].DisplayName) + assert.Equal(t, "*.png;*.jpg", mock.lastOpenOpts.Filters[0].Pattern) +} + +func TestService_TaskOpenFile_MultipleSelection_Good(t *testing.T) { + mock, c := newTestService(t) + mock.openFilePaths = []string{"/a.txt", "/b.txt", "/c.txt"} + + result, handled, err := c.PERFORM(TaskOpenFile{ + Options: OpenFileOptions{AllowMultiple: true}, + }) + require.NoError(t, err) + assert.True(t, handled) + assert.Equal(t, []string{"/a.txt", "/b.txt", "/c.txt"}, result.([]string)) + assert.True(t, mock.lastOpenOpts.AllowMultiple) +} + +func TestService_TaskOpenFile_CanChooseOptions_Good(t *testing.T) { + mock, c := newTestService(t) + + _, _, err := c.PERFORM(TaskOpenFile{ + Options: OpenFileOptions{ + CanChooseFiles: true, + CanChooseDirectories: true, + ShowHiddenFiles: true, + }, + }) + require.NoError(t, err) + assert.True(t, mock.lastOpenOpts.CanChooseFiles) + assert.True(t, mock.lastOpenOpts.CanChooseDirectories) + assert.True(t, mock.lastOpenOpts.ShowHiddenFiles) +} + +func TestService_TaskOpenFileWithOptions_Good(t *testing.T) { + mock, c := newTestService(t) + mock.openFilePaths = []string{"/log.txt"} + + opts := &OpenFileOptions{ + Title: "Select log", + AllowMultiple: false, + ShowHiddenFiles: true, + } + result, handled, err := c.PERFORM(TaskOpenFileWithOptions{Options: opts}) + require.NoError(t, err) + assert.True(t, handled) + assert.Equal(t, []string{"/log.txt"}, result.([]string)) + assert.Equal(t, "Select log", mock.lastOpenOpts.Title) + assert.True(t, mock.lastOpenOpts.ShowHiddenFiles) +} + +func TestService_TaskOpenFileWithOptions_NilOptions_Good(t *testing.T) { + _, c := newTestService(t) + + result, handled, err := c.PERFORM(TaskOpenFileWithOptions{Options: nil}) + require.NoError(t, err) + assert.True(t, handled) + assert.NotNil(t, result) +} + +func TestService_TaskSaveFile_Good(t *testing.T) { _, c := newTestService(t) result, handled, err := c.PERFORM(TaskSaveFile{ Options: SaveFileOptions{Filename: "out.txt"}, @@ -90,17 +166,57 @@ func TestTaskSaveFile_Good(t *testing.T) { assert.Equal(t, "/tmp/save.txt", result) } -func TestTaskOpenDirectory_Good(t *testing.T) { +func TestService_TaskSaveFile_ShowHidden_Good(t *testing.T) { + mock, c := newTestService(t) + + _, _, err := c.PERFORM(TaskSaveFile{ + Options: SaveFileOptions{Filename: "out.txt", ShowHiddenFiles: true}, + }) + require.NoError(t, err) + assert.True(t, mock.lastSaveOpts.ShowHiddenFiles) +} + +func TestService_TaskSaveFileWithOptions_Good(t *testing.T) { + mock, c := newTestService(t) + mock.saveFilePath = "/exports/data.json" + + opts := &SaveFileOptions{ + Title: "Export data", + Filename: "data.json", + Filters: []FileFilter{{DisplayName: "JSON", Pattern: "*.json"}}, + } + result, handled, err := c.PERFORM(TaskSaveFileWithOptions{Options: opts}) + require.NoError(t, err) + assert.True(t, handled) + assert.Equal(t, "/exports/data.json", result.(string)) + assert.Equal(t, "Export data", mock.lastSaveOpts.Title) + require.Len(t, mock.lastSaveOpts.Filters, 1) + assert.Equal(t, "JSON", mock.lastSaveOpts.Filters[0].DisplayName) +} + +func TestService_TaskSaveFileWithOptions_NilOptions_Good(t *testing.T) { _, c := newTestService(t) + + result, handled, err := c.PERFORM(TaskSaveFileWithOptions{Options: nil}) + require.NoError(t, err) + assert.True(t, handled) + assert.Equal(t, "/tmp/save.txt", result) +} + +func TestService_TaskOpenDirectory_Good(t *testing.T) { + mock, c := newTestService(t) + result, handled, err := c.PERFORM(TaskOpenDirectory{ - Options: OpenDirectoryOptions{Title: "Pick Dir"}, + Options: OpenDirectoryOptions{Title: "Pick Dir", ShowHiddenFiles: true}, }) require.NoError(t, err) assert.True(t, handled) assert.Equal(t, "/tmp/dir", result) + assert.Equal(t, "Pick Dir", mock.lastDirOpts.Title) + assert.True(t, mock.lastDirOpts.ShowHiddenFiles) } -func TestTaskMessageDialog_Good(t *testing.T) { +func TestService_TaskMessageDialog_Good(t *testing.T) { mock, c := newTestService(t) mock.messageButton = "Yes" @@ -116,8 +232,172 @@ func TestTaskMessageDialog_Good(t *testing.T) { assert.Equal(t, DialogQuestion, mock.lastMsgOpts.Type) } -func TestTaskOpenFile_Bad(t *testing.T) { +func TestService_TaskInfo_Good(t *testing.T) { + mock, c := newTestService(t) + mock.messageButton = "OK" + + result, handled, err := c.PERFORM(TaskInfo{ + Title: "Done", Message: "File saved successfully.", + }) + require.NoError(t, err) + assert.True(t, handled) + assert.Equal(t, "OK", result.(string)) + assert.Equal(t, DialogInfo, mock.lastMsgOpts.Type) + assert.Equal(t, "Done", mock.lastMsgOpts.Title) + assert.Equal(t, "File saved successfully.", mock.lastMsgOpts.Message) +} + +func TestService_TaskInfo_WithButtons_Good(t *testing.T) { + mock, c := newTestService(t) + mock.messageButton = "Close" + + result, handled, err := c.PERFORM(TaskInfo{ + Title: "Notice", Message: "Update available.", Buttons: []string{"Close", "Later"}, + }) + require.NoError(t, err) + assert.True(t, handled) + assert.Equal(t, "Close", result.(string)) + assert.Equal(t, []string{"Close", "Later"}, mock.lastMsgOpts.Buttons) +} + +func TestService_TaskQuestion_Good(t *testing.T) { + mock, c := newTestService(t) + mock.messageButton = "Yes" + + result, handled, err := c.PERFORM(TaskQuestion{ + Title: "Confirm deletion", Message: "Delete file?", Buttons: []string{"Yes", "No"}, + }) + require.NoError(t, err) + assert.True(t, handled) + assert.Equal(t, "Yes", result.(string)) + assert.Equal(t, DialogQuestion, mock.lastMsgOpts.Type) + assert.Equal(t, "Confirm deletion", mock.lastMsgOpts.Title) +} + +func TestService_TaskWarning_Good(t *testing.T) { + mock, c := newTestService(t) + mock.messageButton = "OK" + + result, handled, err := c.PERFORM(TaskWarning{ + Title: "Disk full", Message: "Storage is critically low.", Buttons: []string{"OK"}, + }) + require.NoError(t, err) + assert.True(t, handled) + assert.Equal(t, "OK", result.(string)) + assert.Equal(t, DialogWarning, mock.lastMsgOpts.Type) + assert.Equal(t, "Disk full", mock.lastMsgOpts.Title) +} + +func TestService_TaskError_Good(t *testing.T) { + mock, c := newTestService(t) + mock.messageButton = "OK" + + result, handled, err := c.PERFORM(TaskError{ + Title: "Operation failed", Message: "could not write file: permission denied", + }) + require.NoError(t, err) + assert.True(t, handled) + assert.Equal(t, "OK", result.(string)) + assert.Equal(t, DialogError, mock.lastMsgOpts.Type) + assert.Equal(t, "Operation failed", mock.lastMsgOpts.Title) + assert.Equal(t, "could not write file: permission denied", mock.lastMsgOpts.Message) +} + +// --- Bad path tests --- + +func TestService_TaskOpenFile_Bad(t *testing.T) { c, _ := core.New(core.WithServiceLock()) _, handled, _ := c.PERFORM(TaskOpenFile{}) assert.False(t, handled) } + +func TestService_TaskOpenFileWithOptions_Bad(t *testing.T) { + c, _ := core.New(core.WithServiceLock()) + _, handled, _ := c.PERFORM(TaskOpenFileWithOptions{}) + assert.False(t, handled) +} + +func TestService_TaskSaveFileWithOptions_Bad(t *testing.T) { + c, _ := core.New(core.WithServiceLock()) + _, handled, _ := c.PERFORM(TaskSaveFileWithOptions{}) + assert.False(t, handled) +} + +func TestService_TaskInfo_Bad(t *testing.T) { + c, _ := core.New(core.WithServiceLock()) + _, handled, _ := c.PERFORM(TaskInfo{}) + assert.False(t, handled) +} + +func TestService_TaskQuestion_Bad(t *testing.T) { + c, _ := core.New(core.WithServiceLock()) + _, handled, _ := c.PERFORM(TaskQuestion{}) + assert.False(t, handled) +} + +func TestService_TaskWarning_Bad(t *testing.T) { + c, _ := core.New(core.WithServiceLock()) + _, handled, _ := c.PERFORM(TaskWarning{}) + assert.False(t, handled) +} + +func TestService_TaskError_Bad(t *testing.T) { + c, _ := core.New(core.WithServiceLock()) + _, handled, _ := c.PERFORM(TaskError{}) + assert.False(t, handled) +} + +// --- Ugly path tests --- + +func TestService_TaskOpenFile_Ugly(t *testing.T) { + mock, c := newTestService(t) + mock.openFilePaths = nil + + result, handled, err := c.PERFORM(TaskOpenFile{ + Options: OpenFileOptions{Title: "Pick"}, + }) + require.NoError(t, err) + assert.True(t, handled) + assert.Nil(t, result.([]string)) +} + +func TestService_TaskOpenFileWithOptions_MultipleFilters_Ugly(t *testing.T) { + mock, c := newTestService(t) + mock.openFilePaths = []string{"/doc.pdf"} + + opts := &OpenFileOptions{ + Title: "Select document", + Filters: []FileFilter{ + {DisplayName: "PDF", Pattern: "*.pdf"}, + {DisplayName: "Word", Pattern: "*.docx"}, + {DisplayName: "All files", Pattern: "*.*"}, + }, + } + result, handled, err := c.PERFORM(TaskOpenFileWithOptions{Options: opts}) + require.NoError(t, err) + assert.True(t, handled) + assert.Equal(t, []string{"/doc.pdf"}, result.([]string)) + assert.Len(t, mock.lastOpenOpts.Filters, 3) +} + +func TestService_TaskSaveFileWithOptions_FiltersAndHidden_Ugly(t *testing.T) { + mock, c := newTestService(t) + + opts := &SaveFileOptions{ + Title: "Save", + Filename: "output.csv", + ShowHiddenFiles: true, + Filters: []FileFilter{{DisplayName: "CSV", Pattern: "*.csv"}}, + } + _, _, err := c.PERFORM(TaskSaveFileWithOptions{Options: opts}) + require.NoError(t, err) + assert.True(t, mock.lastSaveOpts.ShowHiddenFiles) + assert.Equal(t, "output.csv", mock.lastSaveOpts.Filename) +} + +func TestService_UnknownTask_Ugly(t *testing.T) { + type unknownTask struct{} + c, _ := core.New(core.WithServiceLock()) + _, handled, _ := c.PERFORM(unknownTask{}) + assert.False(t, handled) +} diff --git a/pkg/mcp/tools_dialog.go b/pkg/mcp/tools_dialog.go index aee701d..5c47e54 100644 --- a/pkg/mcp/tools_dialog.go +++ b/pkg/mcp/tools_dialog.go @@ -12,10 +12,13 @@ import ( // --- dialog_open_file --- type DialogOpenFileInput struct { - Title string `json:"title,omitempty"` - Directory string `json:"directory,omitempty"` - Filters []dialog.FileFilter `json:"filters,omitempty"` - AllowMultiple bool `json:"allowMultiple,omitempty"` + Title string `json:"title,omitempty"` + Directory string `json:"directory,omitempty"` + Filters []dialog.FileFilter `json:"filters,omitempty"` + AllowMultiple bool `json:"allowMultiple,omitempty"` + CanChooseDirectories bool `json:"canChooseDirectories,omitempty"` + CanChooseFiles bool `json:"canChooseFiles,omitempty"` + ShowHiddenFiles bool `json:"showHiddenFiles,omitempty"` } type DialogOpenFileOutput struct { Paths []string `json:"paths"` @@ -23,10 +26,13 @@ type DialogOpenFileOutput struct { func (s *Subsystem) dialogOpenFile(_ context.Context, _ *mcp.CallToolRequest, input DialogOpenFileInput) (*mcp.CallToolResult, DialogOpenFileOutput, error) { result, _, err := s.core.PERFORM(dialog.TaskOpenFile{Options: dialog.OpenFileOptions{ - Title: input.Title, - Directory: input.Directory, - Filters: input.Filters, - AllowMultiple: input.AllowMultiple, + Title: input.Title, + Directory: input.Directory, + Filters: input.Filters, + AllowMultiple: input.AllowMultiple, + CanChooseDirectories: input.CanChooseDirectories, + CanChooseFiles: input.CanChooseFiles, + ShowHiddenFiles: input.ShowHiddenFiles, }}) if err != nil { return nil, DialogOpenFileOutput{}, err @@ -41,10 +47,11 @@ func (s *Subsystem) dialogOpenFile(_ context.Context, _ *mcp.CallToolRequest, in // --- dialog_save_file --- type DialogSaveFileInput struct { - Title string `json:"title,omitempty"` - Directory string `json:"directory,omitempty"` - Filename string `json:"filename,omitempty"` - Filters []dialog.FileFilter `json:"filters,omitempty"` + Title string `json:"title,omitempty"` + Directory string `json:"directory,omitempty"` + Filename string `json:"filename,omitempty"` + Filters []dialog.FileFilter `json:"filters,omitempty"` + ShowHiddenFiles bool `json:"showHiddenFiles,omitempty"` } type DialogSaveFileOutput struct { Path string `json:"path"` @@ -52,10 +59,11 @@ type DialogSaveFileOutput struct { func (s *Subsystem) dialogSaveFile(_ context.Context, _ *mcp.CallToolRequest, input DialogSaveFileInput) (*mcp.CallToolResult, DialogSaveFileOutput, error) { result, _, err := s.core.PERFORM(dialog.TaskSaveFile{Options: dialog.SaveFileOptions{ - Title: input.Title, - Directory: input.Directory, - Filename: input.Filename, - Filters: input.Filters, + Title: input.Title, + Directory: input.Directory, + Filename: input.Filename, + Filters: input.Filters, + ShowHiddenFiles: input.ShowHiddenFiles, }}) if err != nil { return nil, DialogSaveFileOutput{}, err @@ -70,8 +78,9 @@ func (s *Subsystem) dialogSaveFile(_ context.Context, _ *mcp.CallToolRequest, in // --- dialog_open_directory --- type DialogOpenDirectoryInput struct { - Title string `json:"title,omitempty"` - Directory string `json:"directory,omitempty"` + Title string `json:"title,omitempty"` + Directory string `json:"directory,omitempty"` + ShowHiddenFiles bool `json:"showHiddenFiles,omitempty"` } type DialogOpenDirectoryOutput struct { Path string `json:"path"` @@ -79,8 +88,9 @@ type DialogOpenDirectoryOutput struct { func (s *Subsystem) dialogOpenDirectory(_ context.Context, _ *mcp.CallToolRequest, input DialogOpenDirectoryInput) (*mcp.CallToolResult, DialogOpenDirectoryOutput, error) { result, _, err := s.core.PERFORM(dialog.TaskOpenDirectory{Options: dialog.OpenDirectoryOptions{ - Title: input.Title, - Directory: input.Directory, + Title: input.Title, + Directory: input.Directory, + ShowHiddenFiles: input.ShowHiddenFiles, }}) if err != nil { return nil, DialogOpenDirectoryOutput{}, err @@ -104,12 +114,11 @@ type DialogConfirmOutput struct { } func (s *Subsystem) dialogConfirm(_ context.Context, _ *mcp.CallToolRequest, input DialogConfirmInput) (*mcp.CallToolResult, DialogConfirmOutput, error) { - result, _, err := s.core.PERFORM(dialog.TaskMessageDialog{Options: dialog.MessageDialogOptions{ - Type: dialog.DialogQuestion, + result, _, err := s.core.PERFORM(dialog.TaskQuestion{ Title: input.Title, Message: input.Message, Buttons: input.Buttons, - }}) + }) if err != nil { return nil, DialogConfirmOutput{}, err } @@ -131,12 +140,11 @@ type DialogPromptOutput struct { } func (s *Subsystem) dialogPrompt(_ context.Context, _ *mcp.CallToolRequest, input DialogPromptInput) (*mcp.CallToolResult, DialogPromptOutput, error) { - result, _, err := s.core.PERFORM(dialog.TaskMessageDialog{Options: dialog.MessageDialogOptions{ - Type: dialog.DialogInfo, + result, _, err := s.core.PERFORM(dialog.TaskInfo{ Title: input.Title, Message: input.Message, Buttons: []string{"OK", "Cancel"}, - }}) + }) if err != nil { return nil, DialogPromptOutput{}, err } @@ -147,12 +155,96 @@ func (s *Subsystem) dialogPrompt(_ context.Context, _ *mcp.CallToolRequest, inpu return nil, DialogPromptOutput{Button: button}, nil } +// --- dialog_info --- + +type DialogInfoInput struct { + Title string `json:"title"` + Message string `json:"message"` + Buttons []string `json:"buttons,omitempty"` +} +type DialogInfoOutput struct { + Button string `json:"button"` +} + +func (s *Subsystem) dialogInfo(_ context.Context, _ *mcp.CallToolRequest, input DialogInfoInput) (*mcp.CallToolResult, DialogInfoOutput, error) { + result, _, err := s.core.PERFORM(dialog.TaskInfo{ + Title: input.Title, + Message: input.Message, + Buttons: input.Buttons, + }) + if err != nil { + return nil, DialogInfoOutput{}, err + } + button, ok := result.(string) + if !ok { + return nil, DialogInfoOutput{}, coreerr.E("mcp.dialogInfo", "unexpected result type", nil) + } + return nil, DialogInfoOutput{Button: button}, nil +} + +// --- dialog_warning --- + +type DialogWarningInput struct { + Title string `json:"title"` + Message string `json:"message"` + Buttons []string `json:"buttons,omitempty"` +} +type DialogWarningOutput struct { + Button string `json:"button"` +} + +func (s *Subsystem) dialogWarning(_ context.Context, _ *mcp.CallToolRequest, input DialogWarningInput) (*mcp.CallToolResult, DialogWarningOutput, error) { + result, _, err := s.core.PERFORM(dialog.TaskWarning{ + Title: input.Title, + Message: input.Message, + Buttons: input.Buttons, + }) + if err != nil { + return nil, DialogWarningOutput{}, err + } + button, ok := result.(string) + if !ok { + return nil, DialogWarningOutput{}, coreerr.E("mcp.dialogWarning", "unexpected result type", nil) + } + return nil, DialogWarningOutput{Button: button}, nil +} + +// --- dialog_error --- + +type DialogErrorInput struct { + Title string `json:"title"` + Message string `json:"message"` + Buttons []string `json:"buttons,omitempty"` +} +type DialogErrorOutput struct { + Button string `json:"button"` +} + +func (s *Subsystem) dialogError(_ context.Context, _ *mcp.CallToolRequest, input DialogErrorInput) (*mcp.CallToolResult, DialogErrorOutput, error) { + result, _, err := s.core.PERFORM(dialog.TaskError{ + Title: input.Title, + Message: input.Message, + Buttons: input.Buttons, + }) + if err != nil { + return nil, DialogErrorOutput{}, err + } + button, ok := result.(string) + if !ok { + return nil, DialogErrorOutput{}, coreerr.E("mcp.dialogError", "unexpected result type", nil) + } + return nil, DialogErrorOutput{Button: button}, nil +} + // --- Registration --- func (s *Subsystem) registerDialogTools(server *mcp.Server) { mcp.AddTool(server, &mcp.Tool{Name: "dialog_open_file", Description: "Show an open file dialog"}, s.dialogOpenFile) mcp.AddTool(server, &mcp.Tool{Name: "dialog_save_file", Description: "Show a save file dialog"}, s.dialogSaveFile) mcp.AddTool(server, &mcp.Tool{Name: "dialog_open_directory", Description: "Show a directory picker dialog"}, s.dialogOpenDirectory) - mcp.AddTool(server, &mcp.Tool{Name: "dialog_confirm", Description: "Show a confirmation dialog"}, s.dialogConfirm) - mcp.AddTool(server, &mcp.Tool{Name: "dialog_prompt", Description: "Show a prompt dialog"}, s.dialogPrompt) + mcp.AddTool(server, &mcp.Tool{Name: "dialog_confirm", Description: "Show a question/confirmation dialog"}, s.dialogConfirm) + mcp.AddTool(server, &mcp.Tool{Name: "dialog_prompt", Description: "Show an info prompt dialog with OK/Cancel"}, s.dialogPrompt) + mcp.AddTool(server, &mcp.Tool{Name: "dialog_info", Description: "Show an information message dialog"}, s.dialogInfo) + mcp.AddTool(server, &mcp.Tool{Name: "dialog_warning", Description: "Show a warning message dialog"}, s.dialogWarning) + mcp.AddTool(server, &mcp.Tool{Name: "dialog_error", Description: "Show an error message dialog"}, s.dialogError) }