mirror of
https://github.com/wavetermdev/waveterm
synced 2026-05-14 04:18:26 +00:00
Massive PR, over 13k LOC updated, 128 commits to implement the first pass at the new Wave AI panel. Two backend adapters (OpenAI and Anthropic), layout changes to support the panel, keyboard shortcuts, and a huge focus/layout change to integrate the panel seamlessly into the UI. Also fixes some small issues found during the Wave AI journey (zoom fixes, documentation, more scss removal, circular dependency issues, settings, etc)
233 lines
8.1 KiB
Go
233 lines
8.1 KiB
Go
// Copyright 2025, Command Line Inc.
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package telemetrydata
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
|
|
)
|
|
|
|
var ValidEventNames = map[string]bool{
|
|
"app:startup": true,
|
|
"app:shutdown": true,
|
|
"app:activity": true,
|
|
"app:display": true,
|
|
"app:counts": true,
|
|
"action:magnify": true,
|
|
"action:settabtheme": true,
|
|
"action:runaicmd": true,
|
|
"action:createtab": true,
|
|
"action:createblock": true,
|
|
"action:openwaveai": true,
|
|
"wsh:run": true,
|
|
"debug:panic": true,
|
|
"conn:connect": true,
|
|
"conn:connecterror": true,
|
|
"waveai:enabletelemetry": true,
|
|
"waveai:post": true,
|
|
}
|
|
|
|
type TEvent struct {
|
|
Uuid string `json:"uuid,omitempty" db:"uuid"`
|
|
Ts int64 `json:"ts,omitempty" db:"ts"`
|
|
TsLocal string `json:"tslocal,omitempty" db:"tslocal"` // iso8601 format (wall clock converted to PT)
|
|
Event string `json:"event" db:"event"`
|
|
Props TEventProps `json:"props" db:"-"` // Don't scan directly to map
|
|
|
|
// DB fields
|
|
Uploaded bool `json:"-" db:"uploaded"`
|
|
|
|
// For database scanning
|
|
RawProps string `json:"-" db:"props"`
|
|
}
|
|
|
|
type TEventUserProps struct {
|
|
ClientArch string `json:"client:arch,omitempty"`
|
|
ClientVersion string `json:"client:version,omitempty"`
|
|
ClientInitialVersion string `json:"client:initial_version,omitempty"`
|
|
ClientBuildTime string `json:"client:buildtime,omitempty"`
|
|
ClientOSRelease string `json:"client:osrelease,omitempty"`
|
|
ClientIsDev bool `json:"client:isdev,omitempty"`
|
|
|
|
AutoUpdateChannel string `json:"autoupdate:channel,omitempty"`
|
|
AutoUpdateEnabled bool `json:"autoupdate:enabled,omitempty"`
|
|
|
|
LocCountryCode string `json:"loc:countrycode,omitempty"`
|
|
LocRegionCode string `json:"loc:regioncode,omitempty"`
|
|
|
|
SettingsCustomWidgets int `json:"settings:customwidgets,omitempty"`
|
|
SettingsCustomAIPresets int `json:"settings:customaipresets,omitempty"`
|
|
SettingsCustomSettings int `json:"settings:customsettings,omitempty"`
|
|
}
|
|
|
|
type TEventProps struct {
|
|
TEventUserProps `tstype:"-"` // generally don't need to set these since they will be automatically copied over
|
|
|
|
ActiveMinutes int `json:"activity:activeminutes,omitempty"`
|
|
FgMinutes int `json:"activity:fgminutes,omitempty"`
|
|
OpenMinutes int `json:"activity:openminutes,omitempty"`
|
|
WaveAIActiveMinutes int `json:"activity:waveaiactiveminutes,omitempty"`
|
|
WaveAIFgMinutes int `json:"activity:waveaifgminutes,omitempty"`
|
|
|
|
AppFirstDay bool `json:"app:firstday,omitempty"`
|
|
AppFirstLaunch bool `json:"app:firstlaunch,omitempty"`
|
|
|
|
ActionInitiator string `json:"action:initiator,omitempty" tstype:"\"keyboard\" | \"mouse\""`
|
|
PanicType string `json:"debug:panictype,omitempty"`
|
|
BlockView string `json:"block:view,omitempty"`
|
|
AiBackendType string `json:"ai:backendtype,omitempty"`
|
|
AiLocal bool `json:"ai:local,omitempty"`
|
|
WshCmd string `json:"wsh:cmd,omitempty"`
|
|
WshHadError bool `json:"wsh:haderror,omitempty"`
|
|
ConnType string `json:"conn:conntype,omitempty"`
|
|
|
|
DisplayHeight int `json:"display:height,omitempty"`
|
|
DisplayWidth int `json:"display:width,omitempty"`
|
|
DisplayDPR float64 `json:"display:dpr,omitempty"`
|
|
DisplayCount int `json:"display:count,omitempty"`
|
|
DisplayAll interface{} `json:"display:all,omitempty"`
|
|
|
|
CountBlocks int `json:"count:blocks,omitempty"`
|
|
CountTabs int `json:"count:tabs,omitempty"`
|
|
CountWindows int `json:"count:windows,omitempty"`
|
|
CountWorkspaces int `json:"count:workspaces,omitempty"`
|
|
CountSSHConn int `json:"count:sshconn,omitempty"`
|
|
CountWSLConn int `json:"count:wslconn,omitempty"`
|
|
CountViews map[string]int `json:"count:views,omitempty"`
|
|
|
|
WaveAIAPIType string `json:"waveai:apitype,omitempty"`
|
|
WaveAIModel string `json:"waveai:model,omitempty"`
|
|
WaveAIInputTokens int `json:"waveai:inputtokens,omitempty"`
|
|
WaveAIOutputTokens int `json:"waveai:outputtokens,omitempty"`
|
|
WaveAIRequestCount int `json:"waveai:requestcount,omitempty"`
|
|
WaveAIToolUseCount int `json:"waveai:toolusecount,omitempty"`
|
|
WaveAIToolDetail map[string]int `json:"waveai:tooldetail,omitempty"`
|
|
WaveAIPremiumReq int `json:"waveai:premiumreq,omitempty"`
|
|
WaveAIProxyReq int `json:"waveai:proxyreq,omitempty"`
|
|
WaveAIHadError bool `json:"waveai:haderror,omitempty"`
|
|
WaveAIImageCount int `json:"waveai:imagecount,omitempty"`
|
|
WaveAIPDFCount int `json:"waveai:pdfcount,omitempty"`
|
|
WaveAITextDocCount int `json:"waveai:textdoccount,omitempty"`
|
|
WaveAITextLen int `json:"waveai:textlen,omitempty"`
|
|
WaveAIFirstByteMs int `json:"waveai:firstbytems,omitempty"` // ms
|
|
WaveAIRequestDurMs int `json:"waveai:requestdurms,omitempty"` // ms
|
|
WaveAIWidgetAccess bool `json:"waveai:widgetaccess,omitempty"`
|
|
|
|
UserSet *TEventUserProps `json:"$set,omitempty"`
|
|
UserSetOnce *TEventUserProps `json:"$set_once,omitempty"`
|
|
}
|
|
|
|
func MakeTEvent(event string, props TEventProps) *TEvent {
|
|
now := time.Now()
|
|
// TsLocal gets set in EnsureTimestamps()
|
|
return &TEvent{
|
|
Uuid: uuid.New().String(),
|
|
Ts: now.UnixMilli(),
|
|
Event: event,
|
|
Props: props,
|
|
}
|
|
}
|
|
|
|
func MakeUntypedTEvent(event string, propsMap map[string]any) (*TEvent, error) {
|
|
if event == "" {
|
|
return nil, fmt.Errorf("event name must be non-empty")
|
|
}
|
|
var props TEventProps
|
|
err := utilfn.ReUnmarshal(&props, propsMap)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("error re-marshalling TEvent props: %w", err)
|
|
}
|
|
return MakeTEvent(event, props), nil
|
|
}
|
|
|
|
func (t *TEvent) EnsureTimestamps() {
|
|
if t.Ts == 0 {
|
|
t.Ts = time.Now().UnixMilli()
|
|
}
|
|
gtime := time.UnixMilli(t.Ts)
|
|
t.TsLocal = utilfn.ConvertToWallClockPT(gtime).Format(time.RFC3339)
|
|
}
|
|
|
|
func (t *TEvent) UserSetProps() *TEventUserProps {
|
|
if t.Props.UserSet == nil {
|
|
t.Props.UserSet = &TEventUserProps{}
|
|
}
|
|
return t.Props.UserSet
|
|
}
|
|
|
|
func (t *TEvent) UserSetOnceProps() *TEventUserProps {
|
|
if t.Props.UserSetOnce == nil {
|
|
t.Props.UserSetOnce = &TEventUserProps{}
|
|
}
|
|
return t.Props.UserSetOnce
|
|
}
|
|
|
|
func (t *TEvent) ConvertRawJSON() error {
|
|
if t.RawProps != "" {
|
|
return json.Unmarshal([]byte(t.RawProps), &t.Props)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var eventNameRe = regexp.MustCompile(`^[a-zA-Z0-9.:_/-]+$`)
|
|
|
|
// validates a tevent that was just created (not for validating out of the DB, or an uploaded TEvent)
|
|
// checks that TS is pretty current (or unset)
|
|
func (te *TEvent) Validate(current bool) error {
|
|
if te == nil {
|
|
return fmt.Errorf("TEvent cannot be nil")
|
|
}
|
|
if te.Event == "" {
|
|
return fmt.Errorf("TEvent.Event cannot be empty")
|
|
}
|
|
if !eventNameRe.MatchString(te.Event) {
|
|
return fmt.Errorf("TEvent.Event invalid: %q", te.Event)
|
|
}
|
|
if !ValidEventNames[te.Event] {
|
|
return fmt.Errorf("TEvent.Event not valid: %q", te.Event)
|
|
}
|
|
if te.Uuid == "" {
|
|
return fmt.Errorf("TEvent.Uuid cannot be empty")
|
|
}
|
|
_, err := uuid.Parse(te.Uuid)
|
|
if err != nil {
|
|
return fmt.Errorf("TEvent.Uuid invalid: %v", err)
|
|
}
|
|
if current {
|
|
if te.Ts != 0 {
|
|
now := time.Now().UnixMilli()
|
|
if te.Ts > now+60000 || te.Ts < now-60000 {
|
|
return fmt.Errorf("TEvent.Ts is not current: %d", te.Ts)
|
|
}
|
|
}
|
|
} else {
|
|
if te.Ts == 0 {
|
|
return fmt.Errorf("TEvent.Ts must be set")
|
|
}
|
|
if te.TsLocal == "" {
|
|
return fmt.Errorf("TEvent.TsLocal must be set")
|
|
}
|
|
t, err := time.Parse(time.RFC3339, te.TsLocal)
|
|
if err != nil {
|
|
return fmt.Errorf("TEvent.TsLocal parse error: %v", err)
|
|
}
|
|
now := time.Now()
|
|
if t.Before(now.Add(-30*24*time.Hour)) || t.After(now.Add(2*24*time.Hour)) {
|
|
return fmt.Errorf("tslocal out of valid range")
|
|
}
|
|
}
|
|
barr, err := json.Marshal(te.Props)
|
|
if err != nil {
|
|
return fmt.Errorf("TEvent.Props JSON error: %v", err)
|
|
}
|
|
if len(barr) > 20000 {
|
|
return fmt.Errorf("TEvent.Props too large: %d", len(barr))
|
|
}
|
|
return nil
|
|
}
|