From 4539c27b6f2dbceebf4882fed93508cc52ab615b Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 13 Nov 2025 22:50:00 -0800 Subject: [PATCH 1/4] fix to use bytes.NewReader --- pkg/waveapputil/waveapputil.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/waveapputil/waveapputil.go b/pkg/waveapputil/waveapputil.go index c1bee8818..241420a15 100644 --- a/pkg/waveapputil/waveapputil.go +++ b/pkg/waveapputil/waveapputil.go @@ -4,12 +4,12 @@ package waveapputil import ( + "bytes" "fmt" "os" "os/exec" "path/filepath" "runtime" - "strings" "github.com/wavetermdev/waveterm/pkg/wavebase" "github.com/wavetermdev/waveterm/pkg/wconfig" @@ -69,7 +69,7 @@ func FormatGoCode(contents []byte) []byte { } cmd := exec.Command(gofmtPath) - cmd.Stdin = strings.NewReader(string(contents)) + cmd.Stdin = bytes.NewReader(contents) formattedOutput, err := cmd.Output() if err != nil { return contents From 130e73b30ceb31fe295c8217c433be2d491ec43f Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 13 Nov 2025 22:50:19 -0800 Subject: [PATCH 2/4] have list operations also return app manifest files --- pkg/waveappstore/waveappstore.go | 46 +++++++++++++++++++++----------- pkg/wshrpc/wshrpctypes.go | 5 ++-- 2 files changed, 34 insertions(+), 17 deletions(-) diff --git a/pkg/waveappstore/waveappstore.go b/pkg/waveappstore/waveappstore.go index 87d61302a..1f4a88d7b 100644 --- a/pkg/waveappstore/waveappstore.go +++ b/pkg/waveappstore/waveappstore.go @@ -17,7 +17,6 @@ import ( "github.com/wavetermdev/waveterm/pkg/waveapputil" "github.com/wavetermdev/waveterm/pkg/wavebase" "github.com/wavetermdev/waveterm/pkg/wshrpc" - "github.com/wavetermdev/waveterm/tsunami/engine" ) const ( @@ -456,12 +455,12 @@ func ListAllAppFiles(appId string) (*fileutil.ReadDirResult, error) { return fileutil.ReadDirRecursive(appDir, 10000) } -func ListAllApps() ([]string, error) { +func ListAllApps() ([]wshrpc.AppInfo, error) { homeDir := wavebase.GetHomeDir() waveappsDir := filepath.Join(homeDir, "waveapps") if _, err := os.Stat(waveappsDir); os.IsNotExist(err) { - return []string{}, nil + return []wshrpc.AppInfo{}, nil } namespaces, err := os.ReadDir(waveappsDir) @@ -469,7 +468,7 @@ func ListAllApps() ([]string, error) { return nil, fmt.Errorf("failed to read waveapps directory: %w", err) } - var appIds []string + var appInfos []wshrpc.AppInfo for _, ns := range namespaces { if !ns.IsDir() { @@ -493,13 +492,24 @@ func ListAllApps() ([]string, error) { appId := MakeAppId(namespace, appName) if err := ValidateAppId(appId); err == nil { - appIds = append(appIds, appId) + modTime, _ := GetAppModTime(appId) + appInfo := wshrpc.AppInfo{ + AppId: appId, + ModTime: modTime, + } + + if manifest, err := ReadAppManifest(appId); err == nil { + appInfo.Manifest = manifest + } + + appInfos = append(appInfos, appInfo) } } } - return appIds, nil + return appInfos, nil } + func GetAppModTime(appId string) (int64, error) { if err := ValidateAppId(appId); err != nil { return 0, err @@ -575,7 +585,7 @@ func ListAllEditableApps() ([]wshrpc.AppInfo, error) { var appInfos []wshrpc.AppInfo for appName := range allAppNames { var appId string - var modTimeAppId string + var manifestAppId string if localApps[appName] { appId = MakeAppId(AppNSLocal, appName) } else { @@ -583,17 +593,23 @@ func ListAllEditableApps() ([]wshrpc.AppInfo, error) { } if draftApps[appName] { - modTimeAppId = MakeAppId(AppNSDraft, appName) + manifestAppId = MakeAppId(AppNSDraft, appName) } else { - modTimeAppId = appId + manifestAppId = appId } - modTime, _ := GetAppModTime(modTimeAppId) + modTime, _ := GetAppModTime(manifestAppId) - appInfos = append(appInfos, wshrpc.AppInfo{ + appInfo := wshrpc.AppInfo{ AppId: appId, ModTime: modTime, - }) + } + + if manifest, err := ReadAppManifest(manifestAppId); err == nil { + appInfo.Manifest = manifest + } + + appInfos = append(appInfos, appInfo) } return appInfos, nil @@ -703,7 +719,7 @@ func RenameLocalApp(appName string, newAppName string) error { return nil } -func ReadAppManifest(appId string) (*engine.AppManifest, error) { +func ReadAppManifest(appId string) (*wshrpc.AppManifest, error) { if err := ValidateAppId(appId); err != nil { return nil, fmt.Errorf("invalid appId: %w", err) } @@ -719,7 +735,7 @@ func ReadAppManifest(appId string) (*engine.AppManifest, error) { return nil, fmt.Errorf("failed to read %s: %w", ManifestFileName, err) } - var manifest engine.AppManifest + var manifest wshrpc.AppManifest if err := json.Unmarshal(data, &manifest); err != nil { return nil, fmt.Errorf("failed to parse %s: %w", ManifestFileName, err) } @@ -785,7 +801,7 @@ func WriteAppSecretBindings(appId string, bindings map[string]string) error { return nil } -func BuildAppSecretEnv(appId string, manifest *engine.AppManifest, bindings map[string]string) (map[string]string, error) { +func BuildAppSecretEnv(appId string, manifest *wshrpc.AppManifest, bindings map[string]string) (map[string]string, error) { if manifest == nil { return make(map[string]string), nil } diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index 033346360..49871ff30 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -967,8 +967,9 @@ type CommandTermGetScrollbackLinesRtnData struct { // builder type AppInfo struct { - AppId string `json:"appid"` - ModTime int64 `json:"modtime"` + AppId string `json:"appid"` + ModTime int64 `json:"modtime"` + Manifest *AppManifest `json:"manifest,omitempty"` } type CommandListAllAppFilesData struct { From b6f6f4aaa95caa9f81f67a37dc49d6d0e9468ce3 Mon Sep 17 00:00:00 2001 From: sawka Date: Thu, 13 Nov 2025 23:23:23 -0800 Subject: [PATCH 3/4] show and launch tsunami apps from the sidebar... --- frontend/app/store/wshclientapi.ts | 5 + frontend/app/workspace/widgets.tsx | 188 ++++++++++++++++++++++++++++- frontend/types/gotypes.d.ts | 1 + pkg/wshrpc/wshclient/wshclient.go | 6 + pkg/wshrpc/wshrpctypes.go | 2 + pkg/wshrpc/wshserver/wshserver.go | 4 + 6 files changed, 203 insertions(+), 3 deletions(-) diff --git a/frontend/app/store/wshclientapi.ts b/frontend/app/store/wshclientapi.ts index aafaad7b0..557154a71 100644 --- a/frontend/app/store/wshclientapi.ts +++ b/frontend/app/store/wshclientapi.ts @@ -362,6 +362,11 @@ class RpcApiType { return client.wshRpcCall("listallappfiles", data, opts); } + // command "listallapps" [call] + ListAllAppsCommand(client: WshClient, opts?: RpcOpts): Promise { + return client.wshRpcCall("listallapps", null, opts); + } + // command "listalleditableapps" [call] ListAllEditableAppsCommand(client: WshClient, opts?: RpcOpts): Promise { return client.wshRpcCall("listalleditableapps", null, opts); diff --git a/frontend/app/workspace/widgets.tsx b/frontend/app/workspace/widgets.tsx index f22e784cf..9f7ef208f 100644 --- a/frontend/app/workspace/widgets.tsx +++ b/frontend/app/workspace/widgets.tsx @@ -7,6 +7,15 @@ import { RpcApi } from "@/app/store/wshclientapi"; import { TabRpcClient } from "@/app/store/wshrpcutil"; import { atoms, createBlock, getApi, isDev } from "@/store/global"; import { fireAndForget, isBlank, makeIconClass } from "@/util/util"; +import { + FloatingPortal, + autoUpdate, + offset, + shift, + useDismiss, + useFloating, + useInteractions, +} from "@floating-ui/react"; import clsx from "clsx"; import { useAtomValue } from "jotai"; import { memo, useCallback, useEffect, useRef, useState } from "react"; @@ -67,6 +76,126 @@ const Widget = memo(({ widget, mode }: { widget: WidgetConfigType; mode: "normal ); }); +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; +} + +const AppsFloatingWindow = memo( + ({ + isOpen, + onClose, + referenceElement, + }: { + isOpen: boolean; + onClose: () => void; + referenceElement: HTMLElement; + }) => { + const [apps, setApps] = useState([]); + const [loading, setLoading] = useState(true); + + 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]); + + useEffect(() => { + if (!isOpen) return; + + const fetchApps = async () => { + setLoading(true); + try { + const allApps = await RpcApi.ListAllAppsCommand(TabRpcClient); + const localApps = allApps.filter((app) => !app.appid.startsWith("draft/")); + 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, + }, + }; + createBlock(blockDef); + onClose(); + }} + > +
+ +
+
+ {displayName} +
+
+ ); + })} +
+ )} +
+
+ ); + } +); + const Widgets = memo(() => { const fullConfig = useAtomValue(atoms.fullConfigAtom); const hasCustomAIPresets = useAtomValue(atoms.hasCustomAIPresetsAtom); @@ -100,6 +229,9 @@ const Widgets = memo(() => { : Object.fromEntries(Object.entries(widgetsMap).filter(([key]) => key !== "defwidget@ai")); const widgets = sortByDisplayOrder(filteredWidgets); + const [isAppsOpen, setIsAppsOpen] = useState(false); + const appsButtonRef = useRef(null); + const checkModeNeeded = useCallback(() => { if (!containerRef.current || !measurementRef.current) return; @@ -204,10 +336,27 @@ const Widgets = memo(() => { ))}
- {showHelp ? ( + {isDev() || showHelp ? (
- - + {isDev() ? ( +
setIsAppsOpen(!isAppsOpen)} + > + +
+ +
+
+
+ ) : null} + {showHelp ? ( + <> + + + + ) : null}
) : null} @@ -217,6 +366,24 @@ const Widgets = memo(() => { ))}
+ {isDev() ? ( +
setIsAppsOpen(!isAppsOpen)} + > + +
+ +
+ {mode === "normal" && ( +
+ apps +
+ )} +
+
+ ) : null} {showHelp ? ( <> @@ -234,6 +401,13 @@ const Widgets = memo(() => {
) : null}
+ {isDev() && appsButtonRef.current && ( + setIsAppsOpen(false)} + referenceElement={appsButtonRef.current} + /> + )}
{ ) : null} + {isDev() ? ( +
+
+ +
+
apps
+
+ ) : null} {isDev() ? (
Date: Thu, 13 Nov 2025 23:25:20 -0800 Subject: [PATCH 4/4] sort apps --- frontend/app/workspace/widgets.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/frontend/app/workspace/widgets.tsx b/frontend/app/workspace/widgets.tsx index 9f7ef208f..0390614a7 100644 --- a/frontend/app/workspace/widgets.tsx +++ b/frontend/app/workspace/widgets.tsx @@ -118,7 +118,13 @@ const AppsFloatingWindow = memo( setLoading(true); try { const allApps = await RpcApi.ListAllAppsCommand(TabRpcClient); - const localApps = allApps.filter((app) => !app.appid.startsWith("draft/")); + 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);