Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
7280bf4
update rules
sawka Feb 19, 2026
4c6fd13
first cut at new block-tab based badge system
sawka Feb 20, 2026
51489eb
Merge remote-tracking branch 'origin/main' into sawka/block-indicators
sawka Mar 2, 2026
a75253f
run go generate, fix baseds import
sawka Mar 2, 2026
f7fda6a
Merge remote-tracking branch 'origin/main' into sawka/block-indicators
sawka Mar 5, 2026
f3d27e5
move indicators to their own file (badge.ts)
sawka Mar 5, 2026
95ec727
move tabindicatormap too
sawka Mar 5, 2026
ac7f295
move subscription to badge.ts, clean up some warnings
sawka Mar 5, 2026
e3aa0b8
clean up some warnings
sawka Mar 5, 2026
30788d0
setup FE badge store
sawka Mar 5, 2026
a7acdb9
working on badge integration
sawka Mar 5, 2026
f980494
add clearall for badge event
sawka Mar 5, 2026
260c767
add clearbyid
sawka Mar 5, 2026
c448ee7
add badgewatchpid
sawka Mar 5, 2026
f30f394
working on `wsh badge`
sawka Mar 5, 2026
a88c3bf
hook up pid watching to wsh badge command
sawka Mar 5, 2026
8d6f2ad
checkpoint on moving from tabiindicators to badges
sawka Mar 5, 2026
339cd6c
clear transient tab badges with focus as well
sawka Mar 5, 2026
ccf64e6
more badge migration
sawka Mar 5, 2026
498e0d9
remove tabindicators (backend+frontend), more badges
sawka Mar 6, 2026
09fed4e
getting the badges to show... up to 3 on a tab...
sawka Mar 6, 2026
2fb15c4
add flag color
sawka Mar 6, 2026
1806574
add context menu to flag tab...
sawka Mar 6, 2026
144db86
remove badge persistence
sawka Mar 6, 2026
820f535
focus should not clear pidlinked badges
sawka Mar 6, 2026
897f2d4
update tab bar, change flag to be a flag, resort badges
sawka Mar 6, 2026
c737c6e
Merge remote-tracking branch 'origin/main' into sawka/block-indicators
sawka Mar 6, 2026
e50de18
clean up some scss
sawka Mar 6, 2026
ddc9cff
remove ::after psudo element, just render the dividers in react
sawka Mar 6, 2026
9d1007d
dont use ctx in long running poller
sawka Mar 9, 2026
2518d31
fix nits
sawka Mar 9, 2026
dc2315d
fix nit
sawka Mar 9, 2026
64f6d4f
merge main
sawka Mar 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
first cut at new block-tab based badge system
  • Loading branch information
sawka committed Feb 20, 2026
commit 4c6fd13c7496c322bf0e660d7bbca4102f1c201c
5 changes: 5 additions & 0 deletions cmd/server/main-server.go
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,11 @@ func main() {
jobcontroller.InitJobController()
blockcontroller.InitBlockController()
wcore.InitTabIndicatorStore()
err = wcore.InitBadgeStore()
if err != nil {
log.Printf("error initializing badge store: %v\n", err)
return
}
go func() {
defer func() {
panichandler.PanicHandler("GetSystemSummary", recover())
Expand Down
13 changes: 13 additions & 0 deletions pkg/baseds/baseds.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,16 @@ type RpcInputChType struct {
MsgBytes []byte
IngressLinkId LinkId
}

type Badge struct {
Icon string `json:"icon"`
Color string `json:"color,omitempty"`
Priority float64 `json:"priority"`
}

type BadgeEvent struct {
ORef string `json:"oref"`
Persistent bool `json:"persistent,omitempty"`
Clear bool `json:"clear,omitempty"`
Badge *Badge `json:"badge,omitempty"`
}
16 changes: 10 additions & 6 deletions pkg/waveobj/wtype.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"encoding/json"
"fmt"
"reflect"

"github.com/wavetermdev/waveterm/pkg/baseds"
)

type UpdatesRtnType = []WaveObjUpdate
Expand Down Expand Up @@ -187,12 +189,13 @@ func (*Workspace) GetOType() string {
}

type Tab struct {
OID string `json:"oid"`
Version int `json:"version"`
Name string `json:"name"`
LayoutState string `json:"layoutstate"`
BlockIds []string `json:"blockids"`
Meta MetaMapType `json:"meta"`
OID string `json:"oid"`
Version int `json:"version"`
Name string `json:"name"`
LayoutState string `json:"layoutstate"`
BlockIds []string `json:"blockids"`
Meta MetaMapType `json:"meta"`
Badge *baseds.Badge `json:"badge,omitempty"`
}

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

func (*Block) GetOType() string {
Expand Down
195 changes: 195 additions & 0 deletions pkg/wcore/badge.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

package wcore

import (
"context"
"fmt"
"log"
"sync"
"time"

"github.com/wavetermdev/waveterm/pkg/baseds"
"github.com/wavetermdev/waveterm/pkg/util/utilfn"
"github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wps"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
"github.com/wavetermdev/waveterm/pkg/wstore"
)

// BadgeStore is a write-through cache for badges.
// Each oref can carry two independent badges:
// - a persistent badge (stored in the DB and survives restarts)
// - a transient badge (in-memory only, cleared on restart)
//
// Values are stored by value (not pointer) to prevent external mutation.
type BadgeStore struct {
lock *sync.Mutex
persistent map[string]baseds.Badge // keyed by oref string
transient map[string]baseds.Badge // keyed by oref string
}

var globalBadgeStore = &BadgeStore{
lock: &sync.Mutex{},
persistent: make(map[string]baseds.Badge),
transient: make(map[string]baseds.Badge),
}

// InitBadgeStore loads all persisted badges from the DB into the in-memory
// cache and subscribes to incoming badge events.
func InitBadgeStore() error {
log.Printf("initializing badge store\n")

ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()

// Load persisted badges from all tabs.
tabs, err := wstore.DBGetAllObjsByType[*waveobj.Tab](ctx, waveobj.OType_Tab)
if err != nil {
return fmt.Errorf("badge store: error loading tabs from DB: %w", err)
}
for _, tab := range tabs {
if tab.Badge != nil {
oref := waveobj.MakeORef(waveobj.OType_Tab, tab.OID).String()
globalBadgeStore.persistent[oref] = *tab.Badge
}
}

// Load persisted badges from all blocks.
blocks, err := wstore.DBGetAllObjsByType[*waveobj.Block](ctx, waveobj.OType_Block)
if err != nil {
return fmt.Errorf("badge store: error loading blocks from DB: %w", err)
}
for _, block := range blocks {
if block.Badge != nil {
oref := waveobj.MakeORef(waveobj.OType_Block, block.OID).String()
globalBadgeStore.persistent[oref] = *block.Badge
}
}

log.Printf("badge store: loaded %d persisted badges\n", len(globalBadgeStore.persistent))

// Subscribe to badge events so we can update the cache when events arrive.
rpcClient := wshclient.GetBareRpcClient()
rpcClient.EventListener.On(wps.Event_Badge, handleBadgeEvent)
wshclient.EventSubCommand(rpcClient, wps.SubscriptionRequest{
Event: wps.Event_Badge,
AllScopes: true,
}, nil)

return nil
Comment on lines +31 to +41
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bubble up badge subscription failures.

Line 36 ignores the EventSubCommand error, so the new startup check in cmd/server/main-server.go can never fire and the server can come up with a non-functional badge store.

Suggested fix
 func InitBadgeStore() error {
 	log.Printf("initializing badge store\n")
 
 	rpcClient := wshclient.GetBareRpcClient()
 	rpcClient.EventListener.On(wps.Event_Badge, handleBadgeEvent)
-	wshclient.EventSubCommand(rpcClient, wps.SubscriptionRequest{
+	if err := wshclient.EventSubCommand(rpcClient, wps.SubscriptionRequest{
 		Event:     wps.Event_Badge,
 		AllScopes: true,
-	}, nil)
+	}, nil); err != nil {
+		return err
+	}
 
 	return nil
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func InitBadgeStore() error {
log.Printf("initializing badge store\n")
rpcClient := wshclient.GetBareRpcClient()
rpcClient.EventListener.On(wps.Event_Badge, handleBadgeEvent)
wshclient.EventSubCommand(rpcClient, wps.SubscriptionRequest{
Event: wps.Event_Badge,
AllScopes: true,
}, nil)
return nil
func InitBadgeStore() error {
log.Printf("initializing badge store\n")
rpcClient := wshclient.GetBareRpcClient()
rpcClient.EventListener.On(wps.Event_Badge, handleBadgeEvent)
if err := wshclient.EventSubCommand(rpcClient, wps.SubscriptionRequest{
Event: wps.Event_Badge,
AllScopes: true,
}, nil); err != nil {
return err
}
return nil
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/wcore/badge.go` around lines 31 - 41, InitBadgeStore currently ignores
the error returned by wshclient.EventSubCommand, so subscription failures are
swallowed; modify InitBadgeStore to capture the error from EventSubCommand
(e.g., err := wshclient.EventSubCommand(...)) and return that error (or wrap it
with context) instead of discarding it so callers of InitBadgeStore can detect
and handle subscription failures; ensure the function signature remains
InitBadgeStore() error and that any existing rpcClient.EventListener.On
registration remains intact.

}

func handleBadgeEvent(event *wps.WaveEvent) {
if event.Event != wps.Event_Badge {
return
}
var data baseds.BadgeEvent
err := utilfn.ReUnmarshal(&data, event.Data)
if err != nil {
log.Printf("badge store: error unmarshaling BadgeEvent: %v\n", err)
return
}
if data.ORef == "" {
log.Printf("badge store: received badge event with empty oref\n")
return
}

oref, err := waveobj.ParseORef(data.ORef)
if err != nil {
log.Printf("badge store: error parsing oref %q: %v\n", data.ORef, err)
return
}

setBadge(oref, data.Badge, data.Persistent, data.Clear)
}

// setBadge updates the appropriate in-memory map and, when persistent, writes
// through to the DB and fires a WaveObjUpdate event so the frontend stays in sync.
func setBadge(oref waveobj.ORef, badge *baseds.Badge, persistent bool, clear bool) {
globalBadgeStore.lock.Lock()
defer globalBadgeStore.lock.Unlock()

orefStr := oref.String()

if persistent {
if clear || badge == nil {
delete(globalBadgeStore.persistent, orefStr)
log.Printf("badge store: persistent badge cleared: oref=%s\n", orefStr)
go persistBadge(oref, nil)
} else {
globalBadgeStore.persistent[orefStr] = *badge
log.Printf("badge store: persistent badge set: oref=%s badge=%+v\n", orefStr, *badge)
go persistBadge(oref, badge)
}
} else {
if clear || badge == nil {
delete(globalBadgeStore.transient, orefStr)
log.Printf("badge store: transient badge cleared: oref=%s\n", orefStr)
} else {
globalBadgeStore.transient[orefStr] = *badge
log.Printf("badge store: transient badge set: oref=%s badge=%+v\n", orefStr, *badge)
}
}
}

// persistBadge writes the badge (or nil to clear) to the appropriate DB object.
func persistBadge(oref waveobj.ORef, badge *baseds.Badge) {
ctx, cancelFn := context.WithTimeout(context.Background(), 5*time.Second)
defer cancelFn()

switch oref.OType {
case waveobj.OType_Tab:
err := wstore.DBUpdateFn[*waveobj.Tab](ctx, oref.OID, func(tab *waveobj.Tab) {
tab.Badge = badge
})
if err != nil {
log.Printf("badge store: error persisting badge for tab %s: %v\n", oref.OID, err)
return
}
log.Printf("badge store: persisted badge for tab %s\n", oref.OID)
SendWaveObjUpdate(oref)

case waveobj.OType_Block:
err := wstore.DBUpdateFn[*waveobj.Block](ctx, oref.OID, func(block *waveobj.Block) {
block.Badge = badge
})
if err != nil {
log.Printf("badge store: error persisting badge for block %s: %v\n", oref.OID, err)
return
}
log.Printf("badge store: persisted badge for block %s\n", oref.OID)
SendWaveObjUpdate(oref)

default:
log.Printf("badge store: unsupported oref type for persistence: %s\n", oref.OType)
}
}

// GetAllBadges returns a snapshot of all currently active badges as a slice of
// BadgeEvent values. Each entry carries the ORef, the Persistent flag, and the
// Badge itself. An oref that has both a persistent and a transient badge will
// appear twice in the result.
func GetAllBadges() []baseds.BadgeEvent {
globalBadgeStore.lock.Lock()
defer globalBadgeStore.lock.Unlock()

result := make([]baseds.BadgeEvent, 0, len(globalBadgeStore.persistent)+len(globalBadgeStore.transient))
for orefStr, badge := range globalBadgeStore.persistent {
b := badge // copy
result = append(result, baseds.BadgeEvent{
ORef: orefStr,
Persistent: true,
Badge: &b,
})
}
for orefStr, badge := range globalBadgeStore.transient {
b := badge // copy
result = append(result, baseds.BadgeEvent{
ORef: orefStr,
Badge: &b,
})
}
return result
}
1 change: 1 addition & 0 deletions pkg/wps/wpstypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const (
Event_AIModeConfig = "waveai:modeconfig"
Event_TabIndicator = "tab:indicator"
Event_BlockJobStatus = "block:jobstatus" // type: BlockJobStatusData
Event_Badge = "badge" // type: baseds.BadgeEvent
)

type WaveEvent struct {
Expand Down
2 changes: 2 additions & 0 deletions pkg/wshrpc/wshrpctypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (

"github.com/google/uuid"
"github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes"
"github.com/wavetermdev/waveterm/pkg/baseds"
"github.com/wavetermdev/waveterm/pkg/telemetry/telemetrydata"
"github.com/wavetermdev/waveterm/pkg/vdom"
"github.com/wavetermdev/waveterm/pkg/waveobj"
Expand Down Expand Up @@ -88,6 +89,7 @@ type WshRpcInterface interface {
DisposeSuggestionsCommand(ctx context.Context, widgetId string) error
GetTabCommand(ctx context.Context, tabId string) (*waveobj.Tab, error)
GetAllTabIndicatorsCommand(ctx context.Context) (map[string]*TabIndicator, error)
GetAllBadgesCommand(ctx context.Context) ([]baseds.BadgeEvent, error)

// connection functions
ConnStatusCommand(ctx context.Context) ([]ConnStatus, error)
Expand Down
5 changes: 5 additions & 0 deletions pkg/wshrpc/wshserver/wshserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (

"github.com/skratchdot/open-golang/open"
"github.com/wavetermdev/waveterm/pkg/aiusechat"
"github.com/wavetermdev/waveterm/pkg/baseds"
"github.com/wavetermdev/waveterm/pkg/aiusechat/chatstore"
"github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes"
"github.com/wavetermdev/waveterm/pkg/blockcontroller"
Expand Down Expand Up @@ -1411,6 +1412,10 @@ func (ws *WshServer) GetAllTabIndicatorsCommand(ctx context.Context) (map[string
return wcore.GetAllTabIndicators(), nil
}

func (ws *WshServer) GetAllBadgesCommand(ctx context.Context) ([]baseds.BadgeEvent, error) {
return wcore.GetAllBadges(), nil
}

func (ws *WshServer) GetSecretsCommand(ctx context.Context, names []string) (map[string]string, error) {
result := make(map[string]string)
for _, name := range names {
Expand Down