From 48011409c65c384e97170855c94aa24608536c6a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 00:24:30 +0000 Subject: [PATCH 1/8] Initial plan From ee98b91050a21fb98552af0ee69fbc86a689e062 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 00:35:08 +0000 Subject: [PATCH 2/8] feat(tsgen): add typed WaveEvent union generation and event registry Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- cmd/generatets/main-generatets.go | 24 +++++++++ frontend/types/gotypes.d.ts | 8 --- frontend/types/waveevent.d.ts | 41 +++++++++++++++ pkg/tsgen/tsgenevent.go | 85 +++++++++++++++++++++++++++++++ pkg/tsgen/tsgenevent_test.go | 34 +++++++++++++ pkg/wps/wpstypes.go | 52 +++++++++++++------ 6 files changed, 221 insertions(+), 23 deletions(-) create mode 100644 frontend/types/waveevent.d.ts create mode 100644 pkg/tsgen/tsgenevent.go create mode 100644 pkg/tsgen/tsgenevent_test.go diff --git a/cmd/generatets/main-generatets.go b/cmd/generatets/main-generatets.go index b4ea9c3dad..399da5a45b 100644 --- a/cmd/generatets/main-generatets.go +++ b/cmd/generatets/main-generatets.go @@ -21,6 +21,7 @@ func generateTypesFile(tsTypesMap map[reflect.Type]string) error { fileName := "frontend/types/gotypes.d.ts" fmt.Fprintf(os.Stderr, "generating types file to %s\n", fileName) tsgen.GenerateWaveObjTypes(tsTypesMap) + tsgen.GenerateWaveEventTypes(tsTypesMap) err := tsgen.GenerateServiceTypes(tsTypesMap) if err != nil { fmt.Fprintf(os.Stderr, "Error generating service types: %v\n", err) @@ -62,6 +63,24 @@ func generateTypesFile(tsTypesMap map[reflect.Type]string) error { return err } +func generateWaveEventFile(tsTypesMap map[reflect.Type]string) error { + fileName := "frontend/types/waveevent.d.ts" + fmt.Fprintf(os.Stderr, "generating waveevent file to %s\n", fileName) + var buf bytes.Buffer + fmt.Fprintf(&buf, "// Copyright 2025, Command Line Inc.\n") + fmt.Fprintf(&buf, "// SPDX-License-Identifier: Apache-2.0\n\n") + fmt.Fprintf(&buf, "// generated by cmd/generate/main-generatets.go\n\n") + fmt.Fprintf(&buf, "declare global {\n\n") + fmt.Fprint(&buf, utilfn.IndentString(" ", tsgen.GenerateWaveEventTypes(tsTypesMap))) + fmt.Fprintf(&buf, "}\n\n") + fmt.Fprintf(&buf, "export {}\n") + written, err := utilfn.WriteFileIfDifferent(fileName, buf.Bytes()) + if !written { + fmt.Fprintf(os.Stderr, "no changes to %s\n", fileName) + } + return err +} + func generateServicesFile(tsTypesMap map[reflect.Type]string) error { fileName := "frontend/app/store/services.ts" var buf bytes.Buffer @@ -128,6 +147,11 @@ func main() { fmt.Fprintf(os.Stderr, "Error generating services file: %v\n", err) os.Exit(1) } + err = generateWaveEventFile(tsTypesMap) + if err != nil { + fmt.Fprintf(os.Stderr, "Error generating wave event file: %v\n", err) + os.Exit(1) + } err = generateWshClientApiFile(tsTypesMap) if err != nil { fmt.Fprintf(os.Stderr, "Error generating wshserver file: %v\n", err) diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 4c7660effb..eecaacc395 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -1930,14 +1930,6 @@ declare global { total_tokens?: number; }; - // wps.WaveEvent - type WaveEvent = { - event: string; - scopes?: string[]; - sender?: string; - persist?: number; - data?: any; - }; // filestore.WaveFile type WaveFile = { diff --git a/frontend/types/waveevent.d.ts b/frontend/types/waveevent.d.ts new file mode 100644 index 0000000000..2f743a5dde --- /dev/null +++ b/frontend/types/waveevent.d.ts @@ -0,0 +1,41 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +// generated by cmd/generate/main-generatets.go + +declare global { + + // wps.WaveEvent + type WaveEventName = "blockclose" | "connchange" | "sysinfo" | "controllerstatus" | "builderstatus" | "builderoutput" | "waveobj:update" | "blockfile" | "config" | "userinput" | "route:down" | "route:up" | "workspace:update" | "waveai:ratelimit" | "waveapp:appgoupdated" | "tsunami:updatemeta" | "waveai:modeconfig" | "tab:indicator" | "block:jobstatus"; + + type WaveEvent = { + event: WaveEventName; + scopes?: string[]; + sender?: string; + persist?: number; + data?: any; + } & ( + { event: "blockclose"; data?: string; } | + { event: "connchange"; data?: ConnStatus; } | + { event: "sysinfo"; data?: TimeSeriesData; } | + { event: "controllerstatus"; data?: BlockControllerRuntimeStatus; } | + { event: "builderstatus"; data?: BuilderStatusData; } | + { event: "builderoutput"; data?: {[key: string]: any}; } | + { event: "waveobj:update"; data?: WaveObjUpdate; } | + { event: "blockfile"; data?: WSFileEventData; } | + { event: "config"; data?: WatcherUpdate; } | + { event: "userinput"; data?: UserInputRequest; } | + { event: "route:down"; data?: any; } | + { event: "route:up"; data?: any; } | + { event: "workspace:update"; data?: any; } | + { event: "waveai:ratelimit"; data?: RateLimitInfo; } | + { event: "waveapp:appgoupdated"; data?: any; } | + { event: "tsunami:updatemeta"; data?: AppMeta; } | + { event: "waveai:modeconfig"; data?: AIModeConfigUpdate; } | + { event: "tab:indicator"; data?: TabIndicatorEventData; } | + { event: "block:jobstatus"; data?: BlockJobStatusData; } + ); + +} + +export {} diff --git a/pkg/tsgen/tsgenevent.go b/pkg/tsgen/tsgenevent.go new file mode 100644 index 0000000000..d073b5d1ee --- /dev/null +++ b/pkg/tsgen/tsgenevent.go @@ -0,0 +1,85 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package tsgen + +import ( + "bytes" + "fmt" + "reflect" + "strconv" + + "github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes" + "github.com/wavetermdev/waveterm/pkg/blockcontroller" + "github.com/wavetermdev/waveterm/pkg/userinput" + "github.com/wavetermdev/waveterm/pkg/waveobj" + "github.com/wavetermdev/waveterm/pkg/wconfig" + "github.com/wavetermdev/waveterm/pkg/wps" + "github.com/wavetermdev/waveterm/pkg/wshrpc" +) + +var waveEventRType = reflect.TypeOf(wps.WaveEvent{}) + +var WaveEventDataTypes = map[string]reflect.Type{ + wps.Event_BlockClose: reflect.TypeOf(""), + wps.Event_ConnChange: reflect.TypeOf(wshrpc.ConnStatus{}), + wps.Event_SysInfo: reflect.TypeOf(wshrpc.TimeSeriesData{}), + wps.Event_ControllerStatus: reflect.TypeOf((*blockcontroller.BlockControllerRuntimeStatus)(nil)), + wps.Event_BuilderStatus: reflect.TypeOf(wshrpc.BuilderStatusData{}), + wps.Event_BuilderOutput: reflect.TypeOf(map[string]any{}), + wps.Event_WaveObjUpdate: reflect.TypeOf(waveobj.WaveObjUpdate{}), + wps.Event_BlockFile: reflect.TypeOf((*wps.WSFileEventData)(nil)), + wps.Event_Config: reflect.TypeOf(wconfig.WatcherUpdate{}), + wps.Event_UserInput: reflect.TypeOf((*userinput.UserInputRequest)(nil)), + wps.Event_WaveAIRateLimit: reflect.TypeOf((*uctypes.RateLimitInfo)(nil)), + wps.Event_TsunamiUpdateMeta: reflect.TypeOf(wshrpc.AppMeta{}), + wps.Event_AIModeConfig: reflect.TypeOf(wconfig.AIModeConfigUpdate{}), + wps.Event_TabIndicator: reflect.TypeOf(wshrpc.TabIndicatorEventData{}), + wps.Event_BlockJobStatus: reflect.TypeOf(wshrpc.BlockJobStatusData{}), +} + +func getWaveEventDataTSType(eventName string, tsTypesMap map[reflect.Type]string) string { + rtype := WaveEventDataTypes[eventName] + if rtype == nil { + return "any" + } + tsType, _ := TypeToTSType(rtype, tsTypesMap) + if tsType == "" { + return "any" + } + return tsType +} + +func GenerateWaveEventTypes(tsTypesMap map[reflect.Type]string) string { + for _, rtype := range WaveEventDataTypes { + GenerateTSType(rtype, tsTypesMap) + } + // suppress default struct generation, this type is custom generated + tsTypesMap[waveEventRType] = "" + + var buf bytes.Buffer + buf.WriteString("// wps.WaveEvent\n") + buf.WriteString("type WaveEventName = ") + for idx, eventName := range wps.AllEvents { + if idx > 0 { + buf.WriteString(" | ") + } + buf.WriteString(strconv.Quote(eventName)) + } + buf.WriteString(";\n\n") + buf.WriteString("type WaveEvent = {\n") + buf.WriteString(" event: WaveEventName;\n") + buf.WriteString(" scopes?: string[];\n") + buf.WriteString(" sender?: string;\n") + buf.WriteString(" persist?: number;\n") + buf.WriteString(" data?: any;\n") + buf.WriteString("} & (\n") + for idx, eventName := range wps.AllEvents { + if idx > 0 { + buf.WriteString(" | \n") + } + buf.WriteString(fmt.Sprintf(" { event: %s; data?: %s; }", strconv.Quote(eventName), getWaveEventDataTSType(eventName, tsTypesMap))) + } + buf.WriteString("\n);\n") + return buf.String() +} diff --git a/pkg/tsgen/tsgenevent_test.go b/pkg/tsgen/tsgenevent_test.go new file mode 100644 index 0000000000..9d6d72facf --- /dev/null +++ b/pkg/tsgen/tsgenevent_test.go @@ -0,0 +1,34 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package tsgen + +import ( + "reflect" + "strings" + "testing" + + "github.com/wavetermdev/waveterm/pkg/wps" + "github.com/wavetermdev/waveterm/pkg/wshrpc" +) + +func TestGenerateWaveEventTypes(t *testing.T) { + tsTypesMap := make(map[reflect.Type]string) + waveEventTypeDecl := GenerateWaveEventTypes(tsTypesMap) + + if !strings.Contains(waveEventTypeDecl, `type WaveEventName = "blockclose"`) { + t.Fatalf("expected WaveEventName declaration, got:\n%s", waveEventTypeDecl) + } + if !strings.Contains(waveEventTypeDecl, `{ event: "block:jobstatus"; data?: BlockJobStatusData; }`) { + t.Fatalf("expected typed block:jobstatus event, got:\n%s", waveEventTypeDecl) + } + if !strings.Contains(waveEventTypeDecl, `{ event: "route:up"; data?: any; }`) { + t.Fatalf("expected fallback any for unmapped event, got:\n%s", waveEventTypeDecl) + } + if _, found := tsTypesMap[reflect.TypeOf(wps.WaveEvent{})]; !found { + t.Fatalf("expected WaveEvent type to be seeded in tsTypesMap") + } + if _, found := tsTypesMap[reflect.TypeOf(wshrpc.BlockJobStatusData{})]; !found { + t.Fatalf("expected mapped data types to be generated into tsTypesMap") + } +} diff --git a/pkg/wps/wpstypes.go b/pkg/wps/wpstypes.go index 0bf110a7c5..360c486431 100644 --- a/pkg/wps/wpstypes.go +++ b/pkg/wps/wpstypes.go @@ -8,27 +8,49 @@ import ( ) const ( - Event_BlockClose = "blockclose" - Event_ConnChange = "connchange" - Event_SysInfo = "sysinfo" - Event_ControllerStatus = "controllerstatus" - Event_BuilderStatus = "builderstatus" - Event_BuilderOutput = "builderoutput" - Event_WaveObjUpdate = "waveobj:update" - Event_BlockFile = "blockfile" - Event_Config = "config" - Event_UserInput = "userinput" + Event_BlockClose = "blockclose" // type: string + Event_ConnChange = "connchange" // type: wshrpc.ConnStatus + Event_SysInfo = "sysinfo" // type: wshrpc.TimeSeriesData + Event_ControllerStatus = "controllerstatus" // type: *blockcontroller.BlockControllerRuntimeStatus + Event_BuilderStatus = "builderstatus" // type: wshrpc.BuilderStatusData + Event_BuilderOutput = "builderoutput" // type: map[string]any + Event_WaveObjUpdate = "waveobj:update" // type: waveobj.WaveObjUpdate + Event_BlockFile = "blockfile" // type: *WSFileEventData + Event_Config = "config" // type: wconfig.WatcherUpdate + Event_UserInput = "userinput" // type: *userinput.UserInputRequest Event_RouteDown = "route:down" Event_RouteUp = "route:up" Event_WorkspaceUpdate = "workspace:update" - Event_WaveAIRateLimit = "waveai:ratelimit" + Event_WaveAIRateLimit = "waveai:ratelimit" // type: *uctypes.RateLimitInfo Event_WaveAppAppGoUpdated = "waveapp:appgoupdated" - Event_TsunamiUpdateMeta = "tsunami:updatemeta" - Event_AIModeConfig = "waveai:modeconfig" - Event_TabIndicator = "tab:indicator" - Event_BlockJobStatus = "block:jobstatus" // type: BlockJobStatusData + Event_TsunamiUpdateMeta = "tsunami:updatemeta" // type: wshrpc.AppMeta + Event_AIModeConfig = "waveai:modeconfig" // type: wconfig.AIModeConfigUpdate + Event_TabIndicator = "tab:indicator" // type: wshrpc.TabIndicatorEventData + Event_BlockJobStatus = "block:jobstatus" // type: BlockJobStatusData ) +var AllEvents []string = []string{ + Event_BlockClose, + Event_ConnChange, + Event_SysInfo, + Event_ControllerStatus, + Event_BuilderStatus, + Event_BuilderOutput, + Event_WaveObjUpdate, + Event_BlockFile, + Event_Config, + Event_UserInput, + Event_RouteDown, + Event_RouteUp, + Event_WorkspaceUpdate, + Event_WaveAIRateLimit, + Event_WaveAppAppGoUpdated, + Event_TsunamiUpdateMeta, + Event_AIModeConfig, + Event_TabIndicator, + Event_BlockJobStatus, +} + type WaveEvent struct { Event string `json:"event"` Scopes []string `json:"scopes,omitempty"` From 90f5adcb62b489368a22333e89b8377733510933 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 19 Feb 2026 16:48:15 -0800 Subject: [PATCH 3/8] update copyright to 2026 --- cmd/generatets/main-generatets.go | 8 ++++---- frontend/app/store/services.ts | 2 +- frontend/app/store/wshclientapi.ts | 2 +- frontend/types/gotypes.d.ts | 2 +- frontend/types/waveevent.d.ts | 2 +- pkg/gogen/gogen.go | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cmd/generatets/main-generatets.go b/cmd/generatets/main-generatets.go index 399da5a45b..2202c781fe 100644 --- a/cmd/generatets/main-generatets.go +++ b/cmd/generatets/main-generatets.go @@ -32,7 +32,7 @@ func generateTypesFile(tsTypesMap map[reflect.Type]string) error { return fmt.Errorf("error generating wsh server types: %w", err) } var buf bytes.Buffer - fmt.Fprintf(&buf, "// Copyright 2025, Command Line Inc.\n") + fmt.Fprintf(&buf, "// Copyright 2026, Command Line Inc.\n") fmt.Fprintf(&buf, "// SPDX-License-Identifier: Apache-2.0\n\n") fmt.Fprintf(&buf, "// generated by cmd/generate/main-generatets.go\n\n") fmt.Fprintf(&buf, "declare global {\n\n") @@ -67,7 +67,7 @@ func generateWaveEventFile(tsTypesMap map[reflect.Type]string) error { fileName := "frontend/types/waveevent.d.ts" fmt.Fprintf(os.Stderr, "generating waveevent file to %s\n", fileName) var buf bytes.Buffer - fmt.Fprintf(&buf, "// Copyright 2025, Command Line Inc.\n") + fmt.Fprintf(&buf, "// Copyright 2026, Command Line Inc.\n") fmt.Fprintf(&buf, "// SPDX-License-Identifier: Apache-2.0\n\n") fmt.Fprintf(&buf, "// generated by cmd/generate/main-generatets.go\n\n") fmt.Fprintf(&buf, "declare global {\n\n") @@ -85,7 +85,7 @@ func generateServicesFile(tsTypesMap map[reflect.Type]string) error { fileName := "frontend/app/store/services.ts" var buf bytes.Buffer fmt.Fprintf(os.Stderr, "generating services file to %s\n", fileName) - fmt.Fprintf(&buf, "// Copyright 2025, Command Line Inc.\n") + fmt.Fprintf(&buf, "// Copyright 2026, Command Line Inc.\n") fmt.Fprintf(&buf, "// SPDX-License-Identifier: Apache-2.0\n\n") fmt.Fprintf(&buf, "// generated by cmd/generate/main-generatets.go\n\n") fmt.Fprintf(&buf, "import * as WOS from \"./wos\";\n\n") @@ -108,7 +108,7 @@ func generateWshClientApiFile(tsTypeMap map[reflect.Type]string) error { var buf bytes.Buffer declMap := wshrpc.GenerateWshCommandDeclMap() fmt.Fprintf(os.Stderr, "generating wshclientapi file to %s\n", fileName) - fmt.Fprintf(&buf, "// Copyright 2025, Command Line Inc.\n") + fmt.Fprintf(&buf, "// Copyright 2026, Command Line Inc.\n") fmt.Fprintf(&buf, "// SPDX-License-Identifier: Apache-2.0\n\n") fmt.Fprintf(&buf, "// generated by cmd/generate/main-generatets.go\n\n") fmt.Fprintf(&buf, "import { WshClient } from \"./wshclient\";\n\n") diff --git a/frontend/app/store/services.ts b/frontend/app/store/services.ts index 7a36718c37..d9d7730e18 100644 --- a/frontend/app/store/services.ts +++ b/frontend/app/store/services.ts @@ -1,4 +1,4 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 // generated by cmd/generate/main-generatets.go diff --git a/frontend/app/store/wshclientapi.ts b/frontend/app/store/wshclientapi.ts index 25e2bfe929..6222130700 100644 --- a/frontend/app/store/wshclientapi.ts +++ b/frontend/app/store/wshclientapi.ts @@ -1,4 +1,4 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 // generated by cmd/generate/main-generatets.go diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index eecaacc395..9ef81d4fa9 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -1,4 +1,4 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 // generated by cmd/generate/main-generatets.go diff --git a/frontend/types/waveevent.d.ts b/frontend/types/waveevent.d.ts index 2f743a5dde..666e1e53e6 100644 --- a/frontend/types/waveevent.d.ts +++ b/frontend/types/waveevent.d.ts @@ -1,4 +1,4 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 // generated by cmd/generate/main-generatets.go diff --git a/pkg/gogen/gogen.go b/pkg/gogen/gogen.go index 9c37e88379..25b511de3b 100644 --- a/pkg/gogen/gogen.go +++ b/pkg/gogen/gogen.go @@ -13,7 +13,7 @@ import ( ) func GenerateBoilerplate(buf *strings.Builder, pkgName string, imports []string) { - buf.WriteString("// Copyright 2025, Command Line Inc.\n") + buf.WriteString("// Copyright 2026, Command Line Inc.\n") buf.WriteString("// SPDX-License-Identifier: Apache-2.0\n") buf.WriteString("\n// Generated Code. DO NOT EDIT.\n\n") buf.WriteString(fmt.Sprintf("package %s\n\n", pkgName)) From 4f4727901d0a264d56b8b541d1a5372cee1d27c4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 00:55:33 +0000 Subject: [PATCH 4/8] refine waveevent no-data events to null Co-authored-by: sawka <2722291+sawka@users.noreply.github.com> --- frontend/types/waveevent.d.ts | 8 +++---- pkg/tsgen/tsgenevent.go | 41 ++++++++++++++++++++--------------- pkg/tsgen/tsgenevent_test.go | 7 ++++-- pkg/wps/wpstypes.go | 38 ++++++++++++++++---------------- 4 files changed, 52 insertions(+), 42 deletions(-) diff --git a/frontend/types/waveevent.d.ts b/frontend/types/waveevent.d.ts index 666e1e53e6..cf9f2b4c41 100644 --- a/frontend/types/waveevent.d.ts +++ b/frontend/types/waveevent.d.ts @@ -25,11 +25,11 @@ declare global { { event: "blockfile"; data?: WSFileEventData; } | { event: "config"; data?: WatcherUpdate; } | { event: "userinput"; data?: UserInputRequest; } | - { event: "route:down"; data?: any; } | - { event: "route:up"; data?: any; } | - { event: "workspace:update"; data?: any; } | + { event: "route:down"; data?: null; } | + { event: "route:up"; data?: null; } | + { event: "workspace:update"; data?: null; } | { event: "waveai:ratelimit"; data?: RateLimitInfo; } | - { event: "waveapp:appgoupdated"; data?: any; } | + { event: "waveapp:appgoupdated"; data?: null; } | { event: "tsunami:updatemeta"; data?: AppMeta; } | { event: "waveai:modeconfig"; data?: AIModeConfigUpdate; } | { event: "tab:indicator"; data?: TabIndicatorEventData; } | diff --git a/pkg/tsgen/tsgenevent.go b/pkg/tsgen/tsgenevent.go index d073b5d1ee..fc0822cb5f 100644 --- a/pkg/tsgen/tsgenevent.go +++ b/pkg/tsgen/tsgenevent.go @@ -21,28 +21,35 @@ import ( var waveEventRType = reflect.TypeOf(wps.WaveEvent{}) var WaveEventDataTypes = map[string]reflect.Type{ - wps.Event_BlockClose: reflect.TypeOf(""), - wps.Event_ConnChange: reflect.TypeOf(wshrpc.ConnStatus{}), - wps.Event_SysInfo: reflect.TypeOf(wshrpc.TimeSeriesData{}), - wps.Event_ControllerStatus: reflect.TypeOf((*blockcontroller.BlockControllerRuntimeStatus)(nil)), - wps.Event_BuilderStatus: reflect.TypeOf(wshrpc.BuilderStatusData{}), - wps.Event_BuilderOutput: reflect.TypeOf(map[string]any{}), - wps.Event_WaveObjUpdate: reflect.TypeOf(waveobj.WaveObjUpdate{}), - wps.Event_BlockFile: reflect.TypeOf((*wps.WSFileEventData)(nil)), - wps.Event_Config: reflect.TypeOf(wconfig.WatcherUpdate{}), - wps.Event_UserInput: reflect.TypeOf((*userinput.UserInputRequest)(nil)), - wps.Event_WaveAIRateLimit: reflect.TypeOf((*uctypes.RateLimitInfo)(nil)), - wps.Event_TsunamiUpdateMeta: reflect.TypeOf(wshrpc.AppMeta{}), - wps.Event_AIModeConfig: reflect.TypeOf(wconfig.AIModeConfigUpdate{}), - wps.Event_TabIndicator: reflect.TypeOf(wshrpc.TabIndicatorEventData{}), - wps.Event_BlockJobStatus: reflect.TypeOf(wshrpc.BlockJobStatusData{}), + wps.Event_BlockClose: reflect.TypeOf(""), + wps.Event_ConnChange: reflect.TypeOf(wshrpc.ConnStatus{}), + wps.Event_SysInfo: reflect.TypeOf(wshrpc.TimeSeriesData{}), + wps.Event_ControllerStatus: reflect.TypeOf((*blockcontroller.BlockControllerRuntimeStatus)(nil)), + wps.Event_BuilderStatus: reflect.TypeOf(wshrpc.BuilderStatusData{}), + wps.Event_BuilderOutput: reflect.TypeOf(map[string]any{}), + wps.Event_WaveObjUpdate: reflect.TypeOf(waveobj.WaveObjUpdate{}), + wps.Event_BlockFile: reflect.TypeOf((*wps.WSFileEventData)(nil)), + wps.Event_Config: reflect.TypeOf(wconfig.WatcherUpdate{}), + wps.Event_UserInput: reflect.TypeOf((*userinput.UserInputRequest)(nil)), + wps.Event_RouteDown: nil, + wps.Event_RouteUp: nil, + wps.Event_WorkspaceUpdate: nil, + wps.Event_WaveAIRateLimit: reflect.TypeOf((*uctypes.RateLimitInfo)(nil)), + wps.Event_WaveAppAppGoUpdated: nil, + wps.Event_TsunamiUpdateMeta: reflect.TypeOf(wshrpc.AppMeta{}), + wps.Event_AIModeConfig: reflect.TypeOf(wconfig.AIModeConfigUpdate{}), + wps.Event_TabIndicator: reflect.TypeOf(wshrpc.TabIndicatorEventData{}), + wps.Event_BlockJobStatus: reflect.TypeOf(wshrpc.BlockJobStatusData{}), } func getWaveEventDataTSType(eventName string, tsTypesMap map[reflect.Type]string) string { - rtype := WaveEventDataTypes[eventName] - if rtype == nil { + rtype, found := WaveEventDataTypes[eventName] + if !found { return "any" } + if rtype == nil { + return "null" + } tsType, _ := TypeToTSType(rtype, tsTypesMap) if tsType == "" { return "any" diff --git a/pkg/tsgen/tsgenevent_test.go b/pkg/tsgen/tsgenevent_test.go index 9d6d72facf..0fbf872203 100644 --- a/pkg/tsgen/tsgenevent_test.go +++ b/pkg/tsgen/tsgenevent_test.go @@ -22,8 +22,11 @@ func TestGenerateWaveEventTypes(t *testing.T) { if !strings.Contains(waveEventTypeDecl, `{ event: "block:jobstatus"; data?: BlockJobStatusData; }`) { t.Fatalf("expected typed block:jobstatus event, got:\n%s", waveEventTypeDecl) } - if !strings.Contains(waveEventTypeDecl, `{ event: "route:up"; data?: any; }`) { - t.Fatalf("expected fallback any for unmapped event, got:\n%s", waveEventTypeDecl) + if !strings.Contains(waveEventTypeDecl, `{ event: "route:up"; data?: null; }`) { + t.Fatalf("expected null for known no-data event, got:\n%s", waveEventTypeDecl) + } + if got := getWaveEventDataTSType("unmapped:event", tsTypesMap); got != "any" { + t.Fatalf("expected any for unmapped event fallback, got: %q", got) } if _, found := tsTypesMap[reflect.TypeOf(wps.WaveEvent{})]; !found { t.Fatalf("expected WaveEvent type to be seeded in tsTypesMap") diff --git a/pkg/wps/wpstypes.go b/pkg/wps/wpstypes.go index 360c486431..0947e52577 100644 --- a/pkg/wps/wpstypes.go +++ b/pkg/wps/wpstypes.go @@ -8,25 +8,25 @@ import ( ) const ( - Event_BlockClose = "blockclose" // type: string - Event_ConnChange = "connchange" // type: wshrpc.ConnStatus - Event_SysInfo = "sysinfo" // type: wshrpc.TimeSeriesData - Event_ControllerStatus = "controllerstatus" // type: *blockcontroller.BlockControllerRuntimeStatus - Event_BuilderStatus = "builderstatus" // type: wshrpc.BuilderStatusData - Event_BuilderOutput = "builderoutput" // type: map[string]any - Event_WaveObjUpdate = "waveobj:update" // type: waveobj.WaveObjUpdate - Event_BlockFile = "blockfile" // type: *WSFileEventData - Event_Config = "config" // type: wconfig.WatcherUpdate - Event_UserInput = "userinput" // type: *userinput.UserInputRequest - Event_RouteDown = "route:down" - Event_RouteUp = "route:up" - Event_WorkspaceUpdate = "workspace:update" - Event_WaveAIRateLimit = "waveai:ratelimit" // type: *uctypes.RateLimitInfo - Event_WaveAppAppGoUpdated = "waveapp:appgoupdated" - Event_TsunamiUpdateMeta = "tsunami:updatemeta" // type: wshrpc.AppMeta - Event_AIModeConfig = "waveai:modeconfig" // type: wconfig.AIModeConfigUpdate - Event_TabIndicator = "tab:indicator" // type: wshrpc.TabIndicatorEventData - Event_BlockJobStatus = "block:jobstatus" // type: BlockJobStatusData + Event_BlockClose = "blockclose" // type: string + Event_ConnChange = "connchange" // type: wshrpc.ConnStatus + Event_SysInfo = "sysinfo" // type: wshrpc.TimeSeriesData + Event_ControllerStatus = "controllerstatus" // type: *blockcontroller.BlockControllerRuntimeStatus + Event_BuilderStatus = "builderstatus" // type: wshrpc.BuilderStatusData + Event_BuilderOutput = "builderoutput" // type: map[string]any + Event_WaveObjUpdate = "waveobj:update" // type: waveobj.WaveObjUpdate + Event_BlockFile = "blockfile" // type: *WSFileEventData + Event_Config = "config" // type: wconfig.WatcherUpdate + Event_UserInput = "userinput" // type: *userinput.UserInputRequest + Event_RouteDown = "route:down" // type: none + Event_RouteUp = "route:up" // type: none + Event_WorkspaceUpdate = "workspace:update" // type: none + Event_WaveAIRateLimit = "waveai:ratelimit" // type: *uctypes.RateLimitInfo + Event_WaveAppAppGoUpdated = "waveapp:appgoupdated" // type: none + Event_TsunamiUpdateMeta = "tsunami:updatemeta" // type: wshrpc.AppMeta + Event_AIModeConfig = "waveai:modeconfig" // type: wconfig.AIModeConfigUpdate + Event_TabIndicator = "tab:indicator" // type: wshrpc.TabIndicatorEventData + Event_BlockJobStatus = "block:jobstatus" // type: BlockJobStatusData ) var AllEvents []string = []string{ From f610e63ed224b4e41e8a70624666c908881670de Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 20 Feb 2026 16:42:52 -0800 Subject: [PATCH 5/8] better typing for FE. remove a lot of explicit type casts, fix ts errors --- emain/emain-menu.ts | 4 +- frontend/app/store/global.ts | 107 ++++++++---------- frontend/app/store/wos.ts | 4 +- frontend/app/store/wps.ts | 52 ++++----- frontend/app/tab/workspaceswitcher.tsx | 4 +- frontend/app/view/sysinfo/sysinfo.tsx | 8 +- frontend/app/view/term/term-model.ts | 9 +- frontend/app/view/term/term.tsx | 6 +- frontend/app/view/tsunami/tsunami.tsx | 12 +- frontend/app/view/vdom/vdom-model.tsx | 6 +- .../builder/store/builder-apppanel-model.ts | 8 +- .../builder/store/builder-buildpanel-model.ts | 4 +- frontend/types/waveevent.d.ts | 2 +- pkg/tsgen/tsgenevent.go | 2 +- pkg/waveobj/metaconsts.go | 2 +- pkg/wconfig/metaconsts.go | 2 +- pkg/wshrpc/wshclient/wshclient.go | 2 +- 17 files changed, 110 insertions(+), 124 deletions(-) diff --git a/emain/emain-menu.ts b/emain/emain-menu.ts index f4d45f8639..91daabbac9 100644 --- a/emain/emain-menu.ts +++ b/emain/emain-menu.ts @@ -1,7 +1,7 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { waveEventSubscribe } from "@/app/store/wps"; +import { waveEventSubscribeSingle } from "@/app/store/wps"; import { RpcApi } from "@/app/store/wshclientapi"; import * as electron from "electron"; import { fireAndForget } from "../frontend/util/util"; @@ -385,7 +385,7 @@ export function makeAndSetAppMenu() { } function initMenuEventSubscriptions() { - waveEventSubscribe({ + waveEventSubscribeSingle({ eventType: "workspace:update", handler: makeAndSetAppMenu, }); diff --git a/frontend/app/store/global.ts b/frontend/app/store/global.ts index 4dd25b3eb1..c8014e7b28 100644 --- a/frontend/app/store/global.ts +++ b/frontend/app/store/global.ts @@ -31,7 +31,7 @@ import { globalStore } from "./jotaiStore"; import { modalsModel } from "./modalmodel"; import { ClientService, ObjectService } from "./services"; import * as WOS from "./wos"; -import { getFileSubject, waveEventSubscribe } from "./wps"; +import { getFileSubject, waveEventSubscribeSingle } from "./wps"; let atoms: GlobalAtomsType; let globalEnvironment: "electron" | "renderer"; @@ -198,65 +198,56 @@ function initGlobalAtoms(initOpts: GlobalInitOptions) { } function initGlobalWaveEventSubs(initOpts: WaveInitOpts) { - waveEventSubscribe( - { - eventType: "waveobj:update", - handler: (event) => { - // console.log("waveobj:update wave event handler", event); - const update: WaveObjUpdate = event.data; - WOS.updateWaveObject(update); - }, + waveEventSubscribeSingle({ + eventType: "waveobj:update", + handler: (event) => { + // console.log("waveobj:update wave event handler", event); + WOS.updateWaveObject(event.data); }, - { - eventType: "config", - handler: (event) => { - // console.log("config wave event handler", event); - const fullConfig = (event.data as WatcherUpdate).fullconfig; - globalStore.set(atoms.fullConfigAtom, fullConfig); - }, + }); + waveEventSubscribeSingle({ + eventType: "config", + handler: (event) => { + // console.log("config wave event handler", event); + globalStore.set(atoms.fullConfigAtom, event.data.fullconfig); }, - { - eventType: "waveai:modeconfig", - handler: (event) => { - const modeConfigs = (event.data as AIModeConfigUpdate).configs; - globalStore.set(atoms.waveaiModeConfigAtom, modeConfigs); - }, + }); + waveEventSubscribeSingle({ + eventType: "waveai:modeconfig", + handler: (event) => { + globalStore.set(atoms.waveaiModeConfigAtom, event.data.configs); }, - { - eventType: "userinput", - handler: (event) => { - // console.log("userinput event handler", event); - const data: UserInputRequest = event.data; - modalsModel.pushModal("UserInputModal", { ...data }); - }, - scope: initOpts.windowId, + }); + waveEventSubscribeSingle({ + eventType: "userinput", + handler: (event) => { + // console.log("userinput event handler", event); + modalsModel.pushModal("UserInputModal", { ...event.data }); }, - { - eventType: "blockfile", - handler: (event) => { - // console.log("blockfile event update", event); - const fileData: WSFileEventData = event.data; - const fileSubject = getFileSubject(fileData.zoneid, fileData.filename); - if (fileSubject != null) { - fileSubject.next(fileData); - } - }, + scope: initOpts.windowId, + }); + waveEventSubscribeSingle({ + eventType: "blockfile", + handler: (event) => { + // console.log("blockfile event update", event); + const fileSubject = getFileSubject(event.data.zoneid, event.data.filename); + if (fileSubject != null) { + fileSubject.next(event.data); + } }, - { - eventType: "waveai:ratelimit", - handler: (event) => { - const rateLimitInfo: RateLimitInfo = event.data; - globalStore.set(atoms.waveAIRateLimitInfoAtom, rateLimitInfo); - }, + }); + waveEventSubscribeSingle({ + eventType: "waveai:ratelimit", + handler: (event) => { + globalStore.set(atoms.waveAIRateLimitInfoAtom, event.data); }, - { - eventType: "tab:indicator", - handler: (event) => { - const data: TabIndicatorEventData = event.data; - setTabIndicatorInternal(data.tabid, data.indicator); - }, - } - ); + }); + waveEventSubscribeSingle({ + eventType: "tab:indicator", + handler: (event) => { + setTabIndicatorInternal(event.data.tabid, event.data.indicator); + }, + }); } const blockCache = new Map>(); @@ -762,11 +753,11 @@ async function loadTabIndicators() { } function subscribeToConnEvents() { - waveEventSubscribe({ + waveEventSubscribeSingle({ eventType: "connchange", - handler: (event: WaveEvent) => { + handler: (event) => { try { - const connStatus = event.data as ConnStatus; + const connStatus = event.data; if (connStatus == null || isBlank(connStatus.connection)) { return; } @@ -852,7 +843,7 @@ function setTabIndicator(tabId: string, indicator: TabIndicator) { data: { tabid: tabId, indicator: indicator, - } as TabIndicatorEventData, + }, }; fireAndForget(() => RpcApi.EventPublishCommand(TabRpcClient, eventData)); } diff --git a/frontend/app/store/wos.ts b/frontend/app/store/wos.ts index 3e5a48a925..4ce339acd1 100644 --- a/frontend/app/store/wos.ts +++ b/frontend/app/store/wos.ts @@ -3,7 +3,7 @@ // WaveObjectStore -import { waveEventSubscribe } from "@/app/store/wps"; +import { waveEventSubscribeSingle } from "@/app/store/wps"; import { getWebServerEndpoint } from "@/util/endpoints"; import { fetch } from "@/util/fetchutil"; import { fireAndForget } from "@/util/util"; @@ -79,7 +79,7 @@ function debugLogBackendCall(methodName: string, durationStr: string, args: any[ } function wpsSubscribeToObject(oref: string): () => void { - return waveEventSubscribe({ + return waveEventSubscribeSingle({ eventType: "waveobj:update", scope: oref, handler: (event) => { diff --git a/frontend/app/store/wps.ts b/frontend/app/store/wps.ts index b28ec94c8d..745734123c 100644 --- a/frontend/app/store/wps.ts +++ b/frontend/app/store/wps.ts @@ -12,17 +12,19 @@ function setWpsRpcClient(client: WshClient) { WpsRpcClient = client; } -type WaveEventSubject = { - handler: (event: WaveEvent) => void; +type WaveEventSubject = { + handler: (event: Extract) => void; scope?: string; }; -type WaveEventSubjectContainer = WaveEventSubject & { +type WaveEventSubjectContainer = { + handler: (event: WaveEvent) => void; + scope?: string; id: string; }; -type WaveEventSubscription = WaveEventSubject & { - eventType: string; +type WaveEventSubscription = WaveEventSubject & { + eventType: T; }; type WaveEventUnsubscribe = { @@ -58,29 +60,25 @@ function updateWaveEventSub(eventType: string) { RpcApi.EventSubCommand(WpsRpcClient, subreq, { noresponse: true }); } -function waveEventSubscribe(...subscriptions: WaveEventSubscription[]): () => void { - const unsubs: WaveEventUnsubscribe[] = []; - const eventTypeSet = new Set(); - for (const subscription of subscriptions) { - // console.log("waveEventSubscribe", subscription); - if (subscription.handler == null) { - return; - } - const id: string = crypto.randomUUID(); - let subjects = waveEventSubjects.get(subscription.eventType); - if (subjects == null) { - subjects = []; - waveEventSubjects.set(subscription.eventType, subjects); - } - const subcont: WaveEventSubjectContainer = { id, handler: subscription.handler, scope: subscription.scope }; - subjects.push(subcont); - unsubs.push({ id, eventType: subscription.eventType }); - eventTypeSet.add(subscription.eventType); +function waveEventSubscribeSingle(subscription: WaveEventSubscription): () => void { + // console.log("waveEventSubscribeSingle", subscription); + if (subscription.handler == null) { + return () => {}; } - for (const eventType of eventTypeSet) { - updateWaveEventSub(eventType); + const id: string = crypto.randomUUID(); + let subjects = waveEventSubjects.get(subscription.eventType); + if (subjects == null) { + subjects = []; + waveEventSubjects.set(subscription.eventType, subjects); } - return () => waveEventUnsubscribe(...unsubs); + const subcont: WaveEventSubjectContainer = { + id, + handler: subscription.handler as (event: WaveEvent) => void, + scope: subscription.scope, + }; + subjects.push(subcont); + updateWaveEventSub(subscription.eventType); + return () => waveEventUnsubscribe({ id, eventType: subscription.eventType }); } function waveEventUnsubscribe(...unsubscribes: WaveEventUnsubscribe[]) { @@ -149,7 +147,7 @@ export { getFileSubject, handleWaveEvent, setWpsRpcClient, - waveEventSubscribe, + waveEventSubscribeSingle, waveEventUnsubscribe, wpsReconnectHandler, }; diff --git a/frontend/app/tab/workspaceswitcher.tsx b/frontend/app/tab/workspaceswitcher.tsx index f303f3253a..7a253b81e4 100644 --- a/frontend/app/tab/workspaceswitcher.tsx +++ b/frontend/app/tab/workspaceswitcher.tsx @@ -21,7 +21,7 @@ import { IconButton } from "../element/iconbutton"; import { atoms, getApi } from "../store/global"; import { WorkspaceService } from "../store/services"; import { getObjectValue, makeORef } from "../store/wos"; -import { waveEventSubscribe } from "../store/wps"; +import { waveEventSubscribeSingle } from "../store/wps"; import { WorkspaceEditor } from "./workspaceeditor"; import "./workspaceswitcher.scss"; @@ -59,7 +59,7 @@ const WorkspaceSwitcher = forwardRef((_, ref) => { useEffect( () => - waveEventSubscribe({ + waveEventSubscribeSingle({ eventType: "workspace:update", handler: () => fireAndForget(updateWorkspaceList), }), diff --git a/frontend/app/view/sysinfo/sysinfo.tsx b/frontend/app/view/sysinfo/sysinfo.tsx index 5cb48a7c65..9daa34602c 100644 --- a/frontend/app/view/sysinfo/sysinfo.tsx +++ b/frontend/app/view/sysinfo/sysinfo.tsx @@ -13,7 +13,7 @@ import * as jotai from "jotai"; import * as React from "react"; import { useDimensionsWithExistingRef } from "@/app/hook/useDimensions"; -import { waveEventSubscribe } from "@/app/store/wps"; +import { waveEventSubscribeSingle } from "@/app/store/wps"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { atoms } from "@/store/global"; @@ -80,8 +80,8 @@ for (let i = 0; i < 32; i++) { DefaultPlotMeta[`cpu:${i}`] = defaultCpuMeta(`Core ${i}`); } -function convertWaveEventToDataItem(event: WaveEvent): DataItem { - const eventData: TimeSeriesData = event.data; +function convertWaveEventToDataItem(event: Extract): DataItem { + const eventData = event.data; if (eventData == null || eventData.ts == null || eventData.values == null) { return null; } @@ -360,7 +360,7 @@ function SysinfoView({ model, blockId }: SysinfoViewProps) { } }, [connStatus.status, connName]); React.useEffect(() => { - const unsubFn = waveEventSubscribe({ + const unsubFn = waveEventSubscribeSingle({ eventType: "sysinfo", scope: connName, handler: (event) => { diff --git a/frontend/app/view/term/term-model.ts b/frontend/app/view/term/term-model.ts index cfa4fced76..b2d7b10ffa 100644 --- a/frontend/app/view/term/term-model.ts +++ b/frontend/app/view/term/term-model.ts @@ -6,7 +6,7 @@ import { BlockNodeModel } from "@/app/block/blocktypes"; import { appHandleKeyDown } from "@/app/store/keymodel"; import { modalsModel } from "@/app/store/modalmodel"; import type { TabModel } from "@/app/store/tab-model"; -import { waveEventSubscribe } from "@/app/store/wps"; +import { waveEventSubscribeSingle } from "@/app/store/wps"; import { RpcApi } from "@/app/store/wshclientapi"; import { makeFeBlockRouteId } from "@/app/store/wshrouter"; import { DefaultRouter, TabRpcClient } from "@/app/store/wshrpcutil"; @@ -330,12 +330,11 @@ export class TermViewModel implements ViewModel { initialShellProcStatus.then((rts) => { this.updateShellProcStatus(rts); }); - this.shellProcStatusUnsubFn = waveEventSubscribe({ + this.shellProcStatusUnsubFn = waveEventSubscribeSingle({ eventType: "controllerstatus", scope: WOS.makeORef("block", blockId), handler: (event) => { - let bcRTS: BlockControllerRuntimeStatus = event.data; - this.updateShellProcStatus(bcRTS); + this.updateShellProcStatus(event.data); }, }); this.shellProcStatus = jotai.atom((get) => { @@ -364,7 +363,7 @@ export class TermViewModel implements ViewModel { .catch((error) => { console.log("error getting initial block job status", error); }); - this.blockJobStatusUnsubFn = waveEventSubscribe({ + this.blockJobStatusUnsubFn = waveEventSubscribeSingle({ eventType: "block:jobstatus", scope: `block:${blockId}`, handler: (event) => { diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index 10fd0fb112..764c1f8981 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -6,7 +6,7 @@ import type { BlockNodeModel } from "@/app/block/blocktypes"; import { Search, useSearch } from "@/app/element/search"; import { ContextMenuModel } from "@/app/store/contextmenu"; import { useTabModel } from "@/app/store/tab-model"; -import { waveEventSubscribe } from "@/app/store/wps"; +import { waveEventSubscribeSingle } from "@/app/store/wps"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import type { TermViewModel } from "@/app/view/term/term-model"; @@ -55,7 +55,7 @@ const TermResyncHandler = React.memo(({ blockId, model }: TerminalViewProps) => const TermVDomToolbarNode = ({ vdomBlockId, blockId, model }: TerminalViewProps & { vdomBlockId: string }) => { React.useEffect(() => { - const unsub = waveEventSubscribe({ + const unsub = waveEventSubscribeSingle({ eventType: "blockclose", scope: WOS.makeORef("block", vdomBlockId), handler: (event) => { @@ -98,7 +98,7 @@ const TermVDomToolbarNode = ({ vdomBlockId, blockId, model }: TerminalViewProps const TermVDomNodeSingleId = ({ vdomBlockId, blockId, model }: TerminalViewProps & { vdomBlockId: string }) => { React.useEffect(() => { - const unsub = waveEventSubscribe({ + const unsub = waveEventSubscribeSingle({ eventType: "blockclose", scope: WOS.makeORef("block", vdomBlockId), handler: (event) => { diff --git a/frontend/app/view/tsunami/tsunami.tsx b/frontend/app/view/tsunami/tsunami.tsx index 115b10141b..14ca08574f 100644 --- a/frontend/app/view/tsunami/tsunami.tsx +++ b/frontend/app/view/tsunami/tsunami.tsx @@ -4,7 +4,7 @@ import { BlockNodeModel } from "@/app/block/blocktypes"; import { getApi, globalStore, WOS } from "@/app/store/global"; import type { TabModel } from "@/app/store/tab-model"; -import { waveEventSubscribe } from "@/app/store/wps"; +import { waveEventSubscribeSingle } from "@/app/store/wps"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { WebView, WebViewModel } from "@/app/view/webview/webview"; @@ -37,12 +37,11 @@ class TsunamiViewModel extends WebViewModel { initialShellProcStatus.then((rts) => { this.updateShellProcStatus(rts); }); - this.shellProcStatusUnsubFn = waveEventSubscribe({ + this.shellProcStatusUnsubFn = waveEventSubscribeSingle({ eventType: "controllerstatus", scope: WOS.makeORef("block", blockId), handler: (event) => { - let bcRTS: BlockControllerRuntimeStatus = event.data; - this.updateShellProcStatus(bcRTS); + this.updateShellProcStatus(event.data); }, }); @@ -69,12 +68,11 @@ class TsunamiViewModel extends WebViewModel { globalStore.set(this.appMeta, rtInfo["tsunami:appmeta"]); } }); - this.appMetaUnsubFn = waveEventSubscribe({ + this.appMetaUnsubFn = waveEventSubscribeSingle({ eventType: "tsunami:updatemeta", scope: WOS.makeORef("block", blockId), handler: (event) => { - const meta: AppMeta = event.data; - globalStore.set(this.appMeta, meta); + globalStore.set(this.appMeta, event.data); }, }); } diff --git a/frontend/app/view/vdom/vdom-model.tsx b/frontend/app/view/vdom/vdom-model.tsx index 40877894f2..4751ed1d24 100644 --- a/frontend/app/view/vdom/vdom-model.tsx +++ b/frontend/app/view/vdom/vdom-model.tsx @@ -5,7 +5,7 @@ import { BlockNodeModel } from "@/app/block/blocktypes"; import type { TabModel } from "@/app/store/tab-model"; import { getBlockMetaKeyAtom, globalStore, WOS } from "@/app/store/global"; import { makeORef } from "@/app/store/wos"; -import { waveEventSubscribe } from "@/app/store/wps"; +import { waveEventSubscribeSingle } from "@/app/store/wps"; import { RpcResponseHelper, WshClient } from "@/app/store/wshclient"; import { RpcApi } from "@/app/store/wshclientapi"; import { makeFeBlockRouteId } from "@/app/store/wshrouter"; @@ -161,10 +161,10 @@ export class VDomModel { if (curBackendRoute) { this.queueUpdate(true); } - this.routeGoneUnsub = waveEventSubscribe({ + this.routeGoneUnsub = waveEventSubscribeSingle({ eventType: "route:down", scope: curBackendRoute, - handler: (event: WaveEvent) => { + handler: (_event) => { this.disposed = true; const shouldPersist = globalStore.get(this.persist); if (!shouldPersist) { diff --git a/frontend/builder/store/builder-apppanel-model.ts b/frontend/builder/store/builder-apppanel-model.ts index 0f5eed8371..4decca651a 100644 --- a/frontend/builder/store/builder-apppanel-model.ts +++ b/frontend/builder/store/builder-apppanel-model.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { globalStore } from "@/app/store/jotaiStore"; -import { waveEventSubscribe } from "@/app/store/wps"; +import { waveEventSubscribeSingle } from "@/app/store/wps"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { atoms, getApi, WOS } from "@/store/global"; @@ -79,11 +79,11 @@ export class BuilderAppPanelModel { this.statusUnsubFn(); } - this.statusUnsubFn = waveEventSubscribe({ + this.statusUnsubFn = waveEventSubscribeSingle({ eventType: "builderstatus", scope: WOS.makeORef("builder", builderId), handler: (event) => { - const status: BuilderStatusData = event.data; + const status = event.data; const currentStatus = globalStore.get(this.builderStatusAtom); if (!currentStatus || !currentStatus.version || status.version > currentStatus.version) { globalStore.set(this.builderStatusAtom, status); @@ -105,7 +105,7 @@ export class BuilderAppPanelModel { await this.loadAppFile(appId); await this.loadEnvVars(builderId); - this.appGoUpdateUnsubFn = waveEventSubscribe({ + this.appGoUpdateUnsubFn = waveEventSubscribeSingle({ eventType: "waveapp:appgoupdated", scope: appId, handler: () => { diff --git a/frontend/builder/store/builder-buildpanel-model.ts b/frontend/builder/store/builder-buildpanel-model.ts index 93f4ca734b..c4758c5df8 100644 --- a/frontend/builder/store/builder-buildpanel-model.ts +++ b/frontend/builder/store/builder-buildpanel-model.ts @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { globalStore } from "@/app/store/jotaiStore"; -import { waveEventSubscribe } from "@/app/store/wps"; +import { waveEventSubscribeSingle } from "@/app/store/wps"; import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { atoms, WOS } from "@/store/global"; @@ -36,7 +36,7 @@ export class BuilderBuildPanelModel { this.outputUnsubFn(); } - this.outputUnsubFn = waveEventSubscribe({ + this.outputUnsubFn = waveEventSubscribeSingle({ eventType: "builderoutput", scope: WOS.makeORef("builder", builderId), handler: (event) => { diff --git a/frontend/types/waveevent.d.ts b/frontend/types/waveevent.d.ts index cf9f2b4c41..81eec9bf9d 100644 --- a/frontend/types/waveevent.d.ts +++ b/frontend/types/waveevent.d.ts @@ -13,7 +13,7 @@ declare global { scopes?: string[]; sender?: string; persist?: number; - data?: any; + data?: unknown; } & ( { event: "blockclose"; data?: string; } | { event: "connchange"; data?: ConnStatus; } | diff --git a/pkg/tsgen/tsgenevent.go b/pkg/tsgen/tsgenevent.go index fc0822cb5f..f704b22768 100644 --- a/pkg/tsgen/tsgenevent.go +++ b/pkg/tsgen/tsgenevent.go @@ -79,7 +79,7 @@ func GenerateWaveEventTypes(tsTypesMap map[reflect.Type]string) string { buf.WriteString(" scopes?: string[];\n") buf.WriteString(" sender?: string;\n") buf.WriteString(" persist?: number;\n") - buf.WriteString(" data?: any;\n") + buf.WriteString(" data?: unknown;\n") buf.WriteString("} & (\n") for idx, eventName := range wps.AllEvents { if idx > 0 { diff --git a/pkg/waveobj/metaconsts.go b/pkg/waveobj/metaconsts.go index c1383ee32c..07080a7af3 100644 --- a/pkg/waveobj/metaconsts.go +++ b/pkg/waveobj/metaconsts.go @@ -1,4 +1,4 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 // Generated Code. DO NOT EDIT. diff --git a/pkg/wconfig/metaconsts.go b/pkg/wconfig/metaconsts.go index 598ee5ca02..3bb3afddc8 100644 --- a/pkg/wconfig/metaconsts.go +++ b/pkg/wconfig/metaconsts.go @@ -1,4 +1,4 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 // Generated Code. DO NOT EDIT. diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go index ee14fd7585..474969caa5 100644 --- a/pkg/wshrpc/wshclient/wshclient.go +++ b/pkg/wshrpc/wshclient/wshclient.go @@ -1,4 +1,4 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 // Generated Code. DO NOT EDIT. From e2e4da57afe6b9db4d969a8958a758e8697fb6d6 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 20 Feb 2026 16:47:57 -0800 Subject: [PATCH 6/8] update skill + add some instructions directly to wpstypes.go to keep things in sync --- .kilocode/skills/wps-events/SKILL.md | 41 ++++++++++++++++++++++++---- pkg/wps/wpstypes.go | 9 +++++- 2 files changed, 44 insertions(+), 6 deletions(-) diff --git a/.kilocode/skills/wps-events/SKILL.md b/.kilocode/skills/wps-events/SKILL.md index 58e5cf859b..4bc6be717a 100644 --- a/.kilocode/skills/wps-events/SKILL.md +++ b/.kilocode/skills/wps-events/SKILL.md @@ -40,7 +40,7 @@ const ( Event_BlockClose = "blockclose" Event_ConnChange = "connchange" // ... other events ... - Event_YourNewEvent = "your:newevent" // Use colon notation for namespacing + Event_YourNewEvent = "your:newevent" // type: YourEventData (or "none" if no data) ) ``` @@ -49,8 +49,37 @@ const ( - Use descriptive PascalCase for the constant name with `Event_` prefix - Use lowercase with colons for the string value (e.g., "namespace:eventname") - Group related events with the same namespace prefix +- Always add a `// type: ` comment; use `// type: none` if no data is sent -### Step 2: Define Event Data Structure (Optional) +### Step 2: Add to AllEvents + +Add your new constant to the `AllEvents` slice in `pkg/wps/wpstypes.go`: + +```go +var AllEvents []string = []string{ + // ... existing events ... + Event_YourNewEvent, +} +``` + +### Step 3: Register in WaveEventDataTypes (REQUIRED) + +You **must** add an entry to `WaveEventDataTypes` in `pkg/tsgen/tsgenevent.go`. This drives TypeScript type generation for the event's `data` field: + +```go +var WaveEventDataTypes = map[string]reflect.Type{ + // ... existing entries ... + wps.Event_YourNewEvent: reflect.TypeOf(YourEventData{}), // value type + // wps.Event_YourNewEvent: reflect.TypeOf((*YourEventData)(nil)), // pointer type + // wps.Event_YourNewEvent: nil, // no data (type: none) +} +``` + +- Use `reflect.TypeOf(YourType{})` for value types +- Use `reflect.TypeOf((*YourType)(nil))` for pointer types +- Use `nil` if no data is sent for the event + +### Step 4: Define Event Data Structure (Optional) If your event carries structured data, define a type for it: @@ -61,7 +90,7 @@ type YourEventData struct { } ``` -### Step 3: Expose Type to Frontend (If Needed) +### Step 5: Expose Type to Frontend (If Needed) If your event data type isn't already exposed via an RPC call, you need to add it to `pkg/tsgen/tsgen.go` so TypeScript types are generated: @@ -299,9 +328,11 @@ To debug event flow: When adding a new event: -- [ ] Add event constant to `pkg/wps/wpstypes.go` +- [ ] Add event constant to [`pkg/wps/wpstypes.go`](pkg/wps/wpstypes.go) with a `// type: ` comment (use `none` if no data) +- [ ] Add the constant to `AllEvents` in [`pkg/wps/wpstypes.go`](pkg/wps/wpstypes.go) +- [ ] **REQUIRED**: Add an entry to `WaveEventDataTypes` in [`pkg/tsgen/tsgenevent.go`](pkg/tsgen/tsgenevent.go) — use `nil` for events with no data - [ ] Define event data structure (if needed) -- [ ] Add data type to `pkg/tsgen/tsgen.go` for frontend use +- [ ] Add data type to `pkg/tsgen/tsgen.go` for frontend use (if not already exposed via RPC) - [ ] Run `task generate` to update TypeScript types - [ ] Publish events using `wps.Broker.Publish()` - [ ] Use goroutines for non-blocking publish when appropriate diff --git a/pkg/wps/wpstypes.go b/pkg/wps/wpstypes.go index 0947e52577..352f05e2d4 100644 --- a/pkg/wps/wpstypes.go +++ b/pkg/wps/wpstypes.go @@ -7,6 +7,13 @@ import ( "github.com/wavetermdev/waveterm/pkg/util/utilfn" ) +// IMPORTANT: When adding a new event constant, you MUST also: +// 1. Add a "// type: " comment (use "none" if no data is sent) +// 2. Add the constant to AllEvents below +// 3. Add an entry to WaveEventDataTypes in pkg/tsgen/tsgenevent.go +// - Use reflect.TypeOf(YourType{}) for value types +// - Use reflect.TypeOf((*YourType)(nil)) for pointer types +// - Use nil if no data is sent for the event const ( Event_BlockClose = "blockclose" // type: string Event_ConnChange = "connchange" // type: wshrpc.ConnStatus @@ -26,7 +33,7 @@ const ( Event_TsunamiUpdateMeta = "tsunami:updatemeta" // type: wshrpc.AppMeta Event_AIModeConfig = "waveai:modeconfig" // type: wconfig.AIModeConfigUpdate Event_TabIndicator = "tab:indicator" // type: wshrpc.TabIndicatorEventData - Event_BlockJobStatus = "block:jobstatus" // type: BlockJobStatusData + Event_BlockJobStatus = "block:jobstatus" // type: wshrpc.BlockJobStatusData ) var AllEvents []string = []string{ From 40a5cbd51dbf76a3f9ae759e29cb2561fa64736c Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 20 Feb 2026 16:53:53 -0800 Subject: [PATCH 7/8] set up a copilot-instructions.md file that will unify our rules + skills for github copilot --- .github/copilot-instructions.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..c1715117df --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,23 @@ +# Wave Terminal — Copilot Instructions + +## Project Rules + +Read and follow all guidelines in [`.roo/rules/rules.md`](./.roo/rules/rules.md). + +--- + +## Skill Guides + +This project uses a set of "skill" guides — focused how-to documents for common implementation tasks. When your task matches one of the descriptions below, **read the linked SKILL.md file before proceeding** and follow its instructions precisely. + +| Skill | Description | +|-------|-------------| +| [add-config](./.kilocode/skills/add-config/SKILL.md) | Guide for adding new configuration settings to Wave Terminal. Use when adding a new setting to the configuration system, implementing a new config key, or adding user-customizable settings. | +| [add-rpc](./.kilocode/skills/add-rpc/SKILL.md) | Guide for adding new RPC calls to Wave Terminal. Use when implementing new RPC commands, adding server-client communication methods, or extending the RPC interface with new functionality. | +| [add-wshcmd](./.kilocode/skills/add-wshcmd/SKILL.md) | Guide for adding new wsh commands to Wave Terminal. Use when implementing new CLI commands, adding command-line functionality, or extending the wsh command interface. | +| [context-menu](./.kilocode/skills/context-menu/SKILL.md) | Guide for creating and displaying context menus in Wave Terminal. Use when implementing right-click menus, adding context menu items, creating submenus, or handling menu interactions with checkboxes and separators. | +| [create-view](./.kilocode/skills/create-view/SKILL.md) | Guide for implementing a new view type in Wave Terminal. Use when creating a new view component, implementing the ViewModel interface, registering a new view type in BlockRegistry, or adding a new content type to display within blocks. | +| [electron-api](./.kilocode/skills/electron-api/SKILL.md) | Guide for adding new Electron APIs to Wave Terminal. Use when implementing new frontend-to-electron communications via preload/IPC. | +| [wps-events](./.kilocode/skills/wps-events/SKILL.md) | Guide for working with Wave Terminal's WPS (Wave PubSub) event system. Use when implementing new event types, publishing events, subscribing to events, or adding asynchronous communication between components. | + +> **How skills work:** Each skill is a self-contained guide covering the exact files to edit, patterns to follow, and steps to take for a specific type of task in this codebase. If your task matches a skill's description, open that SKILL.md and treat it as your primary reference for the implementation. From 29a3a14539f8bf1c1f5ca4d3497ab994f517dd5a Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 20 Feb 2026 17:01:13 -0800 Subject: [PATCH 8/8] update copyright --- .roo/rules/rules.md | 2 +- pkg/tsgen/tsgenevent.go | 2 +- pkg/tsgen/tsgenevent_test.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.roo/rules/rules.md b/.roo/rules/rules.md index 9ab5f996d0..6518187a1c 100644 --- a/.roo/rules/rules.md +++ b/.roo/rules/rules.md @@ -92,7 +92,7 @@ The full API is defined in custom.d.ts as type ElectronApi. - **Match response length to question complexity** - For simple, direct questions in Ask mode (especially those that can be answered in 1-2 sentences), provide equally brief answers. Save detailed explanations for complex topics or when explicitly requested. - **CRITICAL** - useAtomValue and useAtom are React HOOKS. They cannot be used inline in JSX code, they must appear at the top of a component in the hooks area of the react code. - for simple functions, we prefer `if (!cond) { return }; functionality;` pattern overn `if (cond) { functionality }` because it produces less indentation and is easier to follow. -- It is now 2026, so if you write new files use 2026 for the copyright year +- It is now 2026, so if you write new files, or update files use 2026 for the copyright year ### Strict Comment Rules diff --git a/pkg/tsgen/tsgenevent.go b/pkg/tsgen/tsgenevent.go index f704b22768..b232d517c7 100644 --- a/pkg/tsgen/tsgenevent.go +++ b/pkg/tsgen/tsgenevent.go @@ -1,4 +1,4 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 package tsgen diff --git a/pkg/tsgen/tsgenevent_test.go b/pkg/tsgen/tsgenevent_test.go index 0fbf872203..902eccda3b 100644 --- a/pkg/tsgen/tsgenevent_test.go +++ b/pkg/tsgen/tsgenevent_test.go @@ -1,4 +1,4 @@ -// Copyright 2025, Command Line Inc. +// Copyright 2026, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 package tsgen