diff --git a/Taskfile.yml b/Taskfile.yml index 8b7cf332ab..5b3e520434 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -536,6 +536,7 @@ tasks: - mkdir -p scaffold - cp ../templates/package.json.tmpl scaffold/package.json - cd scaffold && npm install + - mv scaffold/node_modules scaffold/nm - cp -r dist scaffold/ - mkdir -p scaffold/dist/tw - cp ../templates/app-main.go.tmpl scaffold/app-main.go @@ -556,6 +557,7 @@ tasks: - powershell New-Item -ItemType Directory -Force -Path scaffold - powershell Copy-Item -Path ../templates/package.json.tmpl -Destination scaffold/package.json - powershell -Command "Set-Location scaffold; npm install" + - powershell Move-Item -Path scaffold/node_modules -Destination scaffold/nm - powershell Copy-Item -Recurse -Force -Path dist -Destination scaffold/ - powershell New-Item -ItemType Directory -Force -Path scaffold/dist/tw - powershell Copy-Item -Path ../templates/app-main.go.tmpl -Destination scaffold/app-main.go diff --git a/docs/docs/releasenotes.mdx b/docs/docs/releasenotes.mdx index a992b3621a..eb51b43aa1 100644 --- a/docs/docs/releasenotes.mdx +++ b/docs/docs/releasenotes.mdx @@ -12,6 +12,7 @@ Patch release with Wave AI model upgrade, new secret management features, and im **Wave AI Updates:** - **GPT-5.1 Model** - Upgraded to use OpenAI's GPT-5.1 model for improved responses +- **Thinking Mode Toggle** - New dropdown to select between Quick, Balanced, and Deep thinking modes for optimal response quality vs speed - [bugfix] Fixed path mismatch issue when restoring AI write file backups **New Features:** diff --git a/electron-builder.config.cjs b/electron-builder.config.cjs index a92d50961d..d49f2da616 100644 --- a/electron-builder.config.cjs +++ b/electron-builder.config.cjs @@ -22,7 +22,7 @@ const config = { { from: "./dist", to: "./dist", - filter: ["**/*", "!bin/*", "bin/wavesrv.${arch}*", "bin/wsh*"], + filter: ["**/*", "!bin/*", "bin/wavesrv.${arch}*", "bin/wsh*", "!tsunamiscaffold/**/*"], }, { from: ".", @@ -31,13 +31,18 @@ const config = { }, "!node_modules", // We don't need electron-builder to package in Node modules as Vite has already bundled any code that our program is using. ], + extraResources: [ + { + from: "dist/tsunamiscaffold", + to: "tsunamiscaffold", + }, + ], directories: { output: "make", }, asarUnpack: [ "dist/bin/**/*", // wavesrv and wsh binaries "dist/schema/**/*", // schema files for Monaco editor - "dist/tsunamiscaffold/**/*", // tsunami scaffold files ], mac: { target: [ diff --git a/emain/emain-menu.ts b/emain/emain-menu.ts index 347e4900fd..36efa8ec65 100644 --- a/emain/emain-menu.ts +++ b/emain/emain-menu.ts @@ -110,7 +110,7 @@ function makeEditMenu(): Electron.MenuItemConstructorOptions[] { ]; } -function makeFileMenu(numWaveWindows: number, callbacks: AppMenuCallbacks): Electron.MenuItemConstructorOptions[] { +function makeFileMenu(numWaveWindows: number, callbacks: AppMenuCallbacks, fullConfig: FullConfigType): Electron.MenuItemConstructorOptions[] { const fileMenu: Electron.MenuItemConstructorOptions[] = [ { label: "New Window", @@ -125,7 +125,8 @@ function makeFileMenu(numWaveWindows: number, callbacks: AppMenuCallbacks): Elec }, }, ]; - if (isDev) { + const featureWaveAppBuilder = fullConfig?.settings?.["feature:waveappbuilder"]; + if (isDev || featureWaveAppBuilder) { fileMenu.splice(1, 0, { label: "New WaveApp Builder Window", accelerator: unamePlatform === "darwin" ? "Command+Shift+B" : "Alt+Shift+B", @@ -310,18 +311,19 @@ function makeViewMenu( async function makeFullAppMenu(callbacks: AppMenuCallbacks, workspaceOrBuilderId?: string): Promise { const numWaveWindows = getAllWaveWindows().length; const webContents = workspaceOrBuilderId && getWebContentsByWorkspaceOrBuilderId(workspaceOrBuilderId); - const fileMenu = makeFileMenu(numWaveWindows, callbacks); const appMenuItems = makeAppMenuItems(webContents); const editMenu = makeEditMenu(); const isBuilderWindowFocused = focusedBuilderWindow != null; let fullscreenOnLaunch = false; + let fullConfig: FullConfigType = null; try { - const fullConfig = await RpcApi.GetFullConfigCommand(ElectronWshClient); + fullConfig = await RpcApi.GetFullConfigCommand(ElectronWshClient); fullscreenOnLaunch = fullConfig?.settings["window:fullscreenonlaunch"]; } catch (e) { - console.error("Error fetching fullscreen launch config:", e); + console.error("Error fetching config:", e); } + const fileMenu = makeFileMenu(numWaveWindows, callbacks, fullConfig); const viewMenu = makeViewMenu(webContents, callbacks, isBuilderWindowFocused, fullscreenOnLaunch); let workspaceMenu: Electron.MenuItemConstructorOptions[] = null; try { diff --git a/emain/emain-platform.ts b/emain/emain-platform.ts index 86b49a07e2..32320e4eb4 100644 --- a/emain/emain-platform.ts +++ b/emain/emain-platform.ts @@ -149,6 +149,7 @@ function getWaveDataDir(): string { } function getElectronAppBasePath(): string { + // import.meta.dirname in dev points to waveterm/dist/main return path.dirname(import.meta.dirname); } @@ -156,6 +157,14 @@ function getElectronAppUnpackedBasePath(): string { return getElectronAppBasePath().replace("app.asar", "app.asar.unpacked"); } +function getElectronAppResourcesPath(): string { + if (isDev) { + // import.meta.dirname in dev points to waveterm/dist/main + return path.dirname(import.meta.dirname); + } + return process.resourcesPath; +} + const wavesrvBinName = `wavesrv.${unameArch}`; function getWaveSrvPath(): string { @@ -261,6 +270,7 @@ export { callWithOriginalXdgCurrentDesktop, callWithOriginalXdgCurrentDesktopAsync, getElectronAppBasePath, + getElectronAppResourcesPath, getElectronAppUnpackedBasePath, getWaveConfigDir, getWaveDataDir, diff --git a/emain/emain-util.ts b/emain/emain-util.ts index 89ef975a50..712aeb52a5 100644 --- a/emain/emain-util.ts +++ b/emain/emain-util.ts @@ -5,6 +5,7 @@ import * as electron from "electron"; import { getWebServerEndpoint } from "../frontend/util/endpoints"; export const WaveAppPathVarName = "WAVETERM_APP_PATH"; +export const WaveAppResourcesPathVarName = "WAVETERM_RESOURCES_PATH"; export const WaveAppElectronExecPath = "WAVETERM_ELECTRONEXECPATH"; export function getElectronExecPath(): string { diff --git a/emain/emain-wavesrv.ts b/emain/emain-wavesrv.ts index 18f8c2eb18..8e9f176a55 100644 --- a/emain/emain-wavesrv.ts +++ b/emain/emain-wavesrv.ts @@ -8,6 +8,7 @@ import { WebServerEndpointVarName, WSServerEndpointVarName } from "../frontend/u import { AuthKey, WaveAuthKeyEnv } from "./authkey"; import { setForceQuit } from "./emain-activity"; import { + getElectronAppResourcesPath, getElectronAppUnpackedBasePath, getWaveConfigDir, getWaveDataDir, @@ -17,7 +18,12 @@ import { WaveConfigHomeVarName, WaveDataHomeVarName, } from "./emain-platform"; -import { getElectronExecPath, WaveAppElectronExecPath, WaveAppPathVarName } from "./emain-util"; +import { + getElectronExecPath, + WaveAppElectronExecPath, + WaveAppPathVarName, + WaveAppResourcesPathVarName, +} from "./emain-util"; import { updater } from "./updater"; let isWaveSrvDead = false; @@ -59,6 +65,7 @@ export function runWaveSrv(handleWSEvent: (evtMsg: WSEventType) => void): Promis envCopy["XDG_CURRENT_DESKTOP"] = xdgCurrentDesktop; } envCopy[WaveAppPathVarName] = getElectronAppUnpackedBasePath(); + envCopy[WaveAppResourcesPathVarName] = getElectronAppResourcesPath(); envCopy[WaveAppElectronExecPath] = getElectronExecPath(); envCopy[WaveAuthKeyEnv] = AuthKey; envCopy[WaveDataHomeVarName] = getWaveDataDir(); diff --git a/frontend/app/view/tsunami/tsunami.tsx b/frontend/app/view/tsunami/tsunami.tsx index f550484289..84e5706e5b 100644 --- a/frontend/app/view/tsunami/tsunami.tsx +++ b/frontend/app/view/tsunami/tsunami.tsx @@ -62,14 +62,8 @@ class TsunamiViewModel extends WebViewModel { oref: WOS.makeORef("block", blockId), }); initialRTInfo.then((rtInfo) => { - if (rtInfo) { - const meta: AppMeta = { - title: rtInfo["tsunami:title"], - shortdesc: rtInfo["tsunami:shortdesc"], - icon: rtInfo["tsunami:icon"], - iconcolor: rtInfo["tsunami:iconcolor"], - }; - globalStore.set(this.appMeta, meta); + if (rtInfo && rtInfo["tsunami:appmeta"]) { + globalStore.set(this.appMeta, rtInfo["tsunami:appmeta"]); } }); this.appMetaUnsubFn = waveEventSubscribe({ diff --git a/frontend/app/workspace/widgets.tsx b/frontend/app/workspace/widgets.tsx index 0390614a71..100c9f6869 100644 --- a/frontend/app/workspace/widgets.tsx +++ b/frontend/app/workspace/widgets.tsx @@ -229,6 +229,7 @@ const Widgets = memo(() => { magnified: true, }; const showHelp = fullConfig?.settings?.["widget:showhelp"] ?? true; + const featureWaveAppBuilder = fullConfig?.settings?.["feature:waveappbuilder"] ?? false; const widgetsMap = fullConfig?.widgets ?? {}; const filteredWidgets = hasCustomAIPresets ? widgetsMap @@ -342,9 +343,9 @@ const Widgets = memo(() => { ))}
- {isDev() || showHelp ? ( + {isDev() || featureWaveAppBuilder || showHelp ? (
- {isDev() ? ( + {isDev() || featureWaveAppBuilder ? (
{ ))}
- {isDev() ? ( + {isDev() || featureWaveAppBuilder ? (
{
) : null}
- {isDev() && appsButtonRef.current && ( + {(isDev() || featureWaveAppBuilder) && appsButtonRef.current && ( setIsAppsOpen(false)} diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index a66a25bb2a..0842b047f4 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -914,10 +914,7 @@ declare global { // waveobj.ObjRTInfo type ObjRTInfo = { - "tsunami:title"?: string; - "tsunami:shortdesc"?: string; - "tsunami:icon"?: string; - "tsunami:iconcolor"?: string; + "tsunami:appmeta"?: AppMeta; "tsunami:schemas"?: any; "shell:hascurcwd"?: boolean; "shell:state"?: string; @@ -1030,6 +1027,7 @@ declare global { "app:dismissarchitecturewarning"?: boolean; "app:defaultnewblock"?: string; "app:showoverlayblocknums"?: boolean; + "feature:waveappbuilder"?: boolean; "ai:*"?: boolean; "ai:preset"?: string; "ai:apitype"?: string; diff --git a/package-lock.json b/package-lock.json index ec70442dd7..6a62724f7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "waveterm", - "version": "0.12.2", + "version": "0.12.3-beta.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "waveterm", - "version": "0.12.2", + "version": "0.12.3-beta.1", "hasInstallScript": true, "license": "Apache-2.0", "workspaces": [ diff --git a/pkg/aiusechat/tools_tsunami.go b/pkg/aiusechat/tools_tsunami.go index 239b0155a5..1aeebd3fe1 100644 --- a/pkg/aiusechat/tools_tsunami.go +++ b/pkg/aiusechat/tools_tsunami.go @@ -13,10 +13,23 @@ import ( "github.com/wavetermdev/waveterm/pkg/aiusechat/uctypes" "github.com/wavetermdev/waveterm/pkg/blockcontroller" + "github.com/wavetermdev/waveterm/pkg/util/utilfn" "github.com/wavetermdev/waveterm/pkg/waveobj" + "github.com/wavetermdev/waveterm/pkg/wshrpc" "github.com/wavetermdev/waveterm/pkg/wstore" ) +func getTsunamiShortDesc(rtInfo *waveobj.ObjRTInfo) string { + if rtInfo == nil || rtInfo.TsunamiAppMeta == nil { + return "" + } + var appMeta wshrpc.AppMeta + if err := utilfn.ReUnmarshal(&appMeta, rtInfo.TsunamiAppMeta); err == nil && appMeta.ShortDesc != "" { + return appMeta.ShortDesc + } + return "" +} + func handleTsunamiBlockDesc(block *waveobj.Block) string { status := blockcontroller.GetBlockControllerRuntimeStatus(block.OID) if status == nil || status.ShellProcStatus != blockcontroller.Status_Running { @@ -25,8 +38,8 @@ func handleTsunamiBlockDesc(block *waveobj.Block) string { blockORef := waveobj.MakeORef(waveobj.OType_Block, block.OID) rtInfo := wstore.GetRTInfo(blockORef) - if rtInfo != nil && rtInfo.TsunamiShortDesc != "" { - return fmt.Sprintf("tsunami widget - %s", rtInfo.TsunamiShortDesc) + if shortDesc := getTsunamiShortDesc(rtInfo); shortDesc != "" { + return fmt.Sprintf("tsunami widget - %s", shortDesc) } return "tsunami widget - unknown description" } @@ -111,8 +124,8 @@ func GetTsunamiGetDataToolDefinition(block *waveobj.Block, rtInfo *waveobj.ObjRT toolName := fmt.Sprintf("tsunami_getdata_%s", blockIdPrefix) desc := "tsunami widget" - if rtInfo != nil && rtInfo.TsunamiShortDesc != "" { - desc = rtInfo.TsunamiShortDesc + if shortDesc := getTsunamiShortDesc(rtInfo); shortDesc != "" { + desc = shortDesc } return &uctypes.ToolDefinition{ @@ -136,8 +149,8 @@ func GetTsunamiGetConfigToolDefinition(block *waveobj.Block, rtInfo *waveobj.Obj toolName := fmt.Sprintf("tsunami_getconfig_%s", blockIdPrefix) desc := "tsunami widget" - if rtInfo != nil && rtInfo.TsunamiShortDesc != "" { - desc = rtInfo.TsunamiShortDesc + if shortDesc := getTsunamiShortDesc(rtInfo); shortDesc != "" { + desc = shortDesc } return &uctypes.ToolDefinition{ @@ -174,8 +187,8 @@ func GetTsunamiSetConfigToolDefinition(block *waveobj.Block, rtInfo *waveobj.Obj } desc := "tsunami widget" - if rtInfo != nil && rtInfo.TsunamiShortDesc != "" { - desc = rtInfo.TsunamiShortDesc + if shortDesc := getTsunamiShortDesc(rtInfo); shortDesc != "" { + desc = shortDesc } return &uctypes.ToolDefinition{ diff --git a/pkg/blockcontroller/tsunamicontroller.go b/pkg/blockcontroller/tsunamicontroller.go index 41daaf7601..5b775745b5 100644 --- a/pkg/blockcontroller/tsunamicontroller.go +++ b/pkg/blockcontroller/tsunamicontroller.go @@ -5,18 +5,15 @@ package blockcontroller import ( "context" - "encoding/json" "fmt" "io" "log" - "net/http" "os" "os/exec" "path/filepath" "runtime" "sync" "syscall" - "time" "github.com/wavetermdev/waveterm/pkg/tsunamiutil" "github.com/wavetermdev/waveterm/pkg/utilds" @@ -51,37 +48,31 @@ type TsunamiController struct { port int } - -func (c *TsunamiController) fetchAndSetSchemas(port int) { - url := fmt.Sprintf("http://localhost:%d/api/schemas", port) - client := &http.Client{ - Timeout: 10 * time.Second, - } - - resp, err := client.Get(url) +func (c *TsunamiController) setManifestMetadata(appId string) { + manifest, err := waveappstore.ReadAppManifest(appId) if err != nil { - log.Printf("TsunamiController: failed to fetch schemas from %s: %v", url, err) - return - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - log.Printf("TsunamiController: received non-200 status %d from %s", resp.StatusCode, url) - return - } - - var schemas any - if err := json.NewDecoder(resp.Body).Decode(&schemas); err != nil { - log.Printf("TsunamiController: failed to decode schemas response: %v", err) return } blockRef := waveobj.MakeORef(waveobj.OType_Block, c.blockId) - wstore.SetRTInfo(blockRef, map[string]any{ - "tsunami:schemas": schemas, + rtInfo := make(map[string]any) + rtInfo["tsunami:appmeta"] = manifest.AppMeta + if manifest.ConfigSchema != nil || manifest.DataSchema != nil { + schemas := make(map[string]any) + if manifest.ConfigSchema != nil { + schemas["config"] = manifest.ConfigSchema + } + if manifest.DataSchema != nil { + schemas["data"] = manifest.DataSchema + } + rtInfo["tsunami:schemas"] = schemas + } + wstore.SetRTInfo(blockRef, rtInfo) + wps.Broker.Publish(wps.WaveEvent{ + Event: wps.Event_TsunamiUpdateMeta, + Scopes: []string{waveobj.MakeORef(waveobj.OType_Block, c.blockId).String()}, + Data: manifest.AppMeta, }) - - log.Printf("TsunamiController: successfully fetched and cached schemas for block %s", c.blockId) } func (c *TsunamiController) clearSchemas() { @@ -92,7 +83,6 @@ func (c *TsunamiController) clearSchemas() { log.Printf("TsunamiController: cleared schemas for block %s", c.blockId) } - func isBuildCacheUpToDate(appPath string) (bool, error) { appName := build.GetAppName(appPath) @@ -136,7 +126,7 @@ func (c *TsunamiController) Start(ctx context.Context, blockMeta waveobj.MetaMap appPath := blockMeta.GetString(waveobj.MetaKey_TsunamiAppPath, "") appId := blockMeta.GetString(waveobj.MetaKey_TsunamiAppId, "") - + if appPath == "" { if appId == "" { return fmt.Errorf("tsunami:apppath or tsunami:appid is required") @@ -157,32 +147,8 @@ func (c *TsunamiController) Start(ctx context.Context, blockMeta waveobj.MetaMap } } - // Read and set app metadata from manifest if appId is available if appId != "" { - if manifest, err := waveappstore.ReadAppManifest(appId); err == nil { - blockRef := waveobj.MakeORef(waveobj.OType_Block, c.blockId) - rtInfo := make(map[string]any) - if manifest.AppMeta.Title != "" { - rtInfo["tsunami:title"] = manifest.AppMeta.Title - } - if manifest.AppMeta.ShortDesc != "" { - rtInfo["tsunami:shortdesc"] = manifest.AppMeta.ShortDesc - } - if manifest.AppMeta.Icon != "" { - rtInfo["tsunami:icon"] = manifest.AppMeta.Icon - } - if manifest.AppMeta.IconColor != "" { - rtInfo["tsunami:iconcolor"] = manifest.AppMeta.IconColor - } - if len(rtInfo) > 0 { - wstore.SetRTInfo(blockRef, rtInfo) - wps.Broker.Publish(wps.WaveEvent{ - Event: wps.Event_TsunamiUpdateMeta, - Scopes: []string{waveobj.MakeORef(waveobj.OType_Block, c.blockId).String()}, - Data: manifest.AppMeta, - }) - } - } + c.setManifestMetadata(appId) } appName := build.GetAppName(appPath) @@ -220,6 +186,7 @@ func (c *TsunamiController) Start(ctx context.Context, blockMeta waveobj.MetaMap err = build.TsunamiBuild(opts) if err != nil { log.Printf("TsunamiController build error for block %s: %v", c.blockId, err) + log.Printf("BuildOpts %#v\n", opts) return fmt.Errorf("failed to build tsunami app: %w", err) } } @@ -248,11 +215,6 @@ func (c *TsunamiController) Start(ctx context.Context, blockMeta waveobj.MetaMap }) go c.sendStatusUpdate() - // Asynchronously fetch schemas after port is detected - go func() { - c.fetchAndSetSchemas(tsunamiProc.Port) - }() - // Monitor process completion go func() { <-tsunamiProc.WaitCh diff --git a/pkg/waveapputil/waveapputil.go b/pkg/waveapputil/waveapputil.go index 241420a154..9480f12b45 100644 --- a/pkg/waveapputil/waveapputil.go +++ b/pkg/waveapputil/waveapputil.go @@ -22,7 +22,7 @@ func GetTsunamiScaffoldPath() string { settings := wconfig.GetWatcher().GetFullConfig().Settings scaffoldPath := settings.TsunamiScaffoldPath if scaffoldPath == "" { - scaffoldPath = filepath.Join(wavebase.GetWaveAppPath(), "tsunamiscaffold") + scaffoldPath = filepath.Join(wavebase.GetWaveAppResourcesPath(), "tsunamiscaffold") } return scaffoldPath } @@ -76,4 +76,4 @@ func FormatGoCode(contents []byte) []byte { } return formattedOutput -} \ No newline at end of file +} diff --git a/pkg/wavebase/wavebase.go b/pkg/wavebase/wavebase.go index 72f346874b..625cea5c6e 100644 --- a/pkg/wavebase/wavebase.go +++ b/pkg/wavebase/wavebase.go @@ -29,6 +29,7 @@ const ( WaveConfigHomeEnvVar = "WAVETERM_CONFIG_HOME" WaveDataHomeEnvVar = "WAVETERM_DATA_HOME" WaveAppPathVarName = "WAVETERM_APP_PATH" + WaveAppResourcesPathVarName = "WAVETERM_RESOURCES_PATH" WaveAppElectronExecPathVarName = "WAVETERM_ELECTRONEXECPATH" WaveDevVarName = "WAVETERM_DEV" WaveDevViteVarName = "WAVETERM_DEV_VITE" @@ -50,6 +51,7 @@ const NeedJwtConst = "NEED-JWT" var ConfigHome_VarCache string // caches WAVETERM_CONFIG_HOME var DataHome_VarCache string // caches WAVETERM_DATA_HOME var AppPath_VarCache string // caches WAVETERM_APP_PATH +var AppResourcesPath_VarCache string // caches WAVETERM_RESOURCES_PATH var AppElectronExecPath_VarCache string // caches WAVETERM_ELECTRONEXECPATH var Dev_VarCache string // caches WAVETERM_DEV @@ -98,6 +100,8 @@ func CacheAndRemoveEnvVars() error { os.Unsetenv(WaveDataHomeEnvVar) AppPath_VarCache = os.Getenv(WaveAppPathVarName) os.Unsetenv(WaveAppPathVarName) + AppResourcesPath_VarCache = os.Getenv(WaveAppResourcesPathVarName) + os.Unsetenv(WaveAppResourcesPathVarName) AppElectronExecPath_VarCache = os.Getenv(WaveAppElectronExecPathVarName) os.Unsetenv(WaveAppElectronExecPathVarName) Dev_VarCache = os.Getenv(WaveDevVarName) @@ -114,6 +118,10 @@ func GetWaveAppPath() string { return AppPath_VarCache } +func GetWaveAppResourcesPath() string { + return AppResourcesPath_VarCache +} + func GetWaveDataDir() string { return DataHome_VarCache } diff --git a/pkg/waveobj/objrtinfo.go b/pkg/waveobj/objrtinfo.go index fcbef2331b..ff88f7090c 100644 --- a/pkg/waveobj/objrtinfo.go +++ b/pkg/waveobj/objrtinfo.go @@ -4,11 +4,8 @@ package waveobj type ObjRTInfo struct { - TsunamiTitle string `json:"tsunami:title,omitempty"` - TsunamiShortDesc string `json:"tsunami:shortdesc,omitempty"` - TsunamiIcon string `json:"tsunami:icon,omitempty"` - TsunamiIconColor string `json:"tsunami:iconcolor,omitempty"` - TsunamiSchemas any `json:"tsunami:schemas,omitempty"` + TsunamiAppMeta any `json:"tsunami:appmeta,omitempty" tstype:"AppMeta"` + TsunamiSchemas any `json:"tsunami:schemas,omitempty"` ShellHasCurCwd bool `json:"shell:hascurcwd,omitempty"` ShellState string `json:"shell:state,omitempty"` diff --git a/pkg/wconfig/metaconsts.go b/pkg/wconfig/metaconsts.go index 7fc733e07d..40a40ddf89 100644 --- a/pkg/wconfig/metaconsts.go +++ b/pkg/wconfig/metaconsts.go @@ -12,6 +12,8 @@ const ( ConfigKey_AppDefaultNewBlock = "app:defaultnewblock" ConfigKey_AppShowOverlayBlockNums = "app:showoverlayblocknums" + ConfigKey_FeatureWaveAppBuilder = "feature:waveappbuilder" + ConfigKey_AiClear = "ai:*" ConfigKey_AiPreset = "ai:preset" ConfigKey_AiApiType = "ai:apitype" diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go index 9e70e4ab1d..67aae0a982 100644 --- a/pkg/wconfig/settingsconfig.go +++ b/pkg/wconfig/settingsconfig.go @@ -58,6 +58,8 @@ type SettingsType struct { AppDefaultNewBlock string `json:"app:defaultnewblock,omitempty"` AppShowOverlayBlockNums *bool `json:"app:showoverlayblocknums,omitempty"` + FeatureWaveAppBuilder bool `json:"feature:waveappbuilder,omitempty"` + AiClear bool `json:"ai:*,omitempty"` AiPreset string `json:"ai:preset,omitempty"` AiApiType string `json:"ai:apitype,omitempty"` diff --git a/schema/settings.json b/schema/settings.json index bca7b292e3..e14a53e643 100644 --- a/schema/settings.json +++ b/schema/settings.json @@ -20,6 +20,9 @@ "app:showoverlayblocknums": { "type": "boolean" }, + "feature:waveappbuilder": { + "type": "boolean" + }, "ai:*": { "type": "boolean" }, diff --git a/tsunami/build/build.go b/tsunami/build/build.go index 7d01b75c29..ab1e8e2031 100644 --- a/tsunami/build/build.go +++ b/tsunami/build/build.go @@ -109,6 +109,7 @@ func GetAppName(appPath string) string { type BuildEnv struct { GoVersion string + GoPath string TempDir string cleanupOnce *sync.Once } @@ -329,11 +330,12 @@ func verifyEnvironment(verbose bool, opts BuildOpts) (*BuildEnv, error) { return &BuildEnv{ GoVersion: goVersion, + GoPath: result.GoPath, cleanupOnce: &sync.Once{}, }, nil } -func createGoMod(tempDir, appNS, appName, goVersion string, opts BuildOpts, verbose bool) error { +func createGoMod(tempDir, appNS, appName string, buildEnv *BuildEnv, opts BuildOpts, verbose bool) error { oc := opts.OutputCapture if appNS == "" { appNS = "app" @@ -372,7 +374,7 @@ func createGoMod(tempDir, appNS, appName, goVersion string, opts BuildOpts, verb return fmt.Errorf("failed to add module statement: %w", err) } - if err := modFile.AddGoStmt(goVersion); err != nil { + if err := modFile.AddGoStmt(buildEnv.GoVersion); err != nil { return fmt.Errorf("failed to add go version: %w", err) } @@ -412,7 +414,7 @@ func createGoMod(tempDir, appNS, appName, goVersion string, opts BuildOpts, verb } // Run go mod tidy to clean up dependencies - tidyCmd := exec.Command("go", "mod", "tidy") + tidyCmd := exec.Command(buildEnv.GoPath, "mod", "tidy") tidyCmd.Dir = tempDir if verbose { @@ -428,6 +430,7 @@ func createGoMod(tempDir, appNS, appName, goVersion string, opts BuildOpts, verb } if err := tidyCmd.Run(); err != nil { + oc.Flush() return fmt.Errorf("go mod tidy failed (see output for errors)") } @@ -498,13 +501,13 @@ func verifyScaffoldFs(fsys fs.FS) error { return fmt.Errorf("package.json check failed: %w", err) } - // Check for node_modules directory - if err := isDirOrNotFoundFS(fsys, "node_modules"); err != nil { - return fmt.Errorf("node_modules directory check failed: %w", err) + // Check for nm directory + if err := isDirOrNotFoundFS(fsys, "nm"); err != nil { + return fmt.Errorf("nm (node_modules) directory check failed: %w", err) } - info, err = fs.Stat(fsys, "node_modules") + info, err = fs.Stat(fsys, "nm") if err != nil || !info.IsDir() { - return fmt.Errorf("node_modules directory must exist in scaffold") + return fmt.Errorf("nm (node_modules) directory must exist in scaffold") } return nil @@ -621,6 +624,7 @@ func TsunamiBuildInternal(opts BuildOpts) (*BuildEnv, error) { buildEnv.TempDir = tempDir oc.Printf("Building tsunami app from %s", opts.AppPath) + oc.Printf("[debug] using scaffold path %s", opts.ScaffoldPath) if opts.Verbose || opts.KeepTemp { oc.Printf("[debug] Temp dir: %s", tempDir) @@ -652,7 +656,7 @@ func TsunamiBuildInternal(opts BuildOpts) (*BuildEnv, error) { // Create go.mod file appName := GetAppName(opts.AppPath) - if err := createGoMod(tempDir, opts.AppNS, appName, buildEnv.GoVersion, opts, opts.Verbose); err != nil { + if err := createGoMod(tempDir, opts.AppNS, appName, buildEnv, opts, opts.Verbose); err != nil { return buildEnv, err } @@ -662,7 +666,7 @@ func TsunamiBuildInternal(opts BuildOpts) (*BuildEnv, error) { } // Build the Go application - outputPath, err := runGoBuild(tempDir, opts) + outputPath, err := runGoBuild(tempDir, buildEnv, opts) if err != nil { return buildEnv, err } @@ -743,7 +747,7 @@ func moveFilesBack(tempDir, originalDir string, verbose bool, oc *OutputCapture) return nil } -func runGoBuild(tempDir string, opts BuildOpts) (string, error) { +func runGoBuild(tempDir string, buildEnv *BuildEnv, opts BuildOpts) (string, error) { oc := opts.OutputCapture var outputPath string var absOutputPath string @@ -775,7 +779,7 @@ func runGoBuild(tempDir string, opts BuildOpts) (string, error) { // Build command with explicit go files args := append([]string{"build", "-o", outputPath}, ".") - buildCmd := exec.Command("go", args...) + buildCmd := exec.Command(buildEnv.GoPath, args...) buildCmd.Dir = tempDir if oc != nil || opts.Verbose { @@ -838,7 +842,8 @@ func generateAppTailwindCss(tempDir string, verbose bool, opts BuildOpts) error oc := opts.OutputCapture // tailwind.css is already in tempDir from scaffold copy tailwindOutput := filepath.Join(tempDir, "static", "tw.css") - tailwindCmd := exec.Command(opts.getNodePath(), "node_modules/@tailwindcss/cli/dist/index.mjs", + tailwindCmd := exec.Command(opts.getNodePath(), "--preserve-symlinks-main", "--preserve-symlinks", + "node_modules/@tailwindcss/cli/dist/index.mjs", "-i", "./tailwind.css", "-o", tailwindOutput) tailwindCmd.Dir = tempDir @@ -1074,33 +1079,36 @@ func ParseTsunamiPort(line string) int { func copyScaffoldFS(scaffoldFS fs.FS, destDir string, verbose bool, oc *OutputCapture) (int, error) { fileCount := 0 - // Handle node_modules directory - prefer symlink if possible, otherwise copy - if _, err := fs.Stat(scaffoldFS, "node_modules"); err == nil { + // Handle nm (node_modules) directory - prefer symlink if possible, otherwise copy + if _, err := fs.Stat(scaffoldFS, "nm"); err == nil { destPath := filepath.Join(destDir, "node_modules") // Try to create symlink if we have DirFS + symlinked := false if dirFS, ok := scaffoldFS.(DirFS); ok { - srcPath := dirFS.JoinOS("node_modules") - if err := os.Symlink(srcPath, destPath); err != nil { - return 0, fmt.Errorf("failed to create symlink for node_modules: %w", err) - } - if verbose { - oc.Printf("[debug] Symlinked node_modules directory") + srcPath := dirFS.JoinOS("nm") + if err := os.Symlink(srcPath, destPath); err == nil { + if verbose { + oc.Printf("[debug] Symlinked nm to node_modules directory") + } + fileCount++ + symlinked = true } - fileCount++ - } else { - // Fallback to recursive copy - dirCount, err := copyDirFromFS(scaffoldFS, "node_modules", destPath, false) + } + + // Fallback to recursive copy if symlink failed or not attempted + if !symlinked { + dirCount, err := copyDirFromFS(scaffoldFS, "nm", destPath, false) if err != nil { - return 0, fmt.Errorf("failed to copy node_modules directory: %w", err) + return 0, fmt.Errorf("failed to copy nm (node_modules) directory: %w", err) } if verbose { - oc.Printf("Copied node_modules directory (%d files)", dirCount) + oc.Printf("Copied nm to node_modules directory (%d files)", dirCount) } fileCount += dirCount } } else if !os.IsNotExist(err) { - return 0, fmt.Errorf("error checking node_modules: %w", err) + return 0, fmt.Errorf("error checking nm (node_modules): %w", err) } // Copy package files instead of symlinking