// Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 package userinput import ( "context" "errors" "fmt" "log" "sync" "time" "github.com/google/uuid" "github.com/wavetermdev/waveterm/pkg/blocklogger" "github.com/wavetermdev/waveterm/pkg/genconn" "github.com/wavetermdev/waveterm/pkg/util/utilfn" "github.com/wavetermdev/waveterm/pkg/wps" "github.com/wavetermdev/waveterm/pkg/wstore" ) var MainUserInputHandler = UserInputHandler{Channels: make(map[string](chan *UserInputResponse), 1)} type UserInputRequest struct { RequestId string `json:"requestid"` QueryText string `json:"querytext"` ResponseType string `json:"responsetype"` Title string `json:"title"` Markdown bool `json:"markdown"` TimeoutMs int `json:"timeoutms"` CheckBoxMsg string `json:"checkboxmsg"` PublicText bool `json:"publictext"` OkLabel string `json:"oklabel,omitempty"` CancelLabel string `json:"cancellabel,omitempty"` } type UserInputResponse struct { Type string `json:"type"` RequestId string `json:"requestid"` Text string `json:"text,omitempty"` Confirm bool `json:"confirm,omitempty"` ErrorMsg string `json:"errormsg,omitempty"` CheckboxStat bool `json:"checkboxstat,omitempty"` } type UserInputHandler struct { Lock sync.Mutex Channels map[string](chan *UserInputResponse) } func (ui *UserInputHandler) registerChannel() (string, chan *UserInputResponse) { ui.Lock.Lock() defer ui.Lock.Unlock() id := uuid.New().String() uich := make(chan *UserInputResponse, 1) ui.Channels[id] = uich return id, uich } func (ui *UserInputHandler) unregisterChannel(id string) { ui.Lock.Lock() defer ui.Lock.Unlock() delete(ui.Channels, id) } func (ui *UserInputHandler) sendRequestToFrontend(request *UserInputRequest, scopes []string) { wps.Broker.Publish(wps.WaveEvent{ Event: wps.Event_UserInput, Data: request, Scopes: scopes, }) } func determineScopes(ctx context.Context) ([]string, error) { connData := genconn.GetConnData(ctx) if connData == nil { return nil, fmt.Errorf("context did not contain connection info") } // resolve windowId from blockId tabId, err := wstore.DBFindTabForBlockId(ctx, connData.BlockId) if err != nil { return nil, fmt.Errorf("unabled to determine tab for route: %w", err) } workspaceId, err := wstore.DBFindWorkspaceForTabId(ctx, tabId) if err != nil { return nil, fmt.Errorf("unabled to determine workspace for route: %w", err) } windowId, err := wstore.DBFindWindowForWorkspaceId(ctx, workspaceId) if err != nil { return nil, fmt.Errorf("unabled to determine window for route: %w", err) } return []string{windowId}, nil } func GetUserInput(ctx context.Context, request *UserInputRequest) (*UserInputResponse, error) { id, uiCh := MainUserInputHandler.registerChannel() defer MainUserInputHandler.unregisterChannel(id) request.RequestId = id request.TimeoutMs = int(utilfn.TimeoutFromContext(ctx, 30*time.Second).Milliseconds()) scopes, scopesErr := determineScopes(ctx) if scopesErr != nil { log.Printf("user input scopes could not be found: %v", scopesErr) blocklogger.Infof(ctx, "user input scopes could not be found: %v", scopesErr) allWindows, err := wstore.DBGetAllOIDsByType(ctx, "window") if err != nil { blocklogger.Infof(ctx, "unable to find windows for user input: %v", err) return nil, fmt.Errorf("unable to find windows for user input: %v", err) } scopes = allWindows } MainUserInputHandler.sendRequestToFrontend(request, scopes) var response *UserInputResponse var err error select { case resp := <-uiCh: log.Printf("checking received: %v", resp.RequestId) response = resp case <-ctx.Done(): return nil, fmt.Errorf("timed out waiting for user input") } if response.ErrorMsg != "" { err = errors.New(response.ErrorMsg) } return response, err }