Skip to content

Commit ba573c4

Browse files
authored
tsunami builder 3 (checkpoint) (#2487)
Got preview hooked up, log output hooked up, environment tab hooked up... the controller, restarting the apps. it is actually working! still lots to do and lots of hard coding to fix, but it is coming together...
1 parent 58e000b commit ba573c4

30 files changed

+1670
-318
lines changed

.roo/rules/rules.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ It has a TypeScript/React frontend and a Go backend. They talk together over `ws
3434
- Use all lowercase filenames (except where case is actually important like Taskfile.yml)
3535
- Import the "cn" function from "@/util/util" to do classname / clsx class merge (it uses twMerge underneath)
3636
- For element variants use class-variance-authority
37+
- Do NOT create private fields in classes (they are impossible to inspect)
3738
- **Component Practices**:
3839
- Make sure to add cursor-pointer to buttons/links and clickable items
3940
- NEVER use cursor-help (it looks terrible)

cmd/server/main-server.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,11 @@ func main() {
353353
log.Printf("error ensuring wave presets dir: %v\n", err)
354354
return
355355
}
356+
err = wavebase.EnsureWaveCachesDir()
357+
if err != nil {
358+
log.Printf("error ensuring wave caches dir: %v\n", err)
359+
return
360+
}
356361
waveLock, err := wavebase.AcquireWaveLock()
357362
if err != nil {
358363
log.Printf("error acquiring wave lock (another instance of Wave is likely running): %v\n", err)

emain/emain-builder.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ export async function createBuilderWindow(appId: string): Promise<BuilderWindowT
103103
if (focusedBuilderWindow === typedBuilderWindow) {
104104
focusedBuilderWindow = null;
105105
}
106+
RpcApi.DeleteBuilderCommand(ElectronWshClient, builderId, { noresponse: true });
106107
setTimeout(() => globalEvents.emit("windows-updated"), 50);
107108
});
108109

frontend/app/aipanel/waveai-model.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import * as WOS from "@/app/store/wos";
1414
import { RpcApi } from "@/app/store/wshclientapi";
1515
import { TabRpcClient } from "@/app/store/wshrpcutil";
1616
import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model";
17-
import { BuilderFocusManager } from "@/builder/store/builderFocusManager";
17+
import { BuilderFocusManager } from "@/builder/store/builder-focusmanager";
1818
import { getWebServerEndpoint } from "@/util/endpoints";
1919
import { ChatStatus } from "ai";
2020
import * as jotai from "jotai";

frontend/app/store/wshclientapi.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@ class RpcApiType {
122122
return client.wshRpcCall("deleteblock", data, opts);
123123
}
124124

125+
// command "deletebuilder" [call]
126+
DeleteBuilderCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<void> {
127+
return client.wshRpcCall("deletebuilder", data, opts);
128+
}
129+
125130
// command "deletesubblock" [call]
126131
DeleteSubBlockCommand(client: WshClient, data: CommandDeleteBlockData, opts?: RpcOpts): Promise<void> {
127132
return client.wshRpcCall("deletesubblock", data, opts);
@@ -262,6 +267,16 @@ class RpcApiType {
262267
return client.wshRpcCall("focuswindow", data, opts);
263268
}
264269

270+
// command "getbuilderoutput" [call]
271+
GetBuilderOutputCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<string[]> {
272+
return client.wshRpcCall("getbuilderoutput", data, opts);
273+
}
274+
275+
// command "getbuilderstatus" [call]
276+
GetBuilderStatusCommand(client: WshClient, data: string, opts?: RpcOpts): Promise<BuilderStatusData> {
277+
return client.wshRpcCall("getbuilderstatus", data, opts);
278+
}
279+
265280
// command "getfullconfig" [call]
266281
GetFullConfigCommand(client: WshClient, opts?: RpcOpts): Promise<FullConfigType> {
267282
return client.wshRpcCall("getfullconfig", null, opts);
@@ -462,6 +477,11 @@ class RpcApiType {
462477
return client.wshRpcCall("setview", data, opts);
463478
}
464479

480+
// command "startbuilder" [call]
481+
StartBuilderCommand(client: WshClient, data: CommandStartBuilderData, opts?: RpcOpts): Promise<void> {
482+
return client.wshRpcCall("startbuilder", data, opts);
483+
}
484+
465485
// command "streamcpudata" [responsestream]
466486
StreamCpuDataCommand(client: WshClient, data: CpuDataRequest, opts?: RpcOpts): AsyncGenerator<TimeSeriesData, void, boolean> {
467487
return client.wshRpcStream("streamcpudata", data, opts);

frontend/builder/builder-apppanel.tsx

Lines changed: 145 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,115 @@
11
// Copyright 2025, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
import { BuilderAppPanelModel, type TabType } from "@/builder/store/builderAppPanelModel";
5-
import { BuilderFocusManager } from "@/builder/store/builderFocusManager";
4+
import { BuilderAppPanelModel, type TabType } from "@/builder/store/builder-apppanel-model";
5+
import { BuilderFocusManager } from "@/builder/store/builder-focusmanager";
66
import { BuilderCodeTab } from "@/builder/tabs/builder-codetab";
7+
import { BuilderEnvTab } from "@/builder/tabs/builder-envtab";
78
import { BuilderFilesTab } from "@/builder/tabs/builder-filestab";
89
import { BuilderPreviewTab } from "@/builder/tabs/builder-previewtab";
910
import { builderAppHasSelection } from "@/builder/utils/builder-focus-utils";
1011
import { ErrorBoundary } from "@/element/errorboundary";
1112
import { atoms } from "@/store/global";
1213
import { cn } from "@/util/util";
1314
import { useAtomValue } from "jotai";
14-
import { memo, useCallback, useRef } from "react";
15+
import { memo, useCallback, useEffect, useRef } from "react";
16+
17+
const StatusDot = memo(() => {
18+
const model = BuilderAppPanelModel.getInstance();
19+
const builderStatus = useAtomValue(model.builderStatusAtom);
20+
21+
const getStatusDotColor = (status: string | null | undefined): string => {
22+
if (!status) return "bg-gray-500";
23+
switch (status) {
24+
case "init":
25+
case "stopped":
26+
return "bg-gray-500";
27+
case "building":
28+
return "bg-warning";
29+
case "running":
30+
return "bg-success";
31+
case "error":
32+
return "bg-error";
33+
default:
34+
return "bg-gray-500";
35+
}
36+
};
37+
38+
const statusDotColor = getStatusDotColor(builderStatus?.status);
39+
40+
return <span className={cn("w-2 h-2 rounded-full", statusDotColor)} />;
41+
});
42+
43+
StatusDot.displayName = "StatusDot";
1544

1645
type TabButtonProps = {
1746
label: string;
1847
tabType: TabType;
1948
isActive: boolean;
2049
isAppFocused: boolean;
2150
onClick: () => void;
51+
showStatusDot?: boolean;
2252
};
2353

24-
const TabButton = memo(({ label, tabType, isActive, isAppFocused, onClick }: TabButtonProps) => {
54+
const TabButton = memo(({ label, tabType, isActive, isAppFocused, onClick, showStatusDot }: TabButtonProps) => {
2555
return (
2656
<button
2757
className={cn(
2858
"px-4 py-2 text-sm font-medium transition-colors cursor-pointer",
2959
isActive
30-
? `text-main-text border-b-2 ${isAppFocused ? "border-accent" : "border-gray-500"}`
31-
: "text-gray-500 hover:text-secondary border-b-2 border-transparent"
60+
? `text-primary border-b-2 ${isAppFocused ? "border-accent" : "border-gray-500"}`
61+
: "text-secondary hover:text-primary border-b-2 border-transparent"
3262
)}
3363
onClick={onClick}
3464
>
35-
{label}
65+
<span className="flex items-center gap-2">
66+
{showStatusDot && <StatusDot />}
67+
{label}
68+
</span>
3669
</button>
3770
);
3871
});
3972

4073
TabButton.displayName = "TabButton";
4174

75+
const ErrorStrip = memo(() => {
76+
const model = BuilderAppPanelModel.getInstance();
77+
const errorMsg = useAtomValue(model.errorAtom);
78+
79+
if (!errorMsg) return null;
80+
return (
81+
<div className="shrink-0 bg-error/10 border-b border-error/30 px-4 py-2 flex items-center justify-between gap-4">
82+
<div className="flex items-center gap-3 flex-1 min-w-0">
83+
<i className="fa fa-triangle-exclamation text-error text-sm" />
84+
<span className="text-error text-sm flex-1 truncate">{errorMsg}</span>
85+
</div>
86+
<button
87+
onClick={() => model.clearError()}
88+
className="shrink-0 text-error hover:text-error/80 transition-colors cursor-pointer"
89+
aria-label="Close error"
90+
>
91+
<i className="fa fa-xmark-large text-sm" />
92+
</button>
93+
</div>
94+
);
95+
});
96+
97+
ErrorStrip.displayName = "ErrorStrip";
98+
4299
const BuilderAppPanel = memo(() => {
43100
const model = BuilderAppPanelModel.getInstance();
44101
const focusElemRef = useRef<HTMLInputElement>(null);
45102
const activeTab = useAtomValue(model.activeTab);
46103
const focusType = useAtomValue(BuilderFocusManager.getInstance().focusType);
47104
const isAppFocused = focusType === "app";
48105
const saveNeeded = useAtomValue(model.saveNeededAtom);
106+
const envSaveNeeded = useAtomValue(model.envVarsDirtyAtom);
49107
const builderAppId = useAtomValue(atoms.builderAppId);
108+
const builderId = useAtomValue(atoms.builderId);
109+
110+
useEffect(() => {
111+
model.initialize();
112+
}, []);
50113

51114
if (focusElemRef.current) {
52115
model.setFocusElemRef(focusElemRef.current);
@@ -58,44 +121,54 @@ const BuilderAppPanel = memo(() => {
58121
model.giveFocus();
59122
};
60123

61-
const handleFocusCapture = useCallback(
62-
(event: React.FocusEvent) => {
63-
BuilderFocusManager.getInstance().setAppFocused();
64-
},
65-
[]
66-
);
67-
68-
const handlePanelClick = useCallback((e: React.MouseEvent) => {
69-
const target = e.target as HTMLElement;
70-
const isInteractive = target.closest('button, a, input, textarea, select, [role="button"], [tabindex]');
124+
const handleFocusCapture = useCallback((event: React.FocusEvent) => {
125+
BuilderFocusManager.getInstance().setAppFocused();
126+
}, []);
71127

72-
if (isInteractive) {
73-
return;
74-
}
128+
const handlePanelClick = useCallback(
129+
(e: React.MouseEvent) => {
130+
const target = e.target as HTMLElement;
131+
const isInteractive = target.closest('button, a, input, textarea, select, [role="button"], [tabindex]');
75132

76-
const hasSelection = builderAppHasSelection();
77-
if (hasSelection) {
78-
BuilderFocusManager.getInstance().setAppFocused();
79-
return;
80-
}
133+
if (isInteractive) {
134+
return;
135+
}
81136

82-
setTimeout(() => {
83-
if (!builderAppHasSelection()) {
137+
const hasSelection = builderAppHasSelection();
138+
if (hasSelection) {
84139
BuilderFocusManager.getInstance().setAppFocused();
85-
model.giveFocus();
140+
return;
86141
}
87-
}, 0);
88-
}, [model]);
142+
143+
setTimeout(() => {
144+
if (!builderAppHasSelection()) {
145+
BuilderFocusManager.getInstance().setAppFocused();
146+
model.giveFocus();
147+
}
148+
}, 0);
149+
},
150+
[model]
151+
);
89152

90153
const handleSave = useCallback(() => {
91154
if (builderAppId) {
92155
model.saveAppFile(builderAppId);
93156
}
94157
}, [builderAppId, model]);
95158

159+
const handleEnvSave = useCallback(() => {
160+
if (builderId) {
161+
model.saveEnvVars(builderId);
162+
}
163+
}, [builderId, model]);
164+
165+
const handleRestart = useCallback(() => {
166+
model.restartBuilder();
167+
}, [model]);
168+
96169
return (
97170
<div
98-
className="w-full h-full flex flex-col border-b border-border"
171+
className="w-full h-full flex flex-col border-b-3 border-border shadow-[0_2px_4px_rgba(0,0,0,0.1)]"
99172
data-builder-app-panel="true"
100173
onClick={handlePanelClick}
101174
onFocusCapture={handleFocusCapture}
@@ -118,6 +191,7 @@ const BuilderAppPanel = memo(() => {
118191
isActive={activeTab === "preview"}
119192
isAppFocused={isAppFocused}
120193
onClick={() => handleTabClick("preview")}
194+
showStatusDot={true}
121195
/>
122196
<TabButton
123197
label="Code"
@@ -126,14 +200,31 @@ const BuilderAppPanel = memo(() => {
126200
isAppFocused={isAppFocused}
127201
onClick={() => handleTabClick("code")}
128202
/>
203+
{false && (
204+
<TabButton
205+
label="Static Files"
206+
tabType="files"
207+
isActive={activeTab === "files"}
208+
isAppFocused={isAppFocused}
209+
onClick={() => handleTabClick("files")}
210+
/>
211+
)}
129212
<TabButton
130-
label="Static Files"
131-
tabType="files"
132-
isActive={activeTab === "files"}
213+
label="Env"
214+
tabType="env"
215+
isActive={activeTab === "env"}
133216
isAppFocused={isAppFocused}
134-
onClick={() => handleTabClick("files")}
217+
onClick={() => handleTabClick("env")}
135218
/>
136219
</div>
220+
{activeTab === "preview" && (
221+
<button
222+
className="mr-4 px-3 py-1 text-sm font-medium rounded transition-colors bg-accent/80 text-white hover:bg-accent cursor-pointer"
223+
onClick={handleRestart}
224+
>
225+
Restart App
226+
</button>
227+
)}
137228
{activeTab === "code" && (
138229
<button
139230
className={cn(
@@ -147,8 +238,22 @@ const BuilderAppPanel = memo(() => {
147238
Save
148239
</button>
149240
)}
241+
{activeTab === "env" && (
242+
<button
243+
className={cn(
244+
"mr-4 px-3 py-1 text-sm font-medium rounded transition-colors",
245+
envSaveNeeded
246+
? "bg-accent text-white hover:opacity-80 cursor-pointer"
247+
: "bg-gray-600 text-gray-400 cursor-default"
248+
)}
249+
onClick={envSaveNeeded ? handleEnvSave : undefined}
250+
>
251+
Save
252+
</button>
253+
)}
150254
</div>
151255
</div>
256+
<ErrorStrip />
152257
<div className="flex-1 overflow-auto py-1">
153258
<div className="w-full h-full" style={{ display: activeTab === "preview" ? "block" : "none" }}>
154259
<ErrorBoundary>
@@ -165,6 +270,11 @@ const BuilderAppPanel = memo(() => {
165270
<BuilderFilesTab />
166271
</ErrorBoundary>
167272
</div>
273+
<div className="w-full h-full" style={{ display: activeTab === "env" ? "block" : "none" }}>
274+
<ErrorBoundary>
275+
<BuilderEnvTab />
276+
</ErrorBoundary>
277+
</div>
168278
</div>
169279
</div>
170280
);

0 commit comments

Comments
 (0)