From 2285fc026ebe81be960ecfb1de55c2b0d6841983 Mon Sep 17 00:00:00 2001 From: Kirill Trofimov Date: Sun, 10 May 2026 15:06:42 +0300 Subject: [PATCH 01/10] Removed web features completely --- Taskfile.yml | 157 -- aiprompts/tsunami-builder.md | 261 ---- cmd/server/main-server.go | 1 - cmd/wsh/cmd/wshcmd-blocks.go | 28 +- cmd/wsh/cmd/wshcmd-connserver.go | 20 - cmd/wsh/cmd/wshcmd-view.go | 78 +- cmd/wsh/cmd/wshcmd-web.go | 141 -- electron-builder.config.cjs | 9 +- electron.vite.config.ts | 1 - emain/emain-builder.ts | 135 -- emain/emain-ipc.ts | 164 -- emain/emain-menu.ts | 62 +- emain/emain-platform.ts | 3 - emain/emain-tabview.ts | 1 - emain/emain.ts | 14 +- emain/preload-webview.ts | 39 - emain/preload.ts | 22 +- frontend/app/aipanel/ai-utils.ts | 7 +- frontend/app/aipanel/aimode.tsx | 1 - frontend/app/aipanel/aipanel-contextmenu.ts | 120 +- frontend/app/aipanel/aipanel.tsx | 35 +- frontend/app/aipanel/aipanelheader.tsx | 57 +- frontend/app/aipanel/aipanelinput.tsx | 2 - frontend/app/aipanel/waveai-model.tsx | 77 +- frontend/app/block/blockregistry.ts | 9 - frontend/app/block/blockutil.tsx | 12 - frontend/app/modals/modalregistry.tsx | 6 - frontend/app/store/contextmenu.test.ts | 8 +- frontend/app/store/contextmenu.ts | 10 +- frontend/app/store/global-atoms.ts | 6 +- frontend/app/store/global-model.ts | 2 - frontend/app/store/keymodel.ts | 21 +- frontend/app/store/windowtype.ts | 12 +- frontend/app/store/wshclientapi.ts | 108 -- frontend/app/store/wshrouter.ts | 6 +- frontend/app/view/helpview/helpview.tsx | 134 -- frontend/app/view/sysinfo/sysinfo.tsx | 572 ------- frontend/app/view/tsunami/tsunami.tsx | 311 ---- frontend/app/view/webview/webview.scss | 51 - frontend/app/view/webview/webview.test.tsx | 59 - frontend/app/view/webview/webview.tsx | 1135 -------------- frontend/app/view/webview/webviewenv.ts | 25 - frontend/app/workspace/widgets.tsx | 196 +-- .../app/workspace/workspace-layout-model.ts | 9 +- frontend/builder/app-selection-modal.tsx | 234 --- frontend/builder/builder-app.tsx | 77 - frontend/builder/builder-apppanel.tsx | 400 ----- frontend/builder/builder-buildpanel.tsx | 153 -- frontend/builder/builder-workspace.tsx | 136 -- .../builder/store/builder-apppanel-model.ts | 339 ----- .../builder/store/builder-buildpanel-model.ts | 73 - .../builder/store/builder-focusmanager.ts | 34 - frontend/builder/tabs/builder-codetab.tsx | 107 -- .../builder/tabs/builder-configdatatab.tsx | 226 --- frontend/builder/tabs/builder-filestab.tsx | 399 ----- frontend/builder/tabs/builder-previewtab.tsx | 227 --- frontend/builder/tabs/builder-secrettab.tsx | 292 ---- frontend/builder/utils/builder-focus-utils.ts | 59 - frontend/preview/mock/defaultconfig.ts | 2 + frontend/preview/mock/mockwaveenv.ts | 26 +- frontend/preview/mock/preview-electron-api.ts | 10 +- .../previews/processviewer.preview.tsx | 30 +- .../preview/previews/sysinfo.preview-util.ts | 61 - .../preview/previews/sysinfo.preview.test.ts | 31 - frontend/preview/previews/sysinfo.preview.tsx | 60 - frontend/preview/previews/web.preview.tsx | 27 - frontend/preview/previews/widgets.preview.tsx | 49 +- frontend/types/custom.d.ts | 18 - frontend/types/gotypes.d.ts | 186 +-- frontend/types/waveevent.d.ts | 10 - frontend/wave.ts | 75 +- go.mod | 3 - package-lock.json | 912 +---------- .../anthropic/anthropic-convertmessage.go | 31 - pkg/aiusechat/gemini/gemini-backend.go | 9 - pkg/aiusechat/openai/openai-convertmessage.go | 9 - .../openaichat/openaichat-convertmessage.go | 3 - pkg/aiusechat/tools.go | 48 +- pkg/aiusechat/tools_builder.go | 306 ---- pkg/aiusechat/tools_tsunami.go | 203 --- pkg/aiusechat/tools_web.go | 108 -- pkg/aiusechat/uctypes/uctypes.go | 26 +- pkg/aiusechat/usechat-mode.go | 37 - pkg/aiusechat/usechat.go | 98 +- pkg/blockcontroller/blockcontroller.go | 14 +- pkg/blockcontroller/tsunamicontroller.go | 448 ------ pkg/buildercontroller/buildercontroller.go | 602 -------- pkg/tsgen/tsgenevent.go | 33 +- pkg/tsunamiutil/tsunamiutil.go | 32 - pkg/waveappstore/waveappstore.go | 842 ----------- pkg/waveapputil/waveapputil.go | 79 - pkg/waveobj/metaconsts.go | 12 - pkg/waveobj/objrtinfo.go | 7 - pkg/waveobj/wtype.go | 2 - pkg/waveobj/wtypemeta.go | 16 +- pkg/wconfig/defaultconfig/widgets.json | 20 - pkg/wconfig/metaconsts.go | 8 - pkg/wconfig/settingsconfig.go | 52 +- pkg/wcore/layout.go | 11 - pkg/wps/wpstypes.go | 38 +- pkg/wshrpc/wshclient/wshclient.go | 108 -- pkg/wshrpc/wshremote/sysinfo.go | 72 - pkg/wshrpc/wshrpctypes.go | 4 - pkg/wshrpc/wshrpctypes_builder.go | 169 --- pkg/wshrpc/wshserver/wshserver.go | 262 ---- pkg/wshutil/wshrouter.go | 5 - schema/settings.json | 18 - tsunami/.gitignore | 2 - tsunami/app/atom.go | 139 -- tsunami/app/defaultclient.go | 276 ---- tsunami/app/hooks.go | 333 ---- tsunami/build/build-ast.go | 94 -- tsunami/build/build.go | 1228 --------------- tsunami/build/buildutil.go | 386 ----- tsunami/cmd/main-tsunami.go | 179 --- tsunami/demo/.gitignore | 1 - tsunami/demo/cpuchart/app.go | 380 ----- tsunami/demo/cpuchart/go.mod | 23 - tsunami/demo/cpuchart/go.sum | 36 - tsunami/demo/cpuchart/static/tw.css | 1276 ---------------- tsunami/demo/githubaction/app.go | 449 ------ tsunami/demo/githubaction/go.mod | 12 - tsunami/demo/githubaction/go.sum | 4 - tsunami/demo/githubaction/static/tw.css | 1333 ----------------- tsunami/demo/modaltest/app.go | 158 -- tsunami/demo/modaltest/go.mod | 12 - tsunami/demo/modaltest/go.sum | 4 - tsunami/demo/modaltest/static/tw.css | 1308 ---------------- tsunami/demo/pomodoro/app.go | 212 --- tsunami/demo/pomodoro/go.mod | 12 - tsunami/demo/pomodoro/go.sum | 4 - tsunami/demo/pomodoro/static/tw.css | 1240 --------------- tsunami/demo/recharts/app.go | 470 ------ tsunami/demo/recharts/go.mod | 12 - tsunami/demo/recharts/go.sum | 4 - tsunami/demo/recharts/static/tw.css | 1308 ---------------- tsunami/demo/tabletest/app.go | 120 -- tsunami/demo/tabletest/go.mod | 12 - tsunami/demo/tabletest/go.sum | 4 - tsunami/demo/tabletest/static/tw.css | 1292 ---------------- tsunami/demo/todo/app.go | 184 --- tsunami/demo/todo/go.mod | 12 - tsunami/demo/todo/go.sum | 4 - tsunami/demo/todo/static/tw.css | 1165 -------------- tsunami/demo/todo/style.css | 68 - tsunami/demo/tsunamiconfig/app.go | 377 ----- tsunami/demo/tsunamiconfig/go.mod | 12 - tsunami/demo/tsunamiconfig/go.sum | 4 - tsunami/demo/tsunamiconfig/static/tw.css | 1283 ---------------- tsunami/engine/asyncnotify.go | 162 -- tsunami/engine/atomimpl.go | 109 -- tsunami/engine/clientimpl.go | 537 ------- tsunami/engine/comp.go | 52 - tsunami/engine/errcomponent.go | 22 - tsunami/engine/globalctx.go | 159 -- tsunami/engine/hooks.go | 158 -- tsunami/engine/render.go | 324 ---- tsunami/engine/render.md | 262 ---- tsunami/engine/rootelem.go | 461 ------ tsunami/engine/schema.go | 304 ---- tsunami/engine/serverhandlers.go | 661 -------- tsunami/frontend/.gitignore | 1 - tsunami/frontend/index.html | 13 - tsunami/frontend/package.json | 39 - tsunami/frontend/public/fonts/hack-bold.woff2 | Bin 108008 -> 0 bytes .../frontend/public/fonts/hack-regular.woff2 | Bin 106236 -> 0 bytes .../public/fonts/inter-variable.woff2 | Bin 345588 -> 0 bytes tsunami/frontend/public/wave-logo-256.png | Bin 9793 -> 0 bytes tsunami/frontend/src/app.tsx | 18 - tsunami/frontend/src/element/markdown.tsx | 87 -- tsunami/frontend/src/element/modals.tsx | 97 -- tsunami/frontend/src/element/tsunamiterm.tsx | 157 -- tsunami/frontend/src/input.tsx | 107 -- tsunami/frontend/src/main.tsx | 13 - tsunami/frontend/src/model/model-utils.ts | 145 -- tsunami/frontend/src/model/tsunami-model.tsx | 821 ---------- tsunami/frontend/src/recharts/recharts.tsx | 168 --- tsunami/frontend/src/tailwind.css | 113 -- tsunami/frontend/src/types/custom.d.ts | 16 - tsunami/frontend/src/types/vdom.d.ts | 244 --- tsunami/frontend/src/util/base64.ts | 37 - tsunami/frontend/src/util/clientid.ts | 34 - tsunami/frontend/src/util/keyutil.ts | 354 ----- tsunami/frontend/src/util/platformutil.ts | 18 - tsunami/frontend/src/vdom.tsx | 451 ------ tsunami/frontend/tsconfig.json | 24 - tsunami/frontend/vite.config.ts | 34 - tsunami/go.mod | 15 - tsunami/go.sum | 16 - tsunami/rpctypes/diff.go | 544 ------- tsunami/rpctypes/diff_test.go | 141 -- tsunami/rpctypes/protocoltypes.go | 214 --- tsunami/templates/app-init.go.tmpl | 7 - tsunami/templates/app-main.go.tmpl | 29 - tsunami/templates/empty-gomod.tmpl | 3 - tsunami/templates/gitignore.tmpl | 3 - tsunami/templates/package.json.tmpl | 16 - tsunami/templates/tailwind.css | 60 - tsunami/tsunamibase/tsunamibase.go | 3 - tsunami/ui/table.go | 545 ------- tsunami/util/compare.go | 240 --- tsunami/util/marshal.go | 121 -- tsunami/util/streamtolines.go | 114 -- tsunami/util/util.go | 295 ---- tsunami/vdom/vdom.go | 192 --- tsunami/vdom/vdom_test.go | 100 -- tsunami/vdom/vdom_types.go | 204 --- 207 files changed, 280 insertions(+), 36331 deletions(-) delete mode 100644 aiprompts/tsunami-builder.md delete mode 100644 cmd/wsh/cmd/wshcmd-web.go delete mode 100644 emain/emain-builder.ts delete mode 100644 emain/preload-webview.ts delete mode 100644 frontend/app/view/helpview/helpview.tsx delete mode 100644 frontend/app/view/sysinfo/sysinfo.tsx delete mode 100644 frontend/app/view/tsunami/tsunami.tsx delete mode 100644 frontend/app/view/webview/webview.scss delete mode 100644 frontend/app/view/webview/webview.test.tsx delete mode 100644 frontend/app/view/webview/webview.tsx delete mode 100644 frontend/app/view/webview/webviewenv.ts delete mode 100644 frontend/builder/app-selection-modal.tsx delete mode 100644 frontend/builder/builder-app.tsx delete mode 100644 frontend/builder/builder-apppanel.tsx delete mode 100644 frontend/builder/builder-buildpanel.tsx delete mode 100644 frontend/builder/builder-workspace.tsx delete mode 100644 frontend/builder/store/builder-apppanel-model.ts delete mode 100644 frontend/builder/store/builder-buildpanel-model.ts delete mode 100644 frontend/builder/store/builder-focusmanager.ts delete mode 100644 frontend/builder/tabs/builder-codetab.tsx delete mode 100644 frontend/builder/tabs/builder-configdatatab.tsx delete mode 100644 frontend/builder/tabs/builder-filestab.tsx delete mode 100644 frontend/builder/tabs/builder-previewtab.tsx delete mode 100644 frontend/builder/tabs/builder-secrettab.tsx delete mode 100644 frontend/builder/utils/builder-focus-utils.ts delete mode 100644 frontend/preview/previews/sysinfo.preview-util.ts delete mode 100644 frontend/preview/previews/sysinfo.preview.test.ts delete mode 100644 frontend/preview/previews/sysinfo.preview.tsx delete mode 100644 frontend/preview/previews/web.preview.tsx delete mode 100644 pkg/aiusechat/tools_builder.go delete mode 100644 pkg/aiusechat/tools_tsunami.go delete mode 100644 pkg/aiusechat/tools_web.go delete mode 100644 pkg/blockcontroller/tsunamicontroller.go delete mode 100644 pkg/buildercontroller/buildercontroller.go delete mode 100644 pkg/tsunamiutil/tsunamiutil.go delete mode 100644 pkg/waveappstore/waveappstore.go delete mode 100644 pkg/waveapputil/waveapputil.go delete mode 100644 pkg/wshrpc/wshremote/sysinfo.go delete mode 100644 pkg/wshrpc/wshrpctypes_builder.go delete mode 100644 tsunami/.gitignore delete mode 100644 tsunami/app/atom.go delete mode 100644 tsunami/app/defaultclient.go delete mode 100644 tsunami/app/hooks.go delete mode 100644 tsunami/build/build-ast.go delete mode 100644 tsunami/build/build.go delete mode 100644 tsunami/build/buildutil.go delete mode 100644 tsunami/cmd/main-tsunami.go delete mode 100644 tsunami/demo/.gitignore delete mode 100644 tsunami/demo/cpuchart/app.go delete mode 100644 tsunami/demo/cpuchart/go.mod delete mode 100644 tsunami/demo/cpuchart/go.sum delete mode 100644 tsunami/demo/cpuchart/static/tw.css delete mode 100644 tsunami/demo/githubaction/app.go delete mode 100644 tsunami/demo/githubaction/go.mod delete mode 100644 tsunami/demo/githubaction/go.sum delete mode 100644 tsunami/demo/githubaction/static/tw.css delete mode 100644 tsunami/demo/modaltest/app.go delete mode 100644 tsunami/demo/modaltest/go.mod delete mode 100644 tsunami/demo/modaltest/go.sum delete mode 100644 tsunami/demo/modaltest/static/tw.css delete mode 100644 tsunami/demo/pomodoro/app.go delete mode 100644 tsunami/demo/pomodoro/go.mod delete mode 100644 tsunami/demo/pomodoro/go.sum delete mode 100644 tsunami/demo/pomodoro/static/tw.css delete mode 100644 tsunami/demo/recharts/app.go delete mode 100644 tsunami/demo/recharts/go.mod delete mode 100644 tsunami/demo/recharts/go.sum delete mode 100644 tsunami/demo/recharts/static/tw.css delete mode 100644 tsunami/demo/tabletest/app.go delete mode 100644 tsunami/demo/tabletest/go.mod delete mode 100644 tsunami/demo/tabletest/go.sum delete mode 100644 tsunami/demo/tabletest/static/tw.css delete mode 100644 tsunami/demo/todo/app.go delete mode 100644 tsunami/demo/todo/go.mod delete mode 100644 tsunami/demo/todo/go.sum delete mode 100644 tsunami/demo/todo/static/tw.css delete mode 100644 tsunami/demo/todo/style.css delete mode 100644 tsunami/demo/tsunamiconfig/app.go delete mode 100644 tsunami/demo/tsunamiconfig/go.mod delete mode 100644 tsunami/demo/tsunamiconfig/go.sum delete mode 100644 tsunami/demo/tsunamiconfig/static/tw.css delete mode 100644 tsunami/engine/asyncnotify.go delete mode 100644 tsunami/engine/atomimpl.go delete mode 100644 tsunami/engine/clientimpl.go delete mode 100644 tsunami/engine/comp.go delete mode 100644 tsunami/engine/errcomponent.go delete mode 100644 tsunami/engine/globalctx.go delete mode 100644 tsunami/engine/hooks.go delete mode 100644 tsunami/engine/render.go delete mode 100644 tsunami/engine/render.md delete mode 100644 tsunami/engine/rootelem.go delete mode 100644 tsunami/engine/schema.go delete mode 100644 tsunami/engine/serverhandlers.go delete mode 100644 tsunami/frontend/.gitignore delete mode 100644 tsunami/frontend/index.html delete mode 100644 tsunami/frontend/package.json delete mode 100644 tsunami/frontend/public/fonts/hack-bold.woff2 delete mode 100644 tsunami/frontend/public/fonts/hack-regular.woff2 delete mode 100644 tsunami/frontend/public/fonts/inter-variable.woff2 delete mode 100644 tsunami/frontend/public/wave-logo-256.png delete mode 100644 tsunami/frontend/src/app.tsx delete mode 100644 tsunami/frontend/src/element/markdown.tsx delete mode 100644 tsunami/frontend/src/element/modals.tsx delete mode 100644 tsunami/frontend/src/element/tsunamiterm.tsx delete mode 100644 tsunami/frontend/src/input.tsx delete mode 100644 tsunami/frontend/src/main.tsx delete mode 100644 tsunami/frontend/src/model/model-utils.ts delete mode 100644 tsunami/frontend/src/model/tsunami-model.tsx delete mode 100644 tsunami/frontend/src/recharts/recharts.tsx delete mode 100644 tsunami/frontend/src/tailwind.css delete mode 100644 tsunami/frontend/src/types/custom.d.ts delete mode 100644 tsunami/frontend/src/types/vdom.d.ts delete mode 100644 tsunami/frontend/src/util/base64.ts delete mode 100644 tsunami/frontend/src/util/clientid.ts delete mode 100644 tsunami/frontend/src/util/keyutil.ts delete mode 100644 tsunami/frontend/src/util/platformutil.ts delete mode 100644 tsunami/frontend/src/vdom.tsx delete mode 100644 tsunami/frontend/tsconfig.json delete mode 100644 tsunami/frontend/vite.config.ts delete mode 100644 tsunami/go.mod delete mode 100644 tsunami/go.sum delete mode 100644 tsunami/rpctypes/diff.go delete mode 100644 tsunami/rpctypes/diff_test.go delete mode 100644 tsunami/rpctypes/protocoltypes.go delete mode 100644 tsunami/templates/app-init.go.tmpl delete mode 100644 tsunami/templates/app-main.go.tmpl delete mode 100644 tsunami/templates/empty-gomod.tmpl delete mode 100644 tsunami/templates/gitignore.tmpl delete mode 100644 tsunami/templates/package.json.tmpl delete mode 100644 tsunami/templates/tailwind.css delete mode 100644 tsunami/tsunamibase/tsunamibase.go delete mode 100644 tsunami/ui/table.go delete mode 100644 tsunami/util/compare.go delete mode 100644 tsunami/util/marshal.go delete mode 100644 tsunami/util/streamtolines.go delete mode 100644 tsunami/util/util.go delete mode 100644 tsunami/vdom/vdom.go delete mode 100644 tsunami/vdom/vdom_test.go delete mode 100644 tsunami/vdom/vdom_types.go diff --git a/Taskfile.yml b/Taskfile.yml index bf37a83e45..d939902317 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -23,7 +23,6 @@ tasks: deps: - npm:install - build:backend - - build:tsunamiscaffold env: WAVETERM_ENVFILE: "{{.ROOT_DIR}}/.env" WCLOUD_PING_ENDPOINT: "https://ping-dev.waveterm.dev/central" @@ -129,7 +128,6 @@ tasks: - clean - npm:install - build:backend - - build:tsunamiscaffold build:frontend:dev: desc: Build the frontend in development mode. @@ -153,9 +151,6 @@ tasks: - pkg/**/*.go - pkg/**/*.sh - cmd/**/*.go - - tsunami/go.mod - - tsunami/go.sum - - tsunami/**/*.go - package.json build:schema: @@ -185,7 +180,6 @@ tasks: - "pkg/**/*.go" - "pkg/**/*.json" - "pkg/**/*.sh" - - tsunami/**/*.go - package.json generates: - dist/bin/wavesrv.* @@ -214,7 +208,6 @@ tasks: - "pkg/**/*.go" - "pkg/**/*.json" - "pkg/**/*.sh" - - "tsunami/**/*.go" generates: - dist/bin/wavesrv.* @@ -233,7 +226,6 @@ tasks: - "pkg/**/*.go" - "pkg/**/*.json" - "pkg/**/*.sh" - - "tsunami/**/*.go" generates: - dist/bin/wavesrv.x64.exe @@ -345,21 +337,6 @@ tasks: cmd: (CGO_ENABLED=0 GOOS={{.GOOS}} GOARCH={{.GOARCH}} go build -ldflags="-s -w -X main.BuildTime=$({{.DATE}} +'%Y%m%d%H%M') -X main.WaveVersion={{.VERSION}}" -o dist/bin/wsh-{{.VERSION}}-{{.GOOS}}.{{.NORMALIZEDARCH}}{{.EXT}} cmd/wsh/main-wsh.go) internal: true - build:tsunamiscaffold: - desc: Build and copy tsunami scaffold to dist directory. - cmds: - - cmd: "{{.RMRF}} dist/tsunamiscaffold" - ignore_error: true - - task: copyfiles:'tsunami/frontend/scaffold':'dist/tsunamiscaffold' - - cmd: '{{if eq OS "windows"}}powershell -NoProfile -NonInteractive Copy-Item -Path tsunami/templates/empty-gomod.tmpl -Destination dist/tsunamiscaffold/go.mod{{else}}cp tsunami/templates/empty-gomod.tmpl dist/tsunamiscaffold/go.mod{{end}}' - deps: - - tsunami:scaffold - sources: - - "tsunami/frontend/dist/**/*" - - "tsunami/templates/**/*" - generates: - - "dist/tsunamiscaffold/**/*" - generate: desc: Generate Typescript bindings for the Go backend. cmds: @@ -520,140 +497,6 @@ tasks: - 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: npm run build - dir: tsunami/frontend - - tsunami:frontend:devbuild: - desc: Build the tsunami frontend in development mode (with source maps and symbols) - cmd: npm run 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:packagejson: - desc: Create package.json for tsunami scaffold using npm commands - dir: tsunami/frontend/scaffold - cmds: - - cmd: rm -f package.json - platforms: [darwin, linux] - ignore_error: true - - cmd: powershell -NoProfile -NonInteractive -Command "Remove-Item -Force -ErrorAction SilentlyContinue -Path package.json" - platforms: [windows] - ignore_error: true - - npm --no-workspaces init -y --init-license Apache-2.0 - - npm pkg set name=tsunami-scaffold - - npm pkg delete author - - npm pkg set author.name="Command Line Inc" - - npm pkg set author.email="info@commandline.dev" - - npm --no-workspaces install tailwindcss@4.1.13 @tailwindcss/cli@4.1.13 - - tsunami:scaffold:internal: - desc: Internal task to create scaffold directory structure - internal: true - cmds: - - task: tsunami:scaffold:internal:unix - - task: tsunami:scaffold:internal:windows - - tsunami:scaffold:internal:unix: - desc: Internal task to create scaffold directory structure (Unix) - dir: tsunami/frontend - internal: true - platforms: [darwin, linux] - cmds: - - cmd: "{{.RMRF}} scaffold" - ignore_error: true - - 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/*.go.tmpl scaffold/ - - cp ../templates/tailwind.css scaffold/ - - cp ../templates/gitignore.tmpl scaffold/.gitignore - - cp src/element/*.tsx scaffold/dist/tw/ - - cp ../ui/*.go scaffold/dist/tw/ - - cp ../engine/errcomponent.go scaffold/dist/tw/ - - tsunami:scaffold:internal:windows: - desc: Internal task to create scaffold directory structure (Windows) - dir: tsunami/frontend - internal: true - platforms: [windows] - cmds: - - cmd: "{{.RMRF}} scaffold" - ignore_error: true - - powershell -NoProfile -NonInteractive New-Item -ItemType Directory -Force -Path scaffold - - powershell -NoProfile -NonInteractive Copy-Item -Path ../templates/package.json.tmpl -Destination scaffold/package.json - - powershell -NoProfile -NonInteractive -Command "Set-Location scaffold; npm install" - - powershell -NoProfile -NonInteractive Move-Item -Path scaffold/node_modules -Destination scaffold/nm - - powershell -NoProfile -NonInteractive Copy-Item -Recurse -Force -Path dist -Destination scaffold/ - - powershell -NoProfile -NonInteractive New-Item -ItemType Directory -Force -Path scaffold/dist/tw - - powershell -NoProfile -NonInteractive Copy-Item -Path '../templates/*.go.tmpl' -Destination scaffold/ - - powershell -NoProfile -NonInteractive Copy-Item -Path ../templates/tailwind.css -Destination scaffold/ - - powershell -NoProfile -NonInteractive Copy-Item -Path ../templates/gitignore.tmpl -Destination scaffold/.gitignore - - powershell -NoProfile -NonInteractive Copy-Item -Path 'src/element/*.tsx' -Destination scaffold/dist/tw/ - - powershell -NoProfile -NonInteractive Copy-Item -Path '../ui/*.go' -Destination scaffold/dist/tw/ - - powershell -NoProfile -NonInteractive Copy-Item -Path ../engine/errcomponent.go -Destination scaffold/dist/tw/ - - tsunami:build: - desc: Build the tsunami binary. - cmds: - - cmd: rm -f bin/tsunami* - platforms: [darwin, linux] - ignore_error: true - - cmd: powershell -NoProfile -NonInteractive -Command "Remove-Item -Force -ErrorAction SilentlyContinue -Path bin/tsunami*" - platforms: [windows] - 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 diff --git a/aiprompts/tsunami-builder.md b/aiprompts/tsunami-builder.md deleted file mode 100644 index eb84289563..0000000000 --- a/aiprompts/tsunami-builder.md +++ /dev/null @@ -1,261 +0,0 @@ -# Tsunami AI Builder - V1 Architecture - -## Overview - -A split-screen builder for creating Tsunami applications: chat interface on left, tabbed preview/code/files on right. Users describe what they want, AI edits the code iteratively. - -## UI Layout - -### Left Panel - -- **💬 Chat** - Conversation with AI - -### Right Panel - -**Top Section - Tabs:** -- **👁️ Preview** (default) - Live preview of running Tsunami app, updates automatically after successful compilation -- **📝 Code** - Monaco editor for manual edits to app.go -- **📁 Files** - Static assets browser (images, etc) - -**Bottom Section - Build Panel (closable):** -- Shows compilation status and output (like VSCode's terminal panel) -- Displays success messages or errors with line numbers -- Auto-runs after AI edits -- For manual Code tab edits: auto-reruns or user clicks build button -- Can be manually closed/reopened by user - -### Top Bar - -- Current AppTitle (extracted from app.go) -- **Publish** button - Moves draft → published version -- **Revert** button - Copies published → draft (discards draft changes) - -## Version Management - -**Draft mode**: Auto-saved on every edit, persists when builder closes -**Published version**: What runs in main Wave Terminal, only updates on explicit "Publish" - -Flow: - -1. Edit in builder (always editing draft) -2. Click "Publish" when ready (copies draft → published) -3. Continue editing draft OR click "Revert" to abandon changes - -## Context Structure - -Every AI request includes: - -``` -[System Instructions] - - General system prompt - - Full system.md (Tsunami framework guide) - -[Conversation History] - - Recent messages (with prompt caching) - -[Current Context] (injected fresh each turn, removed from previous turns) - - Current app.go content - - Compilation results (success or errors with line numbers) - - Static files listing (e.g., "/static/logo.png") -``` - -**Context cleanup**: Old "current context" blocks are removed from previous messages and replaced with "[OLD CONTEXT REMOVED]" to save tokens. Only the latest app.go + compile results stay in context. - -## AI Tools - -### edit_appgo (str_replace) - -**Primary editing tool** - -- `old_str` - Unique string to find in app.go -- `new_str` - Replacement string -- `description` - What this change does - -**Backend behavior**: - -1. Apply string replacement to app.go -2. Immediately run `go build` -3. Return tool result: - - ✓ Success: "Edit applied, compilation successful" - - ✗ Failure: "Edit applied, compilation failed: [error details]" - -AI can make multiple edits in one response, getting compile feedback after each. - -### create_appgo - -**Bootstrap new apps** - -- `content` - Full app.go file content -- Only used for initial app creation or total rewrites - -Same compilation behavior as str_replace. - -### web_search - -**Look up APIs, docs, examples** - -- Implemented via provider backend (OpenAI/Anthropic) -- AI can research before making edits - -### read_file - -**Read user-provided documentation** - -- `path` - Path to file (e.g., "/docs/api-spec.md") -- User can upload docs/examples for AI to reference - -## User Actions (Not AI Tools) - -### Manage Static Assets - -- Upload via drag & drop into Files tab or file picker -- Delete files from Files tab -- Rename files from Files tab -- Appear in `/static/` directory -- Auto-injected into AI context as available files - -### Share Screenshot - -- User clicks "📷 Share preview with AI" button -- Captures current preview state -- Attaches to user's next message -- Useful for debugging layout/visual issues - -### Manual Code Editing - -- User can switch to Code tab -- Edit app.go directly in Monaco editor -- Changes auto-compile -- AI sees manual edits in next chat turn - -## Compilation Pipeline - -After every code change (AI or user): - -``` -1. Write app.go to disk -2. Run: go build app.go -3. Show build output in build panel -4. If success: - - Start/restart app process - - Update preview iframe - - Show success message in build panel -5. If failure: - - Parse error output (line numbers, messages) - - Show error in build panel (bottom of right side) - - Inject into AI context for next turn -``` - -**Auto-retry**: AI can fix its own compilation errors within the same response (up to 3 attempts). - -## Error Handling - -### Compilation Errors - -Shown in build panel at bottom of right side. - -Format for AI: - -``` -COMPILATION FAILED - -Error at line 45: - 43 | func(props TodoProps) any { - 44 | return vdom.H("div", nil -> 45 | vdom.H("span", nil, "test") - | ^ missing closing parenthesis - 46 | ) - -Message: expected ')', found 'vdom' -``` - -### Runtime Errors - -- Shown in preview tab (not errors panel) -- User can screenshot and report to AI -- Not auto-injected (v1 simplification) - -### Linting (Future) - -- Could add custom Tsunami-specific linting -- Would inject warnings alongside compile results -- Not required for v1 - -## Secrets/Configuration - -Apps can declare secrets using Tsunami's ConfigAtom: - -```go -var apiKeyAtom = app.ConfigAtom("api_key", "", &app.AtomMeta{ - Desc: "OpenAI API Key", - Secret: true, -}) -``` - -Builder detects these and shows input fields in UI for user to fill in. - -## Conversation Limits - -**V1 approach**: No summarization, no smart handling. - -When context limit hit: Show message "You've hit the conversation limit. Click 'Start Fresh' to continue editing this app in a new chat." - -Starting fresh uses current app.go as the beginning state. - -## Token Optimization - -- System.md + early messages benefit from prompt caching -- Only pay per-turn for: current app.go + new messages -- Old context blocks removed to prevent bloat -- Estimated: 10-20k tokens per turn (very manageable) - -## Example Flow - -``` -User: "Create a counter app" -AI: [calls create_appgo with full counter app] -Backend: ✓ Compiled successfully -Preview: Shows counter app - -User: "Add a reset button" -AI: [calls str_replace to add reset button] -Backend: ✓ Compiled successfully -Preview: Updates with reset button - -User: "Make buttons bigger" -AI: [calls str_replace to update button classes] -Backend: ✓ Compiled successfully -Preview: Updates with larger buttons - -User: [switches to Code tab, tweaks color manually] -Backend: ✓ Compiled successfully -Preview: Updates - -User: "Add a chart showing count over time" -AI: [calls web_search for "go charting library"] -AI: [calls str_replace to add chart] -Backend: ✗ Compilation failed - missing import -AI: [calls str_replace to add import] -Backend: ✓ Compiled successfully -Preview: Shows chart -``` - -## Out of Scope (V1) - -- Version history / snapshots -- Multiple files / project structure -- Collaboration / sharing -- Advanced linting -- Runtime error auto-injection -- Conversation summarization -- Component-specific editing tools - -These can be added in v2+ based on user feedback. - -## Success Criteria - -- User can create functional Tsunami app through chat in <5 minutes -- AI successfully fixes its own compilation errors 80%+ of the time -- Iteration cycle (message → edit → preview) takes <10 seconds -- Users can publish working apps to Wave Terminal -- Draft state persists across sessions diff --git a/cmd/server/main-server.go b/cmd/server/main-server.go index b204643ee8..b735fd3009 100644 --- a/cmd/server/main-server.go +++ b/cmd/server/main-server.go @@ -390,7 +390,6 @@ func createMainWshClient() { sockName := wavebase.GetDomainSocketName() remoteImpl := wshremote.MakeRemoteRpcServerImpl(nil, wshutil.DefaultRouter, wshclient.GetBareRpcClient(), true, localInitialEnv, sockName) localConnWsh := wshutil.MakeWshRpc(wshrpc.RpcContext{Conn: wshrpc.LocalConnName}, remoteImpl, "conn:local") - go wshremote.RunSysInfoLoop(localConnWsh, wshrpc.LocalConnName) wshutil.DefaultRouter.RegisterTrustedLeaf(localConnWsh, wshutil.MakeConnectionRouteId(wshrpc.LocalConnName)) wshfs.RpcClient = localConnWsh wshfs.RpcClientRouteId = wshutil.MakeConnectionRouteId(wshrpc.LocalConnName) diff --git a/cmd/wsh/cmd/wshcmd-blocks.go b/cmd/wsh/cmd/wshcmd-blocks.go index 7e4b935ee3..e499eba812 100644 --- a/cmd/wsh/cmd/wshcmd-blocks.go +++ b/cmd/wsh/cmd/wshcmd-blocks.go @@ -21,7 +21,7 @@ var ( blocksWindowId string // Window ID to filter blocks by blocksWorkspaceId string // Workspace ID to filter blocks by blocksTabId string // Tab ID to filter blocks by - blocksView string // View type to filter blocks by (term, web, etc.) + blocksView string // View type to filter blocks by (term, preview, etc.) blocksJSON bool // Whether to output as JSON blocksTimeout int // Timeout in milliseconds for RPC calls ) @@ -31,7 +31,7 @@ type BlockDetails struct { BlockId string `json:"blockid"` // Unique identifier for the block WorkspaceId string `json:"workspaceid"` // ID of the workspace containing the block TabId string `json:"tabid"` // ID of the tab containing the block - View string `json:"view"` // Canonical view type (term, web, preview, edit, sysinfo, waveai) + View string `json:"view"` // Canonical view type (term, preview, edit, waveai) Meta waveobj.MetaMapType `json:"meta"` // Block metadata including view type } @@ -40,7 +40,7 @@ var blocksListCmd = &cobra.Command{ Use: "list", Aliases: []string{"ls", "get"}, Short: "List blocks in workspaces/windows", - Long: `List blocks with optional filtering by workspace, window, tab, or view type. + Long: `List blocks with optional filtering by workspace, window, tab, or view type. Examples: # List blocks from all workspaces @@ -63,8 +63,8 @@ Examples: # Set a different timeout (in milliseconds) wsh blocks list --timeout=10000`, - RunE: blocksListRun, - PreRunE: preRunSetupRpcClient, + RunE: blocksListRun, + PreRunE: preRunSetupRpcClient, SilenceUsage: true, } @@ -74,7 +74,7 @@ func init() { blocksListCmd.Flags().StringVar(&blocksWindowId, "window", "", "restrict to window id") blocksListCmd.Flags().StringVar(&blocksWorkspaceId, "workspace", "", "restrict to workspace id") blocksListCmd.Flags().StringVar(&blocksTabId, "tab", "", "restrict to specific tab id") - blocksListCmd.Flags().StringVar(&blocksView, "view", "", "restrict to view type (term/terminal, web/browser, preview/edit, sysinfo, waveai)") + blocksListCmd.Flags().StringVar(&blocksView, "view", "", "restrict to view type (term/terminal, preview/edit, waveai)") blocksListCmd.Flags().BoolVar(&blocksJSON, "json", false, "output as JSON") blocksListCmd.Flags().IntVar(&blocksTimeout, "timeout", 5000, "timeout in milliseconds for RPC calls (default: 5000)") @@ -86,9 +86,9 @@ func init() { } blocksCmd := &cobra.Command{ - Use: "blocks", - Short: "Manage blocks", - Long: "Commands for working with blocks", + Use: "blocks", + Short: "Manage blocks", + Long: "Commands for working with blocks", } blocksCmd.AddCommand(blocksListCmd) @@ -100,7 +100,7 @@ func init() { func blocksListRun(cmd *cobra.Command, args []string) error { if v := strings.TrimSpace(blocksView); v != "" { if !isKnownViewFilter(v) { - return fmt.Errorf("unknown --view %q; try one of: term, web, preview, edit, sysinfo, waveai", v) + return fmt.Errorf("unknown --view %q; try one of: term, preview, edit, waveai", v) } } @@ -231,8 +231,6 @@ func blocksListRun(cmd *cobra.Command, args []string) error { switch view { case "preview", "edit": content = b.Meta.GetString(waveobj.MetaKey_File, "") - case "web": - content = b.Meta.GetString(waveobj.MetaKey_Url, "") case "term": content = b.Meta.GetString(waveobj.MetaKey_CmdCwd, "") default: @@ -268,12 +266,8 @@ func matchesViewType(actual, filter string) bool { return strings.EqualFold(actual, "preview") || strings.EqualFold(actual, "edit") case "terminal", "term", "shell", "console": return strings.EqualFold(actual, "term") - case "web", "browser", "url": - return strings.EqualFold(actual, "web") case "ai", "waveai", "assistant": return strings.EqualFold(actual, "waveai") - case "sys", "sysinfo", "system": - return strings.EqualFold(actual, "sysinfo") } return false @@ -283,9 +277,7 @@ func matchesViewType(actual, filter string) bool { func isKnownViewFilter(f string) bool { switch strings.ToLower(strings.TrimSpace(f)) { case "term", "terminal", "shell", "console", - "web", "browser", "url", "preview", "edit", - "sysinfo", "sys", "system", "waveai", "ai", "assistant": return true default: diff --git a/cmd/wsh/cmd/wshcmd-connserver.go b/cmd/wsh/cmd/wshcmd-connserver.go index 1f892a24ce..5f5ac60f6e 100644 --- a/cmd/wsh/cmd/wshcmd-connserver.go +++ b/cmd/wsh/cmd/wshcmd-connserver.go @@ -273,13 +273,6 @@ func serverRunRouter() error { }() runListener(unixListener, router) }() - // run the sysinfo loop - go func() { - defer func() { - panichandler.PanicHandler("serverRunRouter:RunSysInfoLoop", recover()) - }() - wshremote.RunSysInfoLoop(client, connServerConnName) - }() startJobLogCleanup() log.Printf("running server, successfully started") select {} @@ -381,13 +374,6 @@ func serverRunRouterDomainSocket(jwtToken string) error { runListener(unixListener, router) }() - // run the sysinfo loop - go func() { - defer func() { - panichandler.PanicHandler("serverRunRouterDomainSocket:RunSysInfoLoop", recover()) - }() - wshremote.RunSysInfoLoop(client, connServerConnName) - }() startJobLogCleanup() log.Printf("running server (router-domainsocket mode), successfully started") @@ -406,12 +392,6 @@ func serverRunNormal(jwtToken string) error { wshfs.RpcClient = RpcClient wshfs.RpcClientRouteId = RpcClientRouteId WriteStdout("running wsh connserver (%s)\n", RpcContext.Conn) - go func() { - defer func() { - panichandler.PanicHandler("serverRunNormal:RunSysInfoLoop", recover()) - }() - wshremote.RunSysInfoLoop(RpcClient, RpcContext.Conn) - }() startJobLogCleanup() select {} // run forever } diff --git a/cmd/wsh/cmd/wshcmd-view.go b/cmd/wsh/cmd/wshcmd-view.go index 1ba84b516f..473528ed5b 100644 --- a/cmd/wsh/cmd/wshcmd-view.go +++ b/cmd/wsh/cmd/wshcmd-view.go @@ -59,54 +59,42 @@ func viewRun(cmd *cobra.Command, args []string) (rtnErr error) { } fileArg := args[0] conn := RpcContext.Conn - var wshCmd *wshrpc.CommandCreateBlockData if strings.HasPrefix(fileArg, "http://") || strings.HasPrefix(fileArg, "https://") { - wshCmd = &wshrpc.CommandCreateBlockData{ - TabId: tabId, - BlockDef: &waveobj.BlockDef{ - Meta: map[string]any{ - waveobj.MetaKey_View: "web", - waveobj.MetaKey_Url: fileArg, - }, - }, - Magnified: viewMagnified, - Focused: true, - } - } else { - absFile, err := filepath.Abs(fileArg) - if err != nil { - return fmt.Errorf("getting absolute path: %w", err) - } - absParent, err := filepath.Abs(filepath.Dir(fileArg)) - if err != nil { - return fmt.Errorf("getting absolute path of parent dir: %w", err) - } - _, err = os.Stat(absParent) - if err == fs.ErrNotExist { - return fmt.Errorf("parent directory does not exist: %q", absParent) - } - if err != nil { - return fmt.Errorf("getting file info: %w", err) - } - wshCmd = &wshrpc.CommandCreateBlockData{ - TabId: tabId, - BlockDef: &waveobj.BlockDef{ - Meta: map[string]interface{}{ - waveobj.MetaKey_View: "preview", - waveobj.MetaKey_File: absFile, - }, + return fmt.Errorf("URL viewing is not supported; the in-app web browser has been removed") + } + absFile, err := filepath.Abs(fileArg) + if err != nil { + return fmt.Errorf("getting absolute path: %w", err) + } + absParent, err := filepath.Abs(filepath.Dir(fileArg)) + if err != nil { + return fmt.Errorf("getting absolute path of parent dir: %w", err) + } + _, err = os.Stat(absParent) + if err == fs.ErrNotExist { + return fmt.Errorf("parent directory does not exist: %q", absParent) + } + if err != nil { + return fmt.Errorf("getting file info: %w", err) + } + wshCmd := &wshrpc.CommandCreateBlockData{ + TabId: tabId, + BlockDef: &waveobj.BlockDef{ + Meta: map[string]interface{}{ + waveobj.MetaKey_View: "preview", + waveobj.MetaKey_File: absFile, }, - Magnified: viewMagnified, - Focused: true, - } - if cmdName == "edit" { - wshCmd.BlockDef.Meta[waveobj.MetaKey_Edit] = true - } - if conn != "" { - wshCmd.BlockDef.Meta[waveobj.MetaKey_Connection] = conn - } + }, + Magnified: viewMagnified, + Focused: true, + } + if cmdName == "edit" { + wshCmd.BlockDef.Meta[waveobj.MetaKey_Edit] = true + } + if conn != "" { + wshCmd.BlockDef.Meta[waveobj.MetaKey_Connection] = conn } - _, err := wshclient.CreateBlockCommand(RpcClient, *wshCmd, &wshrpc.RpcOpts{Timeout: 2000}) + _, err = wshclient.CreateBlockCommand(RpcClient, *wshCmd, &wshrpc.RpcOpts{Timeout: 2000}) if err != nil { return fmt.Errorf("running view command: %w", err) } diff --git a/cmd/wsh/cmd/wshcmd-web.go b/cmd/wsh/cmd/wshcmd-web.go deleted file mode 100644 index bfda76b82c..0000000000 --- a/cmd/wsh/cmd/wshcmd-web.go +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright 2025, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -package cmd - -import ( - "encoding/json" - "fmt" - - "github.com/spf13/cobra" - "github.com/wavetermdev/waveterm/pkg/waveobj" - "github.com/wavetermdev/waveterm/pkg/wshrpc" - "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" - "github.com/wavetermdev/waveterm/pkg/wshutil" -) - -var webCmd = &cobra.Command{ - Use: "web [open|get|set]", - Short: "web commands", - PersistentPreRunE: preRunSetupRpcClient, -} - -var webOpenCmd = &cobra.Command{ - Use: "open url", - Short: "open a url a web widget", - Args: cobra.ExactArgs(1), - RunE: webOpenRun, -} - -var webGetCmd = &cobra.Command{ - Use: "get [--inner] [--all] [--json] css-selector", - Short: "get the html for a css selector", - Args: cobra.ExactArgs(1), - Hidden: true, - RunE: webGetRun, -} - -var webGetInner bool -var webGetAll bool -var webGetJson bool -var webOpenMagnified bool -var webOpenReplaceBlock string - -func init() { - webOpenCmd.Flags().BoolVarP(&webOpenMagnified, "magnified", "m", false, "open view in magnified mode") - webOpenCmd.Flags().StringVarP(&webOpenReplaceBlock, "replace", "r", "", "replace block") - webCmd.AddCommand(webOpenCmd) - webGetCmd.Flags().BoolVarP(&webGetInner, "inner", "", false, "get inner html (instead of outer)") - webGetCmd.Flags().BoolVarP(&webGetAll, "all", "", false, "get all matches (querySelectorAll)") - webGetCmd.Flags().BoolVarP(&webGetJson, "json", "", false, "output as json") - webCmd.AddCommand(webGetCmd) - rootCmd.AddCommand(webCmd) -} - -func webGetRun(cmd *cobra.Command, args []string) error { - fullORef, err := resolveBlockArg() - if err != nil { - return fmt.Errorf("resolving blockid: %w", err) - } - blockInfo, err := wshclient.BlockInfoCommand(RpcClient, fullORef.OID, nil) - if err != nil { - return fmt.Errorf("getting block info: %w", err) - } - if blockInfo.Block.Meta.GetString(waveobj.MetaKey_View, "") != "web" { - return fmt.Errorf("block %s is not a web block", fullORef.OID) - } - data := wshrpc.CommandWebSelectorData{ - WorkspaceId: blockInfo.WorkspaceId, - BlockId: fullORef.OID, - TabId: blockInfo.TabId, - Selector: args[0], - Opts: &wshrpc.WebSelectorOpts{ - Inner: webGetInner, - All: webGetAll, - }, - } - output, err := wshclient.WebSelectorCommand(RpcClient, data, &wshrpc.RpcOpts{ - Route: wshutil.ElectronRoute, - Timeout: 5000, - }) - if err != nil { - return err - } - if webGetJson { - barr, err := json.MarshalIndent(output, "", " ") - if err != nil { - return fmt.Errorf("json encoding: %w", err) - } - WriteStdout("%s\n", string(barr)) - } else { - for _, item := range output { - WriteStdout("%s\n", item) - } - } - return nil -} - -func webOpenRun(cmd *cobra.Command, args []string) (rtnErr error) { - defer func() { - sendActivity("web", rtnErr == nil) - }() - - var replaceBlockORef *waveobj.ORef - if webOpenReplaceBlock != "" { - var err error - replaceBlockORef, err = resolveSimpleId(webOpenReplaceBlock) - if err != nil { - return fmt.Errorf("resolving -r blockid: %w", err) - } - } - if replaceBlockORef != nil && webOpenMagnified { - return fmt.Errorf("cannot use --replace and --magnified together") - } - - tabId := getTabIdFromEnv() - if tabId == "" { - return fmt.Errorf("no WAVETERM_TABID env var set") - } - - wshCmd := wshrpc.CommandCreateBlockData{ - TabId: tabId, - BlockDef: &waveobj.BlockDef{ - Meta: map[string]any{ - waveobj.MetaKey_View: "web", - waveobj.MetaKey_Url: args[0], - }, - }, - Magnified: webOpenMagnified, - Focused: true, - } - if replaceBlockORef != nil { - wshCmd.TargetBlockId = replaceBlockORef.OID - wshCmd.TargetAction = wshrpc.CreateBlockAction_Replace - } - oref, err := wshclient.CreateBlockCommand(RpcClient, wshCmd, nil) - if err != nil { - return fmt.Errorf("creating block: %w", err) - } - WriteStdout("created block %s\n", oref) - return nil -} diff --git a/electron-builder.config.cjs b/electron-builder.config.cjs index d49f2da616..cc657600c9 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*", "!tsunamiscaffold/**/*"], + filter: ["**/*", "!bin/*", "bin/wavesrv.${arch}*", "bin/wsh*"], }, { from: ".", @@ -31,12 +31,7 @@ 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", - }, - ], + extraResources: [], directories: { output: "make", }, diff --git a/electron.vite.config.ts b/electron.vite.config.ts index d94a166659..3bb936f4c3 100644 --- a/electron.vite.config.ts +++ b/electron.vite.config.ts @@ -107,7 +107,6 @@ export default defineConfig({ rollupOptions: { input: { index: "emain/preload.ts", - "preload-webview": "emain/preload-webview.ts", }, output: { format: "cjs", diff --git a/emain/emain-builder.ts b/emain/emain-builder.ts deleted file mode 100644 index 8b223c0f9c..0000000000 --- a/emain/emain-builder.ts +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright 2025, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { ClientService } from "@/app/store/services"; -import { RpcApi } from "@/app/store/wshclientapi"; -import { randomUUID } from "crypto"; -import { BrowserWindow, webContents } from "electron"; -import { globalEvents } from "emain/emain-events"; -import path from "path"; -import { getElectronAppBasePath, isDevVite, unamePlatform } from "./emain-platform"; -import { calculateWindowBounds, MinWindowHeight, MinWindowWidth } from "./emain-window"; -import { ElectronWshClient } from "./emain-wsh"; - -export type BuilderWindowType = BrowserWindow & { - builderId: string; - builderAppId?: string; - savedInitOpts: BuilderInitOpts; -}; - -const builderWindows: BuilderWindowType[] = []; -export let focusedBuilderWindow: BuilderWindowType = null; - -export function getBuilderWindowById(builderId: string): BuilderWindowType { - return builderWindows.find((win) => win.builderId === builderId); -} - -export function getBuilderWindowByWebContentsId(webContentsId: number): BuilderWindowType { - return builderWindows.find((win) => win.webContents.id === webContentsId); -} - -export function getAllBuilderWindows(): BuilderWindowType[] { - return builderWindows; -} - -export async function createBuilderWindow(appId: string): Promise { - const builderId = randomUUID(); - - const fullConfig = await RpcApi.GetFullConfigCommand(ElectronWshClient); - const clientData = await ClientService.GetClientData(); - const clientId = clientData?.oid; - const windowId = randomUUID(); - - if (appId) { - const oref = `builder:${builderId}`; - await RpcApi.SetRTInfoCommand(ElectronWshClient, { - oref, - data: { "builder:appid": appId }, - }); - } - - const winBounds = calculateWindowBounds(undefined, undefined, fullConfig.settings); - - const builderWindow = new BrowserWindow({ - x: winBounds.x, - y: winBounds.y, - width: winBounds.width, - height: winBounds.height, - minWidth: MinWindowWidth, - minHeight: MinWindowHeight, - titleBarStyle: unamePlatform === "darwin" ? "hiddenInset" : "default", - icon: - unamePlatform === "linux" - ? path.join(getElectronAppBasePath(), "public/logos/wave-logo-dark.png") - : undefined, - show: false, - backgroundColor: "#222222", - webPreferences: { - preload: path.join(getElectronAppBasePath(), "preload", "index.cjs"), - webviewTag: true, - }, - }); - - if (isDevVite) { - await builderWindow.loadURL(`${process.env.ELECTRON_RENDERER_URL}/index.html`); - } else { - await builderWindow.loadFile(path.join(getElectronAppBasePath(), "frontend", "index.html")); - } - - const initOpts: BuilderInitOpts = { - builderId, - clientId, - windowId, - }; - - const typedBuilderWindow = builderWindow as BuilderWindowType; - typedBuilderWindow.builderId = builderId; - typedBuilderWindow.builderAppId = appId; - typedBuilderWindow.savedInitOpts = initOpts; - - typedBuilderWindow.on("close", () => { - const wc = typedBuilderWindow.webContents; - if (wc.isDevToolsOpened()) { - wc.closeDevTools(); - } - for (const guest of webContents.getAllWebContents()) { - if (guest.getType() === "webview" && guest.hostWebContents?.id === wc.id) { - if (guest.isDevToolsOpened()) { - guest.closeDevTools(); - } - } - } - }); - - typedBuilderWindow.on("focus", () => { - focusedBuilderWindow = typedBuilderWindow; - console.log("builder window focused", builderId); - setTimeout(() => globalEvents.emit("windows-updated"), 50); - }); - - typedBuilderWindow.on("blur", () => { - if (focusedBuilderWindow === typedBuilderWindow) { - focusedBuilderWindow = null; - } - setTimeout(() => globalEvents.emit("windows-updated"), 50); - }); - - typedBuilderWindow.on("closed", () => { - console.log("builder window closed", builderId); - const index = builderWindows.indexOf(typedBuilderWindow); - if (index !== -1) { - builderWindows.splice(index, 1); - } - if (focusedBuilderWindow === typedBuilderWindow) { - focusedBuilderWindow = null; - } - RpcApi.DeleteBuilderCommand(ElectronWshClient, builderId, { noresponse: true }); - setTimeout(() => globalEvents.emit("windows-updated"), 50); - }); - - builderWindows.push(typedBuilderWindow); - typedBuilderWindow.show(); - - console.log("created builder window", builderId, appId); - return typedBuilderWindow; -} diff --git a/emain/emain-ipc.ts b/emain/emain-ipc.ts index 5e5f15b302..32e6c7a7f2 100644 --- a/emain/emain-ipc.ts +++ b/emain/emain-ipc.ts @@ -19,7 +19,6 @@ import { incrementTermCommandsWsl, setWasActive, } from "./emain-activity"; -import { createBuilderWindow, getAllBuilderWindows, getBuilderWindowByWebContentsId } from "./emain-builder"; import { callWithOriginalXdgCurrentDesktopAsync, unamePlatform } from "./emain-platform"; import { getWaveTabViewByWebContentsId } from "./emain-tabview"; import { handleCtrlShiftState } from "./emain-util"; @@ -29,20 +28,6 @@ import { ElectronWshClient } from "./emain-wsh"; const electronApp = electron.app; -let webviewFocusId: number = null; -let webviewKeys: string[] = []; - -export function openBuilderWindow(appId?: string) { - const normalizedAppId = appId || ""; - const existingBuilderWindows = getAllBuilderWindows(); - const existingWindow = existingBuilderWindows.find((win) => win.builderAppId === normalizedAppId); - if (existingWindow) { - existingWindow.focus(); - return; - } - fireAndForget(() => createBuilderWindow(normalizedAppId)); -} - type UrlInSessionResult = { stream: Readable; mimeType: string; @@ -207,43 +192,6 @@ export function initIpcHandlers() { } }); - electron.ipcMain.on("webview-image-contextmenu", (event: electron.IpcMainEvent, payload: { src: string }) => { - const menu = new electron.Menu(); - const win = getWaveWindowByWebContentsId(event.sender.hostWebContents?.id); - if (win == null) { - return; - } - menu.append( - new electron.MenuItem({ - label: "Save Image", - click: () => { - const resultP = getUrlInSession(event.sender.session, payload.src); - resultP - .then((result) => { - saveImageFileWithNativeDialog( - event.sender.hostWebContents, - result.fileName, - result.mimeType, - result.stream - ); - }) - .catch((e) => { - console.log("error getting image", e); - }); - }, - }) - ); - menu.popup(); - }); - - electron.ipcMain.on("webview-mouse-navigate", (event: electron.IpcMainEvent, direction: string) => { - if (direction === "back") { - event.sender.navigationHistory.goBack(); - } else if (direction === "forward") { - event.sender.navigationHistory.goForward(); - } - }); - electron.ipcMain.on("download", (event, payload) => { const baseName = encodeURIComponent(path.basename(payload.filePath)); const streamingUrl = @@ -288,50 +236,6 @@ export function initIpcHandlers() { event.returnValue = event.sender.getZoomFactor(); }); - const hasBeforeInputRegisteredMap = new Map(); - - electron.ipcMain.on("webview-focus", (event: Electron.IpcMainEvent, focusedId: number) => { - webviewFocusId = focusedId; - console.log("webview-focus", focusedId); - if (focusedId == null) { - return; - } - const parentWc = event.sender; - const webviewWc = electron.webContents.fromId(focusedId); - if (webviewWc == null) { - webviewFocusId = null; - return; - } - if (!hasBeforeInputRegisteredMap.get(focusedId)) { - hasBeforeInputRegisteredMap.set(focusedId, true); - webviewWc.on("before-input-event", (e, input) => { - let waveEvent = keyutil.adaptFromElectronKeyEvent(input); - handleCtrlShiftState(parentWc, waveEvent); - if (webviewFocusId != focusedId) { - return; - } - if (input.type != "keyDown") { - return; - } - for (let keyDesc of webviewKeys) { - if (keyutil.checkKeyPressed(waveEvent, keyDesc)) { - e.preventDefault(); - parentWc.send("reinject-key", waveEvent); - console.log("webview reinject-key", keyDesc); - return; - } - } - }); - webviewWc.on("destroyed", () => { - hasBeforeInputRegisteredMap.delete(focusedId); - }); - } - }); - - electron.ipcMain.on("register-global-webview-keys", (event, keys: string[]) => { - webviewKeys = keys ?? []; - }); - electron.ipcMain.on("set-keyboard-chord-mode", (event) => { event.returnValue = null; const tabView = getWaveTabViewByWebContentsId(event.sender.id); @@ -380,19 +284,6 @@ export function initIpcHandlers() { }); }); - electron.ipcMain.handle("clear-webview-storage", async (event, webContentsId: number) => { - try { - const wc = electron.webContents.fromId(webContentsId); - if (wc && wc.session) { - await wc.session.clearStorageData(); - console.log("Cleared cookies and storage for webContentsId:", webContentsId); - } - } catch (e) { - console.error("Failed to clear cookies and storage:", e); - throw e; - } - }); - electron.ipcMain.on("open-native-path", (event, filePath: string) => { console.log("open-native-path", filePath); filePath = filePath.replace("~", electronApp.getPath("home")); @@ -420,17 +311,6 @@ export function initIpcHandlers() { return; } - const builderWindow = getBuilderWindowByWebContentsId(event.sender.id); - if (builderWindow != null) { - if (status === "ready") { - if (builderWindow.savedInitOpts) { - console.log("savedInitOpts calling builder-init", builderWindow.savedInitOpts.builderId); - builderWindow.webContents.send("builder-init", builderWindow.savedInitOpts); - } - } - return; - } - console.log("set-window-init-status: no window found for webContentsId", event.sender.id); }); @@ -458,52 +338,8 @@ export function initIpcHandlers() { event.sender.paste(); }); - electron.ipcMain.on("open-builder", (event, appId?: string) => { - openBuilderWindow(appId); - }); - - electron.ipcMain.on("set-builder-window-appid", (event, appId: string) => { - const bw = getBuilderWindowByWebContentsId(event.sender.id); - if (bw == null) { - return; - } - bw.builderAppId = appId; - console.log("set-builder-window-appid", bw.builderId, appId); - }); - electron.ipcMain.on("open-new-window", () => fireAndForget(createNewWaveWindow)); - electron.ipcMain.on("close-builder-window", async (event) => { - const bw = getBuilderWindowByWebContentsId(event.sender.id); - if (bw == null) { - return; - } - const builderId = bw.builderId; - if (builderId) { - try { - await RpcApi.SetRTInfoCommand(ElectronWshClient, { - oref: `builder:${builderId}`, - data: {} as ObjRTInfo, - delete: true, - }); - } catch (e) { - console.error("Error deleting builder rtinfo:", e); - } - } - const wc = bw.webContents; - if (wc.isDevToolsOpened()) { - wc.closeDevTools(); - } - for (const guest of electron.webContents.getAllWebContents()) { - if (guest.getType() === "webview" && guest.hostWebContents?.id === wc.id) { - if (guest.isDevToolsOpened()) { - guest.closeDevTools(); - } - } - } - bw.destroy(); - }); - electron.ipcMain.on("do-refresh", (event) => { event.sender.reloadIgnoringCache(); }); diff --git a/emain/emain-menu.ts b/emain/emain-menu.ts index 1bdf6a7139..ddb54fac06 100644 --- a/emain/emain-menu.ts +++ b/emain/emain-menu.ts @@ -5,9 +5,7 @@ import { waveEventSubscribeSingle } from "@/app/store/wps"; import { RpcApi } from "@/app/store/wshclientapi"; import * as electron from "electron"; import { fireAndForget } from "../frontend/util/util"; -import { focusedBuilderWindow, getBuilderWindowById } from "./emain-builder"; -import { openBuilderWindow } from "./emain-ipc"; -import { isDev, unamePlatform } from "./emain-platform"; +import { unamePlatform } from "./emain-platform"; import { clearTabCache } from "./emain-tabview"; import { decreaseZoomLevel, increaseZoomLevel, resetZoomLevel } from "./emain-util"; import { @@ -31,10 +29,6 @@ function getWindowWebContents(window: electron.BaseWindow): electron.WebContents if (window == null) { return null; } - // Check BrowserWindow first (for Tsunami Builder windows) - if (window instanceof electron.BrowserWindow) { - return window.webContents; - } // Check WaveBrowserWindow (for main Wave windows with tab views) if (window instanceof WaveBrowserWindow) { if (window.activeTabView) { @@ -144,14 +138,6 @@ function makeFileMenu( }, }, ]; - 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", - click: () => openBuilderWindow(""), - }); - } if (numWaveWindows == 0) { fileMenu.push({ label: "New Window (hidden-1)", @@ -203,13 +189,12 @@ function makeAppMenuItems(webContents: electron.WebContents): Electron.MenuItemC function makeViewMenu( webContents: electron.WebContents, callbacks: AppMenuCallbacks, - isBuilderWindowFocused: boolean, fullscreenOnLaunch: boolean ): Electron.MenuItemConstructorOptions[] { const devToolsAccel = unamePlatform === "darwin" ? "Option+Command+I" : "Alt+Shift+I"; return [ { - label: isBuilderWindowFocused ? "Reload Window" : "Reload Tab", + label: "Reload Tab", accelerator: "Shift+CommandOrControl+R", click: (_, window) => { (getWindowWebContents(window) ?? webContents)?.reloadIgnoringCache(); @@ -328,12 +313,11 @@ function makeViewMenu( ]; } -async function makeFullAppMenu(callbacks: AppMenuCallbacks, workspaceOrBuilderId?: string): Promise { +async function makeFullAppMenu(callbacks: AppMenuCallbacks, workspaceId?: string): Promise { const numWaveWindows = getAllWaveWindows().length; - const webContents = workspaceOrBuilderId && getWebContentsByWorkspaceOrBuilderId(workspaceOrBuilderId); + const webContents = workspaceId && getWebContentsByWorkspaceId(workspaceId); const appMenuItems = makeAppMenuItems(webContents); - const isBuilderWindowFocused = focusedBuilderWindow != null; let fullscreenOnLaunch = false; let fullConfig: FullConfigType = null; try { @@ -344,7 +328,7 @@ async function makeFullAppMenu(callbacks: AppMenuCallbacks, workspaceOrBuilderId } const editMenu = makeEditMenu(fullConfig); const fileMenu = makeFileMenu(numWaveWindows, callbacks, fullConfig); - const viewMenu = makeViewMenu(webContents, callbacks, isBuilderWindowFocused, fullscreenOnLaunch); + const viewMenu = makeViewMenu(webContents, callbacks, fullscreenOnLaunch); let workspaceMenu: Electron.MenuItemConstructorOptions[] = null; try { workspaceMenu = await getWorkspaceMenu(); @@ -363,7 +347,7 @@ async function makeFullAppMenu(callbacks: AppMenuCallbacks, workspaceOrBuilderId { role: "editMenu", submenu: editMenu }, { role: "viewMenu", submenu: viewMenu }, ]; - if (workspaceMenu != null && !isBuilderWindowFocused) { + if (workspaceMenu != null) { menuTemplate.push({ label: "Workspace", id: "workspace-menu", @@ -377,13 +361,13 @@ async function makeFullAppMenu(callbacks: AppMenuCallbacks, workspaceOrBuilderId return electron.Menu.buildFromTemplate(menuTemplate); } -export function instantiateAppMenu(workspaceOrBuilderId?: string): Promise { +export function instantiateAppMenu(workspaceId?: string): Promise { return makeFullAppMenu( { createNewWaveWindow, relaunchBrowserWindows, }, - workspaceOrBuilderId + workspaceId ); } @@ -405,17 +389,12 @@ function initMenuEventSubscriptions() { }); } -function getWebContentsByWorkspaceOrBuilderId(workspaceOrBuilderId: string): electron.WebContents { - const ww = getWaveWindowByWorkspaceId(workspaceOrBuilderId); +function getWebContentsByWorkspaceId(workspaceId: string): electron.WebContents { + const ww = getWaveWindowByWorkspaceId(workspaceId); if (ww) { return ww.activeTabView?.webContents; } - const bw = getBuilderWindowById(workspaceOrBuilderId); - if (bw) { - return bw.webContents; - } - return null; } @@ -448,10 +427,10 @@ function convertMenuDefArrToMenu( electron.ipcMain.on( "contextmenu-show", - (event, workspaceOrBuilderId: string, menuDefArr: ElectronContextMenuItem[]) => { - const webContents = getWebContentsByWorkspaceOrBuilderId(workspaceOrBuilderId); + (event, workspaceId: string, menuDefArr: ElectronContextMenuItem[]) => { + const webContents = getWebContentsByWorkspaceId(workspaceId); if (!webContents) { - console.error("invalid window for context menu:", workspaceOrBuilderId); + console.error("invalid window for context menu:", workspaceId); event.returnValue = true; return; } @@ -477,7 +456,7 @@ electron.ipcMain.on( electron.ipcMain.on("workspace-appmenu-show", (event, workspaceId: string) => { fireAndForget(async () => { - const webContents = getWebContentsByWorkspaceOrBuilderId(workspaceId); + const webContents = getWebContentsByWorkspaceId(workspaceId); if (!webContents) { console.error("invalid window for workspace app menu:", workspaceId); return; @@ -488,19 +467,6 @@ electron.ipcMain.on("workspace-appmenu-show", (event, workspaceId: string) => { event.returnValue = true; }); -electron.ipcMain.on("builder-appmenu-show", (event, builderId: string) => { - fireAndForget(async () => { - const webContents = getWebContentsByWorkspaceOrBuilderId(builderId); - if (!webContents) { - console.error("invalid window for builder app menu:", builderId); - return; - } - const menu = await instantiateAppMenu(builderId); - menu.popup(); - }); - event.returnValue = true; -}); - const dockMenu = electron.Menu.buildFromTemplate([ { label: "New Window", diff --git a/emain/emain-platform.ts b/emain/emain-platform.ts index 32320e4eb4..77e2dbc4d6 100644 --- a/emain/emain-platform.ts +++ b/emain/emain-platform.ts @@ -193,9 +193,6 @@ ipcMain.on("get-user-name", (event) => { ipcMain.on("get-host-name", (event) => { event.returnValue = os.hostname(); }); -ipcMain.on("get-webview-preload", (event) => { - event.returnValue = path.join(getElectronAppBasePath(), "preload", "preload-webview.cjs"); -}); ipcMain.on("get-data-dir", (event) => { event.returnValue = getWaveDataDir(); }); diff --git a/emain/emain-tabview.ts b/emain/emain-tabview.ts index 753a53adec..ec507919d4 100644 --- a/emain/emain-tabview.ts +++ b/emain/emain-tabview.ts @@ -138,7 +138,6 @@ export class WaveTabView extends WebContentsView { super({ webPreferences: { preload: path.join(getElectronAppBasePath(), "preload", "index.cjs"), - webviewTag: true, }, }); this.createdTs = Date.now(); diff --git a/emain/emain.ts b/emain/emain.ts index 8b08178aec..c6ecd27cfb 100644 --- a/emain/emain.ts +++ b/emain/emain.ts @@ -3,7 +3,6 @@ import { RpcApi } from "@/app/store/wshclientapi"; import * as electron from "electron"; -import { focusedBuilderWindow, getAllBuilderWindows } from "emain/emain-builder"; import { globalEvents } from "emain/emain-events"; import { sprintf } from "sprintf-js"; import * as services from "../frontend/app/store/services"; @@ -265,12 +264,11 @@ electronApp.on("window-all-closed", () => { }); electronApp.on("before-quit", (e) => { const allWindows = getAllWaveWindows(); - const allBuilders = getAllBuilderWindows(); if ( confirmQuit && !getForceQuit() && !getUserConfirmedQuit() && - (allWindows.length > 0 || allBuilders.length > 0) && + allWindows.length > 0 && !getIsWaveSrvDead() && !process.env.WAVETERM_NOCONFIRMQUIT ) { @@ -306,9 +304,6 @@ electronApp.on("before-quit", (e) => { for (const window of allWindows) { hideWindowWithCatch(window); } - for (const builder of allBuilders) { - builder.hide(); - } if (getIsWaveSrvDead()) { console.log("wavesrv is dead, quitting immediately"); setForceQuit(true); @@ -358,16 +353,13 @@ process.on("uncaughtException", (error) => { }); let lastWaveWindowCount = 0; -let lastIsBuilderWindowActive = false; globalEvents.on("windows-updated", () => { const wwCount = getAllWaveWindows().length; - const isBuilderActive = focusedBuilderWindow != null; - if (wwCount == lastWaveWindowCount && isBuilderActive == lastIsBuilderWindowActive) { + if (wwCount == lastWaveWindowCount) { return; } lastWaveWindowCount = wwCount; - lastIsBuilderWindowActive = isBuilderActive; - console.log("windows-updated", wwCount, "builder-active:", isBuilderActive); + console.log("windows-updated", wwCount); makeAndSetAppMenu(); }); diff --git a/emain/preload-webview.ts b/emain/preload-webview.ts deleted file mode 100644 index e2a39a3b4e..0000000000 --- a/emain/preload-webview.ts +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2025, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { ipcRenderer } from "electron"; - -document.addEventListener("contextmenu", (event) => { - console.log("contextmenu event", event); - if (event.target == null) { - return; - } - const targetElement = event.target as HTMLElement; - // Check if the right-click is on an image - if (targetElement.tagName === "IMG") { - setTimeout(() => { - if (event.defaultPrevented) { - return; - } - event.preventDefault(); - const imgElem = targetElement as HTMLImageElement; - const imageUrl = imgElem.src; - ipcRenderer.send("webview-image-contextmenu", { src: imageUrl }); - }, 50); - return; - } - // do nothing -}); - -document.addEventListener("mouseup", (event) => { - // Mouse button 3 = back, button 4 = forward - if (!event.isTrusted) { - return; - } - if (event.button === 3 || event.button === 4) { - event.preventDefault(); - ipcRenderer.send("webview-mouse-navigate", event.button === 3 ? "back" : "forward"); - } -}); - -console.log("loaded wave preload-webview.ts"); diff --git a/emain/preload.ts b/emain/preload.ts index 8d2b18a308..a49bf48a1f 100644 --- a/emain/preload.ts +++ b/emain/preload.ts @@ -1,7 +1,7 @@ // Copyright 2025, Command Line Inc. // SPDX-License-Identifier: Apache-2.0 -import { contextBridge, ipcRenderer, Rectangle, webUtils, WebviewTag } from "electron"; +import { contextBridge, ipcRenderer, Rectangle, webUtils } from "electron"; // update type in custom.d.ts (ElectronApi type) contextBridge.exposeInMainWorld("api", { @@ -15,11 +15,9 @@ contextBridge.exposeInMainWorld("api", { getConfigDir: () => ipcRenderer.sendSync("get-config-dir"), getHomeDir: () => ipcRenderer.sendSync("get-home-dir"), getAboutModalDetails: () => ipcRenderer.sendSync("get-about-modal-details"), - getWebviewPreload: () => ipcRenderer.sendSync("get-webview-preload"), getZoomFactor: () => ipcRenderer.sendSync("get-zoom-factor"), openNewWindow: () => ipcRenderer.send("open-new-window"), showWorkspaceAppMenu: (workspaceId) => ipcRenderer.send("workspace-appmenu-show", workspaceId), - showBuilderAppMenu: (builderId) => ipcRenderer.send("builder-appmenu-show", builderId), showContextMenu: (workspaceId, menu) => ipcRenderer.send("contextmenu-show", workspaceId, menu), onContextMenuClick: (callback: (id: string | null) => void) => ipcRenderer.on("contextmenu-click", (_event, id: string | null) => callback(id)), @@ -43,8 +41,6 @@ contextBridge.exposeInMainWorld("api", { onMenuItemAbout: (callback) => ipcRenderer.on("menu-item-about", callback), updateWindowControlsOverlay: (rect) => ipcRenderer.send("update-window-controls-overlay", rect), onReinjectKey: (callback) => ipcRenderer.on("reinject-key", (_event, waveEvent) => callback(waveEvent)), - setWebviewFocus: (focused: number) => ipcRenderer.send("webview-focus", focused), - registerGlobalWebviewKeys: (keys) => ipcRenderer.send("register-global-webview-keys", keys), onControlShiftStateUpdate: (callback) => ipcRenderer.on("control-shift-state-update", (_event, state) => callback(state)), createWorkspace: () => ipcRenderer.send("create-workspace"), @@ -55,34 +51,18 @@ contextBridge.exposeInMainWorld("api", { closeTab: (workspaceId, tabId, confirmClose) => ipcRenderer.invoke("close-tab", workspaceId, tabId, confirmClose), setWindowInitStatus: (status) => ipcRenderer.send("set-window-init-status", status), onWaveInit: (callback) => ipcRenderer.on("wave-init", (_event, initOpts) => callback(initOpts)), - onBuilderInit: (callback) => ipcRenderer.on("builder-init", (_event, initOpts) => callback(initOpts)), sendLog: (log) => ipcRenderer.send("fe-log", log), onQuicklook: (filePath: string) => ipcRenderer.send("quicklook", filePath), openNativePath: (filePath: string) => ipcRenderer.send("open-native-path", filePath), captureScreenshot: (rect: Rectangle) => ipcRenderer.invoke("capture-screenshot", rect), setKeyboardChordMode: () => ipcRenderer.send("set-keyboard-chord-mode"), - clearWebviewStorage: (webContentsId: number) => ipcRenderer.invoke("clear-webview-storage", webContentsId), setWaveAIOpen: (isOpen: boolean) => ipcRenderer.send("set-waveai-open", isOpen), - closeBuilderWindow: () => ipcRenderer.send("close-builder-window"), incrementTermCommands: (opts?: { isRemote?: boolean; isWsl?: boolean; isDurable?: boolean }) => ipcRenderer.send("increment-term-commands", opts), nativePaste: () => ipcRenderer.send("native-paste"), - openBuilder: (appId?: string) => ipcRenderer.send("open-builder", appId), - setBuilderWindowAppId: (appId: string) => ipcRenderer.send("set-builder-window-appid", appId), doRefresh: () => ipcRenderer.send("do-refresh"), getPathForFile: (file: File): string => webUtils.getPathForFile(file), saveTextFile: (fileName: string, content: string) => ipcRenderer.invoke("save-text-file", fileName, content), setIsActive: () => ipcRenderer.invoke("set-is-active"), }); -// Custom event for "new-window" -ipcRenderer.on("webview-new-window", (e, webContentsId, details) => { - const event = new CustomEvent("new-window", { detail: details }); - document.getElementById("webview").dispatchEvent(event); -}); - -ipcRenderer.on("webcontentsid-from-blockid", (e, blockId, responseCh) => { - const webviewElem: WebviewTag = document.querySelector("div[data-blockid='" + blockId + "'] webview"); - const wcId = webviewElem?.dataset?.webcontentsid; - ipcRenderer.send(responseCh, wcId); -}); diff --git a/frontend/app/aipanel/ai-utils.ts b/frontend/app/aipanel/ai-utils.ts index 8bfd67bdc0..1fa261ec57 100644 --- a/frontend/app/aipanel/ai-utils.ts +++ b/frontend/app/aipanel/ai-utils.ts @@ -546,15 +546,10 @@ export interface FilteredAIModeConfigs { export const getFilteredAIModeConfigs = ( aiModeConfigs: Record, showCloudModes: boolean, - inBuilder: boolean, hasPremium: boolean, currentMode?: string ): FilteredAIModeConfigs => { - const hideQuick = inBuilder && hasPremium; - - const allConfigs = Object.entries(aiModeConfigs) - .map(([mode, config]) => ({ mode, ...config })) - .filter((config) => !(hideQuick && config.mode === "waveai@quick")); + const allConfigs = Object.entries(aiModeConfigs).map(([mode, config]) => ({ mode, ...config })); const otherProviderConfigs = allConfigs .filter((config) => config["ai:provider"] !== "wave") diff --git a/frontend/app/aipanel/aimode.tsx b/frontend/app/aipanel/aimode.tsx index 3602cdd360..3c3885b18c 100644 --- a/frontend/app/aipanel/aimode.tsx +++ b/frontend/app/aipanel/aimode.tsx @@ -151,7 +151,6 @@ export const AIModeDropdown = memo(({ compatibilityMode = false }: AIModeDropdow const { waveProviderConfigs, otherProviderConfigs } = getFilteredAIModeConfigs( aiModeConfigs, showCloudModes, - model.inBuilder, hasPremium, currentMode ); diff --git a/frontend/app/aipanel/aipanel-contextmenu.ts b/frontend/app/aipanel/aipanel-contextmenu.ts index 4e78389198..7e3a525587 100644 --- a/frontend/app/aipanel/aipanel-contextmenu.ts +++ b/frontend/app/aipanel/aipanel-contextmenu.ts @@ -39,86 +39,58 @@ export async function handleWaveAIContextMenu(e: React.MouseEvent, showCopy: boo oref: model.orefContext, }); - const defaultTokens = model.inBuilder ? 24576 : 4096; - const currentMaxTokens = rtInfo?.["waveai:maxoutputtokens"] ?? defaultTokens; + const currentMaxTokens = rtInfo?.["waveai:maxoutputtokens"] ?? 4096; const maxTokensSubmenu: ContextMenuItem[] = []; - if (model.inBuilder) { - maxTokensSubmenu.push( - { - label: "24k", - type: "checkbox", - checked: currentMaxTokens === 24576, - click: () => { - RpcApi.SetRTInfoCommand(TabRpcClient, { - oref: model.orefContext, - data: { "waveai:maxoutputtokens": 24576 }, - }); - }, + if (isDev()) { + maxTokensSubmenu.push({ + label: "1k (Dev Testing)", + type: "checkbox", + checked: currentMaxTokens === 1024, + click: () => { + RpcApi.SetRTInfoCommand(TabRpcClient, { + oref: model.orefContext, + data: { "waveai:maxoutputtokens": 1024 }, + }); }, - { - label: "64k (Pro)", - type: "checkbox", - checked: currentMaxTokens === 65536, - click: () => { - RpcApi.SetRTInfoCommand(TabRpcClient, { - oref: model.orefContext, - data: { "waveai:maxoutputtokens": 65536 }, - }); - }, - } - ); - } else { - if (isDev()) { - maxTokensSubmenu.push({ - label: "1k (Dev Testing)", - type: "checkbox", - checked: currentMaxTokens === 1024, - click: () => { - RpcApi.SetRTInfoCommand(TabRpcClient, { - oref: model.orefContext, - data: { "waveai:maxoutputtokens": 1024 }, - }); - }, - }); - } - maxTokensSubmenu.push( - { - label: "4k", - type: "checkbox", - checked: currentMaxTokens === 4096, - click: () => { - RpcApi.SetRTInfoCommand(TabRpcClient, { - oref: model.orefContext, - data: { "waveai:maxoutputtokens": 4096 }, - }); - }, + }); + } + maxTokensSubmenu.push( + { + label: "4k", + type: "checkbox", + checked: currentMaxTokens === 4096, + click: () => { + RpcApi.SetRTInfoCommand(TabRpcClient, { + oref: model.orefContext, + data: { "waveai:maxoutputtokens": 4096 }, + }); }, - { - label: "16k (Pro)", - type: "checkbox", - checked: currentMaxTokens === 16384, - click: () => { - RpcApi.SetRTInfoCommand(TabRpcClient, { - oref: model.orefContext, - data: { "waveai:maxoutputtokens": 16384 }, - }); - }, + }, + { + label: "16k (Pro)", + type: "checkbox", + checked: currentMaxTokens === 16384, + click: () => { + RpcApi.SetRTInfoCommand(TabRpcClient, { + oref: model.orefContext, + data: { "waveai:maxoutputtokens": 16384 }, + }); }, - { - label: "64k (Pro)", - type: "checkbox", - checked: currentMaxTokens === 65536, - click: () => { - RpcApi.SetRTInfoCommand(TabRpcClient, { - oref: model.orefContext, - data: { "waveai:maxoutputtokens": 65536 }, - }); - }, - } - ); - } + }, + { + label: "64k (Pro)", + type: "checkbox", + checked: currentMaxTokens === 65536, + click: () => { + RpcApi.SetRTInfoCommand(TabRpcClient, { + oref: model.orefContext, + data: { "waveai:maxoutputtokens": 65536 }, + }); + }, + } + ); menu.push({ label: "Max Output Tokens", diff --git a/frontend/app/aipanel/aipanel.tsx b/frontend/app/aipanel/aipanel.tsx index 32b8582141..a075857538 100644 --- a/frontend/app/aipanel/aipanel.tsx +++ b/frontend/app/aipanel/aipanel.tsx @@ -8,7 +8,6 @@ import { ErrorBoundary } from "@/app/element/errorboundary"; import { atoms, getSettingsKeyAtom } from "@/app/store/global"; import { globalStore } from "@/app/store/jotaiStore"; import { useTabModelMaybe } from "@/app/store/tab-model"; -import { isBuilderWindow } from "@/app/store/windowtype"; import { useWaveEnv } from "@/app/waveenv/waveenv"; import { checkKeyPressed, keydownWrapper } from "@/util/keyutil"; import { isMacOS, isWindows } from "@/util/platformutil"; @@ -183,24 +182,6 @@ const AIWelcomeMessage = memo(() => { AIWelcomeMessage.displayName = "AIWelcomeMessage"; -const AIBuilderWelcomeMessage = memo(() => { - return ( -
-
- -

WaveApp Builder

-
-
-

- The WaveApp builder helps create wave widgets that integrate seamlessly into Wave Terminal. -

-
-
- ); -}); - -AIBuilderWelcomeMessage.displayName = "AIBuilderWelcomeMessage"; - const AIErrorMessage = memo(() => { const model = WaveAIModel.getInstance(); const errorMessage = jotai.useAtomValue(model.errorMessage); @@ -284,12 +265,7 @@ const AIPanelComponentInner = memo(({ roundTopLeft }: AIPanelComponentInnerProps widgetaccess: globalStore.get(model.widgetAccessAtom), aimode: globalStore.get(model.currentAIMode), }; - if (isBuilderWindow()) { - body.builderid = globalStore.get(atoms.builderId); - body.builderappid = globalStore.get(atoms.builderAppId); - } else { - body.tabid = tabModel.tabId; - } + body.tabid = tabModel.tabId; return { body }; }, }), @@ -557,15 +533,14 @@ const AIPanelComponentInner = memo(({ roundTopLeft }: AIPanelComponentInnerProps ref={containerRef} data-waveai-panel="true" className={cn( - "@container bg-zinc-900/70 flex flex-col relative", - model.inBuilder ? "mt-0 h-full" : "mt-1 h-[calc(100%-4px)]", + "@container bg-zinc-900/70 flex flex-col relative mt-1 h-[calc(100%-4px)]", (isDragOver || isReactDndDragOver) && "bg-zinc-800 border-accent", isFocused && !borderColor ? "border-2 border-accent" : "border-2 border-transparent" )} style={{ borderTopLeftRadius: roundTopLeft ? 10 : 0, - borderTopRightRadius: model.inBuilder ? 0 : 10, - borderBottomRightRadius: model.inBuilder ? 0 : 10, + borderTopRightRadius: 10, + borderBottomRightRadius: 10, borderBottomLeftRadius: 10, borderColor: borderColor ?? undefined, }} @@ -598,7 +573,7 @@ const AIPanelComponentInner = memo(({ roundTopLeft }: AIPanelComponentInnerProps
- {model.inBuilder ? : } + ) : ( { const model = WaveAIModel.getInstance(); const widgetAccess = useAtomValue(model.widgetAccessAtom); - const inBuilder = model.inBuilder; const handleKebabClick = (e: React.MouseEvent) => { handleWaveAIContextMenu(e, false); @@ -30,37 +29,35 @@ export const AIPanelHeader = memo(() => {
- {!inBuilder && ( -
- Context - Widget Context - -
- )} + {widgetAccess ? "ON" : "OFF"} + + +
- )} - {isRestarting &&
Starting...
} - - ); -}); - -TsunamiView.displayName = "TsunamiView"; - -export { TsunamiViewModel }; diff --git a/frontend/app/view/webview/webview.scss b/frontend/app/view/webview/webview.scss deleted file mode 100644 index 62d68ae8dd..0000000000 --- a/frontend/app/view/webview/webview.scss +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2024, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -.webview, -.webview-container { - height: 100%; - width: 100%; - border: none !important; - outline: none !important; - overflow: hidden; - padding: 0; - margin: 0; - user-select: none; - border-radius: 0 0 var(--block-border-radius) var(--block-border-radius); - - // try to force pixel alignment to prevent - // subpixel rendering artifacts - transform: translate3d(0, 0, 0); - will-change: transform; -} - -.webview-error { - display: flex; - position: absolute; - background-color: black; - top: 0; - left: 0; - height: 100%; - width: 100%; - z-index: 100; - div { - font-size: x-large; - color: var(--error-color); - display: flex; - margin: auto; - padding: 30px; - } -} - -.block-frame-div-url { - background: rgba(255, 255, 255, 0.1); - - input { - opacity: 1; - } - - .wave-iconbutton { - width: fit-content !important; - margin-right: 5px; - } -} diff --git a/frontend/app/view/webview/webview.test.tsx b/frontend/app/view/webview/webview.test.tsx deleted file mode 100644 index 6114160218..0000000000 --- a/frontend/app/view/webview/webview.test.tsx +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2026, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { globalStore } from "@/app/store/jotaiStore"; -import { makeMockWaveEnv } from "@/preview/mock/mockwaveenv"; -import { renderToStaticMarkup } from "react-dom/server"; -import { describe, expect, it } from "vitest"; -import { atom } from "jotai"; -import { getWebPreviewDisplayUrl, WebViewModel, WebViewPreviewFallback } from "./webview"; - -describe("webview preview fallback", () => { - it("shows the requested URL", () => { - const markup = renderToStaticMarkup(); - - expect(markup).toContain("electron webview unavailable"); - expect(markup).toContain("https://waveterm.dev/docs"); - }); - - it("falls back to about:blank when no URL is available", () => { - expect(getWebPreviewDisplayUrl("")).toBe("about:blank"); - expect(getWebPreviewDisplayUrl(null)).toBe("about:blank"); - }); - - it("uses the supplied env for homepage atoms and config updates", async () => { - const blockId = "webview-env-block"; - const env = makeMockWaveEnv({ - settings: { - "web:defaulturl": "https://default.example", - }, - mockWaveObjs: { - [`block:${blockId}`]: { - otype: "block", - oid: blockId, - version: 1, - meta: { - pinnedurl: "https://block.example", - }, - } as Block, - }, - }); - const model = new WebViewModel({ - blockId, - nodeModel: { - isFocused: atom(true), - focusNode: () => {}, - } as any, - tabModel: {} as any, - waveEnv: env, - }); - - expect(globalStore.get(model.homepageUrl)).toBe("https://block.example"); - - await model.setHomepageUrl("https://global.example", "global"); - - expect(globalStore.get(model.homepageUrl)).toBe("https://global.example"); - expect(globalStore.get(env.getSettingsKeyAtom("web:defaulturl"))).toBe("https://global.example"); - expect(globalStore.get(env.wos.getWaveObjectAtom(`block:${blockId}`))?.meta?.pinnedurl).toBeUndefined(); - }); -}); diff --git a/frontend/app/view/webview/webview.tsx b/frontend/app/view/webview/webview.tsx deleted file mode 100644 index f6d98b8f22..0000000000 --- a/frontend/app/view/webview/webview.tsx +++ /dev/null @@ -1,1135 +0,0 @@ -// Copyright 2026, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { BlockNodeModel } from "@/app/block/blocktypes"; -import { Search, useSearch } from "@/app/element/search"; -import { globalStore } from "@/app/store/jotaiStore"; -import { getSimpleControlShiftAtom } from "@/app/store/keymodel"; -import type { TabModel } from "@/app/store/tab-model"; -import { makeORef } from "@/app/store/wos"; -import { TabRpcClient } from "@/app/store/wshrpcutil"; -import { - BlockHeaderSuggestionControl, - SuggestionControlNoData, - SuggestionControlNoResults, -} from "@/app/suggestion/suggestion"; -import { MockBoundary } from "@/app/waveenv/mockboundary"; -import { useWaveEnv } from "@/app/waveenv/waveenv"; -import { openLink } from "@/store/global"; -import { adaptFromReactOrNativeKeyEvent, checkKeyPressed } from "@/util/keyutil"; -import { fireAndForget, useAtomValueSafe } from "@/util/util"; -import clsx from "clsx"; -import type { WebviewTag } from "electron"; -import { Atom, PrimitiveAtom, atom, useAtomValue, useSetAtom } from "jotai"; -import { Fragment, createRef, memo, useCallback, useEffect, useLayoutEffect, useRef, useState } from "react"; -import "./webview.scss"; -import type { WebViewEnv } from "./webviewenv"; - -// User agent strings for mobile emulation -const USER_AGENT_IPHONE = - "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1"; -const USER_AGENT_ANDROID = - "Mozilla/5.0 (Linux; Android 13) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.6099.43 Mobile Safari/537.36"; - -let webviewPreloadUrl = null; - -function getWebviewPreloadUrl(env: WebViewEnv) { - if (webviewPreloadUrl == null) { - webviewPreloadUrl = env.electron.getWebviewPreload(); - console.log("webviewPreloadUrl", webviewPreloadUrl); - } - if (webviewPreloadUrl == null) { - return null; - } - return "file://" + webviewPreloadUrl; -} - -export class WebViewModel implements ViewModel { - viewType: string; - blockId: string; - tabModel: TabModel; - noPadding?: Atom; - blockAtom: Atom; - viewIcon: Atom; - viewName: Atom; - viewText: Atom; - hideViewName: Atom; - url: PrimitiveAtom; - homepageUrl: Atom; - urlInputFocused: PrimitiveAtom; - isLoading: PrimitiveAtom; - urlWrapperClassName: PrimitiveAtom; - refreshIcon: PrimitiveAtom; - webviewRef: React.RefObject; - urlInputRef: React.RefObject; - nodeModel: BlockNodeModel; - endIconButtons?: Atom; - mediaPlaying: PrimitiveAtom; - mediaMuted: PrimitiveAtom; - modifyExternalUrl?: (url: string) => string; - domReady: PrimitiveAtom; - hideNav: Atom; - searchAtoms?: SearchAtoms; - typeaheadOpen: PrimitiveAtom; - partitionOverride: PrimitiveAtom | null; - userAgentType: Atom; - env: WebViewEnv; - ctrlShiftUnsubFn: (() => void) | null = null; - - constructor({ blockId, nodeModel, tabModel, waveEnv }: ViewModelInitType) { - this.nodeModel = nodeModel; - this.tabModel = tabModel; - this.viewType = "web"; - this.blockId = blockId; - this.env = waveEnv; - this.noPadding = atom(true); - this.blockAtom = this.env.wos.getWaveObjectAtom(`block:${blockId}`); - this.url = atom(); - const defaultUrlAtom = this.env.getSettingsKeyAtom("web:defaulturl"); - this.homepageUrl = atom((get) => { - const defaultUrl = get(defaultUrlAtom); - const pinnedUrl = get(this.blockAtom)?.meta?.pinnedurl; - return pinnedUrl ?? defaultUrl; - }); - this.urlWrapperClassName = atom(""); - this.urlInputFocused = atom(false); - this.isLoading = atom(false); - this.refreshIcon = atom("rotate-right"); - this.viewIcon = atom("globe"); - this.viewName = atom("Web"); - this.hideViewName = atom(true); - this.urlInputRef = createRef(); - this.webviewRef = createRef(); - this.domReady = atom(false); - this.hideNav = this.env.getBlockMetaKeyAtom(blockId, "web:hidenav"); - this.typeaheadOpen = atom(false); - this.partitionOverride = null; - this.userAgentType = this.env.getBlockMetaKeyAtom(blockId, "web:useragenttype"); - - this.mediaPlaying = atom(false); - this.mediaMuted = atom(false); - - this.viewText = atom((get) => { - const homepageUrl = get(this.homepageUrl); - const metaUrl = get(this.blockAtom)?.meta?.url; - const currUrl = get(this.url); - const urlWrapperClassName = get(this.urlWrapperClassName); - const refreshIcon = get(this.refreshIcon); - const mediaPlaying = get(this.mediaPlaying); - const mediaMuted = get(this.mediaMuted); - const url = currUrl ?? metaUrl ?? homepageUrl ?? ""; - const rtn: HeaderElem[] = []; - if (get(this.hideNav)) { - return rtn; - } - - rtn.push({ - elemtype: "iconbutton", - icon: "chevron-left", - click: this.handleBack.bind(this), - disabled: this.shouldDisableBackButton(), - }); - rtn.push({ - elemtype: "iconbutton", - icon: "chevron-right", - click: this.handleForward.bind(this), - disabled: this.shouldDisableForwardButton(), - }); - rtn.push({ - elemtype: "iconbutton", - icon: "house", - click: this.handleHome.bind(this), - disabled: this.shouldDisableHomeButton(), - }); - const divChildren: HeaderElem[] = []; - divChildren.push({ - elemtype: "input", - value: url, - ref: this.urlInputRef, - className: "url-input", - onChange: this.handleUrlChange.bind(this), - onKeyDown: this.handleKeyDown.bind(this), - onFocus: this.handleFocus.bind(this), - onBlur: this.handleBlur.bind(this), - }); - if (mediaPlaying) { - divChildren.push({ - elemtype: "iconbutton", - icon: mediaMuted ? "volume-slash" : "volume", - click: this.handleMuteChange.bind(this), - }); - } - divChildren.push({ - elemtype: "iconbutton", - icon: refreshIcon, - click: this.handleRefresh.bind(this), - }); - rtn.push({ - elemtype: "div", - className: clsx("block-frame-div-url", urlWrapperClassName), - onMouseOver: this.handleUrlWrapperMouseOver.bind(this), - onMouseOut: this.handleUrlWrapperMouseOut.bind(this), - children: divChildren, - }); - return rtn; - }); - - this.endIconButtons = atom((get) => { - if (get(this.hideNav)) { - return null; - } - const url = get(this.url); - const userAgentType = get(this.userAgentType); - const buttons: IconButtonDecl[] = []; - - // Add mobile indicator icon if using mobile user agent - if (userAgentType === "mobile:iphone" || userAgentType === "mobile:android") { - const mobileIcon = userAgentType === "mobile:iphone" ? "mobile-screen" : "mobile-screen-button"; - const mobileTitle = - userAgentType === "mobile:iphone" ? "Mobile User Agent: iPhone" : "Mobile User Agent: Android"; - buttons.push({ - elemtype: "iconbutton", - icon: mobileIcon, - title: mobileTitle, - noAction: true, - }); - } - - buttons.push({ - elemtype: "iconbutton", - icon: "arrow-up-right-from-square", - title: "Open in External Browser", - click: () => { - console.log("open external", url); - if (url != null && url != "") { - const externalUrl = this.modifyExternalUrl?.(url) ?? url; - return this.env.electron.openExternal(externalUrl); - } - }, - }); - - return buttons; - }); - } - - dispose() { - this.ctrlShiftUnsubFn?.(); - this.ctrlShiftUnsubFn = null; - } - - get viewComponent(): ViewComponent { - return WebView; - } - - /** - * Whether the back button in the header should be disabled. - * @returns True if the WebView cannot go back or if the WebView call fails. False otherwise. - */ - shouldDisableBackButton() { - try { - return !this.webviewRef.current?.canGoBack(); - } catch (_) {} - return true; - } - - /** - * Whether the forward button in the header should be disabled. - * @returns True if the WebView cannot go forward or if the WebView call fails. False otherwise. - */ - shouldDisableForwardButton() { - try { - return !this.webviewRef.current?.canGoForward(); - } catch (_) {} - return true; - } - - /** - * Whether the home button in the header should be disabled. - * @returns True if the current url is the pinned url or the pinned url is not set. False otherwise. - */ - shouldDisableHomeButton() { - try { - const homepageUrl = globalStore.get(this.homepageUrl); - return !homepageUrl || this.getUrl() === homepageUrl; - } catch (_) {} - return true; - } - - handleHome(e?: React.MouseEvent) { - if (e) { - e.preventDefault(); - e.stopPropagation(); - } - this.loadUrl(globalStore.get(this.homepageUrl), "home"); - } - - setMediaPlaying(isPlaying: boolean) { - globalStore.set(this.mediaPlaying, isPlaying); - } - - handleMuteChange(e: React.ChangeEvent) { - if (e) { - e.preventDefault(); - e.stopPropagation(); - } - try { - const newMutedVal = !this.webviewRef.current?.isAudioMuted(); - globalStore.set(this.mediaMuted, newMutedVal); - this.webviewRef.current?.setAudioMuted(newMutedVal); - } catch (e) { - console.error("Failed to change mute value", e); - } - } - - setTypeaheadOpen(open: boolean) { - globalStore.set(this.typeaheadOpen, open); - } - - async fetchBookmarkSuggestions( - query: string, - reqContext: SuggestionRequestContext - ): Promise { - const result = await this.env.rpc.FetchSuggestionsCommand(TabRpcClient, { - suggestiontype: "bookmark", - query, - widgetid: reqContext.widgetid, - reqnum: reqContext.reqnum, - }); - return result; - } - - handleUrlWrapperMouseOver(e: React.MouseEvent) { - const urlInputFocused = globalStore.get(this.urlInputFocused); - if (e.type === "mouseover" && !urlInputFocused) { - globalStore.set(this.urlWrapperClassName, "hovered"); - } - } - - handleUrlWrapperMouseOut(e: React.MouseEvent) { - const urlInputFocused = globalStore.get(this.urlInputFocused); - if (e.type === "mouseout" && !urlInputFocused) { - globalStore.set(this.urlWrapperClassName, ""); - } - } - - handleBack(e?: React.MouseEvent) { - if (e) { - e.preventDefault(); - e.stopPropagation(); - } - this.webviewRef.current?.goBack(); - } - - handleForward(e?: React.MouseEvent) { - if (e) { - e.preventDefault(); - e.stopPropagation(); - } - this.webviewRef.current?.goForward(); - } - - handleRefresh(e: React.MouseEvent) { - e.preventDefault(); - e.stopPropagation(); - try { - if (this.webviewRef.current) { - if (globalStore.get(this.isLoading)) { - this.webviewRef.current.stop(); - } else { - this.webviewRef.current.reload(); - } - } - } catch (e) { - console.warn("handleRefresh catch", e); - } - } - - handleUrlChange(event: React.ChangeEvent) { - globalStore.set(this.url, event.target.value); - } - - handleKeyDown(event: React.KeyboardEvent) { - const waveEvent = adaptFromReactOrNativeKeyEvent(event); - if (checkKeyPressed(waveEvent, "Enter")) { - const url = globalStore.get(this.url); - this.loadUrl(url, "enter"); - this.urlInputRef.current?.blur(); - return; - } - if (checkKeyPressed(waveEvent, "Escape")) { - this.webviewRef.current?.focus(); - } - } - - handleFocus(event: React.FocusEvent) { - globalStore.set(this.urlWrapperClassName, "focused"); - globalStore.set(this.urlInputFocused, true); - this.urlInputRef.current.focus(); - event.target.select(); - } - - handleBlur(event: React.FocusEvent) { - globalStore.set(this.urlWrapperClassName, ""); - globalStore.set(this.urlInputFocused, false); - } - - /** - * Update the URL in the state when a navigation event has occurred. - * @param url The URL that has been navigated to. - */ - handleNavigate(url: string) { - fireAndForget(() => - this.env.rpc.SetMetaCommand(TabRpcClient, { - oref: makeORef("block", this.blockId), - meta: { url }, - }) - ); - globalStore.set(this.url, url); - if (this.searchAtoms) { - globalStore.set(this.searchAtoms.isOpen, false); - } - } - - ensureUrlScheme(url: string, searchTemplate: string) { - if (url == null) { - url = ""; - } - - if (/^(http|https|file):/.test(url)) { - // If the URL starts with http: or https:, return it as is - return url; - } - - // Check if the URL looks like a local URL - const isLocal = /^(localhost|(\d{1,3}\.){3}\d{1,3})(:\d+)?$/.test(url.split("/")[0]); - - if (isLocal) { - // If it is a local URL, ensure it has http:// scheme - return `http://${url}`; - } - - // Check if the URL looks like a domain - const domainRegex = /^[a-z0-9.-]+\.[a-z]{2,}$/i; - const isDomain = domainRegex.test(url.split("/")[0]); - - if (isDomain) { - // If it looks like a domain, ensure it has https:// scheme - return `https://${url}`; - } - - // Otherwise, treat it as a search query - if (searchTemplate == null) { - return `https://www.google.com/search?q=${encodeURIComponent(url)}`; - } - return searchTemplate.replace("{query}", encodeURIComponent(url)); - } - - /** - * Load a new URL in the webview. - * @param newUrl The new URL to load in the webview. - */ - loadUrl(newUrl: string, reason: string) { - const defaultSearchAtom = this.env.getSettingsKeyAtom("web:defaultsearch"); - const searchTemplate = globalStore.get(defaultSearchAtom); - const nextUrl = this.ensureUrlScheme(newUrl, searchTemplate); - console.log("webview loadUrl", reason, nextUrl, "cur=", this.webviewRef.current.getURL()); - if (!this.webviewRef.current) { - return; - } - if (this.webviewRef.current.getURL() != nextUrl) { - fireAndForget(() => this.webviewRef.current.loadURL(nextUrl)); - } - if (newUrl != nextUrl) { - globalStore.set(this.url, nextUrl); - } - } - - /** - * Load a new URL in the webview and return a promise. - * @param newUrl The new URL to load in the webview. - * @param reason The reason for loading the URL. - * @returns Promise that resolves when the URL is loaded. - */ - loadUrlPromise(newUrl: string, reason: string): Promise { - const defaultSearchAtom = this.env.getSettingsKeyAtom("web:defaultsearch"); - const searchTemplate = globalStore.get(defaultSearchAtom); - const nextUrl = this.ensureUrlScheme(newUrl, searchTemplate); - console.log("webview loadUrlPromise", reason, nextUrl, "cur=", this.webviewRef.current?.getURL()); - - if (!this.webviewRef.current) { - return Promise.reject(new Error("WebView ref not available")); - } - - if (newUrl != nextUrl) { - globalStore.set(this.url, nextUrl); - } - - if (this.webviewRef.current.getURL() != nextUrl) { - return this.webviewRef.current.loadURL(nextUrl); - } - - return Promise.resolve(); - } - - /** - * Get the current URL from the state. - * @returns The URL from the state. - */ - getUrl() { - return globalStore.get(this.url); - } - - setRefreshIcon(refreshIcon: string) { - globalStore.set(this.refreshIcon, refreshIcon); - } - - setIsLoading(isLoading: boolean) { - globalStore.set(this.isLoading, isLoading); - } - - async setHomepageUrl(url: string, scope: "global" | "block") { - if (url != null && url != "") { - switch (scope) { - case "block": - await this.env.rpc.SetMetaCommand(TabRpcClient, { - oref: makeORef("block", this.blockId), - meta: { pinnedurl: url }, - }); - break; - case "global": - await this.env.rpc.SetMetaCommand(TabRpcClient, { - oref: makeORef("block", this.blockId), - meta: { pinnedurl: null }, - }); - await this.env.rpc.SetConfigCommand(TabRpcClient, { "web:defaulturl": url }); - break; - } - } - } - - giveFocus(): boolean { - console.log("webview giveFocus"); - if (this.searchAtoms && globalStore.get(this.searchAtoms.isOpen)) { - console.log("search is open, not giving focus"); - return true; - } - const ctrlShiftState = globalStore.get(getSimpleControlShiftAtom()); - if (ctrlShiftState && !this.ctrlShiftUnsubFn) { - // this is really weird, we don't get keyup events from webview - this.ctrlShiftUnsubFn = globalStore.sub(getSimpleControlShiftAtom(), () => { - const state = globalStore.get(getSimpleControlShiftAtom()); - if (!state) { - this.ctrlShiftUnsubFn?.(); - this.ctrlShiftUnsubFn = null; - const isStillFocused = globalStore.get(this.nodeModel.isFocused); - if (isStillFocused) { - this.webviewRef.current?.focus(); - } - } - }); - } - if (ctrlShiftState) { - return false; - } - this.webviewRef.current?.focus(); - return true; - } - - copyUrlToClipboard() { - const url = this.getUrl(); - if (url != null && url != "") { - fireAndForget(() => navigator.clipboard.writeText(url)); - } - } - - clearHistory() { - try { - this.webviewRef.current?.clearHistory(); - } catch (e) { - console.error("Failed to clear history", e); - } - } - - async clearCookiesAndStorage() { - try { - const webContentsId = this.webviewRef.current?.getWebContentsId(); - if (webContentsId) { - await this.env.electron.clearWebviewStorage(webContentsId); - } - } catch (e) { - console.error("Failed to clear cookies and storage", e); - } - } - - keyDownHandler(e: WaveKeyboardEvent): boolean { - if (checkKeyPressed(e, "Cmd:l")) { - this.urlInputRef?.current?.focus(); - this.urlInputRef?.current?.select(); - return true; - } - if (checkKeyPressed(e, "Cmd:r")) { - this.webviewRef.current?.reload(); - return true; - } - if (checkKeyPressed(e, "Cmd:ArrowLeft")) { - this.handleBack(null); - return true; - } - if (checkKeyPressed(e, "Cmd:ArrowRight")) { - this.handleForward(null); - return true; - } - if (checkKeyPressed(e, "Cmd:o")) { - const curVal = globalStore.get(this.typeaheadOpen); - globalStore.set(this.typeaheadOpen, !curVal); - return true; - } - return false; - } - - setZoomFactor(factor: number | null) { - // null is ok (will reset to default) - if (factor != null && factor < 0.1) { - factor = 0.1; - } - if (factor != null && factor > 5) { - factor = 5; - } - const domReady = globalStore.get(this.domReady); - if (!domReady) { - return; - } - this.webviewRef.current?.setZoomFactor(factor || 1); - this.env.rpc.SetMetaCommand(TabRpcClient, { - oref: makeORef("block", this.blockId), - meta: { "web:zoom": factor }, // allow null so we can remove the zoom factor here - }); - } - - getSettingsMenuItems(): ContextMenuItem[] { - const zoomSubMenu: ContextMenuItem[] = []; - let curZoom = 1; - if (globalStore.get(this.domReady)) { - curZoom = this.webviewRef.current?.getZoomFactor() || 1; - } - const makeZoomFactorMenuItem = (label: string, factor: number): ContextMenuItem => { - return { - label: label, - type: "checkbox", - click: () => { - this.setZoomFactor(factor); - }, - checked: curZoom == factor, - }; - }; - zoomSubMenu.push({ - label: "Reset", - click: () => { - this.setZoomFactor(null); - }, - }); - zoomSubMenu.push(makeZoomFactorMenuItem("25%", 0.25)); - zoomSubMenu.push(makeZoomFactorMenuItem("50%", 0.5)); - zoomSubMenu.push(makeZoomFactorMenuItem("70%", 0.7)); - zoomSubMenu.push(makeZoomFactorMenuItem("80%", 0.8)); - zoomSubMenu.push(makeZoomFactorMenuItem("90%", 0.9)); - zoomSubMenu.push(makeZoomFactorMenuItem("100%", 1)); - zoomSubMenu.push(makeZoomFactorMenuItem("110%", 1.1)); - zoomSubMenu.push(makeZoomFactorMenuItem("120%", 1.2)); - zoomSubMenu.push(makeZoomFactorMenuItem("130%", 1.3)); - zoomSubMenu.push(makeZoomFactorMenuItem("150%", 1.5)); - zoomSubMenu.push(makeZoomFactorMenuItem("175%", 1.75)); - zoomSubMenu.push(makeZoomFactorMenuItem("200%", 2)); - - // User Agent Type submenu - const curUserAgentType = globalStore.get(this.userAgentType) || "default"; - const userAgentSubMenu: ContextMenuItem[] = [ - { - label: "Default", - type: "checkbox", - click: () => { - fireAndForget(() => { - return this.env.rpc.SetMetaCommand(TabRpcClient, { - oref: makeORef("block", this.blockId), - meta: { "web:useragenttype": null }, - }); - }); - }, - checked: curUserAgentType === "default" || curUserAgentType === "", - }, - { - label: "Mobile: iPhone", - type: "checkbox", - click: () => { - fireAndForget(() => { - return this.env.rpc.SetMetaCommand(TabRpcClient, { - oref: makeORef("block", this.blockId), - meta: { "web:useragenttype": "mobile:iphone" }, - }); - }); - }, - checked: curUserAgentType === "mobile:iphone", - }, - { - label: "Mobile: Android", - type: "checkbox", - click: () => { - fireAndForget(() => { - return this.env.rpc.SetMetaCommand(TabRpcClient, { - oref: makeORef("block", this.blockId), - meta: { "web:useragenttype": "mobile:android" }, - }); - }); - }, - checked: curUserAgentType === "mobile:android", - }, - ]; - - const isNavHidden = globalStore.get(this.hideNav); - return [ - { - label: "Copy URL to Clipboard", - click: () => this.copyUrlToClipboard(), - }, - { - label: "Set Block Homepage", - click: () => fireAndForget(() => this.setHomepageUrl(this.getUrl(), "block")), - }, - { - label: "Set Default Homepage", - click: () => fireAndForget(() => this.setHomepageUrl(this.getUrl(), "global")), - }, - { - type: "separator", - }, - { - label: "User Agent Type", - submenu: userAgentSubMenu, - }, - { - type: "separator", - }, - { - label: isNavHidden ? "Un-Hide Navigation" : "Hide Navigation", - click: () => - fireAndForget(() => { - return this.env.rpc.SetMetaCommand(TabRpcClient, { - oref: makeORef("block", this.blockId), - meta: { "web:hidenav": !isNavHidden }, - }); - }), - }, - { - label: "Set Zoom Factor", - submenu: zoomSubMenu, - }, - { - label: this.webviewRef.current?.isDevToolsOpened() ? "Close DevTools" : "Open DevTools", - click: () => { - if (this.webviewRef.current) { - if (this.webviewRef.current.isDevToolsOpened()) { - this.webviewRef.current.closeDevTools(); - } else { - this.webviewRef.current.openDevTools(); - } - } - }, - }, - { - type: "separator", - }, - { - label: "Clear History", - click: () => this.clearHistory(), - }, - { - label: "Clear Cookies and Storage (All Web Widgets)", - click: () => fireAndForget(() => this.clearCookiesAndStorage()), - }, - ]; - } -} - -const BookmarkTypeahead = memo( - ({ model, blockRef }: { model: WebViewModel; blockRef: React.RefObject }) => { - const env = useWaveEnv(); - const openBookmarksJson = () => { - fireAndForget(async () => { - const path = `${env.electron.getConfigDir()}/presets/bookmarks.json`; - const blockDef: BlockDef = { - meta: { - view: "preview", - file: path, - }, - }; - await env.createBlock(blockDef, false, true); - model.setTypeaheadOpen(false); - }); - }; - return ( - model.setTypeaheadOpen(false)} - onSelect={(suggestion) => { - if (suggestion == null || suggestion.type != "url") { - return true; - } - model.loadUrl(suggestion["url:url"], "bookmark-typeahead"); - return true; - }} - fetchSuggestions={model.fetchBookmarkSuggestions} - placeholderText="Open Bookmark..." - > - -
-

No Bookmarks Configured

-

- Edit your bookmarks.json file to configure bookmarks. -

- -
-
- - -
-

No matching bookmarks

- -
-
-
- ); - } -); - -interface WebViewProps { - blockId: string; - model: WebViewModel; - onFailLoad?: (url: string) => void; - blockRef: React.RefObject; - contentRef: React.RefObject; - initialSrc?: string; -} - -function getWebPreviewDisplayUrl(url?: string | null): string { - return url?.trim() || "about:blank"; -} - -function WebViewPreviewFallback({ url }: { url?: string | null }) { - const displayUrl = getWebPreviewDisplayUrl(url); - - return ( -
-
-
preview mock · electron webview unavailable
-
web widget placeholder
-
- {displayUrl} -
-
-
- ); -} - -const WebView = memo(({ model, onFailLoad, blockRef, initialSrc }: WebViewProps) => { - const env = useWaveEnv(); - const blockData = useAtomValue(model.blockAtom); - const defaultUrl = useAtomValue(model.homepageUrl); - const defaultSearchAtom = env.getSettingsKeyAtom("web:defaultsearch"); - const defaultSearch = useAtomValue(defaultSearchAtom); - let metaUrl = blockData?.meta?.url || defaultUrl || ""; - if (metaUrl) { - metaUrl = model.ensureUrlScheme(metaUrl, defaultSearch); - } - const metaUrlRef = useRef(metaUrl); - const zoomFactor = useAtomValue(env.getBlockMetaKeyAtom(model.blockId, "web:zoom")) || 1; - const partitionOverride = useAtomValueSafe(model.partitionOverride); - const metaPartition = useAtomValue(env.getBlockMetaKeyAtom(model.blockId, "web:partition")); - const webPartition = partitionOverride || metaPartition || undefined; - const userAgentType = useAtomValue(model.userAgentType) || "default"; - - // Determine user agent string based on type - let userAgent: string | undefined = undefined; - if (userAgentType === "mobile:iphone") { - userAgent = USER_AGENT_IPHONE; - } else if (userAgentType === "mobile:android") { - userAgent = USER_AGENT_ANDROID; - } - - // Search - const searchProps = useSearch({ anchorRef: model.webviewRef, viewModel: model }); - const searchVal = useAtomValue(searchProps.searchValue); - const setSearchIndex = useSetAtom(searchProps.resultsIndex); - const setNumSearchResults = useSetAtom(searchProps.resultsCount); - searchProps.onSearch = useCallback((search: string) => { - if (!globalStore.get(model.domReady)) { - return; - } - try { - if (search) { - model.webviewRef.current?.findInPage(search, { findNext: true }); - } else { - model.webviewRef.current?.stopFindInPage("clearSelection"); - } - } catch (e) { - console.error("Failed to search", e); - } - }, []); - searchProps.onNext = useCallback(() => { - if (!globalStore.get(model.domReady)) { - return; - } - try { - console.log("search next", searchVal); - model.webviewRef.current?.findInPage(searchVal, { findNext: false, forward: true }); - } catch (e) { - console.error("Failed to search next", e); - } - }, [searchVal]); - searchProps.onPrev = useCallback(() => { - if (!globalStore.get(model.domReady)) { - return; - } - try { - console.log("search prev", searchVal); - model.webviewRef.current?.findInPage(searchVal, { findNext: false, forward: false }); - } catch (e) { - console.error("Failed to search prev", e); - } - }, [searchVal]); - const onFoundInPage = useCallback((event: any) => { - const result = event.result; - console.log("found in page", result); - if (!result) { - return; - } - setNumSearchResults(result.matches); - setSearchIndex(result.activeMatchOrdinal - 1); - }, []); - // End Search - - // The initial value of the block metadata URL when the component first renders. Used to set the starting src value for the webview. - const [metaUrlInitial] = useState(initialSrc || metaUrl); - const prevUserAgentTypeRef = useRef(userAgentType); - - const [webContentsId, setWebContentsId] = useState(null); - const domReady = useAtomValue(model.domReady); - - const [errorText, setErrorText] = useState(""); - - function setBgColor() { - const webview = model.webviewRef.current; - if (!webview) { - return; - } - setTimeout(() => { - webview - .executeJavaScript( - `!!document.querySelector('meta[name="color-scheme"]') && document.querySelector('meta[name="color-scheme"]').content?.includes('dark') || false` - ) - .then((hasDarkMode) => { - if (hasDarkMode) { - webview.style.backgroundColor = "black"; // Dark mode background - } else { - webview.style.backgroundColor = "white"; // Light mode background - } - }) - .catch((e) => { - webview.style.backgroundColor = "black"; // Dark mode background - console.log("Error getting color scheme, defaulting to dark", e); - }); - }, 100); - } - - useLayoutEffect(() => { - return () => { - const webview = model.webviewRef.current; - if (webview?.isDevToolsOpened()) { - webview.closeDevTools(); - } - }; - }, []); - - useEffect(() => { - return () => { - globalStore.set(model.domReady, false); - }; - }, []); - - useEffect(() => { - if (model.webviewRef.current == null || !domReady) { - return; - } - try { - const wcId = model.webviewRef.current.getWebContentsId?.(); - if (wcId) { - setWebContentsId(wcId); - if (model.webviewRef.current.getZoomFactor() != zoomFactor) { - model.webviewRef.current.setZoomFactor(zoomFactor); - } - } - } catch (e) { - console.error("Failed to get webcontentsid / setzoomlevel (webview)", e); - } - }, [model.webviewRef.current, domReady, zoomFactor]); - - // Load a new URL if the block metadata is updated. - useEffect(() => { - if (initialSrc) { - // Skip URL loading if initialSrc is provided (it's already loaded via src attribute) - return; - } - if (metaUrlRef.current != metaUrl) { - metaUrlRef.current = metaUrl; - model.loadUrl(metaUrl, "meta"); - } - }, [metaUrl, initialSrc]); - - // Reload webview when user agent type changes - useEffect(() => { - if (prevUserAgentTypeRef.current !== userAgentType && domReady && model.webviewRef.current) { - let newUserAgent: string | undefined = undefined; - if (userAgentType === "mobile:iphone") { - newUserAgent = USER_AGENT_IPHONE; - } else if (userAgentType === "mobile:android") { - newUserAgent = USER_AGENT_ANDROID; - } - - if (newUserAgent) { - model.webviewRef.current.setUserAgent(newUserAgent); - } else { - model.webviewRef.current.setUserAgent(""); - } - model.webviewRef.current.reload(); - } - prevUserAgentTypeRef.current = userAgentType; - }, [userAgentType, domReady]); - - useEffect(() => { - const webview = model.webviewRef.current; - if (!webview) { - return; - } - const navigateListener = (e: any) => { - setErrorText(""); - if (e.isMainFrame) { - model.handleNavigate(e.url); - } - }; - const newWindowHandler = (e: any) => { - e.preventDefault(); - const newUrl = e.detail.url; - fireAndForget(() => openLink(newUrl, true)); - }; - const startLoadingHandler = () => { - model.setRefreshIcon("xmark-large"); - model.setIsLoading(true); - webview.style.backgroundColor = "transparent"; - }; - const stopLoadingHandler = () => { - model.setRefreshIcon("rotate-right"); - model.setIsLoading(false); - setBgColor(); - }; - const failLoadHandler = (e: any) => { - if (e.errorCode === -3) { - console.warn("Suppressed ERR_ABORTED error", e); - } else { - const errorMessage = `Failed to load ${e.validatedURL}: ${e.errorDescription}`; - console.error(errorMessage); - setErrorText(errorMessage); - if (onFailLoad) { - const curUrl = model.webviewRef.current.getURL(); - onFailLoad(curUrl); - } - } - }; - const webviewFocus = () => { - env.electron.setWebviewFocus(webview.getWebContentsId()); - model.nodeModel.focusNode(); - }; - const webviewBlur = () => { - env.electron.setWebviewFocus(null); - }; - const handleDomReady = () => { - globalStore.set(model.domReady, true); - setBgColor(); - }; - const handleMediaPlaying = () => { - model.setMediaPlaying(true); - }; - const handleMediaPaused = () => { - model.setMediaPlaying(false); - }; - - webview.addEventListener("did-frame-navigate", navigateListener); - webview.addEventListener("did-navigate-in-page", navigateListener); - webview.addEventListener("did-navigate", navigateListener); - webview.addEventListener("did-start-loading", startLoadingHandler); - webview.addEventListener("did-stop-loading", stopLoadingHandler); - webview.addEventListener("new-window", newWindowHandler); - webview.addEventListener("did-fail-load", failLoadHandler); - webview.addEventListener("focus", webviewFocus); - webview.addEventListener("blur", webviewBlur); - webview.addEventListener("dom-ready", handleDomReady); - webview.addEventListener("media-started-playing", handleMediaPlaying); - webview.addEventListener("media-paused", handleMediaPaused); - webview.addEventListener("found-in-page", onFoundInPage); - - // Clean up event listeners on component unmount - return () => { - webview.removeEventListener("did-frame-navigate", navigateListener); - webview.removeEventListener("did-navigate", navigateListener); - webview.removeEventListener("did-navigate-in-page", navigateListener); - webview.removeEventListener("new-window", newWindowHandler); - webview.removeEventListener("did-fail-load", failLoadHandler); - webview.removeEventListener("did-start-loading", startLoadingHandler); - webview.removeEventListener("did-stop-loading", stopLoadingHandler); - webview.removeEventListener("focus", webviewFocus); - webview.removeEventListener("blur", webviewBlur); - webview.removeEventListener("dom-ready", handleDomReady); - webview.removeEventListener("media-started-playing", handleMediaPlaying); - webview.removeEventListener("media-paused", handleMediaPaused); - webview.removeEventListener("found-in-page", onFoundInPage); - }; - }, []); - - return ( - - }> - - - {errorText && ( -
-
{errorText}
-
- )} - - -
- ); -}); - -export { WebView, WebViewPreviewFallback, getWebPreviewDisplayUrl }; diff --git a/frontend/app/view/webview/webviewenv.ts b/frontend/app/view/webview/webviewenv.ts deleted file mode 100644 index 419b04c4eb..0000000000 --- a/frontend/app/view/webview/webviewenv.ts +++ /dev/null @@ -1,25 +0,0 @@ -// Copyright 2026, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import type { MetaKeyAtomFnType, SettingsKeyAtomFnType, WaveEnv, WaveEnvSubset } from "@/app/waveenv/waveenv"; - -export type WebViewEnv = WaveEnvSubset<{ - electron: { - openExternal: WaveEnv["electron"]["openExternal"]; - getWebviewPreload: WaveEnv["electron"]["getWebviewPreload"]; - clearWebviewStorage: WaveEnv["electron"]["clearWebviewStorage"]; - getConfigDir: WaveEnv["electron"]["getConfigDir"]; - setWebviewFocus: WaveEnv["electron"]["setWebviewFocus"]; - }; - rpc: { - FetchSuggestionsCommand: WaveEnv["rpc"]["FetchSuggestionsCommand"]; - SetMetaCommand: WaveEnv["rpc"]["SetMetaCommand"]; - SetConfigCommand: WaveEnv["rpc"]["SetConfigCommand"]; - }; - wos: WaveEnv["wos"]; - createBlock: WaveEnv["createBlock"]; - getSettingsKeyAtom: SettingsKeyAtomFnType<"web:defaulturl" | "web:defaultsearch">; - getBlockMetaKeyAtom: MetaKeyAtomFnType< - "web:hidenav" | "web:useragenttype" | "web:zoom" | "web:partition" - >; -}>; diff --git a/frontend/app/workspace/widgets.tsx b/frontend/app/workspace/widgets.tsx index f11eca91da..dbac57effb 100644 --- a/frontend/app/workspace/widgets.tsx +++ b/frontend/app/workspace/widgets.tsx @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 import { Tooltip } from "@/app/element/tooltip"; +import { getApi } from "@/app/store/global"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { useWaveEnv, WaveEnv, WaveEnvSubset } from "@/app/waveenv/waveenv"; import { shouldIncludeWidgetForWorkspace } from "@/app/workspace/widgetfilter"; @@ -22,12 +23,6 @@ import { memo, useCallback, useEffect, useRef, useState } from "react"; export type WidgetsEnv = WaveEnvSubset<{ isDev: WaveEnv["isDev"]; - electron: { - openBuilder: WaveEnv["electron"]["openBuilder"]; - }; - rpc: { - ListAllAppsCommand: WaveEnv["rpc"]["ListAllAppsCommand"]; - }; atoms: { fullConfigAtom: WaveEnv["atoms"]["fullConfigAtom"]; hasConfigErrors: WaveEnv["atoms"]["hasConfigErrors"]; @@ -100,14 +95,6 @@ const Widget = memo(({ widget, mode, env }: WidgetPropsType) => { ); }); -function calculateGridSize(appCount: number): number { - if (appCount <= 4) return 2; - if (appCount <= 9) return 3; - if (appCount <= 16) return 4; - if (appCount <= 25) return 5; - return 6; -} - function SettingsTooltipContent({ hasConfigErrors }: { hasConfigErrors: boolean }) { if (!hasConfigErrors) { return "Settings & Help"; @@ -130,129 +117,6 @@ type FloatingWindowPropsType = { hasConfigErrors?: boolean; }; -const AppsFloatingWindow = memo(({ isOpen, onClose, referenceElement }: FloatingWindowPropsType) => { - const [apps, setApps] = useState([]); - const [loading, setLoading] = useState(true); - const env = useWaveEnv(); - - const { refs, floatingStyles, context } = useFloating({ - open: isOpen, - onOpenChange: onClose, - placement: "left-start", - middleware: [offset(-2), shift({ padding: 12 })], - whileElementsMounted: autoUpdate, - elements: { - reference: referenceElement, - }, - }); - - const dismiss = useDismiss(context); - const { getFloatingProps } = useInteractions([dismiss]); - const handleOpenBuilder = useCallback(() => { - env.electron.openBuilder(null); - onClose(); - }, [onClose, env]); - - useEffect(() => { - if (!isOpen) return; - - const fetchApps = async () => { - setLoading(true); - try { - const allApps = await env.rpc.ListAllAppsCommand(TabRpcClient); - const localApps = allApps - .filter((app) => !app.appid.startsWith("draft/")) - .sort((a, b) => { - const aName = a.appid.replace(/^local\//, ""); - const bName = b.appid.replace(/^local\//, ""); - return aName.localeCompare(bName); - }); - setApps(localApps); - } catch (error) { - console.error("Failed to fetch apps:", error); - setApps([]); - } finally { - setLoading(false); - } - }; - - fetchApps(); - }, [isOpen]); - - if (!isOpen) return null; - - const gridSize = calculateGridSize(apps.length); - - return ( - -
-
- {loading ? ( -
- -
- ) : apps.length === 0 ? ( -
No local apps found
- ) : ( -
- {apps.map((app) => { - const appMeta = app.manifest?.appmeta; - const displayName = app.appid.replace(/^local\//, ""); - const icon = appMeta?.icon || "cube"; - const iconColor = appMeta?.iconcolor || "white"; - - return ( -
{ - const blockDef: BlockDef = { - meta: { - view: "tsunami", - controller: "tsunami", - "tsunami:appid": app.appid, - }, - }; - env.createBlock(blockDef); - onClose(); - }} - > -
- -
-
- {displayName} -
-
- ); - })} -
- )} -
- -
-
- ); -}); - const SettingsFloatingWindow = memo( ({ isOpen, onClose, referenceElement, hasConfigErrors }: FloatingWindowPropsType) => { const env = useWaveEnv(); @@ -326,12 +190,7 @@ const SettingsFloatingWindow = memo( icon: "circle-question", label: "Help", onClick: () => { - const blockDef: BlockDef = { - meta: { - view: "help", - }, - }; - env.createBlock(blockDef); + getApi().openExternal("https://docs.waveterm.dev/?ref=app"); onClose(); }, }, @@ -377,15 +236,12 @@ const Widgets = memo(() => { const containerRef = useRef(null); const measurementRef = useRef(null); - const featureWaveAppBuilder = fullConfig?.settings?.["feature:waveappbuilder"] ?? false; const widgetsMap = fullConfig?.widgets ?? {}; const filteredWidgets = Object.fromEntries( Object.entries(widgetsMap).filter(([_key, widget]) => shouldIncludeWidgetForWorkspace(widget, workspaceId)) ); const widgets = sortByDisplayOrder(filteredWidgets); - const [isAppsOpen, setIsAppsOpen] = useState(false); - const appsButtonRef = useRef(null); const [isSettingsOpen, setIsSettingsOpen] = useState(false); const settingsButtonRef = useRef(null); @@ -471,19 +327,6 @@ const Widgets = memo(() => {
- {env.isDev() || featureWaveAppBuilder ? ( -
setIsAppsOpen(!isAppsOpen)} - > - -
- -
-
-
- ) : null}
{ ))}
- {env.isDev() || featureWaveAppBuilder ? ( -
setIsAppsOpen(!isAppsOpen)} - > - -
-
- -
- {mode === "normal" && ( -
- apps -
- )} -
-
-
- ) : null}
{
) : null}
- {(env.isDev() || featureWaveAppBuilder) && appsButtonRef.current && ( - setIsAppsOpen(false)} - referenceElement={appsButtonRef.current} - /> - )} {settingsButtonRef.current && ( {
settings
- {env.isDev() ? ( -
-
- -
-
apps
-
- ) : null} {env.isDev() ? (
Promise }) { - const [newAppName, setNewAppName] = useState(""); - const [inputError, setInputError] = useState(""); - const [isCreating, setIsCreating] = useState(false); - - const validateAppName = (name: string) => { - if (!name.trim()) { - setInputError(""); - return false; - } - if (name.length > MaxAppNameLength) { - setInputError(`Name must be ${MaxAppNameLength} characters or less`); - return false; - } - if (!AppNameRegex.test(name)) { - setInputError("Only letters, numbers, hyphens, and underscores allowed"); - return false; - } - setInputError(""); - return true; - }; - - const handleCreate = async () => { - const trimmedName = newAppName.trim(); - if (!validateAppName(trimmedName)) { - return; - } - - setIsCreating(true); - try { - await onCreateApp(trimmedName); - } finally { - setIsCreating(false); - } - }; - - return ( -
-

Create New WaveApp

-
-
- { - const value = e.target.value; - setNewAppName(value); - validateAppName(value); - }} - onKeyDown={(e) => { - if (e.key === "Enter" && !e.nativeEvent.isComposing && newAppName.trim() && !inputError) { - handleCreate(); - } - }} - placeholder="my-app" - maxLength={MaxAppNameLength} - className={`flex-1 px-3 py-2 bg-panel border rounded-l focus:outline-none transition-colors ${ - inputError ? "border-error" : "border-border focus:border-accent" - }`} - autoFocus - disabled={isCreating} - /> - -
- {inputError && ( -
- - {inputError} -
- )} -
-
- ); -} - -export function AppSelectionModal() { - const [apps, setApps] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(""); - - useEffect(() => { - loadApps(); - }, []); - - const loadApps = async () => { - try { - const appList = await RpcApi.ListAllEditableAppsCommand(TabRpcClient); - const sortedApps = (appList || []).sort((a, b) => b.modtime - a.modtime); - setApps(sortedApps); - } catch (err) { - console.error("Failed to load apps:", err); - setError("Failed to load apps"); - } finally { - setLoading(false); - } - }; - - const handleSelectApp = async (appId: string) => { - let appIdToUse = appId; - - // If selecting a local app, convert it to a draft first - if (appId.startsWith("local/")) { - try { - const result = await RpcApi.MakeDraftFromLocalCommand(TabRpcClient, { localappid: appId }); - appIdToUse = result.draftappid; - } catch (err) { - console.error("Failed to create draft from local app:", err); - setError(`Failed to create draft from ${appId}: ${err.message || String(err)}`); - return; - } - } - - const builderId = globalStore.get(atoms.builderId); - const oref = WOS.makeORef("builder", builderId); - await RpcApi.SetRTInfoCommand(TabRpcClient, { - oref, - data: { "builder:appid": appIdToUse }, - }); - globalStore.set(atoms.builderAppId, appIdToUse); - document.title = `WaveApp Builder (${appIdToUse})`; - getApi().setBuilderWindowAppId(appIdToUse); - }; - - const handleCreateNew = async (appName: string) => { - const draftAppId = `draft/${appName}`; - const builderId = globalStore.get(atoms.builderId); - const oref = WOS.makeORef("builder", builderId); - await RpcApi.SetRTInfoCommand(TabRpcClient, { - oref, - data: { "builder:appid": draftAppId }, - }); - globalStore.set(atoms.builderAppId, draftAppId); - document.title = `WaveApp Builder (${draftAppId})`; - getApi().setBuilderWindowAppId(draftAppId); - }; - - const isDraftApp = (appId: string) => { - return appId.startsWith("draft/"); - }; - - const getAppDisplayName = (appId: string) => { - const parts = appId.split("/"); - if (parts.length === 2) { - const isDraft = parts[0] === "draft"; - return isDraft ? `${parts[1]} (draft)` : parts[1]; - } - return appId; - }; - - if (loading) { - return ( - -
Loading apps...
-
- ); - } - - return ( - -
-

Select a WaveApp to Edit

- - {error && ( -
-
- - {error} -
-
- )} - - {apps.length > 0 && ( -
-

Existing WaveApps

-
- {apps.map((appInfo) => ( - - ))} -
-
- )} - - {apps.length > 0 && ( -
-
- or -
-
- )} - - -
-
- ); -} diff --git a/frontend/builder/builder-app.tsx b/frontend/builder/builder-app.tsx deleted file mode 100644 index 447a16f7c0..0000000000 --- a/frontend/builder/builder-app.tsx +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2026, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { ModalsRenderer } from "@/app/modals/modalsrenderer"; -import { globalStore } from "@/app/store/jotaiStore"; -import { WaveEnvContext } from "@/app/waveenv/waveenv"; -import { makeWaveEnvImpl } from "@/app/waveenv/waveenvimpl"; -import { AppSelectionModal } from "@/builder/app-selection-modal"; -import { BuilderWorkspace } from "@/builder/builder-workspace"; -import { atoms, isDev } from "@/store/global"; -import { appHandleKeyDown } from "@/store/keymodel"; -import * as keyutil from "@/util/keyutil"; -import { isBlank } from "@/util/util"; -import { Provider, useAtomValue } from "jotai"; -import { useEffect, useRef } from "react"; -import { DndProvider } from "react-dnd"; -import { HTML5Backend } from "react-dnd-html5-backend"; - -type BuilderAppProps = { - initOpts: BuilderInitOpts; - onFirstRender: () => void; -}; - -const BuilderKeyHandlers = () => { - useEffect(() => { - const staticKeyDownHandler = keyutil.keydownWrapper(appHandleKeyDown); - document.addEventListener("keydown", staticKeyDownHandler); - - return () => { - document.removeEventListener("keydown", staticKeyDownHandler); - }; - }, []); - return null; -}; - -function BuilderAppInner() { - const builderAppId = useAtomValue(atoms.builderAppId); - const hasDraftApp = !isBlank(builderAppId) && builderAppId.startsWith("draft/"); - - return ( -
- -
- {isDev() ? ( -
- -
- ) : null} -
- WaveApp Builder{!isBlank(builderAppId) && ` (${builderAppId})`} -
-
- - {hasDraftApp ? : } - - -
- ); -} - -export function BuilderApp({ initOpts, onFirstRender }: BuilderAppProps) { - const waveEnvRef = useRef(makeWaveEnvImpl()); - useEffect(() => { - onFirstRender(); - }, []); - - return ( - - - - - - ); -} diff --git a/frontend/builder/builder-apppanel.tsx b/frontend/builder/builder-apppanel.tsx deleted file mode 100644 index 3c7e08f0f8..0000000000 --- a/frontend/builder/builder-apppanel.tsx +++ /dev/null @@ -1,400 +0,0 @@ -// Copyright 2025, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { Modal } from "@/app/modals/modal"; -import { ContextMenuModel } from "@/app/store/contextmenu"; -import { modalsModel } from "@/app/store/modalmodel"; -import { RpcApi } from "@/app/store/wshclientapi"; -import { TabRpcClient } from "@/app/store/wshrpcutil"; -import { BuilderAppPanelModel, type TabType } from "@/builder/store/builder-apppanel-model"; -import { BuilderFocusManager } from "@/builder/store/builder-focusmanager"; -import { BuilderCodeTab } from "@/builder/tabs/builder-codetab"; -import { BuilderConfigDataTab } from "@/builder/tabs/builder-configdatatab"; -import { BuilderFilesTab, DeleteFileModal, RenameFileModal } from "@/builder/tabs/builder-filestab"; -import { BuilderPreviewTab } from "@/builder/tabs/builder-previewtab"; -import { BuilderSecretTab } from "@/builder/tabs/builder-secrettab"; -import { builderAppHasSelection } from "@/builder/utils/builder-focus-utils"; -import { ErrorBoundary } from "@/element/errorboundary"; -import { atoms } from "@/store/global"; -import { cn } from "@/util/util"; -import { useAtomValue } from "jotai"; -import { memo, useCallback, useEffect, useRef, useState } from "react"; - -const StatusDot = memo(() => { - const model = BuilderAppPanelModel.getInstance(); - const builderStatus = useAtomValue(model.builderStatusAtom); - - const getStatusDotColor = (status: string | null | undefined): string => { - if (!status) return "bg-gray-500"; - switch (status) { - case "init": - case "stopped": - return "bg-gray-500"; - case "building": - return "bg-warning"; - case "running": - return "bg-success"; - case "error": - return "bg-error"; - default: - return "bg-gray-500"; - } - }; - - const statusDotColor = getStatusDotColor(builderStatus?.status); - - return ; -}); - -StatusDot.displayName = "StatusDot"; - -type TabButtonProps = { - label: string; - tabType: TabType; - isActive: boolean; - isAppFocused: boolean; - onClick: () => void; - showStatusDot?: boolean; -}; - -const TabButton = memo(({ label, tabType, isActive, isAppFocused, onClick, showStatusDot }: TabButtonProps) => { - return ( - - ); -}); - -TabButton.displayName = "TabButton"; - -const ErrorStrip = memo(() => { - const model = BuilderAppPanelModel.getInstance(); - const errorMsg = useAtomValue(model.errorAtom); - - if (!errorMsg) return null; - return ( -
-
- - {errorMsg} -
- -
- ); -}); - -ErrorStrip.displayName = "ErrorStrip"; - -const PublishAppModal = memo(({ appName }: { appName: string }) => { - const builderAppId = useAtomValue(atoms.builderAppId); - const [state, setState] = useState<"confirm" | "success" | "error">("confirm"); - const [errorMessage, setErrorMessage] = useState(""); - const [publishedAppId, setPublishedAppId] = useState(""); - - const handlePublish = async () => { - if (!builderAppId) { - setErrorMessage("No builder app ID found"); - setState("error"); - return; - } - - try { - const result = await RpcApi.PublishAppCommand(TabRpcClient, { appid: builderAppId }); - setPublishedAppId(result.publishedappid); - setState("success"); - } catch (error) { - setErrorMessage(error instanceof Error ? error.message : String(error)); - setState("error"); - } - }; - - const handleClose = () => { - modalsModel.popModal(); - }; - - if (state === "success") { - return ( - -
-

- - App Published Successfully -

-
-

- Your app has been published to {publishedAppId} -

-
-
-
- ); - } - - if (state === "error") { - return ( - -
-

- - Publish Failed -

-
-

{errorMessage}

-
-
-
- ); - } - - return ( - -
-

Publish App

-
-

- This will publish your app to local/{appName} -

-

- - This will overwrite any existing app with the same name. Are you sure? -

-
-
-
- ); -}); - -PublishAppModal.displayName = "PublishAppModal"; - -const BuilderAppPanel = memo(() => { - const model = BuilderAppPanelModel.getInstance(); - const focusElemRef = useRef(null); - const activeTab = useAtomValue(model.activeTab); - const focusType = useAtomValue(BuilderFocusManager.getInstance().focusType); - const isAppFocused = focusType === "app"; - const builderAppId = useAtomValue(atoms.builderAppId); - const builderId = useAtomValue(atoms.builderId); - const hasSecrets = useAtomValue(model.hasSecretsAtom); - - useEffect(() => { - model.initialize(); - }, []); - - if (focusElemRef.current) { - model.setFocusElemRef(focusElemRef.current); - } - - const handleTabClick = (tab: TabType) => { - model.setActiveTab(tab); - BuilderFocusManager.getInstance().setAppFocused(); - model.giveFocus(); - }; - - const handleFocusCapture = useCallback((event: React.FocusEvent) => { - BuilderFocusManager.getInstance().setAppFocused(); - }, []); - - const handlePanelClick = useCallback( - (e: React.MouseEvent) => { - const target = e.target as HTMLElement; - const isInteractive = target.closest('button, a, input, textarea, select, [role="button"], [tabindex]'); - - if (isInteractive) { - return; - } - - const hasSelection = builderAppHasSelection(); - if (hasSelection) { - BuilderFocusManager.getInstance().setAppFocused(); - return; - } - - setTimeout(() => { - if (!builderAppHasSelection()) { - BuilderFocusManager.getInstance().setAppFocused(); - model.giveFocus(); - } - }, 0); - }, - [model] - ); - - const handleRestart = useCallback(() => { - model.restartBuilder(); - }, [model]); - - const handlePublishClick = useCallback(() => { - if (!builderAppId) return; - const appName = builderAppId.replace("draft/", ""); - modalsModel.pushModal("PublishAppModal", { appName }); - }, [builderAppId]); - - const handleSwitchAppClick = useCallback(() => { - model.switchBuilderApp(); - }, [model]); - - const handleOpenDevToolsClick = useCallback(() => { - model.openPreviewDevTools(); - }, [model]); - - const handleKebabClick = useCallback( - (e: React.MouseEvent) => { - const menu: ContextMenuItem[] = [ - { - label: "Publish App", - click: handlePublishClick, - }, - { - type: "separator", - }, - { - label: "Open DevTools", - click: handleOpenDevToolsClick, - }, - { - type: "separator", - }, - { - label: "Switch App", - click: handleSwitchAppClick, - }, - ]; - ContextMenuModel.getInstance().showContextMenu(menu, e); - }, - [handleSwitchAppClick, handlePublishClick, handleOpenDevToolsClick] - ); - - return ( -
-
- {}} - /> -
-
-
-
- handleTabClick("preview")} - showStatusDot={true} - /> - handleTabClick("code")} - /> - handleTabClick("configdata")} - /> - handleTabClick("files")} - /> - {hasSecrets && ( - handleTabClick("secrets")} - /> - )} -
-
- - -
-
-
- -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
- - - -
-
-
- ); -}); - -BuilderAppPanel.displayName = "BuilderAppPanel"; - -export { BuilderAppPanel, DeleteFileModal, PublishAppModal, RenameFileModal }; diff --git a/frontend/builder/builder-buildpanel.tsx b/frontend/builder/builder-buildpanel.tsx deleted file mode 100644 index 811747e720..0000000000 --- a/frontend/builder/builder-buildpanel.tsx +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright 2025, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { WaveAIModel } from "@/app/aipanel/waveai-model"; -import { ContextMenuModel } from "@/app/store/contextmenu"; -import { globalStore } from "@/app/store/jotaiStore"; -import { BuilderAppPanelModel } from "@/builder/store/builder-apppanel-model"; -import { BuilderBuildPanelModel } from "@/builder/store/builder-buildpanel-model"; -import { useAtomValue } from "jotai"; -import { memo, useCallback, useEffect, useRef } from "react"; -import { debounce } from "throttle-debounce"; - -function handleBuildPanelContextMenu(e: React.MouseEvent, selectedText: string): void { - e.preventDefault(); - e.stopPropagation(); - - if (!selectedText) { - return; - } - - const menu: ContextMenuItem[] = [ - { role: "copy" }, - { type: "separator" }, - { - label: "Add to Context", - click: () => { - const model = WaveAIModel.getInstance(); - const formattedText = `from builder output:\n\`\`\`\n${selectedText}\n\`\`\``; - model.appendText(formattedText, true); - model.focusInput(); - }, - }, - ]; - ContextMenuModel.getInstance().showContextMenu(menu, e); -} - -const BuilderBuildPanel = memo(() => { - const model = BuilderBuildPanelModel.getInstance(); - const outputLines = useAtomValue(model.outputLines); - const showDebug = useAtomValue(model.showDebug); - const scrollRef = useRef(null); - const preRef = useRef(null); - - useEffect(() => { - model.initialize(); - return () => { - model.dispose(); - }; - }, []); - - useEffect(() => { - if (scrollRef.current) { - scrollRef.current.scrollTop = scrollRef.current.scrollHeight; - } - }, [outputLines]); - - const debouncedCopyOnSelect = useCallback( - debounce(50, () => { - const selection = window.getSelection(); - if (selection && selection.toString().length > 0) { - navigator.clipboard.writeText(selection.toString()); - } - }), - [] - ); - - const handleMouseUp = useCallback(() => { - debouncedCopyOnSelect(); - }, [debouncedCopyOnSelect]); - - const handleContextMenu = useCallback((e: React.MouseEvent) => { - const selection = window.getSelection(); - const selectedText = selection ? selection.toString() : ""; - handleBuildPanelContextMenu(e, selectedText); - }, []); - - const handleDebugToggle = useCallback(() => { - globalStore.set(model.showDebug, !showDebug); - }, [model, showDebug]); - - const handleRestart = useCallback(() => { - BuilderAppPanelModel.getInstance().restartBuilder(); - }, []); - - const handleSendToAI = useCallback(() => { - const currentShowDebug = globalStore.get(model.showDebug); - const currentOutputLines = globalStore.get(model.outputLines); - const filtered = currentShowDebug - ? currentOutputLines - : currentOutputLines.filter((line) => !line.startsWith("[debug]") && line.trim().length > 0); - - const linesToSend = filtered.slice(-200); - const text = linesToSend.join("\n"); - const aiModel = WaveAIModel.getInstance(); - const formattedText = `from builder output:\n\`\`\`\n${text}\n\`\`\`\n`; - aiModel.appendText(formattedText, true, { scrollToBottom: true }); - aiModel.focusInput(); - }, [model]); - - const filteredLines = showDebug - ? outputLines - : outputLines.filter((line) => !line.startsWith("[debug]") && line.trim().length > 0); - - return ( -
-
- Build Output -
- - - -
-
-
-
-                    {/* this comment fixes JSX blank line in pre tag */}
-                    {filteredLines.length === 0 ? (
-                        Waiting for output...
-                    ) : (
-                        filteredLines.join("\n")
-                    )}
-                
-
-
- ); -}); - -BuilderBuildPanel.displayName = "BuilderBuildPanel"; - -export { BuilderBuildPanel }; diff --git a/frontend/builder/builder-workspace.tsx b/frontend/builder/builder-workspace.tsx deleted file mode 100644 index aab14ff458..0000000000 --- a/frontend/builder/builder-workspace.tsx +++ /dev/null @@ -1,136 +0,0 @@ -// Copyright 2025, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { AIPanel } from "@/app/aipanel/aipanel"; -import { RpcApi } from "@/app/store/wshclientapi"; -import { TabRpcClient } from "@/app/store/wshrpcutil"; -import { BuilderAppPanel } from "@/builder/builder-apppanel"; -import { BuilderBuildPanel } from "@/builder/builder-buildpanel"; -import { BuilderFocusManager } from "@/builder/store/builder-focusmanager"; -import { atoms } from "@/store/global"; -import { cn } from "@/util/util"; -import { useAtomValue } from "jotai"; -import { memo, useCallback, useEffect, useState } from "react"; -import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels"; -import { debounce } from "throttle-debounce"; - -const DefaultLayoutPercentages = { - chat: 50, - app: 80, - build: 20, -}; - -const BuilderWorkspace = memo(() => { - const builderId = useAtomValue(atoms.builderId); - const [layout, setLayout] = useState>(null); - const [isLoading, setIsLoading] = useState(true); - const focusType = useAtomValue(BuilderFocusManager.getInstance().focusType); - const isAppFocused = focusType === "app"; - - useEffect(() => { - const loadLayout = async () => { - if (!builderId) { - setLayout(DefaultLayoutPercentages); - setIsLoading(false); - return; - } - - try { - const rtInfo = await RpcApi.GetRTInfoCommand(TabRpcClient, { - oref: `builder:${builderId}`, - }); - if (rtInfo?.["builder:layout"]) { - setLayout(rtInfo["builder:layout"] as Record); - } else { - setLayout(DefaultLayoutPercentages); - } - } catch (error) { - console.error("Failed to load builder layout:", error); - setLayout(DefaultLayoutPercentages); - } finally { - setIsLoading(false); - } - }; - - loadLayout(); - }, [builderId]); - - const saveLayout = useCallback( - debounce(500, (newLayout: Record) => { - if (!builderId) return; - - RpcApi.SetRTInfoCommand(TabRpcClient, { - oref: `builder:${builderId}`, - data: { - "builder:layout": newLayout, - }, - }).catch((error) => { - console.error("Failed to save builder layout:", error); - }); - }), - [builderId] - ); - - const handleHorizontalLayout = useCallback( - (sizes: number[]) => { - const newLayout = { ...layout, chat: sizes[0] }; - setLayout(newLayout); - saveLayout(newLayout); - }, - [layout, saveLayout] - ); - - const handleVerticalLayout = useCallback( - (sizes: number[]) => { - const newLayout = { ...layout, app: sizes[0], build: sizes[1] }; - setLayout(newLayout); - saveLayout(newLayout); - }, - [layout, saveLayout] - ); - - if (isLoading || !layout) { - return null; - } - - return ( -
- - - - - - -
- - - - - - - - - -
-
-
-
- ); -}); - -BuilderWorkspace.displayName = "BuilderWorkspace"; - -export { BuilderWorkspace }; diff --git a/frontend/builder/store/builder-apppanel-model.ts b/frontend/builder/store/builder-apppanel-model.ts deleted file mode 100644 index 3065687cde..0000000000 --- a/frontend/builder/store/builder-apppanel-model.ts +++ /dev/null @@ -1,339 +0,0 @@ -// Copyright 2025, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { globalStore } from "@/app/store/jotaiStore"; -import { waveEventSubscribeSingle } from "@/app/store/wps"; -import { RpcApi } from "@/app/store/wshclientapi"; -import { TabRpcClient } from "@/app/store/wshrpcutil"; -import { atoms, getApi, WOS } from "@/store/global"; -import { base64ToString, stringToBase64 } from "@/util/util"; -import type { WebviewTag } from "electron"; -import { atom, type Atom, type PrimitiveAtom } from "jotai"; -import type * as MonacoTypes from "monaco-editor"; -import { debounce } from "throttle-debounce"; - -export type TabType = "preview" | "files" | "code" | "secrets" | "configdata"; - -export type EnvVar = { - name: string; - value: string; - visible?: boolean; -}; - -export class BuilderAppPanelModel { - private static instance: BuilderAppPanelModel | null = null; - - activeTab: PrimitiveAtom = atom("preview"); - codeContentAtom: PrimitiveAtom = atom(""); - originalContentAtom: PrimitiveAtom = atom(""); - envVarsArrayAtom: PrimitiveAtom = atom([]); - envVarIndexAtoms: Atom[] = []; - envVarsDirtyAtom: PrimitiveAtom = atom(false); - isLoadingAtom: PrimitiveAtom = atom(false); - errorAtom: PrimitiveAtom = atom(""); - builderStatusAtom = atom(null) as PrimitiveAtom; - hasSecretsAtom: PrimitiveAtom = atom(false); - saveNeededAtom!: Atom; - focusElemRef: { current: HTMLInputElement | null } = { current: null }; - monacoEditorRef: { current: MonacoTypes.editor.IStandaloneCodeEditor | null } = { current: null }; - webviewRef: { current: WebviewTag | null } = { current: null }; - statusUnsubFn: (() => void) | null = null; - appGoUpdateUnsubFn: (() => void) | null = null; - debouncedRestart: (() => void) & { cancel: () => void }; - initialized = false; - - private constructor() { - this.debouncedRestart = debounce(800, () => { - this.restartBuilder(); - }); - this.saveNeededAtom = atom((get) => { - return get(this.codeContentAtom) !== get(this.originalContentAtom); - }); - } - - static getInstance(): BuilderAppPanelModel { - if (!BuilderAppPanelModel.instance) { - BuilderAppPanelModel.instance = new BuilderAppPanelModel(); - } - return BuilderAppPanelModel.instance; - } - - setActiveTab(tab: TabType) { - globalStore.set(this.activeTab, tab); - } - - getActiveTab(): TabType { - return globalStore.get(this.activeTab); - } - - setCodeContent(content: string) { - globalStore.set(this.codeContentAtom, content); - } - - async initialize() { - if (this.initialized) return; - this.initialized = true; - - // builderId is set in initialization so is always available - const builderId = globalStore.get(atoms.builderId); - - if (this.statusUnsubFn) { - this.statusUnsubFn(); - } - - this.statusUnsubFn = waveEventSubscribeSingle({ - eventType: "builderstatus", - scope: WOS.makeORef("builder", builderId), - handler: (event) => { - const status = event.data; - const currentStatus = globalStore.get(this.builderStatusAtom); - if (!currentStatus || !currentStatus.version || status.version > currentStatus.version) { - globalStore.set(this.builderStatusAtom, status); - this.updateSecretsLatch(status); - } - }, - }); - - try { - const status = await RpcApi.GetBuilderStatusCommand(TabRpcClient, builderId); - globalStore.set(this.builderStatusAtom, status); - this.updateSecretsLatch(status); - } catch (err) { - console.error("Failed to load builder status:", err); - } - - // the apppanel does not render until appId is set, so this will never be null during initialization - const appId = globalStore.get(atoms.builderAppId); - await this.loadAppFile(appId); - await this.loadEnvVars(builderId); - - this.appGoUpdateUnsubFn = waveEventSubscribeSingle({ - eventType: "waveapp:appgoupdated", - scope: appId, - handler: () => { - this.loadAppFile(appId); - }, - }); - } - - updateSecretsLatch(status: BuilderStatusData) { - if (!status?.manifest?.secrets) return; - const secrets = status.manifest.secrets; - if (Object.keys(secrets).length > 0) { - globalStore.set(this.hasSecretsAtom, true); - } - } - - updateSecretBindings(newBindings: { [key: string]: string }) { - const currentStatus = globalStore.get(this.builderStatusAtom); - if (currentStatus) { - globalStore.set(this.builderStatusAtom, { - ...currentStatus, - secretbindings: newBindings, - }); - } - } - - async loadEnvVars(builderId: string) { - try { - const rtInfo = await RpcApi.GetRTInfoCommand(TabRpcClient, { - oref: WOS.makeORef("builder", builderId), - }); - const envVars = rtInfo?.["builder:env"] || {}; - const envVarsArray = Object.entries(envVars).map(([name, value]) => ({ name, value, visible: false })); - globalStore.set(this.envVarsArrayAtom, envVarsArray); - globalStore.set(this.envVarsDirtyAtom, false); - } catch (err) { - console.error("Failed to load environment variables:", err); - } - } - - async saveEnvVars(builderId: string) { - try { - const envVarsArray = globalStore.get(this.envVarsArrayAtom); - const envVars: Record = {}; - envVarsArray.forEach((v) => { - const trimmedName = v.name.trim(); - if (trimmedName) { - envVars[trimmedName] = v.value; - } - }); - const cleanedArray = Object.entries(envVars).map(([name, value]) => ({ name, value, visible: false })); - await RpcApi.SetRTInfoCommand(TabRpcClient, { - oref: WOS.makeORef("builder", builderId), - data: { - "builder:env": envVars, - }, - }); - globalStore.set(this.envVarsArrayAtom, cleanedArray); - globalStore.set(this.envVarsDirtyAtom, false); - globalStore.set(this.errorAtom, ""); - this.debouncedRestart(); - } catch (err) { - console.error("Failed to save environment variables:", err); - globalStore.set(this.errorAtom, `Failed to save environment variables: ${err.message || "Unknown error"}`); - } - } - - getEnvVarIndexAtom(index: number): Atom { - if (!this.envVarIndexAtoms[index]) { - this.envVarIndexAtoms[index] = atom((get) => { - const array = get(this.envVarsArrayAtom); - return array[index] ?? null; - }); - } - return this.envVarIndexAtoms[index]; - } - - addEnvVar() { - const current = globalStore.get(this.envVarsArrayAtom); - globalStore.set(this.envVarsArrayAtom, [...current, { name: "", value: "", visible: false }]); - globalStore.set(this.envVarsDirtyAtom, true); - } - - removeEnvVar(index: number) { - const current = globalStore.get(this.envVarsArrayAtom); - const newArray = current.filter((_, i) => i !== index); - globalStore.set(this.envVarsArrayAtom, newArray); - globalStore.set(this.envVarsDirtyAtom, true); - } - - setEnvVarAtIndex(index: number, envVar: EnvVar, dirty: boolean) { - const current = globalStore.get(this.envVarsArrayAtom); - const newArray = [...current]; - newArray[index] = envVar; - globalStore.set(this.envVarsArrayAtom, newArray); - if (dirty) { - globalStore.set(this.envVarsDirtyAtom, true); - } - } - - async startBuilder() { - const builderId = globalStore.get(atoms.builderId); - try { - await RpcApi.StartBuilderCommand(TabRpcClient, { - builderid: builderId, - }); - } catch (err) { - console.error("Failed to start builder:", err); - globalStore.set(this.errorAtom, `Failed to start builder: ${err.message || "Unknown error"}`); - } - } - - async restartBuilder() { - // the RPC call that starts the builder actually forces a restart, so this works - return this.startBuilder(); - } - - async switchBuilderApp() { - const builderId = globalStore.get(atoms.builderId); - try { - await RpcApi.DeleteBuilderCommand(TabRpcClient, builderId); - await new Promise((resolve) => setTimeout(resolve, 500)); - await RpcApi.SetRTInfoCommand(TabRpcClient, { - oref: WOS.makeORef("builder", builderId), - data: { "builder:appid": null }, - }); - getApi().setBuilderWindowAppId(null); - await new Promise((resolve) => setTimeout(resolve, 100)); - getApi().doRefresh(); - } catch (err) { - console.error("Failed to switch builder app:", err); - globalStore.set(this.errorAtom, `Failed to switch builder app: ${err.message || "Unknown error"}`); - } - } - - async loadAppFile(appId: string) { - try { - globalStore.set(this.isLoadingAtom, true); - globalStore.set(this.errorAtom, ""); - - const result = await RpcApi.ReadAppFileCommand(TabRpcClient, { - appid: appId, - filename: "app.go", - }); - - if (result.notfound) { - globalStore.set(this.codeContentAtom, ""); - globalStore.set(this.originalContentAtom, ""); - } else { - const decoded = base64ToString(result.data64); - globalStore.set(this.codeContentAtom, decoded); - globalStore.set(this.originalContentAtom, decoded); - - if (decoded.trim() !== "") { - const currentStatus = globalStore.get(this.builderStatusAtom); - if (currentStatus?.status !== "running" && currentStatus?.status !== "building") { - await this.startBuilder(); - } - } - } - } catch (err) { - console.error("Failed to load app.go:", err); - globalStore.set(this.errorAtom, `Failed to load app.go: ${err.message || "Unknown error"}`); - } finally { - globalStore.set(this.isLoadingAtom, false); - } - } - - async saveAppFile(appId: string) { - try { - const content = globalStore.get(this.codeContentAtom); - const encoded = stringToBase64(content); - const result = await RpcApi.WriteAppGoFileCommand(TabRpcClient, { - appid: appId, - data64: encoded, - }); - const formattedContent = base64ToString(result.data64); - globalStore.set(this.codeContentAtom, formattedContent); - globalStore.set(this.originalContentAtom, formattedContent); - globalStore.set(this.errorAtom, ""); - this.debouncedRestart(); - } catch (err) { - console.error("Failed to save app.go:", err); - globalStore.set(this.errorAtom, `Failed to save app.go: ${err.message || "Unknown error"}`); - } - } - - clearError() { - globalStore.set(this.errorAtom, ""); - } - - giveFocus() { - const activeTab = globalStore.get(this.activeTab); - if (activeTab === "code" && this.monacoEditorRef.current) { - this.monacoEditorRef.current.focus(); - } else { - this.focusElemRef.current?.focus(); - } - } - - setFocusElemRef(ref: HTMLInputElement | null) { - this.focusElemRef.current = ref; - } - - setMonacoEditorRef(ref: MonacoTypes.editor.IStandaloneCodeEditor | null) { - this.monacoEditorRef.current = ref; - } - - openPreviewDevTools() { - if (!this.webviewRef.current) return; - if (this.webviewRef.current.isDevToolsOpened()) { - this.webviewRef.current.closeDevTools(); - } else { - this.webviewRef.current.openDevTools(); - } - } - - dispose() { - if (this.statusUnsubFn) { - this.statusUnsubFn(); - this.statusUnsubFn = null; - } - if (this.appGoUpdateUnsubFn) { - this.appGoUpdateUnsubFn(); - this.appGoUpdateUnsubFn = null; - } - this.debouncedRestart.cancel(); - } -} diff --git a/frontend/builder/store/builder-buildpanel-model.ts b/frontend/builder/store/builder-buildpanel-model.ts deleted file mode 100644 index c4758c5df8..0000000000 --- a/frontend/builder/store/builder-buildpanel-model.ts +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2025, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { globalStore } from "@/app/store/jotaiStore"; -import { waveEventSubscribeSingle } from "@/app/store/wps"; -import { RpcApi } from "@/app/store/wshclientapi"; -import { TabRpcClient } from "@/app/store/wshrpcutil"; -import { atoms, WOS } from "@/store/global"; -import { atom, type PrimitiveAtom } from "jotai"; - -export class BuilderBuildPanelModel { - private static instance: BuilderBuildPanelModel | null = null; - - outputLines: PrimitiveAtom = atom([]); - showDebug: PrimitiveAtom = atom(false); - outputUnsubFn: (() => void) | null = null; - initialized = false; - - private constructor() {} - - static getInstance(): BuilderBuildPanelModel { - if (!BuilderBuildPanelModel.instance) { - BuilderBuildPanelModel.instance = new BuilderBuildPanelModel(); - } - return BuilderBuildPanelModel.instance; - } - - async initialize() { - if (this.initialized) return; - this.initialized = true; - - const builderId = globalStore.get(atoms.builderId); - if (!builderId) return; - - if (this.outputUnsubFn) { - this.outputUnsubFn(); - } - - this.outputUnsubFn = waveEventSubscribeSingle({ - eventType: "builderoutput", - scope: WOS.makeORef("builder", builderId), - handler: (event) => { - const data = event.data as { lines?: string[]; reset?: boolean }; - if (!data) return; - - if (data.reset) { - globalStore.set(this.outputLines, data.lines || []); - } else if (data.lines && data.lines.length > 0) { - globalStore.set(this.outputLines, (prev) => [...prev, ...data.lines]); - } - }, - }); - - try { - const output = await RpcApi.GetBuilderOutputCommand(TabRpcClient, builderId); - globalStore.set(this.outputLines, output || []); - } catch (err) { - console.error("Failed to load builder output:", err); - } - } - - clearOutput() { - globalStore.set(this.outputLines, []); - } - - dispose() { - if (this.outputUnsubFn) { - this.outputUnsubFn(); - this.outputUnsubFn = null; - } - this.initialized = false; - } -} \ No newline at end of file diff --git a/frontend/builder/store/builder-focusmanager.ts b/frontend/builder/store/builder-focusmanager.ts deleted file mode 100644 index f360663cc9..0000000000 --- a/frontend/builder/store/builder-focusmanager.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright 2025, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { globalStore } from "@/app/store/jotaiStore"; -import { atom, type PrimitiveAtom } from "jotai"; - -export type BuilderFocusType = "waveai" | "app"; - -export class BuilderFocusManager { - private static instance: BuilderFocusManager | null = null; - - focusType: PrimitiveAtom = atom("app"); - - private constructor() {} - - static getInstance(): BuilderFocusManager { - if (!BuilderFocusManager.instance) { - BuilderFocusManager.instance = new BuilderFocusManager(); - } - return BuilderFocusManager.instance; - } - - setWaveAIFocused() { - globalStore.set(this.focusType, "waveai"); - } - - setAppFocused() { - globalStore.set(this.focusType, "app"); - } - - getFocusType(): BuilderFocusType { - return globalStore.get(this.focusType); - } -} diff --git a/frontend/builder/tabs/builder-codetab.tsx b/frontend/builder/tabs/builder-codetab.tsx deleted file mode 100644 index 8b5ff7a08f..0000000000 --- a/frontend/builder/tabs/builder-codetab.tsx +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright 2025, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { CodeEditor } from "@/app/view/codeeditor/codeeditor"; -import { BuilderAppPanelModel } from "@/builder/store/builder-apppanel-model"; -import { atoms } from "@/store/global"; -import * as keyutil from "@/util/keyutil"; -import { cn } from "@/util/util"; -import { useAtomValue } from "jotai"; -import type * as MonacoTypes from "monaco-editor"; -import { memo, useEffect } from "react"; - -const BuilderCodeTab = memo(() => { - const model = BuilderAppPanelModel.getInstance(); - const builderAppId = useAtomValue(atoms.builderAppId); - const codeContent = useAtomValue(model.codeContentAtom); - const isLoading = useAtomValue(model.isLoadingAtom); - const error = useAtomValue(model.errorAtom); - const saveNeeded = useAtomValue(model.saveNeededAtom); - const activeTab = useAtomValue(model.activeTab); - - useEffect(() => { - if (activeTab === "code" && model.monacoEditorRef.current) { - setTimeout(() => { - model.monacoEditorRef.current?.layout(); - }, 0); - } - }, [activeTab, model.monacoEditorRef]); - - const handleCodeChange = (newText: string) => { - model.setCodeContent(newText); - }; - - const handleEditorMount = (editor: MonacoTypes.editor.IStandaloneCodeEditor, monaco: typeof MonacoTypes) => { - model.setMonacoEditorRef(editor); - return () => { - model.setMonacoEditorRef(null); - }; - }; - - const handleSave = () => { - if (builderAppId) { - model.saveAppFile(builderAppId); - } - }; - - const handleKeyDown = keyutil.keydownWrapper((waveEvent: WaveKeyboardEvent) => { - if (keyutil.checkKeyPressed(waveEvent, "Cmd:s")) { - handleSave(); - return true; - } - return false; - }); - - if (!builderAppId) { - return ( -
-
No builder app selected
-
- ); - } - - if (isLoading) { - return ( -
-
Loading app.go...
-
- ); - } - - if (error) { - return ( -
-
{error}
-
- ); - } - - return ( -
- - -
- ); -}); - -BuilderCodeTab.displayName = "BuilderCodeTab"; - -export { BuilderCodeTab }; diff --git a/frontend/builder/tabs/builder-configdatatab.tsx b/frontend/builder/tabs/builder-configdatatab.tsx deleted file mode 100644 index cd0c75f6f7..0000000000 --- a/frontend/builder/tabs/builder-configdatatab.tsx +++ /dev/null @@ -1,226 +0,0 @@ -// Copyright 2025, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { BuilderAppPanelModel } from "@/builder/store/builder-apppanel-model"; -import { CopyButton } from "@/element/copybutton"; -import { atoms } from "@/store/global"; -import { cn } from "@/util/util"; -import { useAtomValue } from "jotai"; -import { memo, useCallback, useEffect, useState } from "react"; - -const NotRunningView = memo(() => { - return ( -
-
- -
-

App Not Running

-

- The tsunami app must be running to view config and data. Please start the app from the Preview - tab first. -

-
-
-
- ); -}); - -NotRunningView.displayName = "NotRunningView"; - -const ErrorView = memo(({ errorMsg }: { errorMsg: string }) => { - return ( -
-
- -
-

Error Loading Data

-
-
{errorMsg}
-
-
-
-
- ); -}); - -ErrorView.displayName = "ErrorView"; - -const LoadingView = memo(() => { - return ( -
-
- -

Loading data...

-
-
- ); -}); - -LoadingView.displayName = "LoadingView"; - -type ConfigDataState = { - config: any; - data: any; - error: string | null; - isLoading: boolean; -}; - -const BuilderConfigDataTab = memo(() => { - const model = BuilderAppPanelModel.getInstance(); - const builderStatus = useAtomValue(model.builderStatusAtom); - const builderId = useAtomValue(atoms.builderId); - const activeTab = useAtomValue(model.activeTab); - const [state, setState] = useState({ - config: null, - data: null, - error: null, - isLoading: false, - }); - - const isRunning = builderStatus?.status === "running" && builderStatus?.port && builderStatus.port !== 0; - - const fetchData = useCallback(async () => { - if (!isRunning || !builderStatus?.port) { - return; - } - - setState((prev) => ({ ...prev, isLoading: true, error: null })); - - try { - const baseUrl = `http://localhost:${builderStatus.port}`; - - const [configResponse, dataResponse] = await Promise.all([ - fetch(`${baseUrl}/api/config`), - fetch(`${baseUrl}/api/data`), - ]); - - if (!configResponse.ok) { - throw new Error(`Failed to fetch config: ${configResponse.statusText}`); - } - if (!dataResponse.ok) { - throw new Error(`Failed to fetch data: ${dataResponse.statusText}`); - } - - const config = await configResponse.json(); - const data = await dataResponse.json(); - - setState({ - config, - data, - error: null, - isLoading: false, - }); - } catch (err) { - setState({ - config: null, - data: null, - error: err instanceof Error ? err.message : String(err), - isLoading: false, - }); - } - }, [isRunning, builderStatus?.port]); - - const handleRefresh = useCallback(async () => { - setState({ - config: null, - data: null, - error: null, - isLoading: true, - }); - - await new Promise((resolve) => setTimeout(resolve, 200)); - await fetchData(); - }, [fetchData]); - - const handleCopyConfig = useCallback(() => { - if (state.config) { - navigator.clipboard.writeText(JSON.stringify(state.config, null, 2)); - } - }, [state.config]); - - const handleCopyData = useCallback(() => { - if (state.data) { - navigator.clipboard.writeText(JSON.stringify(state.data, null, 2)); - } - }, [state.data]); - - useEffect(() => { - if (activeTab === "configdata" && isRunning) { - fetchData(); - } else if (!isRunning) { - setState({ - config: null, - data: null, - error: null, - isLoading: false, - }); - } - }, [activeTab, isRunning, fetchData]); - - if (!isRunning) { - return ; - } - - if (state.isLoading) { - return ; - } - - if (state.error) { - return ; - } - - if (!state.config && !state.data) { - return ; - } - - return ( -
-
-

Config & Data

- -
-
-
-
-
-

- - Config -

- -
-
-
-                                {JSON.stringify(state.config, null, 2)}
-                            
-
-
-
-
-

- - Data -

- -
-
-
-                                {JSON.stringify(state.data, null, 2)}
-                            
-
-
-
-
-
- ); -}); - -BuilderConfigDataTab.displayName = "BuilderConfigDataTab"; - -export { BuilderConfigDataTab }; \ No newline at end of file diff --git a/frontend/builder/tabs/builder-filestab.tsx b/frontend/builder/tabs/builder-filestab.tsx deleted file mode 100644 index b33622b963..0000000000 --- a/frontend/builder/tabs/builder-filestab.tsx +++ /dev/null @@ -1,399 +0,0 @@ -// Copyright 2025, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { formatFileSize } from "@/app/aipanel/ai-utils"; -import { Modal } from "@/app/modals/modal"; -import { ContextMenuModel } from "@/app/store/contextmenu"; -import { modalsModel } from "@/app/store/modalmodel"; -import { RpcApi } from "@/app/store/wshclientapi"; -import { TabRpcClient } from "@/app/store/wshrpcutil"; -import { arrayToBase64 } from "@/util/util"; -import { atoms } from "@/store/global"; -import { useAtomValue } from "jotai"; -import { memo, useCallback, useEffect, useRef, useState } from "react"; - -const MaxFileSize = 5 * 1024 * 1024; // 5MB -const ReadOnlyFileNames = ["static/tw.css"]; - -type FileEntry = { - name: string; - size: number; - modified: string; - isReadOnly: boolean; -}; - -const RenameFileModal = memo( - ({ appId, fileName, onSuccess }: { appId: string; fileName: string; onSuccess: () => void }) => { - const displayName = fileName.replace("static/", ""); - const [newName, setNewName] = useState(displayName); - const [error, setError] = useState(""); - const [isRenaming, setIsRenaming] = useState(false); - - const handleRename = async () => { - const trimmedName = newName.trim(); - if (!trimmedName) { - setError("File name cannot be empty"); - return; - } - if (trimmedName.includes("/") || trimmedName.includes("\\")) { - setError("File name cannot contain / or \\"); - return; - } - if (trimmedName === displayName) { - modalsModel.popModal(); - return; - } - - setIsRenaming(true); - try { - await RpcApi.RenameAppFileCommand(TabRpcClient, { - appid: appId, - fromfilename: fileName, - tofilename: `static/${trimmedName}`, - }); - onSuccess(); - modalsModel.popModal(); - } catch (err) { - console.log("Error renaming file:", err); - setError(err instanceof Error ? err.message : String(err)); - } finally { - setIsRenaming(false); - } - }; - - const handleClose = () => { - modalsModel.popModal(); - }; - - return ( - -
-

Rename File

-
-
- Current name: {displayName} -
- { - setNewName(e.target.value); - setError(""); - }} - onKeyDown={(e) => { - if (e.key === "Enter" && !e.nativeEvent.isComposing && newName.trim() && !error) { - handleRename(); - } - }} - className="px-3 py-2 bg-panel border border-border rounded focus:outline-none focus:border-accent" - autoFocus - disabled={isRenaming} - spellCheck={false} - /> - {error &&
{error}
} -
-
-
- ); - } -); - -RenameFileModal.displayName = "RenameFileModal"; - -const DeleteFileModal = memo( - ({ appId, fileName, onSuccess }: { appId: string; fileName: string; onSuccess: () => void }) => { - const [isDeleting, setIsDeleting] = useState(false); - const [error, setError] = useState(""); - - const handleDelete = async () => { - setIsDeleting(true); - setError(""); - try { - await RpcApi.DeleteAppFileCommand(TabRpcClient, { - appid: appId, - filename: fileName, - }); - onSuccess(); - modalsModel.popModal(); - } catch (err) { - console.log("Error deleting file:", err); - setError(err instanceof Error ? err.message : String(err)); - } finally { - setIsDeleting(false); - } - }; - - const handleClose = () => { - modalsModel.popModal(); - }; - - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Enter" && !isDeleting) { - e.preventDefault(); - handleDelete(); - } else if (e.key === "Escape") { - e.preventDefault(); - handleClose(); - } - }; - - document.addEventListener("keydown", handleKeyDown); - return () => document.removeEventListener("keydown", handleKeyDown); - }, [isDeleting]); - - return ( - -
-

Delete File

-

- Are you sure you want to delete {fileName.replace("static/", "")}? -

-

This action cannot be undone.

- {error &&
{error}
} -
-
- ); - } -); - -DeleteFileModal.displayName = "DeleteFileModal"; - -const BuilderFilesTab = memo(() => { - const builderAppId = useAtomValue(atoms.builderAppId); - const [files, setFiles] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(""); - const [isDragging, setIsDragging] = useState(false); - const [contextMenu, setContextMenu] = useState<{ x: number; y: number; fileName: string } | null>(null); - const fileInputRef = useRef(null); - - const loadFiles = useCallback(async () => { - if (!builderAppId) return; - - setLoading(true); - setError(""); - try { - const result = await RpcApi.ListAllAppFilesCommand(TabRpcClient, { appid: builderAppId }); - const fileEntries: FileEntry[] = result.entries - .filter((entry) => !entry.dir && entry.name.startsWith("static/")) - .map((entry) => ({ - name: entry.name, - size: entry.size || 0, - modified: entry.modified, - isReadOnly: ReadOnlyFileNames.includes(entry.name), - })) - .sort((a, b) => a.name.localeCompare(b.name)); - setFiles(fileEntries); - } catch (err) { - setError(err instanceof Error ? err.message : String(err)); - } finally { - setLoading(false); - } - }, [builderAppId]); - - const handleRefresh = useCallback(async () => { - // Clear files and add delay so UX shows the refresh is happening - setFiles([]); - await new Promise((resolve) => setTimeout(resolve, 100)); - await loadFiles(); - }, [loadFiles]); - - useEffect(() => { - loadFiles(); - }, [loadFiles]); - - useEffect(() => { - const handleClickOutside = () => setContextMenu(null); - if (contextMenu) { - document.addEventListener("click", handleClickOutside); - return () => document.removeEventListener("click", handleClickOutside); - } - }, [contextMenu]); - - const handleFileUpload = async (fileList: FileList) => { - if (!builderAppId || fileList.length === 0) return; - - const file = fileList[0]; - if (file.size > MaxFileSize) { - setError(`File size exceeds maximum allowed size of ${formatFileSize(MaxFileSize)}`); - return; - } - - setError(""); - setLoading(true); - - try { - const arrayBuffer = await file.arrayBuffer(); - const uint8Array = new Uint8Array(arrayBuffer); - const base64Encoded = arrayToBase64(uint8Array); - - await RpcApi.WriteAppFileCommand(TabRpcClient, { - appid: builderAppId, - filename: `static/${file.name}`, - data64: base64Encoded, - }); - - await loadFiles(); - } catch (err) { - console.error("Error uploading file:", err); - setError(err instanceof Error ? err.message : String(err)); - } finally { - setLoading(false); - } - }; - - const handleDrop = (e: React.DragEvent) => { - e.preventDefault(); - setIsDragging(false); - handleFileUpload(e.dataTransfer.files); - }; - - const handleDragOver = (e: React.DragEvent) => { - e.preventDefault(); - setIsDragging(true); - }; - - const handleDragLeave = (e: React.DragEvent) => { - e.preventDefault(); - setIsDragging(false); - }; - - const handleFileInputChange = (e: React.ChangeEvent) => { - if (e.target.files) { - handleFileUpload(e.target.files); - } - }; - - const handleContextMenu = (e: React.MouseEvent, fileName: string) => { - const menu: ContextMenuItem[] = [ - { - label: "Rename File", - click: () => { - modalsModel.pushModal("RenameFileModal", { appId: builderAppId, fileName, onSuccess: loadFiles }); - }, - }, - { - type: "separator", - }, - { - label: "Delete File", - click: () => { - modalsModel.pushModal("DeleteFileModal", { appId: builderAppId, fileName, onSuccess: loadFiles }); - }, - }, - ]; - - ContextMenuModel.getInstance().showContextMenu(menu, e); - }; - - return ( -
-
-

Static Files

-
- - -
- -
- - {error && ( -
- - {error} -
- )} - -
- Drag and drop files here or click "Add File". Maximum file size: {formatFileSize(MaxFileSize)} -
- -
- {loading && files.length === 0 ? ( -
Loading files...
- ) : files.length === 0 ? ( -
- -

No files yet. Drag and drop files here or click "Add File" to get started.

-
- ) : ( -
- {files.map((file) => ( -
!file.isReadOnly && handleContextMenu(e, file.name)} - > - -
-
{file.name.replace("static/", "")}
-
- {formatFileSize(file.size)} - {file.isReadOnly && ( - - - Generated by framework (read-only) - - )} -
-
-
{file.modified}
- {!file.isReadOnly && ( - - )} -
- ))} -
- )} -
-
- ); -}); - -BuilderFilesTab.displayName = "BuilderFilesTab"; - -export { BuilderFilesTab, DeleteFileModal, RenameFileModal }; diff --git a/frontend/builder/tabs/builder-previewtab.tsx b/frontend/builder/tabs/builder-previewtab.tsx deleted file mode 100644 index 2976080680..0000000000 --- a/frontend/builder/tabs/builder-previewtab.tsx +++ /dev/null @@ -1,227 +0,0 @@ -// Copyright 2025, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { WaveAIModel } from "@/app/aipanel/waveai-model"; -import { BuilderAppPanelModel } from "@/builder/store/builder-apppanel-model"; -import { BuilderBuildPanelModel } from "@/builder/store/builder-buildpanel-model"; -import { atoms } from "@/store/global"; -import { useAtomValue } from "jotai"; -import { memo, useState } from "react"; - -const EmptyStateView = memo(() => { - return ( -
-
-
🏗️
-
-

No App to Preview

-

- Get started by using the AI chat interface on the left to create your WaveApp. Describe what you - want to build, and the AI will help you generate the code. -

-
-
- Your app will appear here once app.go is created -
-
-
- ); -}); - -EmptyStateView.displayName = "EmptyStateView"; - -const ErrorStateView = memo(({ errorMsg }: { errorMsg: string }) => { - const displayMsg = errorMsg && errorMsg.trim() ? errorMsg : "Unknown Error"; - const waveAIModel = WaveAIModel.getInstance(); - const buildPanelModel = BuilderBuildPanelModel.getInstance(); - const appPanelModel = BuilderAppPanelModel.getInstance(); - const outputLines = useAtomValue(buildPanelModel.outputLines); - const isStreaming = useAtomValue(waveAIModel.isAIStreaming); - - const isSecretError = displayMsg.includes("ERR-SECRET"); - - const getBuildContext = () => { - const filteredLines = outputLines.filter((line) => !line.startsWith("[debug]")); - const buildOutput = filteredLines.join("\n").trim(); - return `Build Error:\n\`\`\`\n${displayMsg}\n\`\`\`\n\nBuild Output:\n\`\`\`\n${buildOutput}\n\`\`\``; - }; - - const handleAddToContext = () => { - const context = getBuildContext(); - waveAIModel.appendText(context, true); - waveAIModel.focusInput(); - }; - - const handleAskAIToFix = async () => { - const context = getBuildContext(); - waveAIModel.appendText("Please help me fix this build error:\n\n" + context, true); - await waveAIModel.handleSubmit(); - }; - - const handleGoToSecrets = () => { - appPanelModel.setActiveTab("secrets"); - }; - - if (isSecretError) { - return ( -
-
-
🔐
-
-

Secrets Required

-

- This app requires secrets that must be configured. Please use the Secrets tab to set and - bind the required secrets for your app to run. -

-
-
{displayMsg}
-
- -
-
-
- ); - } - - return ( -
-
-
-

Build Error

-
-
{displayMsg}
-
- {!isStreaming && ( -
- - -
- )} -
-
-
- ); -}); - -ErrorStateView.displayName = "ErrorStateView"; - -const BuildingStateView = memo(() => { - return ( -
-
-
⚙️
-
-

App is Building...

-

- Your WaveApp is being compiled and prepared. This may take a few moments. -

-
-
-
- ); -}); - -BuildingStateView.displayName = "BuildingStateView"; - -const StoppedStateView = memo(({ onStart }: { onStart: () => void }) => { - const [isStarting, setIsStarting] = useState(false); - - const handleStart = () => { - setIsStarting(true); - onStart(); - setTimeout(() => setIsStarting(false), 2000); - }; - - return ( -
-
-
-

App is Not Running

-

- Your WaveApp is currently not running. Click the button below to start it. -

-
- {!isStarting && ( - - )} - {isStarting &&
Starting...
} -
-
- ); -}); - -StoppedStateView.displayName = "StoppedStateView"; - -const BuilderPreviewTab = memo(() => { - const model = BuilderAppPanelModel.getInstance(); - const isLoading = useAtomValue(model.isLoadingAtom); - const originalContent = useAtomValue(model.originalContentAtom); - const builderStatus = useAtomValue(model.builderStatusAtom); - const builderId = useAtomValue(atoms.builderId); - const fileExists = originalContent.length > 0; - const [lastKnownUrl, setLastKnownUrl] = useState(null); - - const status = builderStatus?.status || "init"; - const isWebViewActive = status === "running" && builderStatus?.port && builderStatus.port !== 0; - - if (isWebViewActive) { - const previewUrl = `http://localhost:${builderStatus.port}/?clientid=wave:${builderId}`; - if (previewUrl !== lastKnownUrl) { - setLastKnownUrl(previewUrl); - } - } - - let overlay = null; - if (!isLoading && !isWebViewActive) { - if (builderStatus?.status === "error") { - overlay = ; - } else if (!fileExists || status === "init") { - overlay = ; - } else if (status === "building") { - overlay = ; - } else if (status === "stopped") { - overlay = model.startBuilder()} />; - } - } - - return ( -
- {lastKnownUrl && ( - - )} - {overlay &&
{overlay}
} -
- ); -}); - -BuilderPreviewTab.displayName = "BuilderPreviewTab"; - -export { BuilderPreviewTab }; diff --git a/frontend/builder/tabs/builder-secrettab.tsx b/frontend/builder/tabs/builder-secrettab.tsx deleted file mode 100644 index 5e3542f9b0..0000000000 --- a/frontend/builder/tabs/builder-secrettab.tsx +++ /dev/null @@ -1,292 +0,0 @@ -// Copyright 2025, Command Line Inc. -// SPDX-License-Identifier: Apache-2.0 - -import { BuilderAppPanelModel } from "@/builder/store/builder-apppanel-model"; -import { RpcApi } from "@/app/store/wshclientapi"; -import { TabRpcClient } from "@/app/store/wshrpcutil"; -import { atoms } from "@/store/global"; -import { globalStore } from "@/app/store/jotaiStore"; -import { useAtomValue } from "jotai"; -import { memo, useState, useEffect } from "react"; -import { Check, AlertTriangle } from "lucide-react"; -import { Tooltip } from "@/app/element/tooltip"; -import { Modal } from "@/app/modals/modal"; -import { modalsModel } from "@/app/store/modalmodel"; - -type SecretRowProps = { - secretName: string; - secretMeta: SecretMeta; - currentBinding: string; - availableSecrets: string[]; - onMapDefault: (secretName: string) => void; - onSetAndMapDefault: (secretName: string) => void; -}; - -const SecretRow = memo(({ secretName, secretMeta, currentBinding, availableSecrets, onMapDefault, onSetAndMapDefault }: SecretRowProps) => { - const isMapped = currentBinding.trim().length > 0; - const isValid = isMapped && availableSecrets.includes(currentBinding); - const isInvalid = isMapped && !isValid; - const hasMatchingSecret = availableSecrets.includes(secretName); - - return ( -
- -
- {!isMapped && } - {isInvalid && } - {isValid && } -
-
-
- {secretName} - {!secretMeta.optional && ( - Required - )} - {secretMeta.optional && ( - Optional - )} - {secretMeta.desc && — {secretMeta.desc}} -
-
- {!isMapped && hasMatchingSecret && ( - - )} - {!isMapped && !hasMatchingSecret && ( - - )} -
-
- ); -}); - -SecretRow.displayName = "SecretRow"; - -type SetSecretDialogProps = { - secretName: string; - onSetAndMap: (secretName: string, secretValue: string) => Promise; -}; - -const SetSecretDialog = memo(({ secretName, onSetAndMap }: SetSecretDialogProps) => { - const [secretValue, setSecretValue] = useState(""); - const [isSubmitting, setIsSubmitting] = useState(false); - const [error, setError] = useState(""); - - const handleSubmit = async () => { - if (!secretValue.trim()) return; - setIsSubmitting(true); - setError(""); - try { - await onSetAndMap(secretName, secretValue); - modalsModel.popModal(); - } catch (err) { - console.error("Failed to set secret:", err); - setError(err instanceof Error ? err.message : String(err)); - } finally { - setIsSubmitting(false); - } - }; - - const handleClose = () => { - modalsModel.popModal(); - }; - - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - if (e.key === "Escape") { - e.preventDefault(); - handleClose(); - } - }; - - document.addEventListener("keydown", handleKeyDown); - return () => document.removeEventListener("keydown", handleKeyDown); - }, []); - - if (error) { - return ( - -
-

Error Setting Secret

-
{error}
-
-
- ); - } - - return ( - -
-

Set and Map Secret

-
-
- Secret Name: {secretName} -
-