Skip to content

Commit 4c6fd13

Browse files
committed
first cut at new block-tab based badge system
1 parent 7280bf4 commit 4c6fd13

7 files changed

Lines changed: 231 additions & 6 deletions

File tree

cmd/server/main-server.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,11 @@ func main() {
572572
jobcontroller.InitJobController()
573573
blockcontroller.InitBlockController()
574574
wcore.InitTabIndicatorStore()
575+
err = wcore.InitBadgeStore()
576+
if err != nil {
577+
log.Printf("error initializing badge store: %v\n", err)
578+
return
579+
}
575580
go func() {
576581
defer func() {
577582
panichandler.PanicHandler("GetSystemSummary", recover())

pkg/baseds/baseds.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,16 @@ type RpcInputChType struct {
1212
MsgBytes []byte
1313
IngressLinkId LinkId
1414
}
15+
16+
type Badge struct {
17+
Icon string `json:"icon"`
18+
Color string `json:"color,omitempty"`
19+
Priority float64 `json:"priority"`
20+
}
21+
22+
type BadgeEvent struct {
23+
ORef string `json:"oref"`
24+
Persistent bool `json:"persistent,omitempty"`
25+
Clear bool `json:"clear,omitempty"`
26+
Badge *Badge `json:"badge,omitempty"`
27+
}

pkg/waveobj/wtype.go

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"encoding/json"
88
"fmt"
99
"reflect"
10+
11+
"github.com/wavetermdev/waveterm/pkg/baseds"
1012
)
1113

1214
type UpdatesRtnType = []WaveObjUpdate
@@ -187,12 +189,13 @@ func (*Workspace) GetOType() string {
187189
}
188190

189191
type Tab struct {
190-
OID string `json:"oid"`
191-
Version int `json:"version"`
192-
Name string `json:"name"`
193-
LayoutState string `json:"layoutstate"`
194-
BlockIds []string `json:"blockids"`
195-
Meta MetaMapType `json:"meta"`
192+
OID string `json:"oid"`
193+
Version int `json:"version"`
194+
Name string `json:"name"`
195+
LayoutState string `json:"layoutstate"`
196+
BlockIds []string `json:"blockids"`
197+
Meta MetaMapType `json:"meta"`
198+
Badge *baseds.Badge `json:"badge,omitempty"`
196199
}
197200

198201
func (*Tab) GetOType() string {
@@ -292,6 +295,7 @@ type Block struct {
292295
Meta MetaMapType `json:"meta"`
293296
SubBlockIds []string `json:"subblockids,omitempty"`
294297
JobId string `json:"jobid,omitempty"` // if set, the block will render this jobid's pty output
298+
Badge *baseds.Badge `json:"badge,omitempty"`
295299
}
296300

297301
func (*Block) GetOType() string {

pkg/wcore/badge.go

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
// Copyright 2025, Command Line Inc.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package wcore
5+
6+
import (
7+
"context"
8+
"fmt"
9+
"log"
10+
"sync"
11+
"time"
12+
13+
"github.com/wavetermdev/waveterm/pkg/baseds"
14+
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
15+
"github.com/wavetermdev/waveterm/pkg/waveobj"
16+
"github.com/wavetermdev/waveterm/pkg/wps"
17+
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
18+
"github.com/wavetermdev/waveterm/pkg/wstore"
19+
)
20+
21+
// BadgeStore is a write-through cache for badges.
22+
// Each oref can carry two independent badges:
23+
// - a persistent badge (stored in the DB and survives restarts)
24+
// - a transient badge (in-memory only, cleared on restart)
25+
//
26+
// Values are stored by value (not pointer) to prevent external mutation.
27+
type BadgeStore struct {
28+
lock *sync.Mutex
29+
persistent map[string]baseds.Badge // keyed by oref string
30+
transient map[string]baseds.Badge // keyed by oref string
31+
}
32+
33+
var globalBadgeStore = &BadgeStore{
34+
lock: &sync.Mutex{},
35+
persistent: make(map[string]baseds.Badge),
36+
transient: make(map[string]baseds.Badge),
37+
}
38+
39+
// InitBadgeStore loads all persisted badges from the DB into the in-memory
40+
// cache and subscribes to incoming badge events.
41+
func InitBadgeStore() error {
42+
log.Printf("initializing badge store\n")
43+
44+
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
45+
defer cancelFn()
46+
47+
// Load persisted badges from all tabs.
48+
tabs, err := wstore.DBGetAllObjsByType[*waveobj.Tab](ctx, waveobj.OType_Tab)
49+
if err != nil {
50+
return fmt.Errorf("badge store: error loading tabs from DB: %w", err)
51+
}
52+
for _, tab := range tabs {
53+
if tab.Badge != nil {
54+
oref := waveobj.MakeORef(waveobj.OType_Tab, tab.OID).String()
55+
globalBadgeStore.persistent[oref] = *tab.Badge
56+
}
57+
}
58+
59+
// Load persisted badges from all blocks.
60+
blocks, err := wstore.DBGetAllObjsByType[*waveobj.Block](ctx, waveobj.OType_Block)
61+
if err != nil {
62+
return fmt.Errorf("badge store: error loading blocks from DB: %w", err)
63+
}
64+
for _, block := range blocks {
65+
if block.Badge != nil {
66+
oref := waveobj.MakeORef(waveobj.OType_Block, block.OID).String()
67+
globalBadgeStore.persistent[oref] = *block.Badge
68+
}
69+
}
70+
71+
log.Printf("badge store: loaded %d persisted badges\n", len(globalBadgeStore.persistent))
72+
73+
// Subscribe to badge events so we can update the cache when events arrive.
74+
rpcClient := wshclient.GetBareRpcClient()
75+
rpcClient.EventListener.On(wps.Event_Badge, handleBadgeEvent)
76+
wshclient.EventSubCommand(rpcClient, wps.SubscriptionRequest{
77+
Event: wps.Event_Badge,
78+
AllScopes: true,
79+
}, nil)
80+
81+
return nil
82+
}
83+
84+
func handleBadgeEvent(event *wps.WaveEvent) {
85+
if event.Event != wps.Event_Badge {
86+
return
87+
}
88+
var data baseds.BadgeEvent
89+
err := utilfn.ReUnmarshal(&data, event.Data)
90+
if err != nil {
91+
log.Printf("badge store: error unmarshaling BadgeEvent: %v\n", err)
92+
return
93+
}
94+
if data.ORef == "" {
95+
log.Printf("badge store: received badge event with empty oref\n")
96+
return
97+
}
98+
99+
oref, err := waveobj.ParseORef(data.ORef)
100+
if err != nil {
101+
log.Printf("badge store: error parsing oref %q: %v\n", data.ORef, err)
102+
return
103+
}
104+
105+
setBadge(oref, data.Badge, data.Persistent, data.Clear)
106+
}
107+
108+
// setBadge updates the appropriate in-memory map and, when persistent, writes
109+
// through to the DB and fires a WaveObjUpdate event so the frontend stays in sync.
110+
func setBadge(oref waveobj.ORef, badge *baseds.Badge, persistent bool, clear bool) {
111+
globalBadgeStore.lock.Lock()
112+
defer globalBadgeStore.lock.Unlock()
113+
114+
orefStr := oref.String()
115+
116+
if persistent {
117+
if clear || badge == nil {
118+
delete(globalBadgeStore.persistent, orefStr)
119+
log.Printf("badge store: persistent badge cleared: oref=%s\n", orefStr)
120+
go persistBadge(oref, nil)
121+
} else {
122+
globalBadgeStore.persistent[orefStr] = *badge
123+
log.Printf("badge store: persistent badge set: oref=%s badge=%+v\n", orefStr, *badge)
124+
go persistBadge(oref, badge)
125+
}
126+
} else {
127+
if clear || badge == nil {
128+
delete(globalBadgeStore.transient, orefStr)
129+
log.Printf("badge store: transient badge cleared: oref=%s\n", orefStr)
130+
} else {
131+
globalBadgeStore.transient[orefStr] = *badge
132+
log.Printf("badge store: transient badge set: oref=%s badge=%+v\n", orefStr, *badge)
133+
}
134+
}
135+
}
136+
137+
// persistBadge writes the badge (or nil to clear) to the appropriate DB object.
138+
func persistBadge(oref waveobj.ORef, badge *baseds.Badge) {
139+
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
140+
defer cancelFn()
141+
142+
switch oref.OType {
143+
case waveobj.OType_Tab:
144+
err := wstore.DBUpdateFn[*waveobj.Tab](ctx, oref.OID, func(tab *waveobj.Tab) {
145+
tab.Badge = badge
146+
})
147+
if err != nil {
148+
log.Printf("badge store: error persisting badge for tab %s: %v\n", oref.OID, err)
149+
return
150+
}
151+
log.Printf("badge store: persisted badge for tab %s\n", oref.OID)
152+
SendWaveObjUpdate(oref)
153+
154+
case waveobj.OType_Block:
155+
err := wstore.DBUpdateFn[*waveobj.Block](ctx, oref.OID, func(block *waveobj.Block) {
156+
block.Badge = badge
157+
})
158+
if err != nil {
159+
log.Printf("badge store: error persisting badge for block %s: %v\n", oref.OID, err)
160+
return
161+
}
162+
log.Printf("badge store: persisted badge for block %s\n", oref.OID)
163+
SendWaveObjUpdate(oref)
164+
165+
default:
166+
log.Printf("badge store: unsupported oref type for persistence: %s\n", oref.OType)
167+
}
168+
}
169+
170+
// GetAllBadges returns a snapshot of all currently active badges as a slice of
171+
// BadgeEvent values. Each entry carries the ORef, the Persistent flag, and the
172+
// Badge itself. An oref that has both a persistent and a transient badge will
173+
// appear twice in the result.
174+
func GetAllBadges() []baseds.BadgeEvent {
175+
globalBadgeStore.lock.Lock()
176+
defer globalBadgeStore.lock.Unlock()
177+
178+
result := make([]baseds.BadgeEvent, 0, len(globalBadgeStore.persistent)+len(globalBadgeStore.transient))
179+
for orefStr, badge := range globalBadgeStore.persistent {
180+
b := badge // copy
181+
result = append(result, baseds.BadgeEvent{
182+
ORef: orefStr,
183+
Persistent: true,
184+
Badge: &b,
185+
})
186+
}
187+
for orefStr, badge := range globalBadgeStore.transient {
188+
b := badge // copy
189+
result = append(result, baseds.BadgeEvent{
190+
ORef: orefStr,
191+
Badge: &b,
192+
})
193+
}
194+
return result
195+
}

pkg/wps/wpstypes.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const (
2727
Event_AIModeConfig = "waveai:modeconfig"
2828
Event_TabIndicator = "tab:indicator"
2929
Event_BlockJobStatus = "block:jobstatus" // type: BlockJobStatusData
30+
Event_Badge = "badge" // type: baseds.BadgeEvent
3031
)
3132

3233
type WaveEvent struct {

pkg/wshrpc/wshrpctypes.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import (
1111

1212
"github.com/google/uuid"
1313
"github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes"
14+
"github.com/wavetermdev/waveterm/pkg/baseds"
1415
"github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata"
1516
"github.com/wavetermdev/waveterm/pkg/vdom"
1617
"github.com/wavetermdev/waveterm/pkg/waveobj"
@@ -88,6 +89,7 @@ type WshRpcInterface interface {
8889
DisposeSuggestionsCommand(ctx context.Context, widgetId string) error
8990
GetTabCommand(ctx context.Context, tabId string) (*waveobj.Tab, error)
9091
GetAllTabIndicatorsCommand(ctx context.Context) (map[string]*TabIndicator, error)
92+
GetAllBadgesCommand(ctx context.Context) ([]baseds.BadgeEvent, error)
9193

9294
// connection functions
9395
ConnStatusCommand(ctx context.Context) ([]ConnStatus, error)

pkg/wshrpc/wshserver/wshserver.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222

2323
"github.com/skratchdot/open-golang/open"
2424
"github.com/wavetermdev/waveterm/pkg/aiusechat"
25+
"github.com/wavetermdev/waveterm/pkg/baseds"
2526
"github.com/wavetermdev/waveterm/pkg/aiusechat/chatstore"
2627
"github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes"
2728
"github.com/wavetermdev/waveterm/pkg/blockcontroller"
@@ -1411,6 +1412,10 @@ func (ws *WshServer) GetAllTabIndicatorsCommand(ctx context.Context) (map[string
14111412
return wcore.GetAllTabIndicators(), nil
14121413
}
14131414

1415+
func (ws *WshServer) GetAllBadgesCommand(ctx context.Context) ([]baseds.BadgeEvent, error) {
1416+
return wcore.GetAllBadges(), nil
1417+
}
1418+
14141419
func (ws *WshServer) GetSecretsCommand(ctx context.Context, names []string) (map[string]string, error) {
14151420
result := make(map[string]string)
14161421
for _, name := range names {

0 commit comments

Comments
 (0)