Skip to content

Commit a9db209

Browse files
authored
Big Paste Handler Cleanup (#2535)
Handle both types of paste data. Write utility functions to normalize paste events to {text; image}[]. Fix duplication issue (call preventDefault() early). Handle multiple image pasting (by adding a slight delay). Convert Ctrl:Shift:v to use a *native paste* which allows capturing of images!
1 parent 4b6a3ed commit a9db209

File tree

17 files changed

+221
-232
lines changed

17 files changed

+221
-232
lines changed

aiprompts/view-prompt.md

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ Wave Terminal uses a modular ViewModel system to define interactive blocks. Each
77
### Key Concepts
88

99
1. **ViewModel Structure**
10-
1110
- Implements the `ViewModel` interface.
1211
- Defines:
1312
- `viewType`: Unique block type identifier.
@@ -19,28 +18,24 @@ Wave Terminal uses a modular ViewModel system to define interactive blocks. Each
1918
- Lifecycle methods like `dispose()`, `giveFocus()`, `keyDownHandler()`.
2019

2120
2. **ViewComponent Structure**
22-
2321
- A **React function component** implementing `ViewComponentProps<T extends ViewModel>`.
2422
- Uses `blockId`, `blockRef`, `contentRef`, and `model` as props.
2523
- Retrieves ViewModel state using Jotai atoms.
2624
- Returns JSX for rendering.
2725

2826
3. **Header Elements (`HeaderElem[]`)**
29-
3027
- Can include:
3128
- **Icons (`IconButtonDecl`)**: Clickable buttons.
3229
- **Text (`HeaderText`)**: Metadata or status.
3330
- **Inputs (`HeaderInput`)**: Editable fields.
3431
- **Menu Buttons (`MenuButton`)**: Dropdowns.
3532

3633
4. **Jotai Atoms for State Management**
37-
3834
- Use `atom<T>`, `PrimitiveAtom<T>`, `WritableAtom<T>` for dynamic properties.
3935
- `splitAtom` for managing lists of atoms.
4036
- Read settings from `globalStore` and override with block metadata.
4137

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

5247
6. **Useful Helper Functions**
53-
5448
- To avoid repetitive boilerplate, use these global utilities from `global.ts`:
5549
- `useBlockMetaKeyAtom(blockId, key)`: Retrieves and updates block-specific metadata.
5650
- `useOverrideConfigAtom(blockId, key)`: Reads from global config but allows per-block overrides.
@@ -139,7 +133,7 @@ type HeaderTextButton = {
139133
type HeaderText = {
140134
elemtype: "text";
141135
text: string;
142-
ref?: React.MutableRefObject<HTMLDivElement>;
136+
ref?: React.RefObject<HTMLDivElement>;
143137
className?: string;
144138
noGrow?: boolean;
145139
onClick?: (e: React.MouseEvent<any>) => void;
@@ -150,7 +144,7 @@ type HeaderInput = {
150144
value: string;
151145
className?: string;
152146
isDisabled?: boolean;
153-
ref?: React.MutableRefObject<HTMLInputElement>;
147+
ref?: React.RefObject<HTMLInputElement>;
154148
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
155149
onKeyDown?: (e: React.KeyboardEvent<HTMLInputElement>) => void;
156150
onFocus?: (e: React.FocusEvent<HTMLInputElement>) => void;

emain/emain-ipc.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,10 @@ export function initIpcHandlers() {
400400
incrementTermCommandsRun();
401401
});
402402

403+
electron.ipcMain.on("native-paste", (event) => {
404+
event.sender.paste();
405+
});
406+
403407
electron.ipcMain.on("open-builder", (event, appId?: string) => {
404408
fireAndForget(() => createBuilderWindow(appId || ""));
405409
});

emain/preload.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ contextBridge.exposeInMainWorld("api", {
6262
setWaveAIOpen: (isOpen: boolean) => ipcRenderer.send("set-waveai-open", isOpen),
6363
closeBuilderWindow: () => ipcRenderer.send("close-builder-window"),
6464
incrementTermCommands: () => ipcRenderer.send("increment-term-commands"),
65+
nativePaste: () => ipcRenderer.send("native-paste"),
6566
});
6667

6768
// Custom event for "new-window"

frontend/app/element/flyoutmenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ type SubMenuProps = {
206206
};
207207
visibleSubMenus: { [key: string]: any };
208208
hoveredItems: string[];
209-
subMenuRefs: React.MutableRefObject<{ [key: string]: React.RefObject<HTMLDivElement> }>;
209+
subMenuRefs: React.RefObject<{ [key: string]: React.RefObject<HTMLDivElement> }>;
210210
handleMouseEnterItem: (
211211
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
212212
parentKey: string | null,

frontend/app/element/input.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ interface InputProps {
7070
autoSelect?: boolean;
7171
disabled?: boolean;
7272
isNumber?: boolean;
73-
inputRef?: React.MutableRefObject<any>;
73+
inputRef?: React.RefObject<any>;
7474
manageFocus?: (isFocused: boolean) => void;
7575
}
7676

frontend/app/modals/typeaheadmodal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ interface TypeAheadModalProps {
8282
onSelect?: (_: string) => void;
8383
onClickBackdrop?: () => void;
8484
onKeyDown?: (_) => void;
85-
giveFocusRef?: React.MutableRefObject<() => boolean>;
85+
giveFocusRef?: React.RefObject<() => boolean>;
8686
autoFocus?: boolean;
8787
selectIndex?: number;
8888
}

frontend/app/store/wshclientapi.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,11 @@ class RpcApiType {
612612
return client.wshRpcCall("writeappfile", data, opts);
613613
}
614614

615+
// command "writetempfile" [call]
616+
WriteTempFileCommand(client: WshClient, data: CommandWriteTempFileData, opts?: RpcOpts): Promise<string> {
617+
return client.wshRpcCall("writetempfile", data, opts);
618+
}
619+
615620
// command "wshactivity" [call]
616621
WshActivityCommand(client: WshClient, data: {[key: string]: number}, opts?: RpcOpts): Promise<void> {
617622
return client.wshRpcCall("wshactivity", data, opts);

frontend/app/view/preview/csvview.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ type CSVRow = {
2323
};
2424

2525
interface CSVViewProps {
26-
parentRef: React.MutableRefObject<HTMLDivElement>;
26+
parentRef: React.RefObject<HTMLDivElement>;
2727
content: string;
2828
filename: string;
2929
readonly: boolean;

frontend/app/view/preview/preview-model.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -152,11 +152,11 @@ export class PreviewModel implements ViewModel {
152152
openFileModal: PrimitiveAtom<boolean>;
153153
openFileModalDelay: PrimitiveAtom<boolean>;
154154
openFileError: PrimitiveAtom<string>;
155-
openFileModalGiveFocusRef: React.MutableRefObject<() => boolean>;
155+
openFileModalGiveFocusRef: React.RefObject<() => boolean>;
156156

157157
markdownShowToc: PrimitiveAtom<boolean>;
158158

159-
monacoRef: React.MutableRefObject<MonacoTypes.editor.IStandaloneCodeEditor>;
159+
monacoRef: React.RefObject<MonacoTypes.editor.IStandaloneCodeEditor>;
160160

161161
showHiddenFiles: PrimitiveAtom<boolean>;
162162
refreshVersion: PrimitiveAtom<number>;

frontend/app/view/term/term-model.ts

Lines changed: 12 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Copyright 2025, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

4-
54
import { BlockNodeModel } from "@/app/block/blocktypes";
65
import { appHandleKeyDown } from "@/app/store/keymodel";
76
import { waveEventSubscribe } from "@/app/store/wps";
@@ -15,6 +14,7 @@ import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model";
1514
import {
1615
atoms,
1716
getAllBlockComponentModels,
17+
getApi,
1818
getBlockComponentModel,
1919
getBlockMetaKeyAtom,
2020
getConnStatusAtom,
@@ -29,22 +29,15 @@ import * as keyutil from "@/util/keyutil";
2929
import { boundNumber, stringToBase64 } from "@/util/util";
3030
import * as jotai from "jotai";
3131
import * as React from "react";
32-
import {
33-
computeTheme,
34-
createTempFileFromBlob,
35-
DefaultTermTheme,
36-
handleImagePasteBlob as handleImagePasteBlobUtil,
37-
supportsImageInput as supportsImageInputUtil,
38-
} from "./termutil";
39-
import { TermWrap } from "./termwrap";
4032
import { getBlockingCommand } from "./shellblocking";
33+
import { computeTheme, DefaultTermTheme } from "./termutil";
34+
import { TermWrap } from "./termwrap";
4135

4236
export class TermViewModel implements ViewModel {
43-
4437
viewType: string;
4538
nodeModel: BlockNodeModel;
4639
connected: boolean;
47-
termRef: React.MutableRefObject<TermWrap> = { current: null };
40+
termRef: React.RefObject<TermWrap> = { current: null };
4841
blockAtom: jotai.Atom<Block>;
4942
termMode: jotai.Atom<string>;
5043
blockId: string;
@@ -398,51 +391,6 @@ export class TermViewModel implements ViewModel {
398391
RpcApi.ControllerInputCommand(TabRpcClient, { blockid: this.blockId, inputdata64: b64data });
399392
}
400393

401-
async handlePaste() {
402-
try {
403-
const clipboardItems = await navigator.clipboard.read();
404-
405-
for (const item of clipboardItems) {
406-
// Check for images first
407-
const imageTypes = item.types.filter((type) => type.startsWith("image/"));
408-
if (imageTypes.length > 0 && this.supportsImageInput()) {
409-
const blob = await item.getType(imageTypes[0]);
410-
await this.handleImagePasteBlob(blob);
411-
return;
412-
}
413-
414-
// Handle text
415-
if (item.types.includes("text/plain")) {
416-
const blob = await item.getType("text/plain");
417-
const text = await blob.text();
418-
this.termRef.current?.terminal.paste(text);
419-
return;
420-
}
421-
}
422-
} catch (err) {
423-
console.error("Paste error:", err);
424-
// Fallback to text-only paste
425-
try {
426-
const text = await navigator.clipboard.readText();
427-
if (text) {
428-
this.termRef.current?.terminal.paste(text);
429-
}
430-
} catch (fallbackErr) {
431-
console.error("Fallback paste error:", fallbackErr);
432-
}
433-
}
434-
}
435-
436-
supportsImageInput(): boolean {
437-
return supportsImageInputUtil();
438-
}
439-
440-
async handleImagePasteBlob(blob: Blob): Promise<void> {
441-
await handleImagePasteBlobUtil(blob, TabRpcClient, (text) => {
442-
this.termRef.current?.terminal.paste(text);
443-
});
444-
}
445-
446394
setTermMode(mode: "term" | "vdom") {
447395
if (mode == "term") {
448396
mode = null;
@@ -559,22 +507,26 @@ export class TermViewModel implements ViewModel {
559507
const shiftEnterNewlineAtom = getOverrideConfigAtom(this.blockId, "term:shiftenternewline");
560508
const shiftEnterNewlineEnabled = globalStore.get(shiftEnterNewlineAtom) ?? true;
561509
if (shiftEnterNewlineEnabled) {
562-
this.sendDataToController("\u001b\n");
510+
this.sendDataToController("\n");
563511
event.preventDefault();
564512
event.stopPropagation();
565513
return false;
566514
}
567515
}
568516
if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:v")) {
569-
this.handlePaste();
570517
event.preventDefault();
571518
event.stopPropagation();
519+
getApi().nativePaste();
520+
// this.termRef.current?.pasteHandler();
572521
return false;
573522
} else if (keyutil.checkKeyPressed(waveEvent, "Ctrl:Shift:c")) {
574-
const sel = this.termRef.current?.terminal.getSelection();
575-
navigator.clipboard.writeText(sel);
576523
event.preventDefault();
577524
event.stopPropagation();
525+
const sel = this.termRef.current?.terminal.getSelection();
526+
if (!sel) {
527+
return false;
528+
}
529+
navigator.clipboard.writeText(sel);
578530
return false;
579531
} else if (keyutil.checkKeyPressed(waveEvent, "Cmd:k")) {
580532
event.preventDefault();

0 commit comments

Comments
 (0)