Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ storybook-static/
test-results.xml

docsite/
public/excalidraw/

.kilo-format-temp-*
.superpowers
Expand Down
191 changes: 191 additions & 0 deletions cmd/wsh/cmd/wshcmd-excalidraw.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"

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

var excalidrawMagnified bool

var excalidrawCmd = &cobra.Command{
Use: "excalidraw [file]",
Short: "open an Excalidraw diagram",
Args: cobra.MaximumNArgs(1),
RunE: excalidrawRun,
PreRunE: preRunSetupRpcClient,
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

var excalidrawPushCmd = &cobra.Command{
Use: "push <blockid> [file]",
Short: "push Excalidraw JSON into a block's scene",
Args: cobra.RangeArgs(1, 2),
RunE: excalidrawPushRun,
PreRunE: preRunSetupRpcClient,
}

var excalidrawMermaidCmd = &cobra.Command{
Use: "mermaid [blockid] [file]",
Short: "open or push a Mermaid diagram as Excalidraw",
Args: cobra.RangeArgs(0, 2),
RunE: excalidrawMermaidRun,
PreRunE: preRunSetupRpcClient,
}

func init() {
excalidrawCmd.Flags().BoolVarP(&excalidrawMagnified, "magnified", "m", false, "open in magnified mode")
excalidrawCmd.AddCommand(excalidrawPushCmd)
excalidrawCmd.AddCommand(excalidrawMermaidCmd)
rootCmd.AddCommand(excalidrawCmd)
}

func excalidrawRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
sendActivity("excalidraw", rtnErr == nil)
}()
tabId := getTabIdFromEnv()
if tabId == "" {
return fmt.Errorf("no WAVETERM_TABID env var set")
}
meta := map[string]any{
waveobj.MetaKey_View: "excalidraw",
}
if len(args) > 0 {
absFile, err := filepath.Abs(args[0])
if err != nil {
return fmt.Errorf("getting absolute path: %w", err)
}
meta[waveobj.MetaKey_File] = absFile
}
if RpcContext.Conn != "" {
meta[waveobj.MetaKey_Connection] = RpcContext.Conn
}
wshCmd := &wshrpc.CommandCreateBlockData{
TabId: tabId,
BlockDef: &waveobj.BlockDef{
Meta: meta,
},
Magnified: excalidrawMagnified,
Focused: true,
}
_, err := wshclient.CreateBlockCommand(RpcClient, *wshCmd, &wshrpc.RpcOpts{Timeout: 2000})
if err != nil {
return fmt.Errorf("creating excalidraw block: %w", err)
}
return nil
}

func excalidrawPushRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
sendActivity("excalidraw:push", rtnErr == nil)
}()
blockId := args[0]
var jsonData []byte
var err error
if len(args) > 1 {
jsonData, err = os.ReadFile(args[1])
} else {
jsonData, err = io.ReadAll(io.LimitReader(os.Stdin, MaxFileSize+1))
}
if err != nil {
return fmt.Errorf("reading input: %w", err)
}
if len(jsonData) > MaxFileSize {
return fmt.Errorf("input exceeds maximum size of %d bytes", MaxFileSize)
}
var sceneData any
if err := json.Unmarshal(jsonData, &sceneData); err != nil {
return fmt.Errorf("invalid JSON: %w", err)
}
pushData := wshrpc.CommandExcalidrawPushData{
BlockId: blockId,
SceneData: sceneData,
}
err = wshclient.ExcalidrawPushCommand(RpcClient, pushData, &wshrpc.RpcOpts{Timeout: 5000})
if err != nil {
return fmt.Errorf("push failed: %w", err)
}
return nil
}

func excalidrawMermaidRun(cmd *cobra.Command, args []string) (rtnErr error) {
defer func() {
sendActivity("excalidraw:mermaid", rtnErr == nil)
}()
var blockId string
var mermaidData []byte
var err error
switch len(args) {
case 0:
mermaidData, err = io.ReadAll(io.LimitReader(os.Stdin, MaxFileSize+1))
if err != nil {
return fmt.Errorf("reading stdin: %w", err)
}
case 1:
mermaidData, err = os.ReadFile(args[0])
if err != nil {
if !os.IsNotExist(err) {
return fmt.Errorf("reading file: %w", err)
}
if _, uuidErr := uuid.Parse(args[0]); uuidErr != nil {
return fmt.Errorf("file not found: %s", args[0])
}
blockId = args[0]
mermaidData, err = io.ReadAll(io.LimitReader(os.Stdin, MaxFileSize+1))
if err != nil {
return fmt.Errorf("reading stdin: %w", err)
}
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
case 2:
blockId = args[0]
mermaidData, err = os.ReadFile(args[1])
if err != nil {
return fmt.Errorf("reading file: %w", err)
}
}
if len(mermaidData) > MaxFileSize {
return fmt.Errorf("input exceeds maximum size of %d bytes", MaxFileSize)
}
if blockId == "" {
tabId := getTabIdFromEnv()
if tabId == "" {
return fmt.Errorf("no WAVETERM_TABID env var set")
}
createData := &wshrpc.CommandCreateBlockData{
TabId: tabId,
BlockDef: &waveobj.BlockDef{
Meta: map[string]any{
waveobj.MetaKey_View: "excalidraw",
},
},
Magnified: excalidrawMagnified,
Focused: true,
}
oref, err := wshclient.CreateBlockCommand(RpcClient, *createData, &wshrpc.RpcOpts{Timeout: 2000})
if err != nil {
return fmt.Errorf("creating excalidraw block: %w", err)
}
blockId = oref.OID
}
pushData := wshrpc.CommandExcalidrawPushData{
BlockId: blockId,
SceneData: string(mermaidData),
Format: "mermaid",
}
err = wshclient.ExcalidrawPushCommand(RpcClient, pushData, &wshrpc.RpcOpts{Timeout: 5000})
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if err != nil {
return fmt.Errorf("mermaid push failed: %w", err)
}
return nil
}
67 changes: 67 additions & 0 deletions docs/docs/wsh-reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,73 @@ wsh editconfig presets/ai.json

---

## excalidraw

Open an Excalidraw diagram in a new block.

```sh
wsh excalidraw [file]
```

Opens the specified `.excalidraw` file for editing. If the file does not exist, creates an empty canvas with that file path set for autosave. If no file is specified, opens a blank canvas.

Flags:

- `-m, --magnified` - open the block in magnified mode

Examples:

```sh
# Open an existing diagram
wsh excalidraw diagram.excalidraw

# Create a new diagram (file will be created on first save)
wsh excalidraw ~/diagrams/new-design.excalidraw

# Open a blank canvas (no file path)
wsh excalidraw

# Open in magnified mode
wsh excalidraw -m architecture.excalidraw
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

### push

```sh
wsh excalidraw push <blockid> [file]
```

Replaces the scene in an existing Excalidraw block with Excalidraw JSON read from `file`, or from stdin if no file is given. If the block is backed by a file, the pushed scene is autosaved to it.

```sh
# Replace a block's scene from a file
wsh excalidraw push <blockid> diagram.excalidraw

# Pipe a generated scene into a block
cat scene.json | wsh excalidraw push <blockid>
```

### mermaid

```sh
wsh excalidraw mermaid [blockid] [file]
```

Converts a Mermaid diagram to Excalidraw. With no `blockid`, opens the result in a new block. The Mermaid source is read from `file`, or from stdin if no file is given.

```sh
# Convert a Mermaid file and open in a new block
wsh excalidraw mermaid flowchart.mmd

# Push a converted Mermaid diagram into an existing block
wsh excalidraw mermaid <blockid> flowchart.mmd

# Pipe Mermaid source into an existing block
echo "graph TD; A-->B" | wsh excalidraw mermaid <blockid>
```

---

## setbg

The `setbg` command allows you to set a background image or color for the current tab with various customization options.
Expand Down
5 changes: 5 additions & 0 deletions electron.vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,9 @@ export default defineConfig({
},
renderer: {
root: ".",
define: {
"process.env.IS_PREACT": JSON.stringify("false"),
},
build: {
target: CHROME,
sourcemap: true,
Expand All @@ -142,6 +145,8 @@ export default defineConfig({
}
if (p.includes("node_modules/cytoscape") || p.includes("node_modules/@cytoscape"))
return "cytoscape";
if (p.includes("node_modules/excalidraw") || p.includes("node_modules/@excalidraw"))
return "excalidraw";
return undefined;
},
},
Expand Down
2 changes: 2 additions & 0 deletions frontend/app/block/blockregistry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { TabModel } from "@/app/store/tab-model";
import { AiFileDiffViewModel } from "@/app/view/aifilediff/aifilediff";
import { LauncherViewModel } from "@/app/view/launcher/launcher";
import { PreviewModel } from "@/app/view/preview/preview-model";
import { ExcalidrawModel } from "@/app/view/excalidraw/excalidraw-model";
import { ProcessViewerViewModel } from "@/app/view/processviewer/processviewer";
import { SysinfoViewModel } from "@/app/view/sysinfo/sysinfo";
import { TsunamiViewModel } from "@/app/view/tsunami/tsunami";
Expand Down Expand Up @@ -35,6 +36,7 @@ BlockRegistry.set("tsunami", TsunamiViewModel);
BlockRegistry.set("aifilediff", AiFileDiffViewModel);
BlockRegistry.set("waveconfig", WaveConfigViewModel);
BlockRegistry.set("processviewer", ProcessViewerViewModel);
BlockRegistry.set("excalidraw", ExcalidrawModel);

function makeDefaultViewModel(viewType: string): ViewModel {
const viewModel: ViewModel = {
Expand Down
6 changes: 6 additions & 0 deletions frontend/app/block/blockutil.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ export function blockViewToIcon(view: string): string {
if (view == "processviewer") {
return "microchip";
}
if (view == "excalidraw") {
return "pen-ruler";
}
return "square";
}

Expand Down Expand Up @@ -73,6 +76,9 @@ export function blockViewToName(view: string): string {
if (view == "processviewer") {
return "Processes";
}
if (view == "excalidraw") {
return "Excalidraw";
}
return view;
}

Expand Down
6 changes: 6 additions & 0 deletions frontend/app/store/wshclientapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,12 @@ export class RpcApiType {
return client.wshRpcCall("eventunsuball", null, opts);
}

// command "excalidrawpush" [call]
ExcalidrawPushCommand(client: WshClient, data: CommandExcalidrawPushData, opts?: RpcOpts): Promise<void> {
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "excalidrawpush", data, opts);
return client.wshRpcCall("excalidrawpush", data, opts);
}

// command "fetchsuggestions" [call]
FetchSuggestionsCommand(client: WshClient, data: FetchSuggestionsData, opts?: RpcOpts): Promise<FetchSuggestionsResponse> {
if (this.mockClient) return this.mockClient.mockWshRpcCall(client, "fetchsuggestions", data, opts);
Expand Down
Loading