From 5350fbdc68458b7d496998beaf2e993d6c908bc9 Mon Sep 17 00:00:00 2001 From: Oleksandr Hreshchuk Date: Fri, 12 Sep 2025 15:49:19 +0300 Subject: [PATCH 1/5] feat(cli): add blocks list with filtering and JSON output --- cmd/wsh/cmd/wshcmd-blocks.go | 227 +++++++++++++++++++++++++++++ frontend/app/store/wshclientapi.ts | 5 + frontend/types/gotypes.d.ts | 15 ++ pkg/wshrpc/wshclient/wshclient.go | 6 + pkg/wshrpc/wshrpctypes.go | 15 ++ pkg/wshrpc/wshserver/wshserver.go | 67 +++++++++ 6 files changed, 335 insertions(+) create mode 100644 cmd/wsh/cmd/wshcmd-blocks.go diff --git a/cmd/wsh/cmd/wshcmd-blocks.go b/cmd/wsh/cmd/wshcmd-blocks.go new file mode 100644 index 0000000000..a3ac3fba9b --- /dev/null +++ b/cmd/wsh/cmd/wshcmd-blocks.go @@ -0,0 +1,227 @@ +// Copyright 2025, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +package cmd + +import ( + "encoding/json" + "fmt" + "strings" + + "github.com/spf13/cobra" + "github.com/wavetermdev/waveterm/pkg/waveobj" + "github.com/wavetermdev/waveterm/pkg/wshrpc" + "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" +) + +var ( + blocksWindowId string + blocksWorkspaceId string + blocksTabId string + blocksView string + blocksJSON bool +) + +type BlockDetails struct { + BlockId string `json:"blockid"` + WorkspaceId string `json:"workspaceid"` + TabId string `json:"tabid"` + Meta waveobj.MetaMapType `json:"meta"` +} + +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. + +Examples: + # List blocks in the current workspace + wsh blocks list + + # List only terminal blocks + wsh blocks list --view=term + + # Filter by window ID (get IDs from 'wsh workspace list') + wsh blocks list --window=dbca23b5-f89b-4780-a0fe-452f5bc7d900 + + # Output as JSON for scripting + wsh blocks list --json`, + RunE: blocksListRun, + PreRunE: preRunSetupRpcClient, +} + +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 tab id") + blocksListCmd.Flags().StringVar(&blocksView, "view", "", "restrict to view type (term/terminal, web/browser, preview/edit, sysinfo, waveai)") + blocksListCmd.Flags().BoolVar(&blocksJSON, "json", false, "output as JSON") + + for _, cmd := range rootCmd.Commands() { + if cmd.Use == "blocks" { + cmd.AddCommand(blocksListCmd) + return + } + } + + blocksCmd := &cobra.Command{ + Use: "blocks", + Short: "Manage blocks", + Long: "Commands for working with blocks", + } + + blocksCmd.AddCommand(blocksListCmd) + rootCmd.AddCommand(blocksCmd) +} + +func blocksListRun(cmd *cobra.Command, args []string) error { + var allBlocks []BlockDetails + + workspaces, err := wshclient.WorkspaceListCommand(RpcClient, &wshrpc.RpcOpts{Timeout: 5000}) + if err != nil { + return fmt.Errorf("failed to list workspaces: %v", err) + } + + if len(workspaces) == 0 { + return fmt.Errorf("no workspaces found") + } + + var currentWorkspaceId string + if len(workspaces) > 0 { + currentWorkspaceId = workspaces[0].WorkspaceData.OID + } + + var workspaceIdsToQuery []string + + // Determine which workspaces to query + if blocksWorkspaceId != "" { + workspaceIdsToQuery = []string{blocksWorkspaceId} + } else if blocksWindowId != "" { + // Find workspace ID for this window + windowFound := false + for _, ws := range workspaces { + if ws.WindowId == blocksWindowId { + workspaceIdsToQuery = []string{ws.WorkspaceData.OID} + windowFound = true + break + } + } + if !windowFound { + return fmt.Errorf("window %s not found", blocksWindowId) + } + } else if blocksTabId != "" { + // When filtering by tab, we need to check all workspaces + for _, ws := range workspaces { + workspaceIdsToQuery = append(workspaceIdsToQuery, ws.WorkspaceData.OID) + } + } else { + // Default to current workspace + workspaceIdsToQuery = []string{currentWorkspaceId} + } + + // Query each selected workspace + for _, wsId := range workspaceIdsToQuery { + req := wshrpc.BlocksListRequest{ + WorkspaceId: wsId, + } + + blocks, err := wshclient.BlocksListCommand(RpcClient, req, &wshrpc.RpcOpts{Timeout: 5000}) + if err != nil { + WriteStderr("Warning: couldn't list blocks for workspace %s: %v\n", wsId, err) + continue + } + + // Apply filters + for _, b := range blocks { + if blocksTabId != "" && blocksTabId != "current" && b.TabId != blocksTabId { + continue + } + + if blocksView != "" { + view := b.Meta.GetString(waveobj.MetaKey_View, "") + + // Support view type aliases + if !matchesViewType(view, blocksView) { + continue + } + } + + allBlocks = append(allBlocks, BlockDetails{ + BlockId: b.BlockId, + WorkspaceId: b.WorkspaceId, + TabId: b.TabId, + Meta: b.Meta, + }) + } + } + + // Output results + if blocksJSON { + bytes, _ := json.MarshalIndent(allBlocks, "", " ") + WriteStdout("%s\n", string(bytes)) + return nil + } + + if len(allBlocks) == 0 { + WriteStdout("No blocks found\n") + return nil + } + + format := "%-36s %-10s %-36s %-15s %s\n" + WriteStdout(format, "BLOCK ID", "WORKSPACE", "TAB ID", "VIEW", "CONTENT") + + for _, b := range allBlocks { + view := b.Meta.GetString(waveobj.MetaKey_View, "") + var content string + + 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: + content = "" + } + + wsID := b.WorkspaceId + if len(wsID) > 10 { + wsID = wsID[0:8] + ".." + } + + tabID := b.TabId + if len(tabID) > 36 { + tabID = tabID[0:34] + ".." + } + + WriteStdout(format, b.BlockId, wsID, tabID, view, content) + } + + return nil +} + +// matchesViewType checks if a view type matches a filter, supporting aliases +func matchesViewType(actual, filter string) bool { + // Direct match (case insensitive) + if strings.EqualFold(actual, filter) { + return true + } + + // Handle aliases + switch strings.ToLower(filter) { + case "preview", "edit": + 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 +} diff --git a/frontend/app/store/wshclientapi.ts b/frontend/app/store/wshclientapi.ts index fba651911f..e39e026eec 100644 --- a/frontend/app/store/wshclientapi.ts +++ b/frontend/app/store/wshclientapi.ts @@ -32,6 +32,11 @@ class RpcApiType { return client.wshRpcCall("blockinfo", data, opts); } + // command "blockslist" [call] + BlocksListCommand(client: WshClient, data: BlocksListRequest, opts?: RpcOpts): Promise { + return client.wshRpcCall("blockslist", data, opts); + } + // command "connconnect" [call] ConnConnectCommand(client: WshClient, data: ConnRequest, opts?: RpcOpts): Promise { return client.wshRpcCall("connconnect", data, opts); diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index 0d70582e48..7e8fd603b3 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -84,6 +84,21 @@ declare global { inputdata64: string; }; + // wshrpc.BlocksListEntry + type BlocksListEntry = { + windowid: string; + workspaceid: string; + tabid: string; + blockid: string; + meta: MetaType; + }; + + // wshrpc.BlocksListRequest + type BlocksListRequest = { + windowid?: string; + workspaceid?: string; + }; + // waveobj.Client type Client = WaveObj & { windowids: string[]; diff --git a/pkg/wshrpc/wshclient/wshclient.go b/pkg/wshrpc/wshclient/wshclient.go index 243065b9cc..4debe2cc6d 100644 --- a/pkg/wshrpc/wshclient/wshclient.go +++ b/pkg/wshrpc/wshclient/wshclient.go @@ -46,6 +46,12 @@ func BlockInfoCommand(w *wshutil.WshRpc, data string, opts *wshrpc.RpcOpts) (*ws return resp, err } +// command "blockslist", wshserver.BlocksListCommand +func BlocksListCommand(w *wshutil.WshRpc, data wshrpc.BlocksListRequest, opts *wshrpc.RpcOpts) ([]wshrpc.BlocksListEntry, error) { + resp, err := sendRpcRequestCallHelper[[]wshrpc.BlocksListEntry](w, "blockslist", data, opts) + return resp, err +} + // command "connconnect", wshserver.ConnConnectCommand func ConnConnectCommand(w *wshutil.WshRpc, data wshrpc.ConnRequest, opts *wshrpc.RpcOpts) error { _, err := sendRpcRequestCallHelper[any](w, "connconnect", data, opts) diff --git a/pkg/wshrpc/wshrpctypes.go b/pkg/wshrpc/wshrpctypes.go index 0b984a468e..371b6e1173 100644 --- a/pkg/wshrpc/wshrpctypes.go +++ b/pkg/wshrpc/wshrpctypes.go @@ -67,6 +67,7 @@ const ( Command_Mkdir = "mkdir" Command_ResolveIds = "resolveids" Command_BlockInfo = "blockinfo" + Command_BlocksList = "blockslist" Command_CreateBlock = "createblock" Command_DeleteBlock = "deleteblock" @@ -196,6 +197,7 @@ type WshRpcInterface interface { SetConnectionsConfigCommand(ctx context.Context, data ConnConfigRequest) error GetFullConfigCommand(ctx context.Context) (wconfig.FullConfigType, error) BlockInfoCommand(ctx context.Context, blockId string) (*BlockInfoData, error) + BlocksListCommand(ctx context.Context, data BlocksListRequest) ([]BlocksListEntry, error) WaveInfoCommand(ctx context.Context) (*WaveInfoData, error) WshActivityCommand(ct context.Context, data map[string]int) error ActivityCommand(ctx context.Context, data ActivityUpdate) error @@ -678,6 +680,19 @@ type WorkspaceInfoData struct { WorkspaceData *waveobj.Workspace `json:"workspacedata"` } +type BlocksListRequest struct { + WindowId string `json:"windowid,omitempty"` + WorkspaceId string `json:"workspaceid,omitempty"` +} + +type BlocksListEntry struct { + WindowId string `json:"windowid"` + WorkspaceId string `json:"workspaceid"` + TabId string `json:"tabid"` + BlockId string `json:"blockid"` + Meta waveobj.MetaMapType `json:"meta"` +} + type AiMessageData struct { Message string `json:"message,omitempty"` } diff --git a/pkg/wshrpc/wshserver/wshserver.go b/pkg/wshrpc/wshserver/wshserver.go index 70029392bc..f73bef29f9 100644 --- a/pkg/wshrpc/wshserver/wshserver.go +++ b/pkg/wshrpc/wshserver/wshserver.go @@ -813,6 +813,73 @@ func (ws *WshServer) WaveInfoCommand(ctx context.Context) (*wshrpc.WaveInfoData, }, nil } +// BlocksListCommand returns every block visible in the requested +// scope (current workspace by default). +func (ws *WshServer) BlocksListCommand( + ctx context.Context, + req wshrpc.BlocksListRequest) ([]wshrpc.BlocksListEntry, error) { + var results []wshrpc.BlocksListEntry + + // Resolve the set of workspaces to inspect + var workspaceIDs []string + if req.WorkspaceId != "" { + workspaceIDs = []string{req.WorkspaceId} + } else if req.WindowId != "" { + win, err := wcore.GetWindow(ctx, req.WindowId) + if err != nil { + return nil, err + } + workspaceIDs = []string{win.WorkspaceId} + } else { + // "current" == first workspace in client focus list + client, err := wstore.DBGetSingleton[*waveobj.Client](ctx) + if err != nil { + return nil, err + } + if len(client.WindowIds) == 0 { + return nil, fmt.Errorf("no active window") + } + win, err := wcore.GetWindow(ctx, client.WindowIds[0]) + if err != nil { + return nil, err + } + workspaceIDs = []string{win.WorkspaceId} + } + + for _, wsID := range workspaceIDs { + wsData, err := wcore.GetWorkspace(ctx, wsID) + if err != nil { + return nil, err + } + + windowId, err := wstore.DBFindWindowForWorkspaceId(ctx, wsID) + if err != nil { + log.Printf("error finding window for workspace %s: %v", wsID, err) + } + + for _, tabID := range append(wsData.PinnedTabIds, wsData.TabIds...) { + tab, err := wstore.DBMustGet[*waveobj.Tab](ctx, tabID) + if err != nil { + return nil, err + } + for _, blkID := range tab.BlockIds { + blk, err := wstore.DBMustGet[*waveobj.Block](ctx, blkID) + if err != nil { + return nil, err + } + results = append(results, wshrpc.BlocksListEntry{ + WindowId: windowId, + WorkspaceId: wsID, + TabId: tabID, + BlockId: blkID, + Meta: blk.Meta, + }) + } + } + } + return results, nil +} + func (ws *WshServer) WorkspaceListCommand(ctx context.Context) ([]wshrpc.WorkspaceInfoData, error) { workspaceList, err := wcore.ListWorkspaces(ctx) if err != nil { From c5dba041536b09d2a16b6d59814405e7b48ec23b Mon Sep 17 00:00:00 2001 From: Oleksandr Hreshchuk Date: Fri, 12 Sep 2025 16:28:30 +0300 Subject: [PATCH 2/5] add more comments and use all workspaces by default --- cmd/wsh/cmd/wshcmd-blocks.go | 49 ++++++++++++++++++++---------------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/cmd/wsh/cmd/wshcmd-blocks.go b/cmd/wsh/cmd/wshcmd-blocks.go index a3ac3fba9b..d33aec3cbb 100644 --- a/cmd/wsh/cmd/wshcmd-blocks.go +++ b/cmd/wsh/cmd/wshcmd-blocks.go @@ -14,21 +14,24 @@ import ( "github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient" ) +// Command-line flags for the blocks commands var ( - blocksWindowId string - blocksWorkspaceId string - blocksTabId string - blocksView string - blocksJSON bool + 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.) + blocksJSON bool // Whether to output as JSON ) +// BlockDetails represents the information about a block returned by the list command type BlockDetails struct { - BlockId string `json:"blockid"` - WorkspaceId string `json:"workspaceid"` - TabId string `json:"tabid"` - Meta waveobj.MetaMapType `json:"meta"` + 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 + Meta waveobj.MetaMapType `json:"meta"` // Block metadata including view type } +// blocksListCmd represents the 'blocks list' command var blocksListCmd = &cobra.Command{ Use: "list", Aliases: []string{"ls", "get"}, @@ -36,7 +39,7 @@ var blocksListCmd = &cobra.Command{ Long: `List blocks with optional filtering by workspace, window, tab, or view type. Examples: - # List blocks in the current workspace + # List blocks from all workspaces wsh blocks list # List only terminal blocks @@ -45,12 +48,17 @@ Examples: # Filter by window ID (get IDs from 'wsh workspace list') wsh blocks list --window=dbca23b5-f89b-4780-a0fe-452f5bc7d900 + # Filter by workspace ID + wsh blocks list --workspace=12d0c067-378e-454c-872e-77a314248114 + # Output as JSON for scripting wsh blocks list --json`, RunE: blocksListRun, PreRunE: preRunSetupRpcClient, } +// init registers the blocks commands with the root command +// It configures all the flags and command options func init() { blocksListCmd.Flags().StringVar(&blocksWindowId, "window", "", "restrict to window id") blocksListCmd.Flags().StringVar(&blocksWorkspaceId, "workspace", "", "restrict to workspace id") @@ -75,6 +83,8 @@ func init() { rootCmd.AddCommand(blocksCmd) } +// blocksListRun implements the 'blocks list' command +// It retrieves and displays blocks with optional filtering by workspace, window, tab, or view type func blocksListRun(cmd *cobra.Command, args []string) error { var allBlocks []BlockDetails @@ -87,11 +97,6 @@ func blocksListRun(cmd *cobra.Command, args []string) error { return fmt.Errorf("no workspaces found") } - var currentWorkspaceId string - if len(workspaces) > 0 { - currentWorkspaceId = workspaces[0].WorkspaceData.OID - } - var workspaceIdsToQuery []string // Determine which workspaces to query @@ -110,14 +115,11 @@ func blocksListRun(cmd *cobra.Command, args []string) error { if !windowFound { return fmt.Errorf("window %s not found", blocksWindowId) } - } else if blocksTabId != "" { - // When filtering by tab, we need to check all workspaces + } else { + // Default to all workspaces for _, ws := range workspaces { workspaceIdsToQuery = append(workspaceIdsToQuery, ws.WorkspaceData.OID) } - } else { - // Default to current workspace - workspaceIdsToQuery = []string{currentWorkspaceId} } // Query each selected workspace @@ -158,7 +160,10 @@ func blocksListRun(cmd *cobra.Command, args []string) error { // Output results if blocksJSON { - bytes, _ := json.MarshalIndent(allBlocks, "", " ") + bytes, err := json.MarshalIndent(allBlocks, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %v", err) + } WriteStdout("%s\n", string(bytes)) return nil } @@ -203,6 +208,8 @@ func blocksListRun(cmd *cobra.Command, args []string) error { } // matchesViewType checks if a view type matches a filter, supporting aliases +// It handles different aliases for the same view type, allowing flexible filtering +// Examples: "term" matches "terminal", "shell", "console"; "web" matches "browser", "url" func matchesViewType(actual, filter string) bool { // Direct match (case insensitive) if strings.EqualFold(actual, filter) { From a52c831234e27fdd4eed6d95dcf75a73ecf2d8eb Mon Sep 17 00:00:00 2001 From: Oleksandr Hreshchuk Date: Fri, 12 Sep 2025 16:47:42 +0300 Subject: [PATCH 3/5] address Nitpick comments --- cmd/wsh/cmd/wshcmd-blocks.go | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/cmd/wsh/cmd/wshcmd-blocks.go b/cmd/wsh/cmd/wshcmd-blocks.go index d33aec3cbb..2d615d2e76 100644 --- a/cmd/wsh/cmd/wshcmd-blocks.go +++ b/cmd/wsh/cmd/wshcmd-blocks.go @@ -6,6 +6,7 @@ package cmd import ( "encoding/json" "fmt" + "sort" "strings" "github.com/spf13/cobra" @@ -21,6 +22,7 @@ var ( blocksTabId string // Tab ID to filter blocks by blocksView string // View type to filter blocks by (term, web, etc.) blocksJSON bool // Whether to output as JSON + blocksTimeout int // Timeout in seconds for RPC calls ) // BlockDetails represents the information about a block returned by the list command @@ -55,6 +57,7 @@ Examples: wsh blocks list --json`, RunE: blocksListRun, PreRunE: preRunSetupRpcClient, + SilenceUsage: true, } // init registers the blocks commands with the root command @@ -65,6 +68,7 @@ func init() { blocksListCmd.Flags().StringVar(&blocksTabId, "tab", "", "restrict to tab id") blocksListCmd.Flags().StringVar(&blocksView, "view", "", "restrict to view type (term/terminal, web/browser, preview/edit, sysinfo, waveai)") blocksListCmd.Flags().BoolVar(&blocksJSON, "json", false, "output as JSON") + blocksListCmd.Flags().IntVar(&blocksTimeout, "timeout", 5, "timeout in seconds for RPC calls") for _, cmd := range rootCmd.Commands() { if cmd.Use == "blocks" { @@ -88,7 +92,7 @@ func init() { func blocksListRun(cmd *cobra.Command, args []string) error { var allBlocks []BlockDetails - workspaces, err := wshclient.WorkspaceListCommand(RpcClient, &wshrpc.RpcOpts{Timeout: 5000}) + workspaces, err := wshclient.WorkspaceListCommand(RpcClient, &wshrpc.RpcOpts{Timeout: int64(blocksTimeout * 1000)}) if err != nil { return fmt.Errorf("failed to list workspaces: %v", err) } @@ -100,6 +104,9 @@ func blocksListRun(cmd *cobra.Command, args []string) error { var workspaceIdsToQuery []string // Determine which workspaces to query + if blocksWorkspaceId != "" && blocksWindowId != "" { + return fmt.Errorf("--workspace and --window are mutually exclusive; specify only one") + } if blocksWorkspaceId != "" { workspaceIdsToQuery = []string{blocksWorkspaceId} } else if blocksWindowId != "" { @@ -124,11 +131,12 @@ func blocksListRun(cmd *cobra.Command, args []string) error { // Query each selected workspace for _, wsId := range workspaceIdsToQuery { - req := wshrpc.BlocksListRequest{ - WorkspaceId: wsId, + req := wshrpc.BlocksListRequest{WorkspaceId: wsId} + if blocksWindowId != "" { + req.WindowId = blocksWindowId } - blocks, err := wshclient.BlocksListCommand(RpcClient, req, &wshrpc.RpcOpts{Timeout: 5000}) + blocks, err := wshclient.BlocksListCommand(RpcClient, req, &wshrpc.RpcOpts{Timeout: int64(blocksTimeout * 1000)}) if err != nil { WriteStderr("Warning: couldn't list blocks for workspace %s: %v\n", wsId, err) continue @@ -173,10 +181,24 @@ func blocksListRun(cmd *cobra.Command, args []string) error { return nil } + // Stable ordering + sort.Slice(allBlocks, func(i, j int) bool { + if allBlocks[i].WorkspaceId != allBlocks[j].WorkspaceId { + return allBlocks[i].WorkspaceId < allBlocks[j].WorkspaceId + } + if allBlocks[i].TabId != allBlocks[j].TabId { + return allBlocks[i].TabId < allBlocks[j].TabId + } + return allBlocks[i].BlockId < allBlocks[j].BlockId + }) format := "%-36s %-10s %-36s %-15s %s\n" WriteStdout(format, "BLOCK ID", "WORKSPACE", "TAB ID", "VIEW", "CONTENT") for _, b := range allBlocks { + blockID := b.BlockId + if len(blockID) > 36 { + blockID = blockID[:34] + ".." + } view := b.Meta.GetString(waveobj.MetaKey_View, "") var content string @@ -201,15 +223,13 @@ func blocksListRun(cmd *cobra.Command, args []string) error { tabID = tabID[0:34] + ".." } - WriteStdout(format, b.BlockId, wsID, tabID, view, content) + WriteStdout(format, blockID, wsID, tabID, view, content) } return nil } // matchesViewType checks if a view type matches a filter, supporting aliases -// It handles different aliases for the same view type, allowing flexible filtering -// Examples: "term" matches "terminal", "shell", "console"; "web" matches "browser", "url" func matchesViewType(actual, filter string) bool { // Direct match (case insensitive) if strings.EqualFold(actual, filter) { From 52252ce8f431287c598837a6432dbcbdbb306fe4 Mon Sep 17 00:00:00 2001 From: Oleksandr Hreshchuk Date: Fri, 12 Sep 2025 17:21:49 +0300 Subject: [PATCH 4/5] readjust output and fix other remarks from AI --- cmd/wsh/cmd/wshcmd-blocks.go | 67 +++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 23 deletions(-) diff --git a/cmd/wsh/cmd/wshcmd-blocks.go b/cmd/wsh/cmd/wshcmd-blocks.go index 2d615d2e76..54ee577ea4 100644 --- a/cmd/wsh/cmd/wshcmd-blocks.go +++ b/cmd/wsh/cmd/wshcmd-blocks.go @@ -6,8 +6,10 @@ package cmd import ( "encoding/json" "fmt" + "os" "sort" "strings" + "text/tabwriter" "github.com/spf13/cobra" "github.com/wavetermdev/waveterm/pkg/waveobj" @@ -30,6 +32,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) Meta waveobj.MetaMapType `json:"meta"` // Block metadata including view type } @@ -53,8 +56,14 @@ Examples: # Filter by workspace ID wsh blocks list --workspace=12d0c067-378e-454c-872e-77a314248114 + # Filter by tab ID + wsh blocks list --tab=a0459921-cc1a-48cc-ae7b-5f4821e1c9e1 + # Output as JSON for scripting - wsh blocks list --json`, + wsh blocks list --json + + # Set a different timeout (in milliseconds) + wsh blocks list --timeout=10000`, RunE: blocksListRun, PreRunE: preRunSetupRpcClient, SilenceUsage: true, @@ -65,10 +74,10 @@ Examples: 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 tab 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().BoolVar(&blocksJSON, "json", false, "output as JSON") - blocksListCmd.Flags().IntVar(&blocksTimeout, "timeout", 5, "timeout in seconds for RPC calls") + blocksListCmd.Flags().IntVar(&blocksTimeout, "timeout", 5000, "timeout in milliseconds for RPC calls (default: 5000)") for _, cmd := range rootCmd.Commands() { if cmd.Use == "blocks" { @@ -92,7 +101,7 @@ func init() { func blocksListRun(cmd *cobra.Command, args []string) error { var allBlocks []BlockDetails - workspaces, err := wshclient.WorkspaceListCommand(RpcClient, &wshrpc.RpcOpts{Timeout: int64(blocksTimeout * 1000)}) + workspaces, err := wshclient.WorkspaceListCommand(RpcClient, &wshrpc.RpcOpts{Timeout: int64(blocksTimeout)}) if err != nil { return fmt.Errorf("failed to list workspaces: %v", err) } @@ -130,21 +139,23 @@ func blocksListRun(cmd *cobra.Command, args []string) error { } // Query each selected workspace + hadSuccess := false for _, wsId := range workspaceIdsToQuery { req := wshrpc.BlocksListRequest{WorkspaceId: wsId} if blocksWindowId != "" { req.WindowId = blocksWindowId } - blocks, err := wshclient.BlocksListCommand(RpcClient, req, &wshrpc.RpcOpts{Timeout: int64(blocksTimeout * 1000)}) + blocks, err := wshclient.BlocksListCommand(RpcClient, req, &wshrpc.RpcOpts{Timeout: int64(blocksTimeout)}) if err != nil { WriteStderr("Warning: couldn't list blocks for workspace %s: %v\n", wsId, err) continue } + hadSuccess = true // Apply filters for _, b := range blocks { - if blocksTabId != "" && blocksTabId != "current" && b.TabId != blocksTabId { + if blocksTabId != "" && b.TabId != blocksTabId { continue } @@ -157,31 +168,27 @@ func blocksListRun(cmd *cobra.Command, args []string) error { } } + v := b.Meta.GetString(waveobj.MetaKey_View, "") allBlocks = append(allBlocks, BlockDetails{ BlockId: b.BlockId, WorkspaceId: b.WorkspaceId, TabId: b.TabId, + View: v, Meta: b.Meta, }) } } - // Output results - if blocksJSON { - bytes, err := json.MarshalIndent(allBlocks, "", " ") - if err != nil { - return fmt.Errorf("failed to marshal JSON: %v", err) - } - WriteStdout("%s\n", string(bytes)) - return nil - } - + // No blocks found check if len(allBlocks) == 0 { + if !hadSuccess { + return fmt.Errorf("failed to list blocks from all %d workspace(s)", len(workspaceIdsToQuery)) + } WriteStdout("No blocks found\n") return nil } - // Stable ordering + // Stable ordering for both JSON and table output sort.Slice(allBlocks, func(i, j int) bool { if allBlocks[i].WorkspaceId != allBlocks[j].WorkspaceId { return allBlocks[i].WorkspaceId < allBlocks[j].WorkspaceId @@ -191,15 +198,29 @@ func blocksListRun(cmd *cobra.Command, args []string) error { } return allBlocks[i].BlockId < allBlocks[j].BlockId }) - format := "%-36s %-10s %-36s %-15s %s\n" - WriteStdout(format, "BLOCK ID", "WORKSPACE", "TAB ID", "VIEW", "CONTENT") + + // Output results + if blocksJSON { + bytes, err := json.MarshalIndent(allBlocks, "", " ") + if err != nil { + return fmt.Errorf("failed to marshal JSON: %v", err) + } + WriteStdout("%s\n", string(bytes)) + return nil + } + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + defer w.Flush() + fmt.Fprintf(w, "BLOCK ID\tWORKSPACE\tTAB ID\tVIEW\tCONTENT\n") for _, b := range allBlocks { blockID := b.BlockId if len(blockID) > 36 { blockID = blockID[:34] + ".." } - view := b.Meta.GetString(waveobj.MetaKey_View, "") + view := b.View + if view == "" { + view = "" + } var content string switch view { @@ -214,8 +235,8 @@ func blocksListRun(cmd *cobra.Command, args []string) error { } wsID := b.WorkspaceId - if len(wsID) > 10 { - wsID = wsID[0:8] + ".." + if len(wsID) > 36 { + wsID = wsID[:34] + ".." } tabID := b.TabId @@ -223,7 +244,7 @@ func blocksListRun(cmd *cobra.Command, args []string) error { tabID = tabID[0:34] + ".." } - WriteStdout(format, blockID, wsID, tabID, view, content) + fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", blockID, wsID, tabID, view, content) } return nil From 8677306400f51a49a7a71c501abaa9255f0fc518 Mon Sep 17 00:00:00 2001 From: Oleksandr Hreshchuk Date: Fri, 12 Sep 2025 17:40:58 +0300 Subject: [PATCH 5/5] other Nitpick's suggestions --- cmd/wsh/cmd/wshcmd-blocks.go | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/cmd/wsh/cmd/wshcmd-blocks.go b/cmd/wsh/cmd/wshcmd-blocks.go index 54ee577ea4..7e4b935ee3 100644 --- a/cmd/wsh/cmd/wshcmd-blocks.go +++ b/cmd/wsh/cmd/wshcmd-blocks.go @@ -6,7 +6,6 @@ package cmd import ( "encoding/json" "fmt" - "os" "sort" "strings" "text/tabwriter" @@ -24,7 +23,7 @@ var ( blocksTabId string // Tab ID to filter blocks by blocksView string // View type to filter blocks by (term, web, etc.) blocksJSON bool // Whether to output as JSON - blocksTimeout int // Timeout in seconds for RPC calls + blocksTimeout int // Timeout in milliseconds for RPC calls ) // BlockDetails represents the information about a block returned by the list command @@ -99,6 +98,12 @@ func init() { // blocksListRun implements the 'blocks list' command // It retrieves and displays blocks with optional filtering by workspace, window, tab, or view type 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) + } + } + var allBlocks []BlockDetails workspaces, err := wshclient.WorkspaceListCommand(RpcClient, &wshrpc.RpcOpts{Timeout: int64(blocksTimeout)}) @@ -189,7 +194,7 @@ func blocksListRun(cmd *cobra.Command, args []string) error { } // Stable ordering for both JSON and table output - sort.Slice(allBlocks, func(i, j int) bool { + sort.SliceStable(allBlocks, func(i, j int) bool { if allBlocks[i].WorkspaceId != allBlocks[j].WorkspaceId { return allBlocks[i].WorkspaceId < allBlocks[j].WorkspaceId } @@ -208,7 +213,7 @@ func blocksListRun(cmd *cobra.Command, args []string) error { WriteStdout("%s\n", string(bytes)) return nil } - w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + w := tabwriter.NewWriter(WrappedStdout, 0, 0, 2, ' ', 0) defer w.Flush() fmt.Fprintf(w, "BLOCK ID\tWORKSPACE\tTAB ID\tVIEW\tCONTENT\n") @@ -217,7 +222,7 @@ func blocksListRun(cmd *cobra.Command, args []string) error { if len(blockID) > 36 { blockID = blockID[:34] + ".." } - view := b.View + view := strings.ToLower(b.View) if view == "" { view = "" } @@ -273,3 +278,17 @@ func matchesViewType(actual, filter string) bool { return false } + +// isKnownViewFilter checks if a filter value is recognized +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: + return false + } +}