Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
254 changes: 254 additions & 0 deletions cmd/wsh/cmd/wshcmd-blocks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"encoding/json"
"fmt"
"sort"
"strings"

"github.com/spf13/cobra"
"github.com/wavetermdev/waveterm/pkg/waveobj"
"github.com/wavetermdev/waveterm/pkg/wshrpc"
"github.com/wavetermdev/waveterm/pkg/wshrpc/wshclient"
)

// Command-line flags for the blocks commands
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.)
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
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
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"},
Short: "List blocks in workspaces/windows",
Long: `List blocks with optional filtering by workspace, window, tab, or view type.

Examples:
# List blocks from all workspaces
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

# 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,
SilenceUsage: true,
}

// 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")
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" {
cmd.AddCommand(blocksListCmd)
return
}
}

blocksCmd := &cobra.Command{
Use: "blocks",
Short: "Manage blocks",
Long: "Commands for working with blocks",
}

blocksCmd.AddCommand(blocksListCmd)
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

workspaces, err := wshclient.WorkspaceListCommand(RpcClient, &wshrpc.RpcOpts{Timeout: int64(blocksTimeout * 1000)})
if err != nil {
return fmt.Errorf("failed to list workspaces: %v", err)
}

if len(workspaces) == 0 {
return fmt.Errorf("no workspaces found")
}

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 != "" {
// 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 {
// Default to all workspaces
for _, ws := range workspaces {
workspaceIdsToQuery = append(workspaceIdsToQuery, ws.WorkspaceData.OID)
}
}

// Query each selected workspace
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)})
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
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Fix --tab=current: it’s silently ignored; either implement or reject explicitly.

Current logic treats "current" as no-op and returns all tabs, which is misleading. Reject it up-front for now and simplify the in-loop filter.

@@
-    if len(workspaces) == 0 {
-        return fmt.Errorf("no workspaces found")
-    }
+    if len(workspaces) == 0 {
+        return fmt.Errorf("no workspaces found")
+    }
+
+    // TODO: implement resolving the active/current tab via RPC, then accept --tab=current
+    if strings.EqualFold(blocksTabId, "current") {
+        return fmt.Errorf("--tab=current is not supported yet; please specify a tab id")
+    }
@@
-            if blocksTabId != "" && blocksTabId != "current" && b.TabId != blocksTabId {
+            if blocksTabId != "" && b.TabId != blocksTabId {
                 continue
             }

Also applies to: 100-106

🤖 Prompt for AI Agents
In cmd/wsh/cmd/wshcmd-blocks.go around lines 100-106 and 147-158, the code
treats the --tab=current value as a no-op and ends up returning all tabs;
instead, explicitly reject "current" at argument-parsing time and simplify the
in-loop filter. Add an upfront validation that if blocksTabId == "current"
return an error (or print a clear message and exit) so "current" is not silently
ignored, and then remove the special-case handling from the loop so the loop
only checks b.TabId != blocksTabId; leave view filtering unchanged.


allBlocks = append(allBlocks, BlockDetails{
BlockId: b.BlockId,
WorkspaceId: b.WorkspaceId,
TabId: b.TabId,
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
}

if len(allBlocks) == 0 {
WriteStdout("No blocks found\n")
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, "<unknown>")
var content string

switch view {
case "preview", "edit":
content = b.Meta.GetString(waveobj.MetaKey_File, "<no file>")
case "web":
content = b.Meta.GetString(waveobj.MetaKey_Url, "<no url>")
case "term":
content = b.Meta.GetString(waveobj.MetaKey_CmdCwd, "<no cwd>")
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, 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
}
5 changes: 5 additions & 0 deletions frontend/app/store/wshclientapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ class RpcApiType {
return client.wshRpcCall("blockinfo", data, opts);
}

// command "blockslist" [call]
BlocksListCommand(client: WshClient, data: BlocksListRequest, opts?: RpcOpts): Promise<BlocksListEntry[]> {
return client.wshRpcCall("blockslist", data, opts);
}

// command "connconnect" [call]
ConnConnectCommand(client: WshClient, data: ConnRequest, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("connconnect", data, opts);
Expand Down
15 changes: 15 additions & 0 deletions frontend/types/gotypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down
6 changes: 6 additions & 0 deletions pkg/wshrpc/wshclient/wshclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions pkg/wshrpc/wshrpctypes.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ const (
Command_Mkdir = "mkdir"
Command_ResolveIds = "resolveids"
Command_BlockInfo = "blockinfo"
Command_BlocksList = "blockslist"
Command_CreateBlock = "createblock"
Command_DeleteBlock = "deleteblock"

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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"`
}
Expand Down
Loading