Skip to content
Merged
Show file tree
Hide file tree
Changes from 131 commits
Commits
Show all changes
135 commits
Select commit Hold shift + click to select a range
a4def07
copy vdom to tsunami/vdom
sawka Aug 31, 2025
0bfffcc
fix all errors in tsunami packages
sawka Aug 31, 2025
6398a5f
refactor rpctypes (protocol messages)
sawka Aug 31, 2025
819f3ab
moved componentimpl and rootelem to the comp pkg
sawka Aug 31, 2025
cd8ae61
remove vdom.scss
sawka Aug 31, 2025
73b3527
server handlers + listenandserve
sawka Aug 31, 2025
ec54dc9
use handlers intead of rpc
sawka Aug 31, 2025
e58d726
add an sse handler
sawka Aug 31, 2025
fc9f1e1
remove client, implement sse for async initiation
sawka Aug 31, 2025
d7934eb
remove more rpc cruft
sawka Aug 31, 2025
73badad
more cleanup
sawka Aug 31, 2025
1379d9a
add a clientid
sawka Aug 31, 2025
432d333
handle fe update should be in a critical section
sawka Aug 31, 2025
41d04b7
set go 1.24.6
sawka Sep 1, 2025
e256dfd
working on getting frontend setup (checkpoint)
sawka Sep 1, 2025
336dc29
checkpoint
sawka Sep 1, 2025
44f943c
checkpoint, no errors, try to load the view
sawka Sep 1, 2025
53ff0c3
tsunami_listenaddr
sawka Sep 1, 2025
58eaf16
checkpoint -- fixing types, names, packages
sawka Sep 1, 2025
d3d6684
got hello world working with new model
sawka Sep 1, 2025
db9cfb1
add Title AppOpt
sawka Sep 1, 2025
89635ed
work on tailwind, pass renderopts through render funcs... add UseResy…
sawka Sep 1, 2025
e29e47f
hold renderops in vdomcontextval
sawka Sep 1, 2025
0f776ed
deal with static files (for prod builds)
sawka Sep 2, 2025
7bdad4b
add static handlers as well...
sawka Sep 2, 2025
45f94c3
useState and useAtom both now return 3 values....
sawka Sep 2, 2025
4b9154e
3 types of atoms... $shared, $config, and $data...
sawka Sep 2, 2025
e290924
synchronize atoms, implement GET /api/data
sawka Sep 2, 2025
be39586
implement /api/config and /api/data
sawka Sep 2, 2025
12318a4
make some methods private, and implement a SetAppOpts method
sawka Sep 2, 2025
0cfa0f8
default client + working on better type conversions for useatom...
sawka Sep 2, 2025
927be3f
implement /api/manifest (register manifest file)
sawka Sep 2, 2025
a46558c
tsunami bin ... build, cobra, etc
sawka Sep 2, 2025
e9d0258
starting the build/run commands
sawka Sep 2, 2025
eabda0d
update AppOpts, simplify, working on build and tw.css...
sawka Sep 3, 2025
4c5c1af
tsunami_distpath
sawka Sep 3, 2025
7ae7cf8
working on the build
sawka Sep 3, 2025
35a98fe
getting closer with the build...
sawka Sep 3, 2025
bea4592
progress on build, still fighting tailwind
sawka Sep 3, 2025
b5ac154
build/run working!
sawka Sep 3, 2025
f57ace9
implement -o for tsunami build
sawka Sep 3, 2025
296ac34
tempdir cleanup
sawka Sep 3, 2025
013e4f4
fixed SSE asyncinitation, ported pomodoro
sawka Sep 3, 2025
5eb75b2
first cut at new prompt.md for tsunami (ported from waveapps)
sawka Sep 3, 2025
384699a
remove closeonctrlc functionality
sawka Sep 3, 2025
131a3ee
remove AppOpts, move title to hook, derrive GlobalKeyboardHandler... …
sawka Sep 3, 2025
f8cbbe0
simplify. remove vdom.E(), remove gorilla/mux, remove vdom.Bind(), r…
sawka Sep 3, 2025
48b8580
remove FilterFns, add Ternary, one ForEach which includes index...
sawka Sep 4, 2025
aceff3a
godoc tasks
sawka Sep 4, 2025
f4e100b
simplify vdom pkg, remove more public methods and types (hooks implem…
sawka Sep 4, 2025
2893a07
small cleanups and add godocs for public vdom funcs...
sawka Sep 4, 2025
1068fc5
rename parttoelems to toelems...
sawka Sep 4, 2025
41ff2e1
prompt updates, create promps dir, rename to system.md
sawka Sep 4, 2025
bedfe33
prompt updates, vdom bug fix
sawka Sep 4, 2025
6bff9aa
consistent naming
sawka Sep 4, 2025
ce86ded
clean up tsunami/app api surface
sawka Sep 4, 2025
964ccec
greatly simplify + document dynamic content handling
sawka Sep 4, 2025
3f01b49
update app API for 3 different atom types
sawka Sep 4, 2025
0483923
added global state management docs
sawka Sep 4, 2025
ee30814
WithKey takes any. fix tailwind stuff in system.md. fix deps in Use…
sawka Sep 5, 2025
6c6e504
fix errors in example
sawka Sep 5, 2025
c416026
wow recharts demo working
sawka Sep 5, 2025
d7a4b5f
add recharts documentation for prompt
sawka Sep 5, 2025
489285d
updated demos with go.mod and tw.css...
sawka Sep 5, 2025
c2596cc
copy go.mod file
sawka Sep 5, 2025
a75e474
fix rendering bug
sawka Sep 5, 2025
bac641a
tsunami config and githubactions monitor
sawka Sep 5, 2025
cfdff41
small change to not dirty atoms that are the same
sawka Sep 6, 2025
64bd41f
fix remount issue
sawka Sep 6, 2025
8674095
working towards new atom interface
sawka Sep 7, 2025
e49dfa1
more updates to atom API
sawka Sep 7, 2025
2300333
Merge remote-tracking branch 'origin/main' into sawka/tsunami
sawka Sep 7, 2025
a48c80b
remove vdom.UseAtom
sawka Sep 7, 2025
731aaf8
mutex for globalctx
sawka Sep 8, 2025
638c0bd
rename "comp" to "engine" for internal stuff
sawka Sep 8, 2025
6791461
recursive type validation for atoms
sawka Sep 8, 2025
bf4a417
move client / serverhandlers to engine
sawka Sep 8, 2025
2731633
big update, move hooks to app. move client to engine, remove vdomctx.
sawka Sep 8, 2025
7fa0786
add uselocal hook (to replace usestate)
sawka Sep 8, 2025
352182e
removing vdombindings and atom state sync (simplify)
sawka Sep 8, 2025
40969b2
vdom.tsx updates for removing FE atoms and bindings
sawka Sep 8, 2025
20a68e9
createatom flow
sawka Sep 9, 2025
9dd37ed
working on atom impl...
sawka Sep 9, 2025
d4f8687
genatom interface in rootelem
sawka Sep 9, 2025
58a7a77
new generic atoms in engine. update SetVal to deal with conversion (…
sawka Sep 9, 2025
dda17b4
use new deepcopy func and avoid concurrent mutation
sawka Sep 9, 2025
ac85035
cleanup and document rendering pipeline (render.go / render.md). rem…
sawka Sep 9, 2025
c8f8e1b
wire up an optimistic input to fix react cursor issues
sawka Sep 9, 2025
492c238
optimize transfering text with new TransferText field...
sawka Sep 9, 2025
4fc183a
updated system prompt for new app hooks (in app pkg and no ctx passing)
sawka Sep 9, 2025
7b94e94
updated system prompt for new app hooks (app pkg, no ctx)
sawka Sep 9, 2025
78c3391
remove ctx from component renderfns...
sawka Sep 9, 2025
cdf66cc
update todo app to use new UseLocal
sawka Sep 9, 2025
9ef0110
big updates to system prompt to deal with UseLocal instead of UseStat…
sawka Sep 9, 2025
8c796c8
fix some small issues with prompt
sawka Sep 9, 2025
6e614aa
explain atoms more in prompt
sawka Sep 9, 2025
5ad3a14
fix issue with component definitions
sawka Sep 9, 2025
393ce9a
big updates to system prompt for atoms, more consistency, remove redu…
sawka Sep 9, 2025
db74ad9
add UseGoRoutine
sawka Sep 9, 2025
8234325
more system prompt updates
sawka Sep 9, 2025
77f8a00
more prompt updates. migrate cpuchart
sawka Sep 9, 2025
3d18fa7
add timer hooks
sawka Sep 9, 2025
bb523ba
new ticker pattern for cpu
sawka Sep 9, 2025
6bdbc63
use new hooks for githubaction (significant benefit)
sawka Sep 9, 2025
28e79db
implement a true atomic SetFn for atoms
sawka Sep 9, 2025
b6f12b3
new pattern for recharts demo
sawka Sep 9, 2025
af19bbf
update atom. deep copy on SetFn. warning for data mutation
sawka Sep 9, 2025
99ea01d
modify demos and system prompt to say SetFn is safe for modifications…
sawka Sep 9, 2025
60bd7c0
remove legacy rpc code
sawka Sep 10, 2025
0a3fc8f
call twmerge on className prop
sawka Sep 10, 2025
0e5f6b1
working on a backend table component
sawka Sep 10, 2025
da8c718
fix tsunamiconfig, expose data in tabletest
sawka Sep 10, 2025
ee13114
conditionally symlink "ui" pkg
sawka Sep 10, 2025
a7bab98
add a fe-update reason
sawka Sep 10, 2025
40b4ae4
write a log line on render
sawka Sep 10, 2025
5d3d6c6
add rendertype
sawka Sep 10, 2025
beda3c4
track containing component in componentimpl, error messages for vdom …
sawka Sep 10, 2025
ba5655f
panic handlers
sawka Sep 10, 2025
f24b736
create a global event context
sawka Sep 10, 2025
1c91358
add panic handlers for effect work
sawka Sep 10, 2025
c33fc7e
added RootElem to event/effect global contexts
sawka Sep 10, 2025
6cc1969
add clientimpl pointer to rootelem for easy access
sawka Sep 10, 2025
c3fa716
remove UseState and UseAtom
sawka Sep 10, 2025
9f200f3
synchronize renderwork. call notifyfn when we get new render work fr…
sawka Sep 10, 2025
1bd88e8
implement RenderWork based send async initiation
sawka Sep 11, 2025
9db6f91
remove async notify (port pomodoro example to new code)
sawka Sep 11, 2025
094dbbe
remove sendasyncinitiation from docs
sawka Sep 11, 2025
f0305f6
update usedby, double map, track atoms across render
sawka Sep 11, 2025
14f2c99
fix double timestamps
sawka Sep 11, 2025
388f338
better go detection
sawka Sep 11, 2025
fc03346
updated css
sawka Sep 11, 2025
d54fd16
move go.sum back to orig dir. also make sure to copy go.sum to temp …
sawka Sep 11, 2025
fb071a2
always show stderr/stdout output from program
sawka Sep 11, 2025
656eea2
expose remainingseconds as a data atom
sawka Sep 11, 2025
82aec17
updates to pomodoro tw
sawka Sep 11, 2025
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
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"gopls": {
"analyses": {
"QF1003": false
}
},
"directoryFilters": ["-tsunami/frontend/scaffold"]
}
}
87 changes: 87 additions & 0 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -438,3 +438,90 @@ tasks:
ignore_error: true
- cmd: '{{.RMRF}} "dist"'
ignore_error: true

tsunami:demo:todo:
desc: Run the tsunami todo demo application
cmd: go run demo/todo/*.go
dir: tsunami
env:
TSUNAMI_LISTENADDR: "localhost:12026"

tsunami:frontend:dev:
desc: Run the tsunami frontend vite dev server
cmd: npm run dev
dir: tsunami/frontend

tsunami:frontend:build:
desc: Build the tsunami frontend
cmd: yarn build
dir: tsunami/frontend

tsunami:frontend:devbuild:
desc: Build the tsunami frontend in development mode (with source maps and symbols)
cmd: yarn build:dev
dir: tsunami/frontend

tsunami:scaffold:
desc: Build scaffold for tsunami frontend development
deps:
- tsunami:frontend:build
cmds:
- task: tsunami:scaffold:internal

tsunami:devscaffold:
desc: Build scaffold for tsunami frontend development (with source maps and symbols)
deps:
- tsunami:frontend:devbuild
cmds:
- task: tsunami:scaffold:internal

tsunami:scaffold:internal:
desc: Internal task to create scaffold directory structure
dir: tsunami/frontend
internal: true
cmds:
- cmd: "{{.RMRF}} scaffold"
ignore_error: true
- mkdir scaffold
- cd scaffold && npm --no-workspaces init -y --init-license Apache-2.0
- cd scaffold && npm pkg set name=tsunami-scaffold
- cd scaffold && npm pkg delete author
- cd scaffold && npm pkg set author.name="Command Line Inc"
- cd scaffold && npm pkg set author.email="[email protected]"
- cd scaffold && npm --no-workspaces install tailwindcss @tailwindcss/cli
- cp -r dist scaffold/
- cp ../templates/app-main.go.tmpl scaffold/app-main.go
- cp ../templates/tailwind.css scaffold/
- cp ../templates/gitignore.tmpl scaffold/.gitignore

tsunami:build:
desc: Build the tsunami binary.
cmds:
- cmd: "{{.RM}} bin/tsunami*"
ignore_error: true
- mkdir -p bin
- cd tsunami && go build -ldflags "-X main.BuildTime=$({{.DATE}} +'%Y%m%d%H%M') -X main.TsunamiVersion={{.VERSION}}" -o ../bin/tsunami{{exeExt}} cmd/main-tsunami.go
sources:
- "tsunami/**/*.go"
- "tsunami/go.mod"
- "tsunami/go.sum"
generates:
- "bin/tsunami{{exeExt}}"

tsunami:clean:
desc: Clean tsunami frontend build artifacts
dir: tsunami/frontend
cmds:
- cmd: "{{.RMRF}} dist"
ignore_error: true
- cmd: "{{.RMRF}} scaffold"
ignore_error: true

godoc:
desc: Start the Go documentation server for the root module
cmd: $(go env GOPATH)/bin/pkgsite -http=:6060

tsunami:godoc:
desc: Start the Go documentation server for the tsunami module
cmd: $(go env GOPATH)/bin/pkgsite -http=:6060
dir: tsunami
8 changes: 0 additions & 8 deletions frontend/app/view/vdom/vdom.scss

This file was deleted.

3 changes: 1 addition & 2 deletions frontend/app/view/vdom/vdom.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ import {
validateAndWrapCss,
validateAndWrapReactStyle,
} from "@/app/view/vdom/vdom-utils";
import "./vdom.scss";

const TextTag = "#text";
const FragmentTag = "#fragment";
Expand Down Expand Up @@ -506,7 +505,7 @@ function VDomView({ blockId, model }: VDomViewProps) {
model.viewRef = viewRef;
const vdomClass = "vdom-" + blockId;
return (
<div className={clsx("view-vdom", vdomClass)} ref={viewRef}>
<div className={clsx("overflow-auto w-full min-h-full", vdomClass)} ref={viewRef}>
{contextActive ? <VDomInnerView blockId={blockId} model={model} /> : null}
</div>
);
Expand Down
1 change: 1 addition & 0 deletions frontend/types/gotypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -837,6 +837,7 @@ declare global {
"debug:panictype"?: string;
"block:view"?: string;
"ai:backendtype"?: string;
"ai:local"?: boolean;
"wsh:cmd"?: string;
"wsh:haderror"?: boolean;
"conn:conntype"?: string;
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/wavetermdev/waveterm

go 1.24.2
go 1.24.6
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Invalid go directive version; use major.minor and add toolchain

The go directive must not include a patch version. Use go 1.24 and specify the patch via the toolchain directive to ensure reproducible builds.

Apply:

-go 1.24.6
+go 1.24
+toolchain go1.24.6

Run to check for other modules with patch-level go directives:


🏁 Script executed:

#!/bin/bash
rg -nP '^\s*go\s+1\.\d+\.\d+\b' -g '!*vendor/**' -S

Length of output: 148


🏁 Script executed:

#!/bin/bash
# Search all go.mod files for patch-level go directives, excluding vendor
grep -R -nP '^\s*go\s+[0-9]+\.[0-9]+\.[0-9]+' --include=go.mod -not -path '*/vendor/*' .

Length of output: 110


#!/bin/bash
# Search all go.mod files for patch-level go directives, excluding vendor directories
rg -n '^go [0-9]+\.[0-9]+\.[0-9]+' --glob '*.mod' --glob '!vendor/**' .

🏁 Script executed:

#!/bin/bash
# Search for any go.mod files using a patch-level go directive
grep -R -nE '^go [0-9]+\.[0-9]+\.[0-9]+' --include='go.mod' --exclude-dir='vendor' .

Length of output: 312


Update all go.mod files to use major.minor go directive and add toolchain

Every go.mod in the repo (root and under tsunami/) currently has a patch-level directive (go 1.24.6), which is invalid. On line 3 of each file, replace:

-go 1.24.6
+go 1.24
+toolchain go1.24.6

Apply this to:

  • go.mod
  • tsunami/go.mod
  • tsunami/demo/cpuchart/go.mod
  • tsunami/demo/githubaction/go.mod
  • tsunami/demo/pomodoro/go.mod
  • tsunami/demo/recharts/go.mod
  • tsunami/demo/todo/go.mod
  • tsunami/demo/tsunamiconfig/go.mod
📝 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
go 1.24.6
// … other lines …
go 1.24
toolchain go1.24.6
// … remaining lines …
🤖 Prompt for AI Agents
In go.mod around line 3, replace the patch-level directive "go 1.24.6" with a
major.minor directive and add a toolchain directive; change the line to "go
1.24" and immediately add "toolchain go1.24" (or the appropriate toolchain
token) so the file uses the major.minor go directive and declares the toolchain;
apply the same change to the root go.mod and each listed module: tsunami/go.mod,
tsunami/demo/cpuchart/go.mod, tsunami/demo/githubaction/go.mod,
tsunami/demo/pomodoro/go.mod, tsunami/demo/recharts/go.mod,
tsunami/demo/todo/go.mod, and tsunami/demo/tsunamiconfig/go.mod.


require (
github.com/alexflint/go-filemutex v1.3.0
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@
},
"packageManager": "[email protected]",
"workspaces": [
"docs"
"docs",
"tsunami/frontend"
]
}
Binary file added public/logos/wave-logo-256.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions tsunami/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
bin/
123 changes: 123 additions & 0 deletions tsunami/app/atom.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

package app

import (
"log"
"reflect"
"runtime"

"github.com/wavetermdev/waveterm/tsunami/engine"
"github.com/wavetermdev/waveterm/tsunami/util"
)

// logInvalidAtomSet logs an error when an atom is being set during component render
func logInvalidAtomSet(atomName string) {
_, file, line, ok := runtime.Caller(2)
if ok {
log.Printf("invalid Set of atom '%s' in component render function at %s:%d", atomName, file, line)
} else {
log.Printf("invalid Set of atom '%s' in component render function", atomName)
}
}

// sameRef returns true if oldVal and newVal share the same underlying reference
// (pointer, map, or slice). Nil values return false.
func sameRef[T any](oldVal, newVal T) bool {
vOld := reflect.ValueOf(oldVal)
vNew := reflect.ValueOf(newVal)

if !vOld.IsValid() || !vNew.IsValid() {
return false
}

switch vNew.Kind() {
case reflect.Ptr:
// direct comparison works for *T
return any(oldVal) == any(newVal)

case reflect.Map, reflect.Slice:
if vOld.Kind() != vNew.Kind() || vOld.IsZero() || vNew.IsZero() {
return false
}
return vOld.Pointer() == vNew.Pointer()
}
Comment on lines +35 to +45
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix nil-pointer handling in sameRef for pointer types

The docs say “Nil values return false,” but pointer case returns true for two nil pointers. Add an IsZero check before comparing.

Apply this diff:

 case reflect.Ptr:
-    // direct comparison works for *T
-    return any(oldVal) == any(newVal)
+    // guard nils to honor "nil values return false"
+    if vOld.IsZero() || vNew.IsZero() {
+        return false
+    }
+    // direct comparison works for *T
+    return any(oldVal) == any(newVal)
📝 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
switch vNew.Kind() {
case reflect.Ptr:
// direct comparison works for *T
return any(oldVal) == any(newVal)
case reflect.Map, reflect.Slice:
if vOld.Kind() != vNew.Kind() || vOld.IsZero() || vNew.IsZero() {
return false
}
return vOld.Pointer() == vNew.Pointer()
}
switch vNew.Kind() {
case reflect.Ptr:
// guard nils to honor "nil values return false"
if vOld.IsZero() || vNew.IsZero() {
return false
}
// direct comparison works for *T
return any(oldVal) == any(newVal)
case reflect.Map, reflect.Slice:
if vOld.Kind() != vNew.Kind() || vOld.IsZero() || vNew.IsZero() {
return false
}
return vOld.Pointer() == vNew.Pointer()
}
🤖 Prompt for AI Agents
In tsunami/app/atom.go around lines 35 to 45, the pointer (reflect.Ptr) branch
currently returns true for two nil pointers which contradicts the doc that "Nil
values return false"; update the pointer case to first check vOld.IsZero() ||
vNew.IsZero() and return false if either is zero, otherwise perform the existing
direct comparison (any(oldVal) == any(newVal)); keep the rest of the switch
intact.


// primitives, structs, etc. → not a reference type
return false
}

// logMutationWarning logs a warning when mutation is detected
func logMutationWarning(atomName string) {
_, file, line, ok := runtime.Caller(2)
if ok {
log.Printf("WARNING: atom '%s' appears to be mutated instead of copied at %s:%d - use app.DeepCopy to create a copy before mutating", atomName, file, line)
} else {
log.Printf("WARNING: atom '%s' appears to be mutated instead of copied - use app.DeepCopy to create a copy before mutating", atomName)
}
}

// Atom[T] represents a typed atom implementation
type Atom[T any] struct {
name string
client *engine.ClientImpl
}

// AtomName implements the vdom.Atom interface
func (a Atom[T]) AtomName() string {
return a.name
}

// Get returns the current value of the atom. When called during component render,
// it automatically registers the component as a dependency for this atom, ensuring
// the component re-renders when the atom value changes.
func (a Atom[T]) Get() T {
vc := engine.GetGlobalRenderContext()
if vc != nil {
vc.UsedAtoms[a.name] = true
}
val := a.client.Root.GetAtomVal(a.name)
typedVal := util.GetTypedAtomValue[T](val, a.name)
return typedVal
}

// Set updates the atom's value to the provided new value and triggers re-rendering
// of any components that depend on this atom. This method cannot be called during
// render cycles - use effects or event handlers instead.
func (a Atom[T]) Set(newVal T) {
vc := engine.GetGlobalRenderContext()
if vc != nil {
logInvalidAtomSet(a.name)
return
}

// Check for potential mutation bugs with reference types
currentVal := a.client.Root.GetAtomVal(a.name)
currentTyped := util.GetTypedAtomValue[T](currentVal, a.name)
if sameRef(currentTyped, newVal) {
logMutationWarning(a.name)
}

if err := a.client.Root.SetAtomVal(a.name, newVal); err != nil {
log.Printf("Failed to set atom value for %s: %v", a.name, err)
return
}
a.client.Root.AtomAddRenderWork(a.name)
}

// SetFn updates the atom's value by applying the provided function to the current value.
// The function receives a copy of the current atom value, which can be safely mutated
// without affecting the original data. The return value from the function becomes the
// new atom value. This method cannot be called during render cycles.
func (a Atom[T]) SetFn(fn func(T) T) {
Comment on lines +109 to +113
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

SetFn does not provide a safe copy; can mutate current state in place

Comment promises “receives a copy … can be safely mutated,” but typedVal is passed directly to fn. For maps/slices/pointers this can mutate existing atom state before commit. Deep-copy before invoking fn, and warn if the returned value shares the same ref as the original.

Apply this diff:

 func (a Atom[T]) SetFn(fn func(T) T) {
@@
-    err := a.client.Root.SetFnAtomVal(a.name, func(val any) any {
-        typedVal := util.GetTypedAtomValue[T](val, a.name)
-        return fn(typedVal)
-    })
+    err := a.client.Root.SetFnAtomVal(a.name, func(val any) any {
+        typedVal := util.GetTypedAtomValue[T](val, a.name)
+        // ensure user code mutates a copy, not the live state
+        copied := DeepCopy(typedVal)
+        result := fn(copied)
+        if sameRef(typedVal, result) {
+            logMutationWarning(a.name)
+        }
+        return result
+    })

If DeepCopy isn’t available for all T, adjust the comment to remove the “safe to mutate” claim and document that callers must copy first (but the above is preferred).

Also applies to: 120-129

vc := engine.GetGlobalRenderContext()
if vc != nil {
logInvalidAtomSet(a.name)
return
}
currentVal := a.Get()
copiedVal := DeepCopy(currentVal)
newVal := fn(copiedVal)
a.Set(newVal)
}
Comment on lines +109 to +123
Copy link
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

SetFn is not atomic; risk of lost updates

Get–transform–Set can lose concurrent updates. Do the read-modify-write atomically in engine (per-atom lock), and keep the DeepCopy + mutation-warning logic.

Apply in this file:

 func (a Atom[T]) SetFn(fn func(T) T) {
   vc := engine.GetGlobalRenderContext()
   if vc != nil {
     logInvalidAtomSet(a.name)
     return
   }
-  currentVal := a.Get()
-  copiedVal := DeepCopy(currentVal)
-  newVal := fn(copiedVal)
-  a.Set(newVal)
+  err := a.client.Root.SetFnAtomVal(a.name, func(val any) any {
+    typedVal := util.GetTypedAtomValue[T](val, a.name)
+    copied := DeepCopy(typedVal)
+    result := fn(copied)
+    if sameRef(typedVal, result) {
+      logMutationWarning(a.name)
+    }
+    return result
+  })
+  if err != nil {
+    log.Printf("Failed to SetFn atom %s: %v", a.name, err)
+    return
+  }
+  a.client.Root.AtomAddRenderWork(a.name)
 }

And in engine/rootelem.go (see comments there) reintroduce SetFnAtomVal and extend genAtom with SetFnVal(fn func(any) any) error, performing lookup under r.atomLock then calling atom.SetFnVal outside that lock. Please verify genAtom implementations already support SetFnVal.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== repo root ==="
ls -la

echo "=== ripgrep for relevant symbols ==="
rg -n --hidden "SetFnAtomVal|SetFnVal|AtomAddRenderWork|AtomAddRender|genAtom|rootelem|atomLock|DeepCopy|GetTypedAtomValue|sameRef" -S || true

echo "=== show tsunami/app/atom.go (first 240 lines) ==="
if [ -f "tsunami/app/atom.go" ]; then
  sed -n '1,240p' tsunami/app/atom.go
else
  echo "tsunami/app/atom.go not found"
fi

echo "=== show engine/rootelem.go (first 400 lines) ==="
if [ -f "engine/rootelem.go" ]; then
  sed -n '1,400p' engine/rootelem.go
else
  echo "engine/rootelem.go not found"
fi

echo "=== list engine/ dir ==="
ls -la engine || true

echo "=== search for files named like gen*atom*.go or gen_*atom*.go ==="
rg -n --hidden "gen.*atom" -S || true

echo "=== search for methods named SetFnVal or SetFnVal signatures ==="
rg -n --hidden "func\s+\([^\)]*\)\s+SetFnVal\(" -S || true

echo "=== done ==="

Length of output: 12395


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== show tsunami/engine/rootelem.go (first 400 lines) ==="
if [ -f "tsunami/engine/rootelem.go" ]; then
  sed -n '1,400p' tsunami/engine/rootelem.go
else
  echo "tsunami/engine/rootelem.go not found"
fi

echo
echo "=== search for genAtom and related methods across tsunami/ ==="
rg -n --hidden "type genAtom interface|SetAtomVal\(|GetAtomVal\(|SetFnVal\(|SetFnAtomVal\(|AtomAddRenderWork\(|AtomSetUsedBy\(" tsunami -S || true

echo
echo "=== search for implementations that might satisfy genAtom ==="
rg -n --hidden "struct .*Atom|func .*SetAtomVal\(|func .*SetFnVal\(" tsunami -S || true

echo
echo "=== show tsunami/util/util.go (GetTypedAtomValue) ==="
sed -n '1,240p' tsunami/util/util.go || true

echo
echo "=== done ==="

Length of output: 16884


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== list tsunami/engine ==="
ls -la tsunami/engine || true

echo
echo "=== search for GetVal/SetVal/SetUsedBy/GetUsedBy implementations ==="
rg -n --hidden -S "func\s+\([^)]+\)\s+GetVal\(" tsunami || true
rg -n --hidden -S "func\s+\([^)]+\)\s+SetVal\(" tsunami || true
rg -n --hidden -S "func\s+\([^)]+\)\s+SetUsedBy\(" tsunami || true
rg -n --hidden -S "func\s+\([^)]+\)\s+GetUsedBy\(" tsunami || true

echo
echo "=== search for Atom struct types ==="
rg -n --hidden -S "type\s+\w*Atom\s+struct" tsunami || true

echo
echo "=== show files that implement genAtom by looking for SetVal/GetVal usages ==="
rg -n --hidden -S "SetVal\\(|GetVal\\(" tsunami -g '!**/node_modules/**' -C3 || true

echo
echo "=== done ==="

Length of output: 1283


Make SetFn atomic — move read→modify→write into the engine

  • Problem: Atom.SetFn currently does Get → DeepCopy → fn → Set outside engine locks; this can lose concurrent updates.
  • Fix (high level): Replace Atom.SetFn to call a.client.Root.SetFnAtomVal(a.name, func(val any) any { typed := util.GetTypedAtomValue[T](val, a.name); copied := DeepCopy(typed); result := fn(copied); if sameRef(typed, result) { logMutationWarning(a.name) }; return result }) — handle error and then call AtomAddRenderWork.
  • Engine changes required: add RootElem.SetFnAtomVal(name string, fn func(any) any) error which looks up the atom under r.atomLock and then invokes atom.SetFnVal(fn) outside the lock; extend genAtom with SetFnVal(func(any) any) error and implement SetFnVal in tsunami/engine/atomimpl.go (do the DeepCopy + mutation-warning logic there).
  • Verified: genAtom currently has only GetVal, SetVal, SetUsedBy, GetUsedBy — no SetFnVal/SetFnAtomVal exist, so the engine changes above are mandatory.
  • Files to modify: tsunami/app/atom.go, tsunami/engine/rootelem.go, tsunami/engine/atomimpl.go.
🤖 Prompt for AI Agents
In tsunami/app/atom.go around lines 110 to 124, SetFn performs
Get→DeepCopy→fn→Set outside engine locks which can lose concurrent updates;
change it to call the engine API instead: invoke
a.client.Root.SetFnAtomVal(a.name, func(val any) any { typed :=
util.GetTypedAtomValue[T](val, a.name); copied := DeepCopy(typed); result :=
fn(copied); if sameRef(typed, result) { logMutationWarning(a.name) }; return
result }) and handle returned error (log/return) and then call AtomAddRenderWork
as before. To support this add RootElem.SetFnAtomVal(name string, fn func(any)
any) error in tsunami/engine/rootelem.go which finds the atom under r.atomLock
and then calls atom.SetFnVal(fn) outside the lock; extend generated atom
interface (genAtom) with SetFnVal(func(any) any) error and implement SetFnVal in
tsunami/engine/atomimpl.go to perform the DeepCopy + mutation-warning logic and
set the new value safely. Ensure errors are propagated and atom render work is
scheduled after a successful update.

110 changes: 110 additions & 0 deletions tsunami/app/defaultclient.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

package app

import (
"encoding/json"
"io/fs"
"net/http"

"github.com/wavetermdev/waveterm/tsunami/engine"
"github.com/wavetermdev/waveterm/tsunami/vdom"
)

func DefineComponent[P any](name string, renderFn func(props P) any) vdom.Component[P] {
return engine.DefineComponentEx(engine.GetDefaultClient(), name, renderFn)
}

func SetGlobalEventHandler(handler func(event vdom.VDomEvent)) {
engine.GetDefaultClient().SetGlobalEventHandler(handler)
}

// RegisterSetupFn registers a single setup function that is called before the app starts running.
// Only one setup function is allowed, so calling this will replace any previously registered
// setup function.
func RegisterSetupFn(fn func()) {
engine.GetDefaultClient().RegisterSetupFn(fn)
}

// SendAsyncInitiation notifies the frontend that the backend has updated state
// and requires a re-render. Normally the frontend calls the backend in response
// to events, but when the backend changes state independently (e.g., from a
// background process), this function gives the frontend a "nudge" to update.
func SendAsyncInitiation() error {
return engine.GetDefaultClient().SendAsyncInitiation()
}

func ConfigAtom[T any](name string, defaultValue T) Atom[T] {
fullName := "$config." + name
client := engine.GetDefaultClient()
atom := engine.MakeAtomImpl(defaultValue)
client.Root.RegisterAtom(fullName, atom)
return Atom[T]{name: fullName, client: client}
}

func DataAtom[T any](name string, defaultValue T) Atom[T] {
fullName := "$data." + name
client := engine.GetDefaultClient()
atom := engine.MakeAtomImpl(defaultValue)
client.Root.RegisterAtom(fullName, atom)
return Atom[T]{name: fullName, client: client}
}

func SharedAtom[T any](name string, defaultValue T) Atom[T] {
fullName := "$shared." + name
client := engine.GetDefaultClient()
atom := engine.MakeAtomImpl(defaultValue)
client.Root.RegisterAtom(fullName, atom)
return Atom[T]{name: fullName, client: client}
}

// HandleDynFunc registers a dynamic HTTP handler function with the internal http.ServeMux.
// The pattern MUST start with "/dyn/" to be valid. This allows registration of dynamic
// routes that can be handled at runtime.
func HandleDynFunc(pattern string, fn func(http.ResponseWriter, *http.Request)) {
engine.GetDefaultClient().HandleDynFunc(pattern, fn)
}

// RunMain is used internally by generated code and should not be called directly.
func RunMain() {
engine.GetDefaultClient().RunMain()
}

// RegisterEmbeds is used internally by generated code and should not be called directly.
func RegisterEmbeds(assetsFilesystem fs.FS, staticFilesystem fs.FS, manifest []byte) {
client := engine.GetDefaultClient()
client.AssetsFS = assetsFilesystem
client.StaticFS = staticFilesystem
client.ManifestFileBytes = manifest
}

// DeepCopy creates a deep copy of the input value using JSON marshal/unmarshal.
// Panics on JSON errors.
func DeepCopy[T any](v T) T {
data, err := json.Marshal(v)
if err != nil {
panic(err)
}
var result T
err = json.Unmarshal(data, &result)
if err != nil {
panic(err)
}
return result
}

// QueueRefOp queues a reference operation to be executed on the DOM element.
// Operations include actions like "focus", "scrollIntoView", etc.
// If the ref is nil or not current, the operation is ignored.
// This function must be called within a component context.
func QueueRefOp(ref *vdom.VDomRef, op vdom.VDomRefOperation) {
if ref == nil || !ref.HasCurrent {
return
}
if op.RefId == "" {
op.RefId = ref.RefId
}
client := engine.GetDefaultClient()
client.Root.QueueRefOp(op)
}
Loading
Loading