feat(dialog): expand with typed tasks, file options, 3 new MCP tools
Some checks failed
Security Scan / security (push) Failing after 25s
Some checks failed
Security Scan / security (push) Failing after 25s
- TaskOpenFileWithOptions, TaskSaveFileWithOptions with extended options - TaskInfo, TaskQuestion, TaskWarning, TaskError convenience tasks - OpenFileOptions: CanChooseDirectories, CanChooseFiles, ShowHiddenFiles - 3 new MCP tools: dialog_info, dialog_warning, dialog_error - 29 new tests with Good/Bad/Ugly coverage Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
0f6400052c
commit
31f6e0b67e
5 changed files with 555 additions and 47 deletions
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue