Skip to content
Merged
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
10 changes: 2 additions & 8 deletions aiprompts/view-prompt.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ Wave Terminal uses a modular ViewModel system to define interactive blocks. Each
### Key Concepts

1. **ViewModel Structure**

- Implements the `ViewModel` interface.
- Defines:
- `viewType`: Unique block type identifier.
Expand All @@ -19,28 +18,24 @@ Wave Terminal uses a modular ViewModel system to define interactive blocks. Each
- Lifecycle methods like `dispose()`, `giveFocus()`, `keyDownHandler()`.

2. **ViewComponent Structure**

- A **React function component** implementing `ViewComponentProps<T extends ViewModel>`.
- Uses `blockId`, `blockRef`, `contentRef`, and `model` as props.
- Retrieves ViewModel state using Jotai atoms.
- Returns JSX for rendering.

3. **Header Elements (`HeaderElem[]`)**

- Can include:
- **Icons (`IconButtonDecl`)**: Clickable buttons.
- **Text (`HeaderText`)**: Metadata or status.
- **Inputs (`HeaderInput`)**: Editable fields.
- **Menu Buttons (`MenuButton`)**: Dropdowns.

4. **Jotai Atoms for State Management**

- Use `atom<T>`, `PrimitiveAtom<T>`, `WritableAtom<T>` for dynamic properties.
- `splitAtom` for managing lists of atoms.
- Read settings from `globalStore` and override with block metadata.

5. **Metadata vs. Global Config**

- **Block Metadata (`SetMetaCommand`)**: Each block persists its **own configuration** in its metadata (`blockAtom.meta`).
- **Global Config (`SetConfigCommand`)**: Provides **default settings** for all blocks, stored in config files.
- **Cascading Behavior**:
Expand All @@ -50,7 +45,6 @@ Wave Terminal uses a modular ViewModel system to define interactive blocks. Each
- Updating a global setting is done via `SetConfigCommand` (applies globally unless overridden).

6. **Useful Helper Functions**

- To avoid repetitive boilerplate, use these global utilities from `global.ts`:
- `useBlockMetaKeyAtom(blockId, key)`: Retrieves and updates block-specific metadata.
- `useOverrideConfigAtom(blockId, key)`: Reads from global config but allows per-block overrides.
Expand Down Expand Up @@ -139,7 +133,7 @@ type HeaderTextButton = {
type HeaderText = {
elemtype: "text";
text: string;
ref?: React.MutableRefObject<HTMLDivElement>;
ref?: React.RefObject<HTMLDivElement>;
className?: string;
noGrow?: boolean;
onClick?: (e: React.MouseEvent<any>) => void;
Expand All @@ -150,7 +144,7 @@ type HeaderInput = {
value: string;
className?: string;
isDisabled?: boolean;
ref?: React.MutableRefObject<HTMLInputElement>;
ref?: React.RefObject<HTMLInputElement>;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void;
Expand Down
4 changes: 4 additions & 0 deletions emain/emain-ipc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,10 @@ export function initIpcHandlers() {
incrementTermCommandsRun();
});

electron.ipcMain.on("native-paste", (event) => {
event.sender.paste();
});

electron.ipcMain.on("open-builder", (event, appId?: string) => {
fireAndForget(() => createBuilderWindow(appId || ""));
});
Expand Down
1 change: 1 addition & 0 deletions emain/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ contextBridge.exposeInMainWorld("api", {
setWaveAIOpen: (isOpen: boolean) => ipcRenderer.send("set-waveai-open", isOpen),
closeBuilderWindow: () => ipcRenderer.send("close-builder-window"),
incrementTermCommands: () => ipcRenderer.send("increment-term-commands"),
nativePaste: () => ipcRenderer.send("native-paste"),
});

// Custom event for "new-window"
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/element/flyoutmenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ type SubMenuProps = {
};
visibleSubMenus: { [key: string]: any };
hoveredItems: string[];
subMenuRefs: React.MutableRefObject<{ [key: string]: React.RefObject<HTMLDivElement> }>;
subMenuRefs: React.RefObject<{ [key: string]: React.RefObject<HTMLDivElement> }>;
handleMouseEnterItem: (
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
parentKey: string | null,
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/element/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ interface InputProps {
autoSelect?: boolean;
disabled?: boolean;
isNumber?: boolean;
inputRef?: React.MutableRefObject<any>;
inputRef?: React.RefObject<any>;
manageFocus?: (isFocused: boolean) => void;
}

Expand Down
2 changes: 1 addition & 1 deletion frontend/app/modals/typeaheadmodal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ interface TypeAheadModalProps {
onSelect?: (_: string) => void;
onClickBackdrop?: () => void;
onKeyDown?: (_) => void;
giveFocusRef?: React.MutableRefObject<() => boolean>;
giveFocusRef?: React.RefObject<() => boolean>;
autoFocus?: boolean;
selectIndex?: number;
}
Expand Down
5 changes: 5 additions & 0 deletions frontend/app/store/wshclientapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -612,6 +612,11 @@ class RpcApiType {
return client.wshRpcCall("writeappfile", data, opts);
}

// command "writetempfile" [call]
WriteTempFileCommand(client: WshClient, data: CommandWriteTempFileData, opts?: RpcOpts): Promise<string> {
return client.wshRpcCall("writetempfile", data, opts);
}

// command "wshactivity" [call]
WshActivityCommand(client: WshClient, data: {[key: string]: number}, opts?: RpcOpts): Promise<void> {
return client.wshRpcCall("wshactivity", data, opts);
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/view/preview/csvview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ type CSVRow = {
};

interface CSVViewProps {
parentRef: React.MutableRefObject<HTMLDivElement>;
parentRef: React.RefObject<HTMLDivElement>;
content: string;
filename: string;
readonly: boolean;
Expand Down
4 changes: 2 additions & 2 deletions frontend/app/view/preview/preview-model.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -152,11 +152,11 @@ export class PreviewModel implements ViewModel {
openFileModal: PrimitiveAtom<boolean>;
openFileModalDelay: PrimitiveAtom<boolean>;
openFileError: PrimitiveAtom<string>;
openFileModalGiveFocusRef: React.MutableRefObject<() => boolean>;
openFileModalGiveFocusRef: React.RefObject<() => boolean>;

markdownShowToc: PrimitiveAtom<boolean>;

monacoRef: React.MutableRefObject<MonacoTypes.editor.IStandaloneCodeEditor>;
monacoRef: React.RefObject<MonacoTypes.editor.IStandaloneCodeEditor>;

showHiddenFiles: PrimitiveAtom<boolean>;
refreshVersion: PrimitiveAtom<number>;
Expand Down
72 changes: 12 additions & 60 deletions frontend/app/view/term/term-model.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0


import { BlockNodeModel } from "@/app/block/blocktypes";
import { appHandleKeyDown } from "@/app/store/keymodel";
import { waveEventSubscribe } from "@/app/store/wps";
Expand All @@ -15,6 +14,7 @@ import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model";
import {
atoms,
getAllBlockComponentModels,
getApi,
getBlockComponentModel,
getBlockMetaKeyAtom,
getConnStatusAtom,
Expand All @@ -29,22 +29,15 @@ import * as keyutil from "@/util/keyutil";
import { boundNumber, stringToBase64 } from "@/util/util";
import * as jotai from "jotai";
import * as React from "react";
import {
computeTheme,
createTempFileFromBlob,
DefaultTermTheme,
handleImagePasteBlob as handleImagePasteBlobUtil,
supportsImageInput as supportsImageInputUtil,
} from "./termutil";
import { TermWrap } from "./termwrap";
import { getBlockingCommand } from "./shellblocking";
import { computeTheme, DefaultTermTheme } from "./termutil";
import { TermWrap } from "./termwrap";

export class TermViewModel implements ViewModel {

viewType: string;
nodeModel: BlockNodeModel;
connected: boolean;
termRef: React.MutableRefObject<TermWrap> = { current: null };
termRef: React.RefObject<TermWrap> = { current: null };
blockAtom: jotai.Atom<Block>;
termMode: jotai.Atom<string>;
blockId: string;
Expand Down Expand Up @@ -398,51 +391,6 @@ export class TermViewModel implements ViewModel {
RpcApi.ControllerInputCommand(TabRpcClient, { blockid: this.blockId, inputdata64: b64data });
}

async handlePaste() {
try {
const clipboardItems = await navigator.clipboard.read();

for (const item of clipboardItems) {
// Check for images first
const imageTypes = item.types.filter((type) => type.startsWith("image/"));
if (imageTypes.length > 0 && this.supportsImageInput()) {
const blob = await item.getType(imageTypes[0]);
await this.handleImagePasteBlob(blob);
return;
}

// Handle text
if (item.types.includes("text/plain")) {
const blob = await item.getType("text/plain");
const text = await blob.text();
this.termRef.current?.terminal.paste(text);
return;
}
}
} catch (err) {
console.error("Paste error:", err);
// Fallback to text-only paste
try {
const text = await navigator.clipboard.readText();
if (text) {
this.termRef.current?.terminal.paste(text);
}
} catch (fallbackErr) {
console.error("Fallback paste error:", fallbackErr);
}
}
}

supportsImageInput(): boolean {
return supportsImageInputUtil();
}

async handleImagePasteBlob(blob: Blob): Promise<void> {
await handleImagePasteBlobUtil(blob, TabRpcClient, (text) => {
this.termRef.current?.terminal.paste(text);
});
}

setTermMode(mode: "term" | "vdom") {
if (mode == "term") {
mode = null;
Expand Down Expand Up @@ -559,22 +507,26 @@ export class TermViewModel implements ViewModel {
const shiftEnterNewlineAtom = getOverrideConfigAtom(this.blockId, "term:shiftenternewline");
const shiftEnterNewlineEnabled = globalStore.get(shiftEnterNewlineAtom) ?? true;
if (shiftEnterNewlineEnabled) {
this.sendDataToController("\u001b\n");
this.sendDataToController("\n");
event.preventDefault();
event.stopPropagation();
return false;
}
}
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:v")) {
this.handlePaste();
event.preventDefault();
event.stopPropagation();
getApi().nativePaste();
// this.termRef.current?.pasteHandler();
return false;
} else if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:c")) {
const sel = this.termRef.current?.terminal.getSelection();
navigator.clipboard.writeText(sel);
event.preventDefault();
event.stopPropagation();
const sel = this.termRef.current?.terminal.getSelection();
if (!sel) {
return false;
}
navigator.clipboard.writeText(sel);
return false;
} else if (keyutil.checkKeyPressed(waveEvent, "Cmd:k")) {
event.preventDefault();
Expand Down
Loading
Loading