diff --git a/src/app/common/elements/index.tsx b/src/app/common/elements/index.tsx index 30895855..7096d328 100644 --- a/src/app/common/elements/index.tsx +++ b/src/app/common/elements/index.tsx @@ -13,7 +13,6 @@ export { NumberField } from "./numberfield"; export { PasswordField } from "./passwordfield"; export { ResizableSidebar } from "./resizablesidebar"; export { SettingsError } from "./settingserror"; -export { ShowWaveShellInstallPrompt } from "./showwaveshellinstallprompt"; export { Status } from "./status"; export { TextField } from "./textfield"; export { Toggle } from "./toggle"; diff --git a/src/app/common/elements/showwaveshellinstallprompt.tsx b/src/app/common/elements/showwaveshellinstallprompt.tsx deleted file mode 100644 index 647cd74d..00000000 --- a/src/app/common/elements/showwaveshellinstallprompt.tsx +++ /dev/null @@ -1,28 +0,0 @@ -// Copyright 2023, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { GlobalModel } from "@/models"; -import * as appconst from "@/app/appconst"; - -function ShowWaveShellInstallPrompt(callbackFn: () => void) { - let message: string = ` -In order to use Wave's advanced features like unified history and persistent sessions, Wave installs a small, open-source helper program called WaveShell on your remote machine. WaveShell does not open any external ports and only communicates with your *local* Wave terminal instance over ssh. For more information please see [the docs](https://docs.waveterm.dev/reference/waveshell). - `; - message = message.trim(); - let prtn = GlobalModel.showAlert({ - message: message, - confirm: true, - markdown: true, - confirmflag: appconst.ConfirmKey_HideShellPrompt, - }); - prtn.then((confirm) => { - if (!confirm) { - return; - } - if (callbackFn) { - callbackFn(); - } - }); -} - -export { ShowWaveShellInstallPrompt }; diff --git a/src/app/common/modals/createremoteconn.tsx b/src/app/common/modals/createremoteconn.tsx index 297e6797..56f4e6ab 100644 --- a/src/app/common/modals/createremoteconn.tsx +++ b/src/app/common/modals/createremoteconn.tsx @@ -7,16 +7,7 @@ import * as mobx from "mobx"; import { boundMethod } from "autobind-decorator"; import { If } from "tsx-control-statements/components"; import { GlobalModel, GlobalCommandRunner, RemotesModel } from "@/models"; -import { - Modal, - TextField, - NumberField, - InputDecoration, - Dropdown, - PasswordField, - Tooltip, - ShowWaveShellInstallPrompt, -} from "@/elements"; +import { Modal, TextField, NumberField, InputDecoration, Dropdown, PasswordField, Tooltip } from "@/elements"; import * as util from "@/util/util"; import "./createremoteconn.less"; @@ -73,12 +64,7 @@ class CreateRemoteConnModal extends React.Component<{}, {}> { } @boundMethod - handleOk(): void { - ShowWaveShellInstallPrompt(this.submitRemote); - } - - @boundMethod - submitRemote(): void { + handleSubmitRemote(): void { mobx.action(() => { this.errorStr.set(null); })(); @@ -275,7 +261,7 @@ class CreateRemoteConnModal extends React.Component<{}, {}> { { value: "none", label: "none" }, { value: "key", label: "key" }, { value: "password", label: "password" }, - { value: "key+password", label: "key+password" }, + { value: "key+password", label: "key+passphrase" }, ]} value={this.tempAuthMode.get()} onChange={(val: string) => { @@ -288,17 +274,18 @@ class CreateRemoteConnModal extends React.Component<{}, {}> { message={ } @@ -374,7 +361,7 @@ class CreateRemoteConnModal extends React.Component<{}, {}> {
Error: {this.getErrorStr()}
- + ); } diff --git a/src/app/common/modals/userinput.less b/src/app/common/modals/userinput.less index f86c11ca..fd6b3e6f 100644 --- a/src/app/common/modals/userinput.less +++ b/src/app/common/modals/userinput.less @@ -3,10 +3,14 @@ .wave-modal-content { .wave-modal-body { - padding: 20px 20px; + padding: 0px 20px 0px 20px; - .userinput-query { - margin-bottom: 10px; + .wave-modal-dialog { + padding: 20px 0px 20px 0px; + + .userinput-query { + margin-bottom: 10px; + } } } } diff --git a/src/app/common/modals/userinput.tsx b/src/app/common/modals/userinput.tsx index 47a3c55a..19e0bbee 100644 --- a/src/app/common/modals/userinput.tsx +++ b/src/app/common/modals/userinput.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import { GlobalModel } from "@/models"; import { Choose, When, If } from "tsx-control-statements/components"; -import { Modal, PasswordField, Markdown } from "@/elements"; +import { Modal, PasswordField, Markdown, Checkbox } from "@/elements"; import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil"; import "./userinput.less"; @@ -9,6 +9,7 @@ import "./userinput.less"; export const UserInputModal = (userInputRequest: UserInputRequest) => { const [responseText, setResponseText] = React.useState(""); const [countdown, setCountdown] = React.useState(Math.floor(userInputRequest.timeoutms / 1000)); + const checkboxStatus = React.useRef(false); const handleSendCancel = React.useCallback(() => { GlobalModel.sendUserInput({ @@ -24,16 +25,19 @@ export const UserInputModal = (userInputRequest: UserInputRequest) => { type: "userinputresp", requestid: userInputRequest.requestid, text: responseText, + checkboxstat: checkboxStatus.current, }); GlobalModel.remotesModel.closeModal(); }, [responseText, userInputRequest]); const handleSendConfirm = React.useCallback( (response: boolean) => { + console.log(`checkbox ${checkboxStatus}\n\n`); GlobalModel.sendUserInput({ type: "userinputresp", requestid: userInputRequest.requestid, confirm: response, + checkboxstat: checkboxStatus.current, }); GlobalModel.remotesModel.closeModal(); }, @@ -71,23 +75,32 @@ export const UserInputModal = (userInputRequest: UserInputRequest) => {
-
- - - - {userInputRequest.querytext} +
+
+ + + + {userInputRequest.querytext} +
+ + + handleTextKeyDown(e)} + /> + +
- - - handleTextKeyDown(e)} - /> - - + + (checkboxStatus.current = !checkboxStatus.current)} + label={userInputRequest.checkboxmsg} + className="checkbox-text" + /> +
diff --git a/src/app/common/modals/viewremoteconndetail.less b/src/app/common/modals/viewremoteconndetail.less index aee472d8..44d47417 100644 --- a/src/app/common/modals/viewremoteconndetail.less +++ b/src/app/common/modals/viewremoteconndetail.less @@ -1,5 +1,6 @@ .rconndetail-modal { - width: 631px; + width: auto; + max-width: 80vw; min-height: 565px; .wave-modal-content { diff --git a/src/app/connections/connections.tsx b/src/app/connections/connections.tsx index f5c715b8..9cc73462 100644 --- a/src/app/connections/connections.tsx +++ b/src/app/connections/connections.tsx @@ -8,7 +8,7 @@ import { boundMethod } from "autobind-decorator"; import { If, For } from "tsx-control-statements/components"; import cn from "classnames"; import { GlobalModel, RemotesModel, GlobalCommandRunner } from "@/models"; -import { Button, Status, ShowWaveShellInstallPrompt } from "@/common/elements"; +import { Button, Status } from "@/common/elements"; import * as util from "@/util/util"; import "./connections.less"; @@ -71,14 +71,9 @@ class ConnectionsView extends React.Component<{ model: RemotesModel }, { hovered GlobalModel.remotesModel.openAddModal({ remoteedit: true }); } - @boundMethod - importSshConfig(): void { - GlobalCommandRunner.importSshConfig(); - } - @boundMethod handleImportSshConfig(): void { - ShowWaveShellInstallPrompt(this.importSshConfig); + GlobalCommandRunner.importSshConfig(); } @boundMethod diff --git a/src/types/custom.d.ts b/src/types/custom.d.ts index 0c5018b8..4395ca3b 100644 --- a/src/types/custom.d.ts +++ b/src/types/custom.d.ts @@ -666,6 +666,7 @@ declare global { title: string; markdown: boolean; timeoutms: number; + checkboxmsg: string; }; type UserInputResponsePacket = { @@ -674,6 +675,7 @@ declare global { text?: string; confirm?: boolean; errormsg?: string; + checkboxstat?: boolean; }; type RenderModeType = "normal" | "collapsed" | "expanded"; diff --git a/wavesrv/pkg/cmdrunner/cmdrunner.go b/wavesrv/pkg/cmdrunner/cmdrunner.go index 9291203a..3af7379b 100644 --- a/wavesrv/pkg/cmdrunner/cmdrunner.go +++ b/wavesrv/pkg/cmdrunner/cmdrunner.go @@ -2202,7 +2202,7 @@ func NewHostInfo(hostName string) (*HostInfoType, error) { func RemoteConfigParseCommand(ctx context.Context, pk *scpacket.FeCommandPacketType) (scbus.UpdatePacket, error) { home := base.GetHomeDir() localConfig := filepath.Join(home, ".ssh", "config") - systemConfig := filepath.Join("/", "ssh", "config") + systemConfig := filepath.Join("/etc", "ssh", "config") sshConfigFiles := []string{localConfig, systemConfig} ssh_config.ReloadConfigs() hostPatterns, hostPatternsErr := resolveSshConfigPatterns(sshConfigFiles) diff --git a/wavesrv/pkg/remote/remote.go b/wavesrv/pkg/remote/remote.go index 9d51f9d7..07f61dec 100644 --- a/wavesrv/pkg/remote/remote.go +++ b/wavesrv/pkg/remote/remote.go @@ -1202,14 +1202,54 @@ func (msh *MShellProc) RunInstall(autoInstall bool) { msh.WriteToPtyBuffer("*error: cannot install on archived remote\n") return } - if autoInstall { + + var makeClientCtx context.Context + var makeClientCancelFn context.CancelFunc + msh.WithLock(func() { + makeClientCtx, makeClientCancelFn = context.WithCancel(context.Background()) + msh.MakeClientCancelFn = makeClientCancelFn + msh.MakeClientDeadline = nil + go msh.NotifyRemoteUpdate() + }) + defer makeClientCancelFn() + clientData, err := sstore.EnsureClientData(makeClientCtx) + if err != nil { + msh.WriteToPtyBuffer("*error: cannot obtain client data: %v", err) + return + } + hideShellPrompt := clientData.ClientOpts.ConfirmFlags["hideshellprompt"] + baseStatus := msh.GetStatus() + + if baseStatus == StatusConnected { + ctx, cancelFn := context.WithTimeout(makeClientCtx, 60*time.Second) + defer cancelFn() + request := &userinput.UserInputRequestType{ + ResponseType: "confirm", + QueryText: "Waveshell is running on your connection and must be restarted to re-install. Would you like to continue?", + Title: "Restart Waveshell", + } + response, err := userinput.GetUserInput(ctx, scbus.MainRpcBus, request) + if err != nil { + if err == context.Canceled { + msh.WriteToPtyBuffer("installation canceled by user\n") + } else { + msh.WriteToPtyBuffer("timed out waiting for user input\n") + } + return + } + if !response.Confirm { + msh.WriteToPtyBuffer("installation canceled by user\n") + return + } + } else if !hideShellPrompt { + ctx, cancelFn := context.WithTimeout(makeClientCtx, 60*time.Second) + defer cancelFn() request := &userinput.UserInputRequestType{ ResponseType: "confirm", QueryText: "Waveshell must be reinstalled on the connection to continue. Would you like to install it?", Title: "Install Waveshell", + CheckBoxMsg: "Don't show me this again", } - ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) - defer cancelFn() response, err := userinput.GetUserInput(ctx, scbus.MainRpcBus, request) if err != nil { var errMsg error @@ -1234,28 +1274,24 @@ func (msh *MShellProc) RunInstall(autoInstall bool) { }) return } - } - baseStatus := msh.GetStatus() - if baseStatus == StatusConnected { - request := &userinput.UserInputRequestType{ - ResponseType: "confirm", - QueryText: "Waveshell is running on your connection and must be restarted to re-install. Would you like to continue?", - Title: "Restart Waveshell", - } - ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) - defer cancelFn() - response, err := userinput.GetUserInput(ctx, scbus.MainRpcBus, request) - if err != nil { - if err == context.Canceled { - msh.WriteToPtyBuffer("installation canceled by user\n") - } else { - msh.WriteToPtyBuffer("timed out waiting for user input\n") + if response.CheckboxStat { + clientData.ClientOpts.ConfirmFlags["hideshellprompt"] = true + err = sstore.SetClientOpts(makeClientCtx, clientData.ClientOpts) + if err != nil { + msh.WriteToPtyBuffer("*error, %s\n", err) + msh.setErrorStatus(err) + return } - return - } - if !response.Confirm { - msh.WriteToPtyBuffer("installation canceled by user\n") - return + + //reload updated clientdata before sending + clientData, err = sstore.EnsureClientData(makeClientCtx) + if err != nil { + msh.WriteToPtyBuffer("*error, %s\n", err) + msh.setErrorStatus(err) + return + } + update := scbus.MakeUpdatePacket() + update.AddUpdate(*clientData) } } curStatus := msh.GetInstallStatus() @@ -1267,14 +1303,14 @@ func (msh *MShellProc) RunInstall(autoInstall bool) { msh.WriteToPtyBuffer("*error: cannot install on a local remote\n") return } - _, err := shellapi.MakeShellApi(packet.ShellType_bash) + _, err = shellapi.MakeShellApi(packet.ShellType_bash) if err != nil { msh.WriteToPtyBuffer("*error: %v\n", err) return } if msh.Client == nil { remoteDisplayName := fmt.Sprintf("%s [%s]", remoteCopy.RemoteAlias, remoteCopy.RemoteCanonicalName) - client, err := ConnectToClient(remoteCopy.SSHOpts, remoteDisplayName) + client, err := ConnectToClient(makeClientCtx, remoteCopy.SSHOpts, remoteDisplayName) if err != nil { statusErr := fmt.Errorf("ssh cannot connect to client: %w", err) msh.setInstallErrorStatus(statusErr) @@ -1481,7 +1517,7 @@ func (msh *MShellProc) getActiveShellTypes(ctx context.Context) ([]string, error return utilfn.CombineStrArrays(rtn, activeShells), nil } -func (msh *MShellProc) createWaveshellSession(remoteCopy sstore.RemoteType) (shexec.ConnInterface, error) { +func (msh *MShellProc) createWaveshellSession(clientCtx context.Context, remoteCopy sstore.RemoteType) (shexec.ConnInterface, error) { msh.WithLock(func() { msh.Err = nil msh.ErrNoInitPk = false @@ -1510,7 +1546,7 @@ func (msh *MShellProc) createWaveshellSession(remoteCopy sstore.RemoteType) (she wsSession = shexec.CmdWrap{Cmd: ecmd} } else if msh.Client == nil { remoteDisplayName := fmt.Sprintf("%s [%s]", remoteCopy.RemoteAlias, remoteCopy.RemoteCanonicalName) - client, err := ConnectToClient(remoteCopy.SSHOpts, remoteDisplayName) + client, err := ConnectToClient(clientCtx, remoteCopy.SSHOpts, remoteDisplayName) if err != nil { return nil, fmt.Errorf("ssh cannot connect to client: %w", err) } @@ -1559,16 +1595,6 @@ func (NewLauncher) Launch(msh *MShellProc, interactive bool) { msh.WriteToPtyBuffer("remote is trying to install, cancel install before trying to connect again\n") return } - msh.WriteToPtyBuffer("connecting to %s...\n", remoteCopy.RemoteCanonicalName) - wsSession, err := msh.createWaveshellSession(remoteCopy) - if err != nil { - msh.WriteToPtyBuffer("*error, %s\n", err.Error()) - msh.setErrorStatus(err) - msh.WithLock(func() { - msh.Client = nil - }) - return - } var makeClientCtx context.Context var makeClientCancelFn context.CancelFunc msh.WithLock(func() { @@ -1578,6 +1604,16 @@ func (NewLauncher) Launch(msh *MShellProc, interactive bool) { go msh.NotifyRemoteUpdate() }) defer makeClientCancelFn() + msh.WriteToPtyBuffer("connecting to %s...\n", remoteCopy.RemoteCanonicalName) + wsSession, err := msh.createWaveshellSession(makeClientCtx, remoteCopy) + if err != nil { + msh.WriteToPtyBuffer("*error, %s\n", err.Error()) + msh.setErrorStatus(err) + msh.WithLock(func() { + msh.Client = nil + }) + return + } cproc, err := shexec.MakeClientProc(makeClientCtx, wsSession) msh.WithLock(func() { msh.MakeClientCancelFn = nil diff --git a/wavesrv/pkg/remote/sshclient.go b/wavesrv/pkg/remote/sshclient.go index ed770ca2..7eff78ef 100644 --- a/wavesrv/pkg/remote/sshclient.go +++ b/wavesrv/pkg/remote/sshclient.go @@ -65,7 +65,7 @@ func createDummySigner() ([]ssh.Signer, error) { // they were successes. An error in this function prevents any other // keys from being attempted. But if there's an error because of a dummy // file, the library can still try again with a new key. -func createPublicKeyCallback(sshKeywords *SshKeywords, passphrase string) func() ([]ssh.Signer, error) { +func createPublicKeyCallback(connCtx context.Context, sshKeywords *SshKeywords, passphrase string) func() ([]ssh.Signer, error) { var identityFiles []string existingKeys := make(map[string][]byte) @@ -124,7 +124,7 @@ func createPublicKeyCallback(sshKeywords *SshKeywords, passphrase string) func() QueryText: fmt.Sprintf("Enter passphrase for the SSH key: %s", identityFile), Title: "Publickey Auth + Passphrase", } - ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) + ctx, cancelFn := context.WithTimeout(connCtx, 60*time.Second) defer cancelFn() response, err := userinput.GetUserInput(ctx, scbus.MainRpcBus, request) if err != nil { @@ -150,11 +150,11 @@ func createDefaultPasswordCallbackPrompt(password string) func() (secret string, } } -func createInteractivePasswordCallbackPrompt(remoteDisplayName string) func() (secret string, err error) { +func createInteractivePasswordCallbackPrompt(connCtx context.Context, remoteDisplayName string) func() (secret string, err error) { return func() (secret string, err error) { // limited to 15 seconds for some reason. this should be investigated more // in the future - ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) + ctx, cancelFn := context.WithTimeout(connCtx, 60*time.Second) defer cancelFn() queryText := fmt.Sprintf( "Password Authentication requested from connection \n"+ @@ -174,13 +174,13 @@ func createInteractivePasswordCallbackPrompt(remoteDisplayName string) func() (s } } -func createCombinedPasswordCallbackPrompt(password string, remoteDisplayName string) func() (secret string, err error) { +func createCombinedPasswordCallbackPrompt(connCtx context.Context, password string, remoteDisplayName string) func() (secret string, err error) { var once sync.Once return func() (secret string, err error) { var prompt func() (secret string, err error) once.Do(func() { prompt = createDefaultPasswordCallbackPrompt(password) }) if prompt == nil { - prompt = createInteractivePasswordCallbackPrompt(remoteDisplayName) + prompt = createInteractivePasswordCallbackPrompt(connCtx, remoteDisplayName) } return prompt() } @@ -199,14 +199,14 @@ func createNaiveKbdInteractiveChallenge(password string) func(name, instruction } } -func createInteractiveKbdInteractiveChallenge(remoteName string) func(name, instruction string, questions []string, echos []bool) (answers []string, err error) { +func createInteractiveKbdInteractiveChallenge(connCtx context.Context, remoteName string) func(name, instruction string, questions []string, echos []bool) (answers []string, err error) { return func(name, instruction string, questions []string, echos []bool) (answers []string, err error) { if len(questions) != len(echos) { return nil, fmt.Errorf("bad response from server: questions has len %d, echos has len %d", len(questions), len(echos)) } for i, question := range questions { echo := echos[i] - answer, err := promptChallengeQuestion(question, echo, remoteName) + answer, err := promptChallengeQuestion(connCtx, question, echo, remoteName) if err != nil { return nil, err } @@ -216,10 +216,10 @@ func createInteractiveKbdInteractiveChallenge(remoteName string) func(name, inst } } -func promptChallengeQuestion(question string, echo bool, remoteName string) (answer string, err error) { +func promptChallengeQuestion(connCtx context.Context, question string, echo bool, remoteName string) (answer string, err error) { // limited to 15 seconds for some reason. this should be investigated more // in the future - ctx, cancelFn := context.WithTimeout(context.Background(), 60*time.Second) + ctx, cancelFn := context.WithTimeout(connCtx, 60*time.Second) defer cancelFn() queryText := fmt.Sprintf( "Keyboard Interactive Authentication requested from connection \n"+ @@ -238,13 +238,13 @@ func promptChallengeQuestion(question string, echo bool, remoteName string) (ans return response.Text, nil } -func createCombinedKbdInteractiveChallenge(password string, remoteName string) ssh.KeyboardInteractiveChallenge { +func createCombinedKbdInteractiveChallenge(connCtx context.Context, password string, remoteName string) ssh.KeyboardInteractiveChallenge { var once sync.Once return func(name, instruction string, questions []string, echos []bool) (answers []string, err error) { var challenge ssh.KeyboardInteractiveChallenge once.Do(func() { challenge = createNaiveKbdInteractiveChallenge(password) }) if challenge == nil { - challenge = createInteractiveKbdInteractiveChallenge(remoteName) + challenge = createInteractiveKbdInteractiveChallenge(connCtx, remoteName) } return challenge(name, instruction, questions, echos) } @@ -505,7 +505,20 @@ func createHostKeyCallback(opts *sstore.SSHOpts) (ssh.HostKeyCallback, error) { return waveHostKeyCallback, nil } -func ConnectToClient(opts *sstore.SSHOpts, remoteDisplayName string) (*ssh.Client, error) { +func DialContext(ctx context.Context, network string, addr string, config *ssh.ClientConfig) (*ssh.Client, error) { + d := net.Dialer{Timeout: config.Timeout} + conn, err := d.DialContext(ctx, network, addr) + if err != nil { + return nil, err + } + c, chans, reqs, err := ssh.NewClientConn(conn, addr, config) + if err != nil { + return nil, err + } + return ssh.NewClient(c, chans, reqs), nil +} + +func ConnectToClient(connCtx context.Context, opts *sstore.SSHOpts, remoteDisplayName string) (*ssh.Client, error) { sshConfigKeywords, err := findSshConfigKeywords(opts.SSHHost) if err != nil { return nil, err @@ -516,9 +529,9 @@ func ConnectToClient(opts *sstore.SSHOpts, remoteDisplayName string) (*ssh.Clien return nil, err } - publicKeyCallback := ssh.PublicKeysCallback(createPublicKeyCallback(sshKeywords, opts.SSHPassword)) - keyboardInteractive := ssh.KeyboardInteractive(createCombinedKbdInteractiveChallenge(opts.SSHPassword, remoteDisplayName)) - passwordCallback := ssh.PasswordCallback(createCombinedPasswordCallbackPrompt(opts.SSHPassword, remoteDisplayName)) + publicKeyCallback := ssh.PublicKeysCallback(createPublicKeyCallback(connCtx, sshKeywords, opts.SSHPassword)) + keyboardInteractive := ssh.KeyboardInteractive(createCombinedKbdInteractiveChallenge(connCtx, opts.SSHPassword, remoteDisplayName)) + passwordCallback := ssh.PasswordCallback(createCombinedPasswordCallbackPrompt(connCtx, opts.SSHPassword, remoteDisplayName)) // batch mode turns off interactive input. this means the number of // attemtps must drop to 1 with this setup @@ -566,7 +579,7 @@ func ConnectToClient(opts *sstore.SSHOpts, remoteDisplayName string) (*ssh.Clien HostKeyCallback: hostKeyCallback, } networkAddr := sshKeywords.HostName + ":" + sshKeywords.Port - return ssh.Dial("tcp", networkAddr, clientConfig) + return DialContext(connCtx, "tcp", networkAddr, clientConfig) } type SshKeywords struct { diff --git a/wavesrv/pkg/userinput/userinput.go b/wavesrv/pkg/userinput/userinput.go index 0ab0fe41..50212acb 100644 --- a/wavesrv/pkg/userinput/userinput.go +++ b/wavesrv/pkg/userinput/userinput.go @@ -21,6 +21,7 @@ type UserInputRequestType struct { Title string `json:"title"` Markdown bool `json:"markdown"` TimeoutMs int `json:"timeoutms"` + CheckBoxMsg string `json:"checkboxmsg"` } func (*UserInputRequestType) GetType() string { @@ -39,11 +40,12 @@ const UserInputResponsePacketStr = "userinputresp" // An RpcResponse for user input requests type UserInputResponsePacketType struct { - Type string `json:"type"` - RequestId string `json:"requestid"` - Text string `json:"text,omitempty"` - Confirm bool `json:"confirm,omitempty"` - ErrorMsg string `json:"errormsg,omitempty"` + 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"` } func (*UserInputResponsePacketType) GetType() string {