Skip to content

Commit cd10f88

Browse files
committed
tsunami apps, autoload (resync), fix logging, fix sse handling (if multiple conns), update tw css...
1 parent bc9e8e6 commit cd10f88

10 files changed

Lines changed: 148 additions & 25 deletions

File tree

Taskfile.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -498,7 +498,7 @@ tasks:
498498
- cd scaffold && npm pkg delete author
499499
- cd scaffold && npm pkg set author.name="Command Line Inc"
500500
- cd scaffold && npm pkg set author.email="info@commandline.dev"
501-
- cd scaffold && npm --no-workspaces install tailwindcss@4.1.12 @tailwindcss/cli@4.1.12
501+
- cd scaffold && npm --no-workspaces install tailwindcss@4.1.13 @tailwindcss/cli@4.1.13
502502
- cp -r dist scaffold/
503503
- cp ../templates/app-main.go.tmpl scaffold/app-main.go
504504
- cp ../templates/tailwind.css scaffold/

frontend/app/view/tsunami/tsunami.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { RpcApi } from "@/app/store/wshclientapi";
88
import { TabRpcClient } from "@/app/store/wshrpcutil";
99
import * as services from "@/store/services";
1010
import * as jotai from "jotai";
11-
import { memo } from "react";
11+
import { memo, useEffect } from "react";
1212

1313
class TsunamiViewModel implements ViewModel {
1414
viewType: string;
@@ -65,6 +65,15 @@ class TsunamiViewModel implements ViewModel {
6565
}, 300);
6666
}
6767

68+
resyncController() {
69+
const prtn = RpcApi.ControllerResyncCommand(TabRpcClient, {
70+
tabid: globalStore.get(atoms.staticTabId),
71+
blockid: this.blockId,
72+
forcerestart: false,
73+
});
74+
prtn.catch((e) => console.log("error controller resync", e));
75+
}
76+
6877
forceRestartController() {
6978
if (globalStore.get(this.isRestarting)) {
7079
return;
@@ -98,6 +107,10 @@ const TsunamiView = memo(({ model }: TsunamiViewProps) => {
98107
const blockData = jotai.useAtomValue(model.blockAtom);
99108
const isRestarting = jotai.useAtomValue(model.isRestarting);
100109

110+
useEffect(() => {
111+
model.resyncController();
112+
}, [model]);
113+
101114
const appPath = blockData?.meta?.["tsunami:apppath"];
102115
const controller = blockData?.meta?.controller;
103116

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/blockcontroller/tsunamicontroller.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ func (c *TsunamiController) Start(ctx context.Context, blockMeta waveobj.MetaMap
217217
return fmt.Errorf("app cache is not executable: %s", cachePath)
218218
}
219219

220-
tsunamiProc, err := runTsunamiAppBinary(ctx, cachePath)
220+
tsunamiProc, err := runTsunamiAppBinary(ctx, cachePath, appPath)
221221
if err != nil {
222222
return fmt.Errorf("failed to run tsunami app: %w", err)
223223
}
@@ -300,7 +300,7 @@ func (c *TsunamiController) SendInput(input *BlockInputUnion) error {
300300
return fmt.Errorf("tsunami controller send input not implemented")
301301
}
302302

303-
func runTsunamiAppBinary(ctx context.Context, appBinPath string) (*TsunamiAppProc, error) {
303+
func runTsunamiAppBinary(ctx context.Context, appBinPath string, appPath string) (*TsunamiAppProc, error) {
304304
cmd := exec.Command(appBinPath, "--close-on-stdin")
305305

306306
stdoutPipe, err := cmd.StdoutPipe()
@@ -318,8 +318,17 @@ func runTsunamiAppBinary(ctx context.Context, appBinPath string) (*TsunamiAppPro
318318
return nil, fmt.Errorf("failed to create stdin pipe: %w", err)
319319
}
320320

321+
appName := build.GetAppName(appPath)
322+
321323
stdoutBuffer := utilds.MakeReaderLineBuffer(stdoutPipe, 1000)
324+
stdoutBuffer.SetLineCallback(func(line string) {
325+
log.Printf("[tsunami:%s] %s\n", appName, line)
326+
})
327+
322328
stderrBuffer := utilds.MakeReaderLineBuffer(stderrPipe, 1000)
329+
stderrBuffer.SetLineCallback(func(line string) {
330+
log.Printf("[tsunami:%s] %s\n", appName, line)
331+
})
323332

324333
err = cmd.Start()
325334
if err != nil {
@@ -371,7 +380,6 @@ func runTsunamiAppBinary(ctx context.Context, appBinPath string) (*TsunamiAppPro
371380
errChan <- fmt.Errorf("stderr buffer error: %w", err)
372381
return
373382
}
374-
log.Printf("[stderr-readline] %s\n", line)
375383

376384
port := build.ParseTsunamiPort(line)
377385
if port > 0 {

pkg/utilds/readerlinebuffer.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ type ReaderLineBuffer struct {
1414
reader io.Reader
1515
scanner *bufio.Scanner
1616
done bool
17+
lineCallback func(string)
1718
}
1819

1920
func MakeReaderLineBuffer(reader io.Reader, maxLines int) *ReaderLineBuffer {
@@ -33,6 +34,12 @@ func MakeReaderLineBuffer(reader io.Reader, maxLines int) *ReaderLineBuffer {
3334
return rlb
3435
}
3536

37+
func (rlb *ReaderLineBuffer) SetLineCallback(callback func(string)) {
38+
rlb.lock.Lock()
39+
defer rlb.lock.Unlock()
40+
rlb.lineCallback = callback
41+
}
42+
3643
func (rlb *ReaderLineBuffer) IsDone() bool {
3744
rlb.lock.Lock()
3845
defer rlb.lock.Unlock()
@@ -106,9 +113,12 @@ func (rlb *ReaderLineBuffer) GetTotalLineCount() int {
106113

107114
func (rlb *ReaderLineBuffer) ReadAll() {
108115
for {
109-
_, err := rlb.ReadLine()
116+
line, err := rlb.ReadLine()
110117
if err != nil {
111118
break
112119
}
120+
if rlb.lineCallback != nil {
121+
rlb.lineCallback(line)
122+
}
113123
}
114124
}

tsunami/build/build.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,10 @@ func (b BuildOpts) AppName() string {
4343
return filepath.Base(b.AppPath)
4444
}
4545

46+
func GetAppName(appPath string) string {
47+
return filepath.Base(appPath)
48+
}
49+
4650
type BuildEnv struct {
4751
GoVersion string
4852
TempDir string

tsunami/engine/clientimpl.go

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ type ClientImpl struct {
4343
IsDone bool
4444
DoneReason string
4545
DoneCh chan struct{}
46-
SSEventCh chan ssEvent
46+
SSEChannels map[string]chan ssEvent // map of connectionId to SSE channel
47+
SSEChannelsLock *sync.Mutex
4748
GlobalEventHandler func(event vdom.VDomEvent)
4849
UrlHandlerMux *http.ServeMux
4950
SetupFn func()
@@ -62,12 +63,13 @@ type ClientImpl struct {
6263

6364
func makeClient() *ClientImpl {
6465
client := &ClientImpl{
65-
Lock: &sync.Mutex{},
66-
DoneCh: make(chan struct{}),
67-
SSEventCh: make(chan ssEvent, 100),
68-
UrlHandlerMux: http.NewServeMux(),
69-
ServerId: uuid.New().String(),
70-
RootElem: vdom.H(DefaultComponentName, nil),
66+
Lock: &sync.Mutex{},
67+
DoneCh: make(chan struct{}),
68+
SSEChannels: make(map[string]chan ssEvent),
69+
SSEChannelsLock: &sync.Mutex{},
70+
UrlHandlerMux: http.NewServeMux(),
71+
ServerId: uuid.New().String(),
72+
RootElem: vdom.H(DefaultComponentName, nil),
7173
}
7274
client.Root = MakeRoot(client)
7375
return client
@@ -214,18 +216,49 @@ func (c *ClientImpl) listenAndServe(ctx context.Context) error {
214216
return nil
215217
}
216218

217-
func (c *ClientImpl) SendAsyncInitiation() error {
218-
log.Printf("send async initiation\n")
219+
func (c *ClientImpl) RegisterSSEChannel(connectionId string) chan ssEvent {
220+
c.SSEChannelsLock.Lock()
221+
defer c.SSEChannelsLock.Unlock()
222+
223+
ch := make(chan ssEvent, 100)
224+
c.SSEChannels[connectionId] = ch
225+
return ch
226+
}
227+
228+
func (c *ClientImpl) UnregisterSSEChannel(connectionId string) {
229+
c.SSEChannelsLock.Lock()
230+
defer c.SSEChannelsLock.Unlock()
231+
232+
if ch, exists := c.SSEChannels[connectionId]; exists {
233+
close(ch)
234+
delete(c.SSEChannels, connectionId)
235+
}
236+
}
237+
238+
func (c *ClientImpl) SendSSEvent(event ssEvent) error {
219239
if c.GetIsDone() {
220240
return fmt.Errorf("client is done")
221241
}
222242

223-
select {
224-
case c.SSEventCh <- ssEvent{Event: "asyncinitiation", Data: nil}:
225-
return nil
226-
default:
227-
return fmt.Errorf("SSEvent channel is full")
243+
c.SSEChannelsLock.Lock()
244+
defer c.SSEChannelsLock.Unlock()
245+
246+
// Send to all registered SSE channels
247+
for connectionId, ch := range c.SSEChannels {
248+
select {
249+
case ch <- event:
250+
// Successfully sent
251+
default:
252+
log.Printf("SSEvent channel is full for connection %s, skipping event", connectionId)
253+
}
228254
}
255+
256+
return nil
257+
}
258+
259+
func (c *ClientImpl) SendAsyncInitiation() error {
260+
log.Printf("send async initiation\n")
261+
return c.SendSSEvent(ssEvent{Event: "asyncinitiation", Data: nil})
229262
}
230263

231264
func makeNullRendered() *rpctypes.RenderedElem {

tsunami/engine/serverhandlers.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,13 @@ func (h *httpHandlers) handleSSE(w http.ResponseWriter, r *http.Request) {
340340
return
341341
}
342342

343+
// Generate unique connection ID for this SSE connection
344+
connectionId := fmt.Sprintf("%s-%d", clientId, time.Now().UnixNano())
345+
346+
// Register SSE channel for this connection
347+
eventCh := h.Client.RegisterSSEChannel(connectionId)
348+
defer h.Client.UnregisterSSEChannel(connectionId)
349+
343350
// Set SSE headers
344351
setNoCacheHeaders(w)
345352
w.Header().Set("Content-Type", "text/event-stream")
@@ -366,7 +373,7 @@ func (h *httpHandlers) handleSSE(w http.ResponseWriter, r *http.Request) {
366373
// Send keepalive comment
367374
fmt.Fprintf(w, ": keepalive\n\n")
368375
rc.Flush()
369-
case event := <-h.Client.SSEventCh:
376+
case event := <-eventCh:
370377
if event.Event == "" {
371378
break
372379
}

tsunami/frontend/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,12 +27,12 @@
2727
"tailwind-merge": "^3.3.1"
2828
},
2929
"devDependencies": {
30-
"@tailwindcss/cli": "^4.1.12",
30+
"@tailwindcss/cli": "^4.1.13",
3131
"@tailwindcss/vite": "^4.0.17",
3232
"@types/react": "^19",
3333
"@types/react-dom": "^19",
3434
"@vitejs/plugin-react-swc": "^4.0.1",
35-
"tailwindcss": "^4.1.12",
35+
"tailwindcss": "^4.1.13",
3636
"typescript": "^5.9.2",
3737
"vite": "^6.3.6"
3838
}

tsunami/frontend/src/tailwind.css

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,51 @@
6060
--ansi-brightcyan: #b7b8cb;
6161
--ansi-brightwhite: #f0f0f0;
6262
}
63+
64+
/* Disable overscroll behavior */
65+
html, body {
66+
overscroll-behavior: none;
67+
overscroll-behavior-x: none;
68+
overscroll-behavior-y: none;
69+
}
70+
71+
/* Custom dark theme scrollbar */
72+
::-webkit-scrollbar {
73+
width: 6px !important;
74+
height: 6px !important;
75+
}
76+
77+
::-webkit-scrollbar-track {
78+
background: rgba(0, 0, 0, 0.1);
79+
width: 6px !important;
80+
}
81+
82+
::-webkit-scrollbar-thumb {
83+
background: rgba(255, 255, 255, 0.15);
84+
border-radius: 3px;
85+
width: 6px !important;
86+
}
87+
88+
::-webkit-scrollbar-thumb:hover {
89+
background: rgba(255, 255, 255, 0.25);
90+
width: 6px !important;
91+
}
92+
93+
::-webkit-scrollbar-corner {
94+
background: rgba(0, 0, 0, 0.1);
95+
}
96+
97+
/* Force consistent scrollbar width across all states */
98+
::-webkit-scrollbar:horizontal {
99+
height: 6px !important;
100+
}
101+
102+
::-webkit-scrollbar:vertical {
103+
width: 6px !important;
104+
}
105+
106+
/* Firefox scrollbar styling */
107+
* {
108+
scrollbar-width: thin;
109+
scrollbar-color: rgba(255, 255, 255, 0.15) rgba(0, 0, 0, 0.1);
110+
}

0 commit comments

Comments
 (0)