Skip to content

Commit df24959

Browse files
Copilotsawka
andauthored
Add native 2+ arg RPC support and wire a concrete TestMultiArgCommand through server, generated clients, and CLI (#2963)
This PR extends WSH RPC command signatures to support `ctx + 2+ typed args` while preserving existing `ctx` and `ctx + 1 arg` behavior. It also adds a concrete `TestMultiArgCommand` end-to-end so the generated Go/TS client surfaces can be inspected and exercised from CLI. - **RPC wire + dispatch model** - Added `wshrpc.MultiArg` (`args []any`) as the over-the-wire envelope for 2+ arg commands. - Extended RPC metadata to track all command arg types (`CommandDataTypes`) and exposed a helper for normalized access. - Updated server adapter unmarshalling to: - decode `MultiArg` for 2+ arg commands, - validate arg count, - re-unmarshal each arg into its declared type before invoking typed handlers. - Kept single-arg commands on the existing non-`MultiArg` path. - **Code generation (Go + TS)** - Go codegen now emits multi-parameter wrappers for 2+ arg methods and packs payload as `wshrpc.MultiArg`. - TS codegen now emits multi-parameter API methods and packs payload as `{ args: [...] }`. - 0/1-arg generation remains unchanged to avoid wire/API churn. - **Concrete command added for validation** - Added to `WshRpcInterface`: - `TestMultiArgCommand(ctx context.Context, arg1 string, arg2 int, arg3 bool) (string, error)` - Implemented in `wshserver` with deterministic formatted return output including source + all args. - Updated `wsh test` command to call `TestMultiArgCommand` and print the returned string. - **Focused coverage** - Added/updated targeted tests around RPC metadata and Go/TS multi-arg codegen behavior, including command declaration for `testmultiarg`. Example generated call shape: ```go func TestMultiArgCommand(w *wshutil.WshRpc, arg1 string, arg2 int, arg3 bool, opts *wshrpc.RpcOpts) (string, error) { return sendRpcRequestCallHelper[string]( w, "testmultiarg", wshrpc.MultiArg{Args: []any{arg1, arg2, arg3}}, opts, ) } ``` <!-- START COPILOT CODING AGENT TIPS --> --- 💡 You can make Copilot smarter by setting up custom instructions, customizing its development environment and configuring Model Context Protocol (MCP) servers. Learn more [Copilot coding agent tips](https://gh.io/copilot-coding-agent-tips) in the docs. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> Co-authored-by: sawka <mike@commandline.dev>
1 parent 9d89f43 commit df24959

12 files changed

Lines changed: 263 additions & 52 deletions

File tree

cmd/wsh/cmd/wshcmd-test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package cmd
55

66
import (
77
"github.com/spf13/cobra"
8+
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
89
)
910

1011
var testCmd = &cobra.Command{
@@ -20,5 +21,10 @@ func init() {
2021
}
2122

2223
func runTestCmd(cmd *cobra.Command, args []string) error {
24+
rtn, err := wshclient.TestMultiArgCommand(RpcClient, "testarg", 42, true, nil)
25+
if err != nil {
26+
return err
27+
}
28+
WriteStdout("%s\n", rtn)
2329
return nil
2430
}

frontend/app/store/wshclientapi.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -757,6 +757,11 @@ class RpcApiType {
757757
return client.wshRpcCall("test", data, opts);
758758
}
759759

760+
// command "testmultiarg" [call]
761+
TestMultiArgCommand(client: WshClient, arg1: string, arg2: number, arg3: boolean, opts?: RpcOpts): Promise<string> {
762+
return client.wshRpcCall("testmultiarg", { args: [arg1, arg2, arg3] }, opts);
763+
}
764+
760765
// command "vdomasyncinitiation" [call]
761766
VDomAsyncInitiationCommand(client: WshClient, data: VDomAsyncInitiationRequest, opts?: RpcOpts): Promise<void> {
762767
return client.wshRpcCall("vdomasyncinitiation", data, opts);

pkg/gogen/gogen.go

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -75,12 +75,7 @@ func GenerateMetaMapConsts(buf *strings.Builder, constPrefix string, rtype refle
7575

7676
func GenMethod_Call(buf *strings.Builder, methodDecl *wshrpc.WshRpcMethodDecl) {
7777
fmt.Fprintf(buf, "// command %q, wshserver.%s\n", methodDecl.Command, methodDecl.MethodName)
78-
var dataType string
79-
dataVarName := "nil"
80-
if methodDecl.CommandDataType != nil {
81-
dataType = ", data " + methodDecl.CommandDataType.String()
82-
dataVarName = "data"
83-
}
78+
dataType, dataVarName := getWshMethodDataParamsAndExpr(methodDecl)
8479
returnType := "error"
8580
respName := "_"
8681
tParamVal := "any"
@@ -101,12 +96,7 @@ func GenMethod_Call(buf *strings.Builder, methodDecl *wshrpc.WshRpcMethodDecl) {
10196

10297
func GenMethod_ResponseStream(buf *strings.Builder, methodDecl *wshrpc.WshRpcMethodDecl) {
10398
fmt.Fprintf(buf, "// command %q, wshserver.%s\n", methodDecl.Command, methodDecl.MethodName)
104-
var dataType string
105-
dataVarName := "nil"
106-
if methodDecl.CommandDataType != nil {
107-
dataType = ", data " + methodDecl.CommandDataType.String()
108-
dataVarName = "data"
109-
}
99+
dataType, dataVarName := getWshMethodDataParamsAndExpr(methodDecl)
110100
respType := "any"
111101
if methodDecl.DefaultResponseDataType != nil {
112102
respType = methodDecl.DefaultResponseDataType.String()
@@ -115,3 +105,27 @@ func GenMethod_ResponseStream(buf *strings.Builder, methodDecl *wshrpc.WshRpcMet
115105
fmt.Fprintf(buf, "\treturn sendRpcRequestResponseStreamHelper[%s](w, %q, %s, opts)\n", respType, methodDecl.Command, dataVarName)
116106
fmt.Fprintf(buf, "}\n\n")
117107
}
108+
109+
func getWshMethodDataParamsAndExpr(methodDecl *wshrpc.WshRpcMethodDecl) (string, string) {
110+
dataTypes := methodDecl.GetCommandDataTypes()
111+
if len(dataTypes) == 0 {
112+
return "", "nil"
113+
}
114+
if len(dataTypes) == 1 {
115+
return ", data " + dataTypes[0].String(), "data"
116+
}
117+
var paramBuilder strings.Builder
118+
var argBuilder strings.Builder
119+
for idx, dataType := range dataTypes {
120+
argName := fmt.Sprintf("arg%d", idx+1)
121+
paramBuilder.WriteString(", ")
122+
paramBuilder.WriteString(argName)
123+
paramBuilder.WriteString(" ")
124+
paramBuilder.WriteString(dataType.String())
125+
if idx > 0 {
126+
argBuilder.WriteString(", ")
127+
}
128+
argBuilder.WriteString(argName)
129+
}
130+
return paramBuilder.String(), fmt.Sprintf("wshrpc.MultiArg{Args: []any{%s}}", argBuilder.String())
131+
}

pkg/gogen/gogen_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// Copyright 2026, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package gogen
5+
6+
import (
7+
"reflect"
8+
"strings"
9+
"testing"
10+
11+
"github.com/wavetermdev/waveterm/pkg/wshrpc"
12+
)
13+
14+
func TestGetWshMethodDataParamsAndExpr_MultiArg(t *testing.T) {
15+
methodDecl := &wshrpc.WshRpcMethodDecl{
16+
CommandDataTypes: []reflect.Type{
17+
reflect.TypeOf(""),
18+
reflect.TypeOf(0),
19+
},
20+
}
21+
params, expr := getWshMethodDataParamsAndExpr(methodDecl)
22+
if params != ", arg1 string, arg2 int" {
23+
t.Fatalf("unexpected params: %q", params)
24+
}
25+
if expr != "wshrpc.MultiArg{Args: []any{arg1, arg2}}" {
26+
t.Fatalf("unexpected expr: %q", expr)
27+
}
28+
}
29+
30+
func TestGenMethodCall_MultiArg(t *testing.T) {
31+
methodDecl := &wshrpc.WshRpcMethodDecl{
32+
Command: "test",
33+
CommandType: wshrpc.RpcType_Call,
34+
MethodName: "TestCommand",
35+
CommandDataTypes: []reflect.Type{reflect.TypeOf(""), reflect.TypeOf(0)},
36+
}
37+
var sb strings.Builder
38+
GenMethod_Call(&sb, methodDecl)
39+
out := sb.String()
40+
if !strings.Contains(out, "func TestCommand(w *wshutil.WshRpc, arg1 string, arg2 int, opts *wshrpc.RpcOpts) error {") {
41+
t.Fatalf("generated method missing multi-arg signature:\n%s", out)
42+
}
43+
if !strings.Contains(out, "sendRpcRequestCallHelper[any](w, \"test\", wshrpc.MultiArg{Args: []any{arg1, arg2}}, opts)") {
44+
t.Fatalf("generated method missing MultiArg payload:\n%s", out)
45+
}
46+
}

pkg/tsgen/tsgen.go

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -464,16 +464,12 @@ func generateWshClientApiMethod_ResponseStream(methodDecl *wshrpc.WshRpcMethodDe
464464
if methodDecl.DefaultResponseDataType != nil {
465465
respType, _ = TypeToTSType(methodDecl.DefaultResponseDataType, tsTypesMap)
466466
}
467-
dataName := "null"
468-
if methodDecl.CommandDataType != nil {
469-
dataName = "data"
470-
}
467+
methodSigDataParams, dataName := getTsWshMethodDataParamsAndExpr(methodDecl, tsTypesMap)
471468
genRespType := fmt.Sprintf("AsyncGenerator<%s, void, boolean>", respType)
472-
if methodDecl.CommandDataType != nil {
473-
cmdDataTsName, _ := TypeToTSType(methodDecl.CommandDataType, tsTypesMap)
474-
sb.WriteString(fmt.Sprintf(" %s(client: WshClient, data: %s, opts?: RpcOpts): %s {\n", methodDecl.MethodName, cmdDataTsName, genRespType))
475-
} else {
469+
if methodSigDataParams == "" {
476470
sb.WriteString(fmt.Sprintf(" %s(client: WshClient, opts?: RpcOpts): %s {\n", methodDecl.MethodName, genRespType))
471+
} else {
472+
sb.WriteString(fmt.Sprintf(" %s(client: WshClient, %s, opts?: RpcOpts): %s {\n", methodDecl.MethodName, methodSigDataParams, genRespType))
477473
}
478474
sb.WriteString(fmt.Sprintf(" return client.wshRpcStream(%q, %s, opts);\n", methodDecl.Command, dataName))
479475
sb.WriteString(" }\n")
@@ -488,22 +484,42 @@ func generateWshClientApiMethod_Call(methodDecl *wshrpc.WshRpcMethodDecl, tsType
488484
rtnTypeName, _ := TypeToTSType(methodDecl.DefaultResponseDataType, tsTypesMap)
489485
rtnType = fmt.Sprintf("Promise<%s>", rtnTypeName)
490486
}
491-
dataName := "null"
492-
if methodDecl.CommandDataType != nil {
493-
dataName = "data"
494-
}
495-
if methodDecl.CommandDataType != nil {
496-
cmdDataTsName, _ := TypeToTSType(methodDecl.CommandDataType, tsTypesMap)
497-
sb.WriteString(fmt.Sprintf(" %s(client: WshClient, data: %s, opts?: RpcOpts): %s {\n", methodDecl.MethodName, cmdDataTsName, rtnType))
498-
} else {
487+
methodSigDataParams, dataName := getTsWshMethodDataParamsAndExpr(methodDecl, tsTypesMap)
488+
if methodSigDataParams == "" {
499489
sb.WriteString(fmt.Sprintf(" %s(client: WshClient, opts?: RpcOpts): %s {\n", methodDecl.MethodName, rtnType))
490+
} else {
491+
sb.WriteString(fmt.Sprintf(" %s(client: WshClient, %s, opts?: RpcOpts): %s {\n", methodDecl.MethodName, methodSigDataParams, rtnType))
500492
}
501493
methodBody := fmt.Sprintf(" return client.wshRpcCall(%q, %s, opts);\n", methodDecl.Command, dataName)
502494
sb.WriteString(methodBody)
503495
sb.WriteString(" }\n")
504496
return sb.String()
505497
}
506498

499+
func getTsWshMethodDataParamsAndExpr(methodDecl *wshrpc.WshRpcMethodDecl, tsTypesMap map[reflect.Type]string) (string, string) {
500+
dataTypes := methodDecl.GetCommandDataTypes()
501+
if len(dataTypes) == 0 {
502+
return "", "null"
503+
}
504+
if len(dataTypes) == 1 {
505+
cmdDataTsName, _ := TypeToTSType(dataTypes[0], tsTypesMap)
506+
return fmt.Sprintf("data: %s", cmdDataTsName), "data"
507+
}
508+
var methodParamBuilder strings.Builder
509+
var argBuilder strings.Builder
510+
for idx, dataType := range dataTypes {
511+
if idx > 0 {
512+
methodParamBuilder.WriteString(", ")
513+
argBuilder.WriteString(", ")
514+
}
515+
argName := fmt.Sprintf("arg%d", idx+1)
516+
cmdDataTsName, _ := TypeToTSType(dataType, tsTypesMap)
517+
methodParamBuilder.WriteString(fmt.Sprintf("%s: %s", argName, cmdDataTsName))
518+
argBuilder.WriteString(argName)
519+
}
520+
return methodParamBuilder.String(), fmt.Sprintf("{ args: [%s] }", argBuilder.String())
521+
}
522+
507523
func GenerateWaveObjTypes(tsTypesMap map[reflect.Type]string) {
508524
for _, typeUnion := range TypeUnions {
509525
GenerateTSTypeUnion(typeUnion, tsTypesMap)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2026, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package tsgen
5+
6+
import (
7+
"reflect"
8+
"strings"
9+
"testing"
10+
11+
"github.com/wavetermdev/waveterm/pkg/wshrpc"
12+
)
13+
14+
func TestGenerateWshClientApiMethodCall_MultiArg(t *testing.T) {
15+
methodDecl := &wshrpc.WshRpcMethodDecl{
16+
Command: "test",
17+
CommandType: wshrpc.RpcType_Call,
18+
MethodName: "TestCommand",
19+
CommandDataTypes: []reflect.Type{reflect.TypeOf(""), reflect.TypeOf(0)},
20+
}
21+
out := GenerateWshClientApiMethod(methodDecl, map[reflect.Type]string{})
22+
if !strings.Contains(out, "TestCommand(client: WshClient, arg1: string, arg2: number, opts?: RpcOpts): Promise<void> {") {
23+
t.Fatalf("generated method missing multi-arg signature:\n%s", out)
24+
}
25+
if !strings.Contains(out, "return client.wshRpcCall(\"test\", { args: [arg1, arg2] }, opts);") {
26+
t.Fatalf("generated method missing MultiArg payload:\n%s", out)
27+
}
28+
}

pkg/wshrpc/wshclient/wshclient.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -908,6 +908,12 @@ func TestCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) error {
908908
return err
909909
}
910910

911+
// command "testmultiarg", wshserver.TestMultiArgCommand
912+
func TestMultiArgCommand(w *wshutil.WshRpc, arg1 string, arg2 int, arg3 bool, opts *wshrpc.RpcOpts) (string, error) {
913+
resp, err := sendRpcRequestCallHelper[string](w, "testmultiarg", wshrpc.MultiArg{Args: []any{arg1, arg2, arg3}}, opts)
914+
return resp, err
915+
}
916+
911917
// command "vdomasyncinitiation", wshserver.VDomAsyncInitiationCommand
912918
func VDomAsyncInitiationCommand(w *wshutil.WshRpc, data vdom.VDomAsyncInitiationRequest, opts *wshrpc.RpcOpts) error {
913919
_, err := sendRpcRequestCallHelper[any](w, "vdomasyncinitiation", data, opts)

pkg/wshrpc/wshrpcmeta.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,14 @@ type WshRpcMethodDecl struct {
1515
Command string
1616
CommandType string
1717
MethodName string
18-
CommandDataType reflect.Type
18+
CommandDataTypes []reflect.Type
1919
DefaultResponseDataType reflect.Type
2020
}
2121

22+
func (decl *WshRpcMethodDecl) GetCommandDataTypes() []reflect.Type {
23+
return decl.CommandDataTypes
24+
}
25+
2226
var contextRType = reflect.TypeOf((*context.Context)(nil)).Elem()
2327
var wshRpcInterfaceRType = reflect.TypeOf((*WshRpcInterface)(nil)).Elem()
2428

@@ -75,11 +79,11 @@ func generateWshCommandDecl(method reflect.Method) *WshRpcMethodDecl {
7579
decl.Command = strings.ToLower(cmdStr)
7680
decl.CommandType = getWshCommandType(method)
7781
decl.MethodName = method.Name
78-
var cdataType reflect.Type
79-
if method.Type.NumIn() > 1 {
80-
cdataType = method.Type.In(1)
82+
var cdataTypes []reflect.Type
83+
for idx := 1; idx < method.Type.NumIn(); idx++ {
84+
cdataTypes = append(cdataTypes, method.Type.In(idx))
8185
}
82-
decl.CommandDataType = cdataType
86+
decl.CommandDataTypes = cdataTypes
8387
decl.DefaultResponseDataType = getWshMethodResponseType(decl.CommandType, method)
8488
return decl
8589
}

pkg/wshrpc/wshrpcmeta_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright 2026, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package wshrpc
5+
6+
import (
7+
"context"
8+
"reflect"
9+
"testing"
10+
)
11+
12+
type testRpcInterfaceForDecls interface {
13+
NoArgCommand(ctx context.Context) error
14+
OneArgCommand(ctx context.Context, data string) error
15+
TwoArgCommand(ctx context.Context, arg1 string, arg2 int) error
16+
}
17+
18+
func TestGenerateWshCommandDecl_MultiArgs(t *testing.T) {
19+
rtype := reflect.TypeOf((*testRpcInterfaceForDecls)(nil)).Elem()
20+
method, ok := rtype.MethodByName("TwoArgCommand")
21+
if !ok {
22+
t.Fatalf("TwoArgCommand method not found")
23+
}
24+
decl := generateWshCommandDecl(method)
25+
if decl.Command != "twoarg" {
26+
t.Fatalf("expected command twoarg, got %q", decl.Command)
27+
}
28+
if len(decl.CommandDataTypes) != 2 {
29+
t.Fatalf("expected 2 command data types, got %d", len(decl.CommandDataTypes))
30+
}
31+
if decl.CommandDataTypes[0].Kind() != reflect.String || decl.CommandDataTypes[1].Kind() != reflect.Int {
32+
t.Fatalf("unexpected command data types: %#v", decl.CommandDataTypes)
33+
}
34+
if len(decl.GetCommandDataTypes()) != 2 {
35+
t.Fatalf("expected helper to return two command data types")
36+
}
37+
}
38+
39+
func TestGenerateWshCommandDeclMap_TestMultiArgCommand(t *testing.T) {
40+
decl := GenerateWshCommandDeclMap()["testmultiarg"]
41+
if decl == nil {
42+
t.Fatalf("expected testmultiarg command declaration")
43+
}
44+
if decl.MethodName != "TestMultiArgCommand" {
45+
t.Fatalf("expected TestMultiArgCommand method name, got %q", decl.MethodName)
46+
}
47+
if len(decl.GetCommandDataTypes()) != 3 {
48+
t.Fatalf("expected 3 command args, got %d", len(decl.GetCommandDataTypes()))
49+
}
50+
}

pkg/wshrpc/wshrpctypes.go

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,14 @@ type RespOrErrorUnion[T any] struct {
2323
Error error
2424
}
2525

26+
type MultiArg struct {
27+
Args []any `json:"args"`
28+
}
29+
2630
// Instructions for adding a new RPC call
2731
// * methods must end with Command
2832
// * methods must take context as their first parameter
29-
// * methods may take up to one parameter, and may return either just an error, or one return value plus an error
33+
// * methods may take additional typed parameters, and may return either just an error, or one return value plus an error
3034
// * after modifying WshRpcInterface, run `task generate` to regnerate bindings
3135

3236
type WshRpcInterface interface {
@@ -69,6 +73,7 @@ type WshRpcInterface interface {
6973
StreamWaveAiCommand(ctx context.Context, request WaveAIStreamRequest) chan RespOrErrorUnion[WaveAIPacketType]
7074
StreamCpuDataCommand(ctx context.Context, request CpuDataRequest) chan RespOrErrorUnion[TimeSeriesData]
7175
TestCommand(ctx context.Context, data string) error
76+
TestMultiArgCommand(ctx context.Context, arg1 string, arg2 int, arg3 bool) (string, error)
7277
SetConfigCommand(ctx context.Context, data MetaSettingsType) error
7378
SetConnectionsConfigCommand(ctx context.Context, data ConnConfigRequest) error
7479
GetFullConfigCommand(ctx context.Context) (wconfig.FullConfigType, error)
@@ -895,13 +900,13 @@ type BlockJobStatusData struct {
895900
}
896901

897902
type FocusedBlockData struct {
898-
BlockId string `json:"blockid"`
899-
ViewType string `json:"viewtype"`
900-
Controller string `json:"controller"`
901-
ConnName string `json:"connname"`
902-
BlockMeta waveobj.MetaMapType `json:"blockmeta"`
903-
TermJobStatus *BlockJobStatusData `json:"termjobstatus,omitempty"`
904-
ConnStatus *ConnStatus `json:"connstatus,omitempty"`
905-
TermShellIntegrationStatus string `json:"termshellintegrationstatus,omitempty"`
906-
TermLastCommand string `json:"termlastcommand,omitempty"`
903+
BlockId string `json:"blockid"`
904+
ViewType string `json:"viewtype"`
905+
Controller string `json:"controller"`
906+
ConnName string `json:"connname"`
907+
BlockMeta waveobj.MetaMapType `json:"blockmeta"`
908+
TermJobStatus *BlockJobStatusData `json:"termjobstatus,omitempty"`
909+
ConnStatus *ConnStatus `json:"connstatus,omitempty"`
910+
TermShellIntegrationStatus string `json:"termshellintegrationstatus,omitempty"`
911+
TermLastCommand string `json:"termlastcommand,omitempty"`
907912
}

0 commit comments

Comments
 (0)