feat(dialog): expand with typed tasks, file options, 3 new MCP tools
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:
Claude 2026-03-31 14:55:55 +01:00
parent 0f6400052c
commit 31f6e0b67e
No known key found for this signature in database
GPG key ID: AF404715446AEB41
5 changed files with 555 additions and 47 deletions

View file

@ -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
}

View file

@ -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.

View file

@ -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
}

View file

@ -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)
}

View file

@ -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)
}