From f34ef339eaff77cd5f58890cac0e6e91227aa5cf Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 12 Dec 2025 15:59:53 -0800 Subject: [PATCH 01/15] diagnostic ping impl first cut --- Taskfile.yml | 4 +++ cmd/server/main-server.go | 38 +++++++++++++++++++++ pkg/wcloud/wcloud.go | 69 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+) diff --git a/Taskfile.yml b/Taskfile.yml index 6d329f3bf8..0cf2568bba 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -27,6 +27,7 @@ tasks: - build:tsunamiscaffold env: WAVETERM_ENVFILE: "{{.ROOT_DIR}}/.env" + WCLOUD_PING_ENDPOINT: "https://ping-dev.waveterm.dev/central" WCLOUD_ENDPOINT: "https://api-dev.waveterm.dev/central" WCLOUD_WS_ENDPOINT: "wss://wsapi-dev.waveterm.dev" @@ -40,6 +41,7 @@ tasks: - build:backend env: WAVETERM_ENVFILE: "{{.ROOT_DIR}}/.env" + WCLOUD_PING_ENDPOINT: "https://ping-dev.waveterm.dev/central" WCLOUD_ENDPOINT: "https://api-dev.waveterm.dev/central" WCLOUD_WS_ENDPOINT: "wss://wsapi-dev.waveterm.dev" @@ -51,6 +53,7 @@ tasks: - build:backend:quickdev env: WAVETERM_ENVFILE: "{{.ROOT_DIR}}/.env" + WCLOUD_PING_ENDPOINT: "https://ping-dev.waveterm.dev/central" WCLOUD_ENDPOINT: "https://api-dev.waveterm.dev/central" WCLOUD_WS_ENDPOINT: "wss://wsapi-dev.waveterm.dev/" @@ -62,6 +65,7 @@ tasks: - build:backend:quickdev:windows env: WAVETERM_ENVFILE: "{{.ROOT_DIR}}/.env" + WCLOUD_PING_ENDPOINT: "https://ping-dev.waveterm.dev/central" WCLOUD_ENDPOINT: "https://api-dev.waveterm.dev/central" WCLOUD_WS_ENDPOINT: "wss://wsapi-dev.waveterm.dev/" diff --git a/cmd/server/main-server.go b/cmd/server/main-server.go index 5cac190894..8a69a51cd9 100644 --- a/cmd/server/main-server.go +++ b/cmd/server/main-server.go @@ -59,6 +59,9 @@ const TelemetryInitialCountsWait = 5 * time.Second const TelemetryCountsInterval = 1 * time.Hour const BackupCleanupTick = 2 * time.Minute const BackupCleanupInterval = 4 * time.Hour +const InitialDiagnosticWait = 5 * time.Minute +const DiagnosticTick = 10 * time.Minute +const DiagnosticInterval = 24 * time.Hour var shutdownOnce sync.Once @@ -128,6 +131,40 @@ func telemetryLoop() { } } +func diagnosticLoop() { + defer func() { + panichandler.PanicHandler("diagnosticLoop", recover()) + }() + if os.Getenv("WAVETERM_NOPING") != "" { + log.Printf("WAVETERM_NOPING set, disabling diagnostic ping\n") + return + } + var nextSend int64 + time.Sleep(InitialDiagnosticWait) + for { + if time.Now().Unix() > nextSend { + nextSend = time.Now().Add(DiagnosticInterval).Unix() + sendDiagnosticPing() + } + time.Sleep(DiagnosticTick) + } +} + +func sendDiagnosticPing() { + ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) + defer cancelFn() + clientData, err := wstore.DBGetSingleton[*waveobj.Client](ctx) + if err != nil { + return + } + if clientData == nil { + return + } + arch := runtime.GOARCH + usageTelemetry := telemetry.IsTelemetryEnabled() + wcloud.SendDiagnosticPing(ctx, clientData.OID, arch, WaveVersion, usageTelemetry) +} + func sendNoTelemetryUpdate(telemetryEnabled bool) { ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() @@ -533,6 +570,7 @@ func main() { maybeStartPprofServer() go stdinReadWatch() go telemetryLoop() + go diagnosticLoop() setupTelemetryConfigHandler() go updateTelemetryCountsLoop() go backupCleanupLoop() diff --git a/pkg/wcloud/wcloud.go b/pkg/wcloud/wcloud.go index 455acd8944..b8960919dd 100644 --- a/pkg/wcloud/wcloud.go +++ b/pkg/wcloud/wcloud.go @@ -27,9 +27,12 @@ const WCloudEndpoint = "https://api.waveterm.dev/central" const WCloudEndpointVarName = "WCLOUD_ENDPOINT" const WCloudWSEndpoint = "wss://wsapi.waveterm.dev/" const WCloudWSEndpointVarName = "WCLOUD_WS_ENDPOINT" +const WCloudPingEndpoint = "https://ping.waveterm.dev/central" +const WCloudPingEndpointVarName = "WCLOUD_PING_ENDPOINT" var WCloudWSEndpoint_VarCache string var WCloudEndpoint_VarCache string +var WCloudPingEndpoint_VarCache string const APIVersion = 1 const MaxPtyUpdateSize = (128 * 1024) @@ -47,6 +50,7 @@ const TelemetryUrl = "/telemetry" const TEventsUrl = "/tevents" const NoTelemetryUrl = "/no-telemetry" const WebShareUpdateUrl = "/auth/web-share-update" +const PingUrl = "/ping" func CacheAndRemoveEnvVars() error { WCloudEndpoint_VarCache = os.Getenv(WCloudEndpointVarName) @@ -61,6 +65,8 @@ func CacheAndRemoveEnvVars() error { return err } os.Unsetenv(WCloudWSEndpointVarName) + WCloudPingEndpoint_VarCache = os.Getenv(WCloudPingEndpointVarName) + os.Unsetenv(WCloudPingEndpointVarName) return nil } @@ -101,6 +107,14 @@ func GetWSEndpoint() string { return endpoint } +func GetPingEndpoint() string { + if !wavebase.IsDevMode() { + return WCloudPingEndpoint + } + endpoint := WCloudPingEndpoint_VarCache + return endpoint +} + func makeAnonPostReq(ctx context.Context, apiUrl string, data interface{}) (*http.Request, error) { endpoint := GetEndpoint() if endpoint == "" { @@ -277,3 +291,58 @@ func SendNoTelemetryUpdate(ctx context.Context, clientId string, noTelemetryVal } return nil } + +func makePingPostReq(ctx context.Context, apiUrl string, data interface{}) (*http.Request, error) { + endpoint := GetPingEndpoint() + if endpoint == "" { + return nil, errors.New("wcloud ping endpoint not set") + } + var dataReader io.Reader + if data != nil { + byteArr, err := json.Marshal(data) + if err != nil { + return nil, fmt.Errorf("error marshaling json for %s request: %v", apiUrl, err) + } + dataReader = bytes.NewReader(byteArr) + } + fullUrl := endpoint + apiUrl + req, err := http.NewRequestWithContext(ctx, "POST", fullUrl, dataReader) + if err != nil { + return nil, fmt.Errorf("error creating %s request: %v", apiUrl, err) + } + req.Header.Set("Content-Type", "application/json") + req.Close = true + return req, nil +} + +type PingInputType struct { + ClientId string `json:"clientid"` + Arch string `json:"arch"` + Version string `json:"version"` + LocalDate string `json:"localdate"` + UsageTelemetry bool `json:"usagetelemetry"` +} + +func SendDiagnosticPing(ctx context.Context, clientId string, arch string, version string, usageTelemetry bool) error { + endpoint := GetPingEndpoint() + if endpoint == "" { + return nil + } + localDate := time.Now().Format("2006-01-02") + input := PingInputType{ + ClientId: clientId, + Arch: arch, + Version: version, + LocalDate: localDate, + UsageTelemetry: usageTelemetry, + } + req, err := makePingPostReq(ctx, PingUrl, input) + if err != nil { + return err + } + _, err = doRequest(req, nil) + if err != nil { + return err + } + return nil +} From 65e2fea2320aee37ff18a44cec2dbb8efdffa165 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 12 Dec 2025 16:14:42 -0800 Subject: [PATCH 02/15] implement networkonline (via electron) --- emain/emain-wsh.ts | 6 +++++- frontend/app/store/wshclientapi.ts | 5 +++++ frontend/types/gotypes.d.ts | 2 ++ pkg/wshrpc/wshclient/wshclient.go | 6 ++++++ pkg/wshrpc/wshrpctypes.go | 2 ++ 5 files changed, 20 insertions(+), 1 deletion(-) diff --git a/emain/emain-wsh.ts b/emain/emain-wsh.ts index 639f4dfd35..24f8f5bb15 100644 --- a/emain/emain-wsh.ts +++ b/emain/emain-wsh.ts @@ -4,7 +4,7 @@ import { WindowService } from "@/app/store/services"; import { RpcResponseHelper, WshClient } from "@/app/store/wshclient"; import { RpcApi } from "@/app/store/wshclientapi"; -import { Notification, safeStorage } from "electron"; +import { Notification, net, safeStorage } from "electron"; import { getResolvedUpdateChannel } from "emain/updater"; import { unamePlatform } from "./emain-platform"; import { getWebContentsByBlockId, webGetSelector } from "./emain-web"; @@ -102,6 +102,10 @@ export class ElectronWshClientType extends WshClient { }; } + async handle_networkonline(rh: RpcResponseHelper): Promise { + return net.isOnline(); + } + // async handle_workspaceupdate(rh: RpcResponseHelper) { // console.log("workspaceupdate"); // fireAndForget(async () => { diff --git a/frontend/app/store/wshclientapi.ts b/frontend/app/store/wshclientapi.ts index 59945312d9..4b7ea1b53f 100644 --- a/frontend/app/store/wshclientapi.ts +++ b/frontend/app/store/wshclientapi.ts @@ -392,6 +392,11 @@ class RpcApiType { return client.wshRpcCall("message", data, opts); } + // command "networkonline" [call] + NetworkOnlineCommand(client: WshClient, opts?: RpcOpts): Promise { + return client.wshRpcCall("networkonline", null, opts); + } + // command "notify" [call] NotifyCommand(client: WshClient, data: WaveNotificationOptions, opts?: RpcOpts): Promise { return client.wshRpcCall("notify", data, opts); diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 23a9008399..56b9253b39 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -1222,6 +1222,7 @@ declare global { "settings:customsettings"?: number; "settings:customaimodes"?: number; "settings:secretscount"?: number; + "settings:transparent"?: boolean; "activity:activeminutes"?: number; "activity:fgminutes"?: number; "activity:openminutes"?: number; @@ -1306,6 +1307,7 @@ declare global { "settings:customsettings"?: number; "settings:customaimodes"?: number; "settings:secretscount"?: number; + "settings:transparent"?: boolean; }; // waveobj.Tab diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go index 0490b403fb..e9f518ab02 100644 --- a/pkg/wshrpc/wshclient/wshclient.go +++ b/pkg/wshrpc/wshclient/wshclient.go @@ -476,6 +476,12 @@ func MessageCommand(w *wshutil.WshRpc, data wshrpc.CommandMessageData, opts *wsh return err } +// command "networkonline", wshserver.NetworkOnlineCommand +func NetworkOnlineCommand(w *wshutil.WshRpc, opts *wshrpc.RpcOpts) (bool, error) { + resp, err := sendRpcRequestCallHelper[bool](w, "networkonline", nil, opts) + return resp, err +} + // command "notify", wshserver.NotifyCommand func NotifyCommand(w *wshutil.WshRpc, data wshrpc.WaveNotificationOptions, opts *wshrpc.RpcOpts) error { _, err := sendRpcRequestCallHelper[any](w, "notify", data, opts) diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index 0191a4e4e0..8c1bc0ddbf 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -180,6 +180,7 @@ const ( // electron Command_ElectronEncrypt = "electronencrypt" Command_ElectronDecrypt = "electrondecrypt" + Command_NetworkOnline = "networkonline" // secrets Command_GetSecrets = "getsecrets" @@ -302,6 +303,7 @@ type WshRpcInterface interface { FocusWindowCommand(ctx context.Context, windowId string) error ElectronEncryptCommand(ctx context.Context, data CommandElectronEncryptData) (*CommandElectronEncryptRtnData, error) ElectronDecryptCommand(ctx context.Context, data CommandElectronDecryptData) (*CommandElectronDecryptRtnData, error) + NetworkOnlineCommand(ctx context.Context) (bool, error) // secrets GetSecretsCommand(ctx context.Context, names []string) (map[string]string, error) From 73ce54231e48ed158189b81122f50abd44ffd612 Mon Sep 17 00:00:00 2001 From: sawka Date: Fri, 12 Dec 2025 18:24:54 -0800 Subject: [PATCH 03/15] fix ping logic --- cmd/server/main-server.go | 31 ++++++++++++++++++++----------- pkg/wcloud/wcloud.go | 6 +++--- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/cmd/server/main-server.go b/cmd/server/main-server.go index 8a69a51cd9..095cd525d9 100644 --- a/cmd/server/main-server.go +++ b/cmd/server/main-server.go @@ -38,6 +38,7 @@ import ( "github.com/wavetermdev/waveterm/pkg/web" "github.com/wavetermdev/waveterm/pkg/wps" "github.com/wavetermdev/waveterm/pkg/wshrpc" + "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" "github.com/wavetermdev/waveterm/pkg/wshrpc/wshremote" "github.com/wavetermdev/waveterm/pkg/wshrpc/wshserver" "github.com/wavetermdev/waveterm/pkg/wshutil" @@ -139,30 +140,38 @@ func diagnosticLoop() { log.Printf("WAVETERM_NOPING set, disabling diagnostic ping\n") return } - var nextSend int64 + var lastSentDate string time.Sleep(InitialDiagnosticWait) for { - if time.Now().Unix() > nextSend { - nextSend = time.Now().Add(DiagnosticInterval).Unix() - sendDiagnosticPing() + currentDate := time.Now().Format("2006-01-02") + if lastSentDate == "" || lastSentDate != currentDate { + if sendDiagnosticPing() { + lastSentDate = currentDate + } } time.Sleep(DiagnosticTick) } } -func sendDiagnosticPing() { +func sendDiagnosticPing() bool { ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) defer cancelFn() + + rpcClient := wshclient.GetBareRpcClient() + isOnline, err := wshclient.NetworkOnlineCommand(rpcClient, &wshrpc.RpcOpts{Route: "electron", Timeout: 2000}) + if err != nil || !isOnline { + return false + } clientData, err := wstore.DBGetSingleton[*waveobj.Client](ctx) if err != nil { - return + return false } if clientData == nil { - return + return false } - arch := runtime.GOARCH usageTelemetry := telemetry.IsTelemetryEnabled() - wcloud.SendDiagnosticPing(ctx, clientData.OID, arch, WaveVersion, usageTelemetry) + wcloud.SendDiagnosticPing(ctx, clientData.OID, usageTelemetry) + return true } func sendNoTelemetryUpdate(telemetryEnabled bool) { @@ -355,8 +364,8 @@ func startupActivityUpdate(firstLaunch bool) { fullConfig := wconfig.GetWatcher().GetFullConfig() props := telemetrydata.TEventProps{ UserSet: &telemetrydata.TEventUserProps{ - ClientVersion: "v" + WaveVersion, - ClientBuildTime: BuildTime, + ClientVersion: "v" + wavebase.WaveVersion, + ClientBuildTime: wavebase.BuildTime, ClientArch: wavebase.ClientArch(), ClientOSRelease: wavebase.UnameKernelRelease(), ClientIsDev: wavebase.IsDevMode(), diff --git a/pkg/wcloud/wcloud.go b/pkg/wcloud/wcloud.go index b8960919dd..f599a24163 100644 --- a/pkg/wcloud/wcloud.go +++ b/pkg/wcloud/wcloud.go @@ -323,7 +323,7 @@ type PingInputType struct { UsageTelemetry bool `json:"usagetelemetry"` } -func SendDiagnosticPing(ctx context.Context, clientId string, arch string, version string, usageTelemetry bool) error { +func SendDiagnosticPing(ctx context.Context, clientId string, usageTelemetry bool) error { endpoint := GetPingEndpoint() if endpoint == "" { return nil @@ -331,8 +331,8 @@ func SendDiagnosticPing(ctx context.Context, clientId string, arch string, versi localDate := time.Now().Format("2006-01-02") input := PingInputType{ ClientId: clientId, - Arch: arch, - Version: version, + Arch: wavebase.ClientArch(), + Version: "v" + wavebase.WaveVersion, LocalDate: localDate, UsageTelemetry: usageTelemetry, } From 5298353b5862ad32acf071a7a8b55fc79471b11a Mon Sep 17 00:00:00 2001 From: sawka Date: Sat, 13 Dec 2025 12:46:08 -0800 Subject: [PATCH 04/15] add block controller to createblock tdata --- pkg/telemetry/telemetrydata/telemetrydata.go | 1 + pkg/wcore/block.go | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/telemetry/telemetrydata/telemetrydata.go b/pkg/telemetry/telemetrydata/telemetrydata.go index db3e3c464a..6bc1e6ee91 100644 --- a/pkg/telemetry/telemetrydata/telemetrydata.go +++ b/pkg/telemetry/telemetrydata/telemetrydata.go @@ -106,6 +106,7 @@ type TEventProps struct { ActionType string `json:"action:type,omitempty"` PanicType string `json:"debug:panictype,omitempty"` BlockView string `json:"block:view,omitempty"` + BlockController string `json:"block:controller,omitempty"` AiBackendType string `json:"ai:backendtype,omitempty"` AiLocal bool `json:"ai:local,omitempty"` WshCmd string `json:"wsh:cmd,omitempty"` diff --git a/pkg/wcore/block.go b/pkg/wcore/block.go index 822779774f..3c6e2e197b 100644 --- a/pkg/wcore/block.go +++ b/pkg/wcore/block.go @@ -100,12 +100,13 @@ func CreateBlockWithTelemetry(ctx context.Context, tabId string, blockDef *waveo } if recordTelemetry { blockView := blockDef.Meta.GetString(waveobj.MetaKey_View, "") - go recordBlockCreationTelemetry(blockView) + blockController := blockDef.Meta.GetString(waveobj.MetaKey_Controller, "") + go recordBlockCreationTelemetry(blockView, blockController) } return blockData, nil } -func recordBlockCreationTelemetry(blockView string) { +func recordBlockCreationTelemetry(blockView string, blockController string) { defer func() { panichandler.PanicHandler("CreateBlock:telemetry", recover()) }() @@ -120,7 +121,8 @@ func recordBlockCreationTelemetry(blockView string) { telemetry.RecordTEvent(tctx, &telemetrydata.TEvent{ Event: "action:createblock", Props: telemetrydata.TEventProps{ - BlockView: blockView, + BlockView: blockView, + BlockController: blockController, }, }) } From e75434b8f4827b85467774525c0742437731c39a Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 15 Dec 2025 14:00:58 -0800 Subject: [PATCH 05/15] working on enabling AI panel without telemetry when local models --- frontend/app/aipanel/aipanel.tsx | 38 ++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/frontend/app/aipanel/aipanel.tsx b/frontend/app/aipanel/aipanel.tsx index 8d1d8d36f0..e33ab104f2 100644 --- a/frontend/app/aipanel/aipanel.tsx +++ b/frontend/app/aipanel/aipanel.tsx @@ -233,6 +233,12 @@ const AIPanelComponentInner = memo(() => { const isFocused = jotai.useAtomValue(model.isWaveAIFocusedAtom); const telemetryEnabled = jotai.useAtomValue(getSettingsKeyAtom("telemetry:enabled")) ?? false; const isPanelVisible = jotai.useAtomValue(model.getPanelVisibleAtom()); + const defaultMode = jotai.useAtomValue(getSettingsKeyAtom("waveai:defaultmode")) ?? "waveai@balanced"; + const aiModeConfigs = jotai.useAtomValue(model.aiModeConfigs); + + const hasCustomModes = Object.keys(aiModeConfigs).some((key) => !key.startsWith("waveai@")); + const isUsingCustomMode = !defaultMode.startsWith("waveai@"); + const allowAccess = telemetryEnabled || (hasCustomModes && isUsingCustomMode); const { messages, sendMessage, status, setMessages, error, stop } = useChat({ transport: new DefaultChatTransport({ @@ -331,6 +337,10 @@ const AIPanelComponentInner = memo(() => { }; const handleDragOver = (e: React.DragEvent) => { + if (!allowAccess) { + return; + } + const hasFiles = hasFilesDragged(e.dataTransfer); // Only handle native file drags here, let react-dnd handle FILE_ITEM drags @@ -347,6 +357,10 @@ const AIPanelComponentInner = memo(() => { }; const handleDragEnter = (e: React.DragEvent) => { + if (!allowAccess) { + return; + } + const hasFiles = hasFilesDragged(e.dataTransfer); // Only handle native file drags here, let react-dnd handle FILE_ITEM drags @@ -361,6 +375,10 @@ const AIPanelComponentInner = memo(() => { }; const handleDragLeave = (e: React.DragEvent) => { + if (!allowAccess) { + return; + } + const hasFiles = hasFilesDragged(e.dataTransfer); // Only handle native file drags here, let react-dnd handle FILE_ITEM drags @@ -382,6 +400,13 @@ const AIPanelComponentInner = memo(() => { }; const handleDrop = async (e: React.DragEvent) => { + if (!allowAccess) { + e.preventDefault(); + e.stopPropagation(); + setIsDragOver(false); + return; + } + // Check if this is a FILE_ITEM drag from react-dnd // If so, let react-dnd handle it instead if (!e.dataTransfer.files.length) { @@ -415,8 +440,13 @@ const AIPanelComponentInner = memo(() => { }; const handleFileItemDrop = useCallback( - (draggedFile: DraggedFile) => model.addFileFromRemoteUri(draggedFile), - [model] + (draggedFile: DraggedFile) => { + if (!allowAccess) { + return; + } + model.addFileFromRemoteUri(draggedFile); + }, + [model, allowAccess] ); const [{ isOver, canDrop }, drop] = useDrop( @@ -501,13 +531,13 @@ const AIPanelComponentInner = memo(() => { onClick={handleClick} inert={!isPanelVisible ? true : undefined} > - {(isDragOver || isReactDndDragOver) && } + {(isDragOver || isReactDndDragOver) && allowAccess && } {showBlockMask && }
- {!telemetryEnabled ? ( + {!allowAccess ? ( ) : ( <> From d497c93466dd54f7e09adc75e7d89cdd4c53ebf6 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 15 Dec 2025 14:16:24 -0800 Subject: [PATCH 06/15] better FE aimode dropdown when telemetry is disabled --- frontend/app/aipanel/aimode.tsx | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/frontend/app/aipanel/aimode.tsx b/frontend/app/aipanel/aimode.tsx index 4c6c52a7a4..d8fc34e471 100644 --- a/frontend/app/aipanel/aimode.tsx +++ b/frontend/app/aipanel/aimode.tsx @@ -58,6 +58,7 @@ interface ConfigSection { sectionName: string; configs: AIModeConfigWithMode[]; isIncompatible?: boolean; + noTelemetry?: boolean; } function computeCompatibleSections( @@ -111,12 +112,17 @@ function computeCompatibleSections( function computeWaveCloudSections( waveProviderConfigs: AIModeConfigWithMode[], - otherProviderConfigs: AIModeConfigWithMode[] + otherProviderConfigs: AIModeConfigWithMode[], + telemetryEnabled: boolean ): ConfigSection[] { const sections: ConfigSection[] = []; if (waveProviderConfigs.length > 0) { - sections.push({ sectionName: "Wave AI Cloud", configs: waveProviderConfigs }); + sections.push({ + sectionName: "Wave AI Cloud", + configs: waveProviderConfigs, + noTelemetry: !telemetryEnabled + }); } if (otherProviderConfigs.length > 0) { sections.push({ sectionName: "Custom", configs: otherProviderConfigs }); @@ -138,6 +144,7 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow const rateLimitInfo = useAtomValue(atoms.waveAIRateLimitInfoAtom); const showCloudModes = useAtomValue(getSettingsKeyAtom("waveai:showcloudmodes")); const defaultMode = useAtomValue(getSettingsKeyAtom("waveai:defaultmode")) ?? "waveai@balanced"; + const telemetryEnabled = useAtomValue(getSettingsKeyAtom("telemetry:enabled")) ?? false; const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); @@ -163,7 +170,7 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow const sections: ConfigSection[] = compatibilityMode ? computeCompatibleSections(currentMode, aiModeConfigs, waveProviderConfigs, otherProviderConfigs) - : computeWaveCloudSections(waveProviderConfigs, otherProviderConfigs); + : computeWaveCloudSections(waveProviderConfigs, otherProviderConfigs, telemetryEnabled); const showSectionHeaders = compatibilityMode || sections.length > 1; @@ -260,6 +267,11 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow (Start a New Chat to Switch)
)} + {section.noTelemetry && ( +
+ (not available when telemetry is disabled) +
+ )} )} {section.configs.map((config, index) => { @@ -267,7 +279,8 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow const isLast = index === section.configs.length - 1 && isLastSection; const isPremiumDisabled = !hasPremium && config["waveai:premium"]; const isIncompatibleDisabled = section.isIncompatible || false; - const isDisabled = isPremiumDisabled || isIncompatibleDisabled; + const isTelemetryDisabled = section.noTelemetry || false; + const isDisabled = isPremiumDisabled || isIncompatibleDisabled || isTelemetryDisabled; const isSelected = currentMode === config.mode; return ( Date: Mon, 15 Dec 2025 14:40:43 -0800 Subject: [PATCH 07/15] backend update to not allow waveai could without telemetry --- pkg/aiusechat/openai/openai-backend.go | 14 ++++++++------ pkg/aiusechat/usechat.go | 3 +++ 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/pkg/aiusechat/openai/openai-backend.go b/pkg/aiusechat/openai/openai-backend.go index 4a266dc671..bca2ea44f9 100644 --- a/pkg/aiusechat/openai/openai-backend.go +++ b/pkg/aiusechat/openai/openai-backend.go @@ -24,17 +24,19 @@ import ( "github.com/wavetermdev/waveterm/pkg/web/sse" ) -// sanitizeHostnameInError removes the specific hostname from error messages -func sanitizeHostnameInError(err error, baseURL string) error { +// sanitizeHostnameInError removes the Wave cloud hostname from error messages +func sanitizeHostnameInError(err error) error { if err == nil { return nil } errStr := err.Error() - parsedURL, parseErr := url.Parse(baseURL) + parsedURL, parseErr := url.Parse(uctypes.DefaultAIEndpoint) if parseErr == nil && parsedURL.Host != "" { - errStr = strings.ReplaceAll(errStr, baseURL, "AI service") - errStr = strings.ReplaceAll(errStr, parsedURL.Host, "host") + if strings.Contains(errStr, parsedURL.Host) { + errStr = strings.ReplaceAll(errStr, uctypes.DefaultAIEndpoint, "AI service") + errStr = strings.ReplaceAll(errStr, parsedURL.Host, "host") + } } return fmt.Errorf("%s", errStr) @@ -520,7 +522,7 @@ func RunOpenAIChatStep( resp, err := httpClient.Do(req) if err != nil { - return nil, nil, nil, sanitizeHostnameInError(err, chatOpts.Config.Endpoint) + return nil, nil, nil, sanitizeHostnameInError(err) } defer resp.Body.Close() diff --git a/pkg/aiusechat/usechat.go b/pkg/aiusechat/usechat.go index e6d7737ed0..3e5093d543 100644 --- a/pkg/aiusechat/usechat.go +++ b/pkg/aiusechat/usechat.go @@ -84,6 +84,9 @@ func getWaveAISettings(premium bool, builderMode bool, rtInfo waveobj.ObjRTInfo) if err != nil { return nil, err } + if config.WaveAICloud && !telemetry.IsTelemetryEnabled() { + return nil, fmt.Errorf("Wave AI cloud modes require telemetry to be enabled") + } apiToken := config.APIToken if apiToken == "" && config.APITokenSecretName != "" { secret, exists, err := secretstore.GetSecret(config.APITokenSecretName) From 4ee6c7dd06453a244fe8b16daa3f00376776f30d Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 15 Dec 2025 14:55:52 -0800 Subject: [PATCH 08/15] use the waveaiModeConfigAtom, not the fullConfig modes... --- frontend/app/aipanel/aipanel.tsx | 6 ++---- frontend/app/aipanel/waveai-model.tsx | 5 +---- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/frontend/app/aipanel/aipanel.tsx b/frontend/app/aipanel/aipanel.tsx index e33ab104f2..46c9ae9fb1 100644 --- a/frontend/app/aipanel/aipanel.tsx +++ b/frontend/app/aipanel/aipanel.tsx @@ -84,10 +84,8 @@ KeyCap.displayName = "KeyCap"; const AIWelcomeMessage = memo(() => { const modKey = isMacOS() ? "⌘" : "Alt"; - const fullConfig = jotai.useAtomValue(atoms.fullConfigAtom); - const hasCustomModes = fullConfig?.waveai - ? Object.keys(fullConfig.waveai).some((key) => !key.startsWith("waveai@")) - : false; + const aiModeConfigs = jotai.useAtomValue(atoms.waveaiModeConfigAtom); + const hasCustomModes = Object.keys(aiModeConfigs).some((key) => !key.startsWith("waveai@")); return (
diff --git a/frontend/app/aipanel/waveai-model.tsx b/frontend/app/aipanel/waveai-model.tsx index abc2615868..9dd7c3c04a 100644 --- a/frontend/app/aipanel/waveai-model.tsx +++ b/frontend/app/aipanel/waveai-model.tsx @@ -85,10 +85,7 @@ export class WaveAIModel { const modelMetaAtom = getOrefMetaKeyAtom(this.orefContext, "waveai:model"); return get(modelMetaAtom) ?? "gpt-5.1"; }); - this.aiModeConfigs = jotai.atom((get) => { - const fullConfig = get(atoms.fullConfigAtom); - return fullConfig?.waveai ?? {}; - }); + this.aiModeConfigs = atoms.waveaiModeConfigAtom; this.widgetAccessAtom = jotai.atom((get) => { From 22d4c24070d38dc6f360ed4d006925660c661de8 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 15 Dec 2025 15:48:24 -0800 Subject: [PATCH 09/15] frontend AI mode is now definitive, no more backend using rtinfo or defaulting --- frontend/app/aipanel/aipanel.tsx | 16 +++++ frontend/app/aipanel/waveai-model.tsx | 85 ++++++++++++++++++++++++--- pkg/aiusechat/usechat-mode.go | 7 --- pkg/aiusechat/usechat.go | 11 +++- 4 files changed, 100 insertions(+), 19 deletions(-) diff --git a/frontend/app/aipanel/aipanel.tsx b/frontend/app/aipanel/aipanel.tsx index 46c9ae9fb1..7641735c62 100644 --- a/frontend/app/aipanel/aipanel.tsx +++ b/frontend/app/aipanel/aipanel.tsx @@ -219,6 +219,20 @@ const AIErrorMessage = memo(({ errorMessage, onClear }: AIErrorMessageProps) => AIErrorMessage.displayName = "AIErrorMessage"; +const RTInfoModeFixer = memo(() => { + const model = WaveAIModel.getInstance(); + const telemetryEnabled = jotai.useAtomValue(getSettingsKeyAtom("telemetry:enabled")) ?? false; + const aiModeConfigs = jotai.useAtomValue(model.aiModeConfigs); + + useEffect(() => { + model.fixRTInfoMode(); + }, [telemetryEnabled, aiModeConfigs, model]); + + return null; +}); + +RTInfoModeFixer.displayName = "RTInfoModeFixer"; + const AIPanelComponentInner = memo(() => { const [isDragOver, setIsDragOver] = useState(false); const [isReactDndDragOver, setIsReactDndDragOver] = useState(false); @@ -248,6 +262,7 @@ const AIPanelComponentInner = memo(() => { msg, chatid: globalStore.get(model.chatId), widgetaccess: globalStore.get(model.widgetAccessAtom), + aimode: globalStore.get(model.currentAIMode), }; if (windowType === "builder") { body.builderid = globalStore.get(atoms.builderId); @@ -529,6 +544,7 @@ const AIPanelComponentInner = memo(() => { onClick={handleClick} inert={!isPanelVisible ? true : undefined} > + {(isDragOver || isReactDndDragOver) && allowAccess && } {showBlockMask && } diff --git a/frontend/app/aipanel/waveai-model.tsx b/frontend/app/aipanel/waveai-model.tsx index 9dd7c3c04a..a688ae5445 100644 --- a/frontend/app/aipanel/waveai-model.tsx +++ b/frontend/app/aipanel/waveai-model.tsx @@ -57,10 +57,11 @@ export class WaveAIModel { widgetAccessAtom!: jotai.Atom; droppedFiles: jotai.PrimitiveAtom = jotai.atom([]); chatId!: jotai.PrimitiveAtom; - currentAIMode: jotai.PrimitiveAtom = jotai.atom("waveai@balanced"); + currentAIMode!: jotai.PrimitiveAtom; aiModeConfigs!: jotai.Atom>; + hasPremiumAtom!: jotai.Atom; + defaultModeAtom!: jotai.Atom; errorMessage: jotai.PrimitiveAtom = jotai.atom(null) as jotai.PrimitiveAtom; - modelAtom!: jotai.Atom; containerWidth: jotai.PrimitiveAtom = jotai.atom(0); codeBlockMaxWidth!: jotai.Atom; inputAtom: jotai.PrimitiveAtom = jotai.atom(""); @@ -77,16 +78,13 @@ export class WaveAIModel { private constructor(orefContext: ORef, inBuilder: boolean) { this.orefContext = orefContext; this.inBuilder = inBuilder; - const defaultMode = globalStore.get(getSettingsKeyAtom("waveai:defaultmode")) ?? "waveai@balanced"; - this.currentAIMode = jotai.atom(defaultMode); this.chatId = jotai.atom(null) as jotai.PrimitiveAtom; - - this.modelAtom = jotai.atom((get) => { - const modelMetaAtom = getOrefMetaKeyAtom(this.orefContext, "waveai:model"); - return get(modelMetaAtom) ?? "gpt-5.1"; - }); this.aiModeConfigs = atoms.waveaiModeConfigAtom; + this.hasPremiumAtom = jotai.atom((get) => { + const rateLimitInfo = get(atoms.waveAIRateLimitInfoAtom); + return !rateLimitInfo || rateLimitInfo.unknown || rateLimitInfo.preq > 0; + }); this.widgetAccessAtom = jotai.atom((get) => { if (this.inBuilder) { @@ -115,6 +113,39 @@ export class WaveAIModel { } return get(WorkspaceLayoutModel.getInstance().panelVisibleAtom); }); + + this.defaultModeAtom = jotai.atom((get) => { + const telemetryEnabled = get(getSettingsKeyAtom("telemetry:enabled")) ?? false; + + if (this.inBuilder) { + return telemetryEnabled ? "waveai@balanced" : "unknown"; + } + + const aiModeConfigs = get(this.aiModeConfigs); + const hasPremium = get(this.hasPremiumAtom); + + const waveFallback = hasPremium ? "waveai@balanced" : "waveai@quick"; + let mode = get(getSettingsKeyAtom("waveai:defaultmode")) ?? waveFallback; + + const modeExists = aiModeConfigs != null && mode in aiModeConfigs; + + if (!modeExists) { + if (telemetryEnabled) { + mode = waveFallback; + } else { + return "unknown"; + } + } + + if (mode.startsWith("waveai@") && !telemetryEnabled) { + return "unknown"; + } + + return mode; + }); + + const defaultMode = globalStore.get(this.defaultModeAtom); + this.currentAIMode = jotai.atom(defaultMode); } getPanelVisibleAtom(): jotai.Atom { @@ -350,6 +381,42 @@ export class WaveAIModel { }); } + async fixRTInfoMode(): Promise { + const rtInfo = await RpcApi.GetRTInfoCommand(TabRpcClient, { + oref: this.orefContext, + }); + const mode = rtInfo?.["waveai:mode"]; + + if (mode == null) { + return; + } + + let shouldClear = false; + + if (mode.startsWith("waveai@")) { + const telemetryEnabled = globalStore.get(getSettingsKeyAtom("telemetry:enabled")) ?? false; + if (!telemetryEnabled) { + shouldClear = true; + } + } + + if (!shouldClear) { + const aiModeConfigs = globalStore.get(this.aiModeConfigs); + if (aiModeConfigs == null || !(mode in aiModeConfigs)) { + shouldClear = true; + } + } + + if (shouldClear) { + const defaultMode = globalStore.get(this.defaultModeAtom); + globalStore.set(this.currentAIMode, defaultMode); + RpcApi.SetRTInfoCommand(TabRpcClient, { + oref: this.orefContext, + data: { "waveai:mode": null }, + }); + } + } + async loadInitialChat(): Promise { const rtInfo = await RpcApi.GetRTInfoCommand(TabRpcClient, { oref: this.orefContext, diff --git a/pkg/aiusechat/usechat-mode.go b/pkg/aiusechat/usechat-mode.go index be2853d0df..b2d6c84614 100644 --- a/pkg/aiusechat/usechat-mode.go +++ b/pkg/aiusechat/usechat-mode.go @@ -36,13 +36,6 @@ const ( func resolveAIMode(requestedMode string, premium bool) (string, *wconfig.AIModeConfigType, error) { mode := requestedMode - if mode == "" { - fullConfig := wconfig.GetWatcher().GetFullConfig() - mode = fullConfig.Settings.WaveAiDefaultMode - if mode == "" { - mode = uctypes.AIModeBalanced - } - } config, err := getAIModeConfig(mode) if err != nil { diff --git a/pkg/aiusechat/usechat.go b/pkg/aiusechat/usechat.go index 3e5093d543..45846ece9a 100644 --- a/pkg/aiusechat/usechat.go +++ b/pkg/aiusechat/usechat.go @@ -72,7 +72,7 @@ func isLocalEndpoint(endpoint string) bool { return strings.Contains(endpointLower, "localhost") || strings.Contains(endpointLower, "127.0.0.1") } -func getWaveAISettings(premium bool, builderMode bool, rtInfo waveobj.ObjRTInfo) (*uctypes.AIOptsType, error) { +func getWaveAISettings(premium bool, builderMode bool, rtInfo waveobj.ObjRTInfo, aiModeName string) (*uctypes.AIOptsType, error) { maxTokens := DefaultMaxTokens if builderMode { maxTokens = BuilderMaxTokens @@ -80,7 +80,7 @@ func getWaveAISettings(premium bool, builderMode bool, rtInfo waveobj.ObjRTInfo) if rtInfo.WaveAIMaxOutputTokens > 0 { maxTokens = rtInfo.WaveAIMaxOutputTokens } - aiMode, config, err := resolveAIMode(rtInfo.WaveAIMode, premium) + aiMode, config, err := resolveAIMode(aiModeName, premium) if err != nil { return nil, err } @@ -600,6 +600,7 @@ type PostMessageRequest struct { ChatID string `json:"chatid"` Msg uctypes.AIMessage `json:"msg"` WidgetAccess bool `json:"widgetaccess,omitempty"` + AIMode string `json:"aimode,omitempty"` } func WaveAIPostMessageHandler(w http.ResponseWriter, r *http.Request) { @@ -642,7 +643,11 @@ func WaveAIPostMessageHandler(w http.ResponseWriter, r *http.Request) { // Get WaveAI settings premium := shouldUsePremium() builderMode := req.BuilderId != "" - aiOpts, err := getWaveAISettings(premium, builderMode, *rtInfo) + if req.AIMode == "" { + http.Error(w, "aimode is required in request body", http.StatusBadRequest) + return + } + aiOpts, err := getWaveAISettings(premium, builderMode, *rtInfo, req.AIMode) if err != nil { http.Error(w, fmt.Sprintf("WaveAI configuration error: %v", err), http.StatusInternalServerError) return From 0311c59721d12f7f48c436d911f0580afd992506 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 15 Dec 2025 16:07:01 -0800 Subject: [PATCH 10/15] working on more consistent modes --- frontend/app/aipanel/aimode.tsx | 4 +- frontend/app/aipanel/aipanel-contextmenu.ts | 3 +- frontend/app/aipanel/waveai-model.tsx | 70 ++++++++++++--------- 3 files changed, 42 insertions(+), 35 deletions(-) diff --git a/frontend/app/aipanel/aimode.tsx b/frontend/app/aipanel/aimode.tsx index d8fc34e471..be21938347 100644 --- a/frontend/app/aipanel/aimode.tsx +++ b/frontend/app/aipanel/aimode.tsx @@ -141,15 +141,13 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow const aiModeConfigs = useAtomValue(model.aiModeConfigs); const waveaiModeConfigs = useAtomValue(atoms.waveaiModeConfigAtom); const widgetContextEnabled = useAtomValue(model.widgetAccessAtom); - const rateLimitInfo = useAtomValue(atoms.waveAIRateLimitInfoAtom); + const hasPremium = useAtomValue(model.hasPremiumAtom); const showCloudModes = useAtomValue(getSettingsKeyAtom("waveai:showcloudmodes")); const defaultMode = useAtomValue(getSettingsKeyAtom("waveai:defaultmode")) ?? "waveai@balanced"; const telemetryEnabled = useAtomValue(getSettingsKeyAtom("telemetry:enabled")) ?? false; const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); - const hasPremium = !rateLimitInfo || rateLimitInfo.unknown || rateLimitInfo.preq > 0; - const { waveProviderConfigs, otherProviderConfigs } = getFilteredAIModeConfigs( aiModeConfigs, showCloudModes, diff --git a/frontend/app/aipanel/aipanel-contextmenu.ts b/frontend/app/aipanel/aipanel-contextmenu.ts index ffa9336d8e..111bbae221 100644 --- a/frontend/app/aipanel/aipanel-contextmenu.ts +++ b/frontend/app/aipanel/aipanel-contextmenu.ts @@ -40,8 +40,7 @@ export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boo oref: model.orefContext, }); - const rateLimitInfo = globalStore.get(atoms.waveAIRateLimitInfoAtom); - const hasPremium = !rateLimitInfo || rateLimitInfo.unknown || rateLimitInfo.preq > 0; + const hasPremium = globalStore.get(model.hasPremiumAtom); const aiModeConfigs = globalStore.get(model.aiModeConfigs); const showCloudModes = globalStore.get(getSettingsKeyAtom("waveai:showcloudmodes")); const currentAIMode = rtInfo?.["waveai:mode"] ?? (hasPremium ? "waveai@balanced" : "waveai@quick"); diff --git a/frontend/app/aipanel/waveai-model.tsx b/frontend/app/aipanel/waveai-model.tsx index a688ae5445..1059ab5d7f 100644 --- a/frontend/app/aipanel/waveai-model.tsx +++ b/frontend/app/aipanel/waveai-model.tsx @@ -373,11 +373,38 @@ export class WaveAIModel { }); } + isValidMode(mode: string): boolean { + const telemetryEnabled = globalStore.get(getSettingsKeyAtom("telemetry:enabled")) ?? false; + if (mode.startsWith("waveai@") && !telemetryEnabled) { + return false; + } + + const aiModeConfigs = globalStore.get(this.aiModeConfigs); + if (aiModeConfigs == null || !(mode in aiModeConfigs)) { + return false; + } + + return true; + } + setAIMode(mode: string) { - globalStore.set(this.currentAIMode, mode); + if (!this.isValidMode(mode)) { + this.setAIModeToDefault(); + } else { + globalStore.set(this.currentAIMode, mode); + RpcApi.SetRTInfoCommand(TabRpcClient, { + oref: this.orefContext, + data: { "waveai:mode": mode }, + }); + } + } + + setAIModeToDefault() { + const defaultMode = globalStore.get(this.defaultModeAtom); + globalStore.set(this.currentAIMode, defaultMode); RpcApi.SetRTInfoCommand(TabRpcClient, { oref: this.orefContext, - data: { "waveai:mode": mode }, + data: { "waveai:mode": null }, }); } @@ -386,34 +413,11 @@ export class WaveAIModel { oref: this.orefContext, }); const mode = rtInfo?.["waveai:mode"]; - if (mode == null) { return; } - - let shouldClear = false; - - if (mode.startsWith("waveai@")) { - const telemetryEnabled = globalStore.get(getSettingsKeyAtom("telemetry:enabled")) ?? false; - if (!telemetryEnabled) { - shouldClear = true; - } - } - - if (!shouldClear) { - const aiModeConfigs = globalStore.get(this.aiModeConfigs); - if (aiModeConfigs == null || !(mode in aiModeConfigs)) { - shouldClear = true; - } - } - - if (shouldClear) { - const defaultMode = globalStore.get(this.defaultModeAtom); - globalStore.set(this.currentAIMode, defaultMode); - RpcApi.SetRTInfoCommand(TabRpcClient, { - oref: this.orefContext, - data: { "waveai:mode": null }, - }); + if (!this.isValidMode(mode)) { + this.setAIModeToDefault(); } } @@ -431,9 +435,15 @@ export class WaveAIModel { } globalStore.set(this.chatId, chatIdValue); - const defaultMode = globalStore.get(getSettingsKeyAtom("waveai:defaultmode")) ?? "waveai@balanced"; - const aiModeValue = rtInfo?.["waveai:mode"] ?? defaultMode; - globalStore.set(this.currentAIMode, aiModeValue); + const aiModeValue = rtInfo?.["waveai:mode"]; + if (aiModeValue == null) { + const defaultMode = globalStore.get(this.defaultModeAtom); + globalStore.set(this.currentAIMode, defaultMode); + } else if (this.isValidMode(aiModeValue)) { + globalStore.set(this.currentAIMode, aiModeValue); + } else { + this.setAIModeToDefault(); + } try { const chatData = await RpcApi.GetWaveAIChatCommand(TabRpcClient, { chatid: chatIdValue }); From 1189b371de1004aa6bfb891f586acc8d2ca58521 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 15 Dec 2025 17:06:26 -0800 Subject: [PATCH 11/15] updates to make everything more robust, change/update when settings change. --- frontend/app/aipanel/ai-utils.ts | 6 +- frontend/app/aipanel/aimode.tsx | 38 +++++----- frontend/app/aipanel/aipanel-contextmenu.ts | 77 +-------------------- frontend/app/aipanel/aipanel.tsx | 37 ++++++---- frontend/app/aipanel/waveai-model.tsx | 44 ++++++------ pkg/aiusechat/chatstore/chatstore.go | 6 +- 6 files changed, 73 insertions(+), 135 deletions(-) diff --git a/frontend/app/aipanel/ai-utils.ts b/frontend/app/aipanel/ai-utils.ts index dd725571d6..8bfd67bdc0 100644 --- a/frontend/app/aipanel/ai-utils.ts +++ b/frontend/app/aipanel/ai-utils.ts @@ -547,7 +547,8 @@ export const getFilteredAIModeConfigs = ( aiModeConfigs: Record, showCloudModes: boolean, inBuilder: boolean, - hasPremium: boolean + hasPremium: boolean, + currentMode?: string ): FilteredAIModeConfigs => { const hideQuick = inBuilder && hasPremium; @@ -560,7 +561,8 @@ export const getFilteredAIModeConfigs = ( .sort(sortByDisplayOrder); const hasCustomModels = otherProviderConfigs.length > 0; - const shouldShowCloudModes = showCloudModes || !hasCustomModels; + const isCurrentModeCloud = currentMode?.startsWith("waveai@") ?? false; + const shouldShowCloudModes = showCloudModes || !hasCustomModels || isCurrentModeCloud; const waveProviderConfigs = shouldShowCloudModes ? allConfigs.filter((config) => config["ai:provider"] === "wave").sort(sortByDisplayOrder) diff --git a/frontend/app/aipanel/aimode.tsx b/frontend/app/aipanel/aimode.tsx index be21938347..69ee736885 100644 --- a/frontend/app/aipanel/aimode.tsx +++ b/frontend/app/aipanel/aimode.tsx @@ -121,7 +121,7 @@ function computeWaveCloudSections( sections.push({ sectionName: "Wave AI Cloud", configs: waveProviderConfigs, - noTelemetry: !telemetryEnabled + noTelemetry: !telemetryEnabled, }); } if (otherProviderConfigs.length > 0) { @@ -137,13 +137,12 @@ interface AIModeDropdownProps { export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdownProps) => { const model = WaveAIModel.getInstance(); - const aiMode = useAtomValue(model.currentAIMode); + const currentMode = useAtomValue(model.currentAIMode); const aiModeConfigs = useAtomValue(model.aiModeConfigs); const waveaiModeConfigs = useAtomValue(atoms.waveaiModeConfigAtom); const widgetContextEnabled = useAtomValue(model.widgetAccessAtom); const hasPremium = useAtomValue(model.hasPremiumAtom); const showCloudModes = useAtomValue(getSettingsKeyAtom("waveai:showcloudmodes")); - const defaultMode = useAtomValue(getSettingsKeyAtom("waveai:defaultmode")) ?? "waveai@balanced"; const telemetryEnabled = useAtomValue(getSettingsKeyAtom("telemetry:enabled")) ?? false; const [isOpen, setIsOpen] = useState(false); const dropdownRef = useRef(null); @@ -152,20 +151,10 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow aiModeConfigs, showCloudModes, model.inBuilder, - hasPremium + hasPremium, + currentMode ); - let currentMode = aiMode || defaultMode; - const currentConfig = aiModeConfigs[currentMode]; - if (currentConfig) { - if (!hasPremium && currentConfig["waveai:premium"]) { - currentMode = "waveai@quick"; - } - if (model.inBuilder && hasPremium && currentMode === "waveai@quick") { - currentMode = "waveai@balanced"; - } - } - const sections: ConfigSection[] = compatibilityMode ? computeCompatibleSections(currentMode, aiModeConfigs, waveProviderConfigs, otherProviderConfigs) : computeWaveCloudSections(waveProviderConfigs, otherProviderConfigs, telemetryEnabled); @@ -183,12 +172,17 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow }; const displayConfig = aiModeConfigs[currentMode]; - const displayName = displayConfig ? getModeDisplayName(displayConfig) : "Unknown"; - const displayIcon = displayConfig?.["display:icon"] || "sparkles"; + const displayName = displayConfig ? getModeDisplayName(displayConfig) : `Invalid (${currentMode})`; + const displayIcon = displayConfig ? (displayConfig["display:icon"] || "sparkles") : "question"; const resolvedConfig = waveaiModeConfigs[currentMode]; const hasToolsSupport = resolvedConfig && resolvedConfig["ai:capabilities"]?.includes("tools"); const showNoToolsWarning = widgetContextEnabled && resolvedConfig && !hasToolsSupport; + const handleNewChatClick = () => { + model.clearChat(); + setIsOpen(false); + }; + const handleConfigureClick = () => { fireAndForget(async () => { RpcApi.RecordTEventCommand( @@ -278,7 +272,8 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow const isPremiumDisabled = !hasPremium && config["waveai:premium"]; const isIncompatibleDisabled = section.isIncompatible || false; const isTelemetryDisabled = section.noTelemetry || false; - const isDisabled = isPremiumDisabled || isIncompatibleDisabled || isTelemetryDisabled; + const isDisabled = + isPremiumDisabled || isIncompatibleDisabled || isTelemetryDisabled; const isSelected = currentMode === config.mode; return ( + -
{errorMessage}
+
+ {errorMessage} + +
); }); AIErrorMessage.displayName = "AIErrorMessage"; -const RTInfoModeFixer = memo(() => { +const ConfigChangeModeFixer = memo(() => { const model = WaveAIModel.getInstance(); const telemetryEnabled = jotai.useAtomValue(getSettingsKeyAtom("telemetry:enabled")) ?? false; const aiModeConfigs = jotai.useAtomValue(model.aiModeConfigs); useEffect(() => { - model.fixRTInfoMode(); + model.fixModeAfterConfigChange(); }, [telemetryEnabled, aiModeConfigs, model]); return null; }); -RTInfoModeFixer.displayName = "RTInfoModeFixer"; +ConfigChangeModeFixer.displayName = "ConfigChangeModeFixer"; const AIPanelComponentInner = memo(() => { const [isDragOver, setIsDragOver] = useState(false); @@ -239,7 +249,6 @@ const AIPanelComponentInner = memo(() => { const [initialLoadDone, setInitialLoadDone] = useState(false); const model = WaveAIModel.getInstance(); const containerRef = useRef(null); - const errorMessage = jotai.useAtomValue(model.errorMessage); const isLayoutMode = jotai.useAtomValue(atoms.controlShiftDelayAtom); const showOverlayBlockNums = jotai.useAtomValue(getSettingsKeyAtom("app:showoverlayblocknums")) ?? true; const isFocused = jotai.useAtomValue(model.isWaveAIFocusedAtom); @@ -544,7 +553,7 @@ const AIPanelComponentInner = memo(() => { onClick={handleClick} inert={!isPanelVisible ? true : undefined} > - + {(isDragOver || isReactDndDragOver) && allowAccess && } {showBlockMask && } @@ -572,9 +581,7 @@ const AIPanelComponentInner = memo(() => { onContextMenu={(e) => handleWaveAIContextMenu(e, true)} /> )} - {errorMessage && ( - model.clearError()} /> - )} + diff --git a/frontend/app/aipanel/waveai-model.tsx b/frontend/app/aipanel/waveai-model.tsx index 1059ab5d7f..ded5057bd3 100644 --- a/frontend/app/aipanel/waveai-model.tsx +++ b/frontend/app/aipanel/waveai-model.tsx @@ -116,31 +116,27 @@ export class WaveAIModel { this.defaultModeAtom = jotai.atom((get) => { const telemetryEnabled = get(getSettingsKeyAtom("telemetry:enabled")) ?? false; - if (this.inBuilder) { - return telemetryEnabled ? "waveai@balanced" : "unknown"; + return telemetryEnabled ? "waveai@balanced" : "invalid"; } - const aiModeConfigs = get(this.aiModeConfigs); + if (!telemetryEnabled) { + let mode = get(getSettingsKeyAtom("waveai:defaultmode")); + if (mode == null || mode.startsWith("waveai@")) { + return "unknown"; + } + return mode; + } const hasPremium = get(this.hasPremiumAtom); - const waveFallback = hasPremium ? "waveai@balanced" : "waveai@quick"; let mode = get(getSettingsKeyAtom("waveai:defaultmode")) ?? waveFallback; - + if (!hasPremium && mode.startsWith("waveai@")) { + mode = "waveai@quick"; + } const modeExists = aiModeConfigs != null && mode in aiModeConfigs; - if (!modeExists) { - if (telemetryEnabled) { - mode = waveFallback; - } else { - return "unknown"; - } + mode = waveFallback; } - - if (mode.startsWith("waveai@") && !telemetryEnabled) { - return "unknown"; - } - return mode; }); @@ -165,6 +161,7 @@ export class WaveAIModel { orefContext = WOS.makeORef("tab", tabId); } WaveAIModel.instance = new WaveAIModel(orefContext, inBuilder); + (window as any).WaveAIModel = WaveAIModel.instance; } return WaveAIModel.instance; } @@ -273,6 +270,7 @@ export class WaveAIModel { clearChat() { this.useChatStop?.(); this.clearFiles(); + this.clearError(); this.isChatEmpty = true; const newChatId = crypto.randomUUID(); globalStore.set(this.chatId, newChatId); @@ -408,19 +406,23 @@ export class WaveAIModel { }); } - async fixRTInfoMode(): Promise { + async fixModeAfterConfigChange(): Promise { const rtInfo = await RpcApi.GetRTInfoCommand(TabRpcClient, { oref: this.orefContext, }); const mode = rtInfo?.["waveai:mode"]; - if (mode == null) { - return; - } - if (!this.isValidMode(mode)) { + if (mode == null || !this.isValidMode(mode)) { this.setAIModeToDefault(); } } + async getRTInfo(): Promise> { + const rtInfo = await RpcApi.GetRTInfoCommand(TabRpcClient, { + oref: this.orefContext, + }); + return rtInfo ?? {}; + } + async loadInitialChat(): Promise { const rtInfo = await RpcApi.GetRTInfoCommand(TabRpcClient, { oref: this.orefContext, diff --git a/pkg/aiusechat/chatstore/chatstore.go b/pkg/aiusechat/chatstore/chatstore.go index a5badcca9b..7625a05331 100644 --- a/pkg/aiusechat/chatstore/chatstore.go +++ b/pkg/aiusechat/chatstore/chatstore.go @@ -84,13 +84,13 @@ func (cs *ChatStore) PostMessage(chatId string, aiOpts *uctypes.AIOptsType, mess } else { // Verify that the AI options match if chat.APIType != aiOpts.APIType { - return fmt.Errorf("API type mismatch: expected %s, got %s", chat.APIType, aiOpts.APIType) + return fmt.Errorf("API type mismatch: expected %s, got %s (must start a new chat)", chat.APIType, aiOpts.APIType) } if !uctypes.AreModelsCompatible(chat.APIType, chat.Model, aiOpts.Model) { - return fmt.Errorf("model mismatch: expected %s, got %s", chat.Model, aiOpts.Model) + return fmt.Errorf("model mismatch: expected %s, got %s (must start a new chat)", chat.Model, aiOpts.Model) } if chat.APIVersion != aiOpts.APIVersion { - return fmt.Errorf("API version mismatch: expected %s, got %s", chat.APIVersion, aiOpts.APIVersion) + return fmt.Errorf("API version mismatch: expected %s, got %s (must start a new chat)", chat.APIVersion, aiOpts.APIVersion) } } From 4dc517ac7c741f1490a41807deb01e181001d941 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 15 Dec 2025 18:08:06 -0800 Subject: [PATCH 12/15] try to fix tab focus on rename tab --- frontend/app/tab/tab.tsx | 18 ++++++++++-------- pkg/aiusechat/uctypes/uctypes.go | 1 + 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/frontend/app/tab/tab.tsx b/frontend/app/tab/tab.tsx index 26e807a8ef..b04e5f451f 100644 --- a/frontend/app/tab/tab.tsx +++ b/frontend/app/tab/tab.tsx @@ -12,8 +12,8 @@ import { useAtomValue } from "jotai"; import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useRef, useState } from "react"; import { ObjectService } from "../store/services"; import { makeORef, useWaveObjectValue } from "../store/wos"; -import { TabBarModel } from "./tabbar-model"; import "./tab.scss"; +import { TabBarModel } from "./tabbar-model"; interface TabProps { id: string; @@ -79,13 +79,15 @@ const Tab = memo( }, []); const selectEditableText = useCallback(() => { - if (editableRef.current) { - const range = document.createRange(); - const selection = window.getSelection(); - range.selectNodeContents(editableRef.current); - selection.removeAllRanges(); - selection.addRange(range); + if (!editableRef.current) { + return; } + editableRef.current.focus(); + const range = document.createRange(); + const selection = window.getSelection(); + range.selectNodeContents(editableRef.current); + selection.removeAllRanges(); + selection.addRange(range); }, []); const handleRenameTab: React.MouseEventHandler = (event) => { @@ -93,7 +95,7 @@ const Tab = memo( setIsEditable(true); editableTimeoutRef.current = setTimeout(() => { selectEditableText(); - }, 0); + }, 50); }; const handleBlur = () => { diff --git a/pkg/aiusechat/uctypes/uctypes.go b/pkg/aiusechat/uctypes/uctypes.go index 7cedcb19eb..f8bdc21691 100644 --- a/pkg/aiusechat/uctypes/uctypes.go +++ b/pkg/aiusechat/uctypes/uctypes.go @@ -630,6 +630,7 @@ func AreModelsCompatible(apiType, model1, model2 string) bool { if apiType == APIType_OpenAIResponses { gpt5Models := map[string]bool{ + "gpt-5.2": true, "gpt-5.1": true, "gpt-5": true, "gpt-5-mini": true, From 04557474b535a50f1a6407ce39849f74e57a7b45 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 15 Dec 2025 19:17:29 -0800 Subject: [PATCH 13/15] updates to docs to explain how to enable wave ai without turning on telemetry. update telemetry page as well --- docs/docs/faq.mdx | 14 ++++ docs/docs/telemetry.mdx | 143 +++++++------------------------------ docs/docs/waveai-modes.mdx | 4 ++ 3 files changed, 45 insertions(+), 116 deletions(-) diff --git a/docs/docs/faq.mdx b/docs/docs/faq.mdx index 37c714e610..fe3a2124be 100644 --- a/docs/docs/faq.mdx +++ b/docs/docs/faq.mdx @@ -4,6 +4,8 @@ id: "faq" title: "FAQ" --- +import { VersionBadge } from "@site/src/components/versionbadge"; + # FAQ ### How can I see the block numbers? @@ -52,3 +54,15 @@ If you've installed via Snap, you can use the following command: ```sh sudo snap install waveterm --classic --beta ``` + +## Can I use Wave AI without enabling telemetry? + + + +Yes! Wave AI is normally disabled when telemetry is not enabled. However, you can enable Wave AI features without telemetry by configuring your own custom AI model (either a local model or using your own API key). + +To enable Wave AI without telemetry: +1. Configure a custom AI mode (see [Wave AI documentation](./waveai-modes)) +2. Set `waveai:defaultmode` to your custom mode's key in your Wave settings + +Once you've completed both steps, Wave AI will be enabled and you can use it completely privately without telemetry. This allows you to use local models like Ollama or your own API keys with providers like OpenAI, OpenRouter, or others. diff --git a/docs/docs/telemetry.mdx b/docs/docs/telemetry.mdx index 958e6e3538..da2dfc4ae5 100644 --- a/docs/docs/telemetry.mdx +++ b/docs/docs/telemetry.mdx @@ -8,152 +8,63 @@ id: "telemetry" Wave Terminal collects telemetry data to help us track feature use, direct future product efforts, and generate aggregate metrics on Wave's popularity and usage. We do NOT collect personal information (PII), keystrokes, file contents, AI prompts, IP addresses, hostnames, or commands. We attach all information to an anonymous, randomly generated _ClientId_ (UUID). You may opt out of collection at any time. -Here’s a quick summary of what is collected: +Here's a quick summary of what is collected: -- Basic App/System Info – OS, architecture, app version, update settings -- Usage Metrics – App start/shutdown, active minutes, foreground time, tab/block counts/usage -- Feature Interactions – When you create tabs, run commands, change settings, etc. -- Display Info – Monitor resolution, number of displays -- Connection Events – SSH/WSL connection attempts (but NOT hostnames/IPs) -- AI Commands – Only which AI backend is used (e.g., OpenAI, Claude) – no text or prompts sent -- Error Reports – Crash/panic events with minimal debugging info, but no stack traces or detailed errors +- Basic App/System Info - OS, architecture, app version, update settings +- Usage Metrics - App start/shutdown, active minutes, foreground time, tab/block counts/usage +- Feature Interactions - When you create tabs, run commands, change settings, etc. +- Display Info - Monitor resolution, number of displays +- Connection Events - SSH/WSL connection attempts (but NOT hostnames/IPs) +- Wave AI Usage - Model/provider selection, token counts, request metrics, latency (but NOT prompts or responses) +- Error Reports - Crash/panic events with minimal debugging info, but no stack traces or detailed errors Telemetry can be disabled at any time in settings. If not disabled it is sent on startup, on shutdown, and every 4-hours. ## How to Disable Telemetry -If you would like to turn telemetry on or off, the first opportunity is a button on the initial welcome page. After this, it can be turned off by adding `"telemetry:enabled": false` to the `config/settings.json` file. It can alternatively be turned on by adding `"telemetry:enabled": true` to the `config/settings.json` file. +Telemetry can be enabled or disabled on the initial welcome screen when Wave first starts. After setup, telemetry can be disabled by setting the `telemetry:enabled` key to `false` in Wave’s general configuration file. It can also be disabled using the CLI command `wsh setconfig telemetry:enabled=false`. -:::tip - -You can also change your telemetry setting (true/false) by running the wsh command: +:::info -``` -wsh setconfig telemetry:enabled=true -``` +This document outlines the current telemetry system as of v0.11.1. As of v0.12.5, Wave Terminal no longer sends legacy telemetry. The previous telemetry documentation can be found in our [Legacy Telemetry Documentation](./telemetry-old.mdx) for historical reference. ::: -:::info +## Diagnostics Ping -This document outlines the new telemetry system as of v0.11.1. The previous telemetry documentation is still relevant and can be found in our [Legacy Telemetry Documentation](./telemetry-old.mdx), but in general, the new telemetry is a superset of the old. +Wave sends a small, anonymous diagnostics ping after the app has been running for a short time and at most once per day thereafter. This is used to estimate active installs and understand which versions are still in use, so we know what to support and when it's safe to remove old code paths. -::: +The ping includes only: your Wave version, OS/CPU arch, local date (yyyy-mm-dd, no timezone or clock time), your randomly generated anonymous client ID, and whether usage telemetry is enabled or disabled. + +It does not include usage data, commands, files, or any telemetry events. + +This ping is intentionally separate from telemetry so Wave can count active installs. If you'd like to disable it, set the WAVETERM_NOPING environment variable. ## Sending Telemetry -Provided that telemetry is enabled, it is sent 10 seconds after Waveterm is first booted and then again every 4 hours thereafter. It can also be sent in response to a few special cases listed below. When telemetry is sent, it is grouped into individual days as determined by your time zone. Any data from a previous day is marked as `Uploaded` so it will not need to be sent again. +Provided that telemetry is enabled, it is sent shortly after Wave is first launched and then again every 4 hours thereafter. It can also be sent in response to a few special cases listed below. When telemetry is sent, events are marked as sent to prevent duplicate transmissions. ### Sending Once Telemetry is Enabled As soon as telemetry is enabled, a telemetry update is sent regardless of how long it has been since the last send. This does not reset the usual timer for telemetry sends. -### Notifying that Telemetry is Disabled - -As soon as telemetry is disabled, Waveterm sends a special update that notifies us of this change. See [When Telemetry is Turned Off](#when-telemetry-is-turned-off) for more info. The timer still runs in the background but no data is sent. - -### When Waveterm is Closed +### When Wave is Closed Provided that telemetry is enabled, it will be sent when Waveterm is closed. -## Event Types - -Below is a list of the event types collected in the new telemetry system. More events are likely to be added in the future. - -| Event Name | Description | -| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `app:startup` | Logged every time you start the app. Contains basic app information like architecture, version, buildtime, etc. | -| `app:shutdown` | Logged on every shutdown | -| `app:activity` | Logged once per hour of app activity | -| `app:display` | Logged on startup and contains information about size of displays | -| `app:counts` | Logged once per hour when app is active, contains basic counts like number of windows, tabs, workspaces, blocks, number of settings customizations, etc. | -| `action:magnify` | Logged each time a block is magnified | -| `action:settabtheme` | Logged each time a tab theme is changed | -| `action:runaicmd` | Logged each time an AI request is made (no prompt information or text is sent), only sends "ai:backendtype" to know what type of AI backend is being used (OpenAI, Claude, Gemini, etc.) | -| `action:createtab` | Logged when a new tab is created | -| `action:createblock` | Logged when a new block is created (contains the block view type) | -| `wsh:run` | Logged when a wsh command is executed (contains the command type) | -| `debug:panic` | Logged when a backend (Go) panic happens. Contains a debugging string that can be used to find which panic was hit in our source code. No data is sent | -| `conn:connect` | Logged each time a backend ssh/wsl connection connects (logs the conneciton type, no hostname or IP is sent) | -| `conn:connecterror` | Logged when you try to connect but it fails (logs the connection type, no hostname or IP is set, and no detailed error information is sent) | -| `waveai:post` | Logged after AI request completion with usage metrics (tokens, request counts, latency, etc. - no prompts or responses) | - -## Event Properties - -Each event may contain the following properties that are relevant to the particular events. - -| Property | Description | -| ------------------------ | ------------------------------------------------------------------------------------------------------ | -| `client:arch` | Wave architecture (darwin, windows, linux) and x64 vs arm64 | -| `client:version` | The Wave version (e.g. v0.11.1) | -| `client:initial_version` | Initial installed wave version | -| `client:buildtime` | The buildtime (more exact wave version) | -| `client:osrelease` | A string representing the version of the OS you're running -- different for darwin, windows, and linux | -| `client:isdev` | True/False if using the dev build | -| `autoupdate:channel` | What auto-update channel you're on (latest vs beta) | -| `autoupdate:enabled` | True/False if auto-updated is enabled | -| `loc:countrycode` | Two character country code (e.g. US, CN, FR, JP) | -| `loc:regioncode` | Two character region code (usually the State or Province within a country) | -| `activity:activeminutes` | For app:activity, a number between 0-60 of how many minutes were active within the hour | -| `activity:fgminutes` | For app:activity, a number between 0-60 of how many minutes Wave was the foreground application | -| `activity:openminutes` | For app:activity, a number between 0-60 of how many minutes Wave was open | -| `action:initiator` | For certain actions logs if the action was initiated by the UI or the backend | -| `debug:panictype` | The string that identifies the panic location within our Go code | -| `block:view` | Type of block, e.g. "preview", "waveai", "term", "sysinfo", etc. | -| `ai:backendtype` | AI backend type (e.g. OpenAI, Gemini, Anthropic, etc.) | -| `wsh:cmd` | The wsh command that was run, e.g. "view", "edit", "run", "editconfig" etc. | -| `wsh:haderror` | True/False whether the wsh command returned an error | -| `conn:conntype` | Type of connnection (ssh / wsl) | -| `display:height` | Height of the main display in px | -| `display:width` | Width of the main display in px | -| `display:dpr` | DPR of the main display | -| `display:count` | How many total displays | -| `display:all` | JSON for all the displays attached (same attributes as above) | -| `count:blocks` | Total number of blocks | -| `count:tabs` | Total number of tabs | -| `count:windows` | Total number of windows | -| `count:workspaces` | Total number of workspaces | -| `count:sshconn` | Total number of SSH connections | -| `count:wslconn` | Total number of WSL connections | -| `count:views` | Counts of the types of blocks (views) | -| `waveai:apitype` | AI API provider (OpenAI, Anthropic, etc.) | -| `waveai:model` | AI model name | -| `waveai:inputtokens` | Number of input tokens used | -| `waveai:outputtokens` | Number of output tokens generated | -| `waveai:requestcount` | Number of requests in conversation | -| `waveai:toolusecount` | Number of tool uses | -| `waveai:tooluseerrorcount` | Number of tool use errors | -| `waveai:tooldetail` | Map of tool names to usage counts | -| `waveai:premiumreq` | Number of premium API requests | -| `waveai:proxyreq` | Number of proxy requests | -| `waveai:haderror` | True/False if request had errors | -| `waveai:imagecount` | Number of images in context | -| `waveai:pdfcount` | Number of PDFs in context | -| `waveai:textdoccount` | Number of text documents in context | -| `waveai:textlen` | Total text length in context | -| `waveai:firstbytems` | Latency to first byte in milliseconds | -| `waveai:requestdurms` | Total request duration in milliseconds | -| `waveai:widgetaccess` | True/False if accessed via widget | +## Event Types and Properties ---- - -## When Telemetry is Turned Off - -When a user disables telemetry, Waveterm sends a notification that their anonymous _ClientId_ has had its telemetry disabled. This is done with the `wcloud.NoTelemetryInputType` type in the source code. Beyond that, no further information is sent unless telemetry is turned on again. If it is turned on again, the previous 30 days of telemetry will be sent. - ---- - -## A Note on IP Addresses +Wave collects the event types and properties described in the summary above. As we add features, new events and properties may be added to track their usage. -Telemetry is uploaded via https, which means your IP address is known to the telemetry server. We **do not** store your IP address in our telemetry table and **do not** associate it with your _ClientId_. +For the complete, current list of all telemetry events and properties, see the source code: [telemetrydata.go](https://github.com/wavetermdev/waveterm/blob/main/pkg/telemetry/telemetrydata/telemetrydata.go) ---- +## GDPR Opt-Out Compliance -## Previously Collected Telemetry Data +When telemetry is disabled, Wave sends a single minimal opt-out record associated with the anonymous client ID, recording that telemetry was turned off and when it occurred. This record is retained for compliance purposes. After that, no telemetry or usage data is sent. -While we believe the data we collect with telemetry is fairly minimal, we cannot make that decision for every user. If you ever change your mind about what has been collected previously, you may request that your data be deleted by emailing us at [support@waveterm.dev](mailto:support@waveterm.dev). If you do, we will need your _ClientId_ to remove it. +## Deleting Your Data ---- +If you want your previously collected telemetry data deleted, email us at support (at) waveterm.dev with your _ClientId_ and we'll remove it. ## Privacy Policy diff --git a/docs/docs/waveai-modes.mdx b/docs/docs/waveai-modes.mdx index d8b94ee460..437a6ba99d 100644 --- a/docs/docs/waveai-modes.mdx +++ b/docs/docs/waveai-modes.mdx @@ -74,6 +74,10 @@ wsh setconfig waveai:defaultmode="ollama-llama" This will make the specified mode the default selection when opening Wave AI features. +:::note +Wave AI normally requires telemetry to be enabled. However, if you configure your own custom model (local or BYOK) and set `waveai:defaultmode` to that custom mode's key, you will not receive telemetry requirement messages. This allows you to use Wave AI features completely privately with your own models. +::: + ### Hiding Wave Cloud Modes If you prefer to use only your local or custom models and want to hide Wave's cloud AI modes from the mode dropdown, set `waveai:showcloudmodes` to `false`: From 0f82dbdec422885d0f2c57d4214dde84883b3c36 Mon Sep 17 00:00:00 2001 From: sawka Date: Mon, 15 Dec 2025 21:15:33 -0800 Subject: [PATCH 14/15] fix redundant events, link docs from no-telemetry page, click to enable from mode switcher --- cmd/server/main-server.go | 21 +--------------- frontend/app/aipanel/aimode.tsx | 20 ++++++++++++--- frontend/app/aipanel/telemetryrequired.tsx | 14 ++++++++++- pkg/service/clientservice/clientservice.go | 21 ---------------- pkg/wcore/wcore.go | 29 ++++++++++++++++++++-- pkg/wshrpc/wshserver/wshserver.go | 10 -------- 6 files changed, 57 insertions(+), 58 deletions(-) diff --git a/cmd/server/main-server.go b/cmd/server/main-server.go index 095cd525d9..af74e79661 100644 --- a/cmd/server/main-server.go +++ b/cmd/server/main-server.go @@ -174,25 +174,6 @@ func sendDiagnosticPing() bool { return true } -func sendNoTelemetryUpdate(telemetryEnabled bool) { - ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second) - defer cancelFn() - clientData, err := wstore.DBGetSingleton[*waveobj.Client](ctx) - if err != nil { - log.Printf("telemetry update: error getting client data: %v\n", err) - return - } - if clientData == nil { - log.Printf("telemetry update: client data is nil\n") - return - } - err = wcloud.SendNoTelemetryUpdate(ctx, clientData.OID, !telemetryEnabled) - if err != nil { - log.Printf("[error] sending no-telemetry update: %v\n", err) - return - } -} - func setupTelemetryConfigHandler() { watcher := wconfig.GetWatcher() if watcher == nil { @@ -205,7 +186,7 @@ func setupTelemetryConfigHandler() { newTelemetryEnabled := newConfig.Settings.TelemetryEnabled if newTelemetryEnabled != currentTelemetryEnabled { currentTelemetryEnabled = newTelemetryEnabled - go sendNoTelemetryUpdate(newTelemetryEnabled) + wcore.GoSendNoTelemetryUpdate(newTelemetryEnabled) } }) } diff --git a/frontend/app/aipanel/aimode.tsx b/frontend/app/aipanel/aimode.tsx index 69ee736885..9848c2327d 100644 --- a/frontend/app/aipanel/aimode.tsx +++ b/frontend/app/aipanel/aimode.tsx @@ -173,7 +173,7 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow const displayConfig = aiModeConfigs[currentMode]; const displayName = displayConfig ? getModeDisplayName(displayConfig) : `Invalid (${currentMode})`; - const displayIcon = displayConfig ? (displayConfig["display:icon"] || "sparkles") : "question"; + const displayIcon = displayConfig ? displayConfig["display:icon"] || "sparkles" : "question"; const resolvedConfig = waveaiModeConfigs[currentMode]; const hasToolsSupport = resolvedConfig && resolvedConfig["ai:capabilities"]?.includes("tools"); const showNoToolsWarning = widgetContextEnabled && resolvedConfig && !hasToolsSupport; @@ -200,6 +200,15 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow }); }; + const handleEnableTelemetry = () => { + fireAndForget(async () => { + await RpcApi.WaveAIEnableTelemetryCommand(TabRpcClient); + setTimeout(() => { + model.focusInput(); + }, 100); + }); + }; + return (
)} )} diff --git a/frontend/app/aipanel/telemetryrequired.tsx b/frontend/app/aipanel/telemetryrequired.tsx index b0043c3ad2..692dec73d5 100644 --- a/frontend/app/aipanel/telemetryrequired.tsx +++ b/frontend/app/aipanel/telemetryrequired.tsx @@ -55,11 +55,23 @@ const TelemetryRequiredMessage = ({ className }: TelemetryRequiredMessageProps) This helps us block abuse by automated systems and ensure it's used by real people like you.

-

+

We never collect your files, prompts, keystrokes, hostnames, or personally identifying information. Wave AI is powered by OpenAI's APIs, please refer to OpenAI's privacy policy for details on how they handle your data.

+

+ For information about BYOK and local model support, see{" "} + + https://docs.waveterm.dev/waveai-modes + + . +