Skip to content

Commit f4f10a6

Browse files
committed
Merge remote-tracking branch 'origin/main' into sawka/preview-updates
2 parents 00a1d26 + 76f78f0 commit f4f10a6

14 files changed

Lines changed: 138 additions & 68 deletions

File tree

docs/docs/wsh-reference.mdx

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ You can open a preview block with the contents of any file or directory by runni
2323

2424
```sh
2525
wsh view [path]
26+
wsh view -m [path] # opens in magnified block
2627
```
2728

2829
You can use this command to easily preview images, markdown files, and directories. For code/text files this will open
@@ -34,9 +35,29 @@ a codeedit block which you can use to quickly edit the file using Wave's embedde
3435

3536
```sh
3637
wsh edit [path]
38+
wsh edit -m [path] # opens in magnified block
3739
```
3840

39-
This will open up codeedit for the specified file. This is useful for quickly editing files on a local or remote machine in our graphical editor. This command will wait until the file is closed before exiting (unlike `view`) so you can set your `$EDITOR` to `wsh editor` for a seamless experience. You can combine this with a `-m` flag to open the editor in magnified mode.
41+
This will open up a codeedit block for the specified file. This is useful for quickly editing files on a local or remote machine in Wave's graphical editor. This command returns immediately after opening the block.
42+
43+
For `$EDITOR` integration (e.g. with `git commit`), see [`wsh editor`](#editor) which blocks until the editor is closed.
44+
45+
---
46+
47+
## editor
48+
49+
```sh
50+
wsh editor [path]
51+
wsh editor -m [path] # opens in magnified block
52+
```
53+
54+
This opens a codeedit block for the specified file and **blocks until the editor is closed**. This is useful for setting your `$EDITOR` environment variable so that CLI tools (e.g. `git commit`, `crontab -e`) open files in Wave's graphical editor:
55+
56+
```sh
57+
export EDITOR="wsh editor"
58+
```
59+
60+
The file must already exist. Use `-m` to open the editor in magnified mode.
4061

4162
---
4263

emain/emain-ipc.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import { callWithOriginalXdgCurrentDesktopAsync, unamePlatform } from "./emain-p
2424
import { getWaveTabViewByWebContentsId } from "./emain-tabview";
2525
import { handleCtrlShiftState } from "./emain-util";
2626
import { getWaveVersion } from "./emain-wavesrv";
27-
import { createNewWaveWindow, focusedWaveWindow, getWaveWindowByWebContentsId } from "./emain-window";
27+
import { createNewWaveWindow, getWaveWindowByWebContentsId } from "./emain-window";
2828
import { ElectronWshClient } from "./emain-wsh";
2929

3030
const electronApp = electron.app;
@@ -130,12 +130,18 @@ function getUrlInSession(session: Electron.Session, url: string): Promise<UrlInS
130130
});
131131
}
132132

133-
function saveImageFileWithNativeDialog(defaultFileName: string, mimeType: string, readStream: Readable) {
133+
function saveImageFileWithNativeDialog(
134+
sender: electron.WebContents,
135+
defaultFileName: string,
136+
mimeType: string,
137+
readStream: Readable
138+
) {
134139
if (defaultFileName == null || defaultFileName == "") {
135140
defaultFileName = "image";
136141
}
137-
const ww = focusedWaveWindow;
142+
const ww = electron.BrowserWindow.fromWebContents(sender);
138143
if (ww == null) {
144+
readStream.destroy();
139145
return;
140146
}
141147
const mimeToExtension: { [key: string]: string } = {
@@ -164,6 +170,7 @@ function saveImageFileWithNativeDialog(defaultFileName: string, mimeType: string
164170
})
165171
.then((file) => {
166172
if (file.canceled) {
173+
readStream.destroy();
167174
return;
168175
}
169176
const writeStream = fs.createWriteStream(file.filePath);
@@ -213,7 +220,12 @@ export function initIpcHandlers() {
213220
const resultP = getUrlInSession(event.sender.session, payload.src);
214221
resultP
215222
.then((result) => {
216-
saveImageFileWithNativeDialog(result.fileName, result.mimeType, result.stream);
223+
saveImageFileWithNativeDialog(
224+
event.sender.hostWebContents,
225+
result.fileName,
226+
result.mimeType,
227+
result.stream
228+
);
217229
})
218230
.catch((e) => {
219231
console.log("error getting image", e);
@@ -477,7 +489,7 @@ export function initIpcHandlers() {
477489
});
478490

479491
electron.ipcMain.handle("save-text-file", async (event, fileName: string, content: string) => {
480-
const ww = focusedWaveWindow;
492+
const ww = electron.BrowserWindow.fromWebContents(event.sender);
481493
if (ww == null) {
482494
return false;
483495
}

emain/emain-menu.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { focusedBuilderWindow, getBuilderWindowById } from "./emain-builder";
99
import { openBuilderWindow } from "./emain-ipc";
1010
import { isDev, unamePlatform } from "./emain-platform";
1111
import { clearTabCache } from "./emain-tabview";
12-
import { decreaseZoomLevel, increaseZoomLevel } from "./emain-util";
12+
import { decreaseZoomLevel, increaseZoomLevel, resetZoomLevel } from "./emain-util";
1313
import {
1414
createNewWaveWindow,
1515
createWorkspace,
@@ -238,8 +238,7 @@ function makeViewMenu(
238238
click: (_, window) => {
239239
const wc = getWindowWebContents(window) ?? webContents;
240240
if (wc) {
241-
wc.setZoomFactor(1);
242-
wc.send("zoom-factor-change", 1);
241+
resetZoomLevel(wc);
243242
}
244243
},
245244
},

emain/emain-tabview.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
handleCtrlShiftFocus,
1616
handleCtrlShiftState,
1717
increaseZoomLevel,
18+
resetZoomLevel,
1819
shFrameNavHandler,
1920
shNavHandler,
2021
} from "./emain-util";
@@ -48,8 +49,7 @@ function handleWindowsMenuAccelerators(
4849
}
4950

5051
if (checkKeyPressed(waveEvent, "Ctrl:0")) {
51-
tabView.webContents.setZoomFactor(1);
52-
tabView.webContents.send("zoom-factor-change", 1);
52+
resetZoomLevel(tabView.webContents);
5353
return true;
5454
}
5555

@@ -165,9 +165,6 @@ export class WaveTabView extends WebContentsView {
165165
removeWaveTabView(this.waveTabId);
166166
this.isDestroyed = true;
167167
});
168-
this.webContents.on("zoom-changed", (_event, zoomDirection) => {
169-
this.webContents.send("zoom-factor-change", this.webContents.getZoomFactor());
170-
});
171168
this.setBackgroundColor(computeBgColor(fullConfig));
172169
}
173170

@@ -339,9 +336,6 @@ export async function getOrCreateWebViewForTab(waveWindowId: string, tabId: stri
339336
}
340337
}
341338
});
342-
tabView.webContents.on("zoom-changed", (e) => {
343-
tabView.webContents.send("zoom-changed");
344-
});
345339
tabView.webContents.setWindowOpenHandler(({ url, frameName }) => {
346340
if (url.startsWith("http://") || url.startsWith("https://") || url.startsWith("file://")) {
347341
console.log("openExternal fallback", url);

emain/emain-util.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,36 @@ const MinZoomLevel = 0.4;
1212
const MaxZoomLevel = 2.6;
1313
const ZoomDelta = 0.2;
1414

15+
// Note: Chromium automatically syncs zoom factor across all WebContents
16+
// sharing the same origin/session, so we only need to notify renderers
17+
// to update their CSS/state — not call setZoomFactor on each one.
18+
// We broadcast to all WebContents (including devtools, webviews, etc.) but
19+
// that is safe because "zoom-factor-change" is a custom app-defined event
20+
// that only our renderers listen to; unrecognized IPC messages are ignored.
21+
function broadcastZoomFactorChanged(newZoomFactor: number): void {
22+
for (const wc of electron.webContents.getAllWebContents()) {
23+
if (wc.isDestroyed()) {
24+
continue;
25+
}
26+
wc.send("zoom-factor-change", newZoomFactor);
27+
}
28+
}
29+
1530
export function increaseZoomLevel(webContents: electron.WebContents): void {
1631
const newZoom = Math.min(MaxZoomLevel, webContents.getZoomFactor() + ZoomDelta);
1732
webContents.setZoomFactor(newZoom);
18-
webContents.send("zoom-factor-change", newZoom);
33+
broadcastZoomFactorChanged(newZoom);
1934
}
2035

2136
export function decreaseZoomLevel(webContents: electron.WebContents): void {
2237
const newZoom = Math.max(MinZoomLevel, webContents.getZoomFactor() - ZoomDelta);
2338
webContents.setZoomFactor(newZoom);
24-
webContents.send("zoom-factor-change", newZoom);
39+
broadcastZoomFactorChanged(newZoom);
40+
}
41+
42+
export function resetZoomLevel(webContents: electron.WebContents): void {
43+
webContents.setZoomFactor(1);
44+
broadcastZoomFactorChanged(1);
2545
}
2646

2747
export function getElectronExecPath(): string {

emain/emain-window.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,7 @@ export class WaveBrowserWindow extends BaseWindow {
273273
if (getGlobalIsRelaunching()) {
274274
return;
275275
}
276+
focusedWaveWindow = this; // eslint-disable-line @typescript-eslint/no-this-alias
276277
console.log("focus win", this.waveWindowId);
277278
fireAndForget(() => ClientService.FocusWindow(this.waveWindowId));
278279
setWasInFg(true);

emain/emain.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,10 @@ async function appMain() {
384384
electronApp.quit();
385385
return;
386386
}
387+
electronApp.on("second-instance", (_event, argv, workingDirectory) => {
388+
console.log("second-instance event, argv:", argv, "workingDirectory:", workingDirectory);
389+
fireAndForget(createNewWaveWindow);
390+
});
387391
try {
388392
await runWaveSrv(handleWSEvent);
389393
} catch (e) {

frontend/app/modals/about.tsx

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
1-
// Copyright 2025, Command Line Inc.
1+
// Copyright 2026, Command Line Inc.
22
// SPDX-License-Identifier: Apache-2.0
33

44
import Logo from "@/app/asset/logo.svg";
5+
import { OnboardingGradientBg } from "@/app/onboarding/onboarding-common";
56
import { modalsModel } from "@/app/store/modalmodel";
6-
import { Modal } from "./modal";
7-
7+
import { RpcApi } from "@/app/store/wshclientapi";
8+
import { TabRpcClient } from "@/app/store/wshrpcutil";
89
import { isDev } from "@/util/isdev";
9-
import { useState } from "react";
10+
import { fireAndForget } from "@/util/util";
11+
import { useEffect, useState } from "react";
1012
import { getApi } from "../store/global";
13+
import { Modal } from "./modal";
1114

1215
interface AboutModalVProps {
1316
versionString: string;
@@ -19,13 +22,14 @@ const AboutModalV = ({ versionString, updaterChannel, onClose }: AboutModalVProp
1922
const currentDate = new Date();
2023

2124
return (
22-
<Modal className="pt-[34px] pb-[34px]" onClose={onClose}>
23-
<div className="flex flex-col gap-[26px] w-full">
25+
<Modal className="pt-[34px] pb-[34px] overflow-hidden w-[450px]" onClose={onClose}>
26+
<OnboardingGradientBg />
27+
<div className="flex flex-col gap-[26px] w-full relative z-10">
2428
<div className="flex flex-col items-center justify-center gap-4 self-stretch w-full text-center">
2529
<Logo />
2630
<div className="text-[25px]">Wave Terminal</div>
2731
<div className="leading-5">
28-
Open-Source AI-Native Terminal
32+
Open-Source AI-Integrated Terminal
2933
<br />
3034
Built for Seamless Workflows
3135
</div>
@@ -35,30 +39,38 @@ const AboutModalV = ({ versionString, updaterChannel, onClose }: AboutModalVProp
3539
<br />
3640
Update Channel: {updaterChannel}
3741
</div>
38-
<div className="flex items-start gap-[10px] self-stretch w-full text-center">
42+
<div className="grid grid-cols-2 gap-[10px] self-stretch w-full">
3943
<a
4044
href="https://github.com/wavetermdev/waveterm?ref=about"
4145
target="_blank"
4246
rel="noopener"
43-
className="inline-flex items-center px-4 py-2 rounded border border-border hover:bg-hoverbg transition-colors duration-200"
47+
className="inline-flex items-center justify-center px-4 py-2 rounded border border-border hover:bg-hoverbg transition-colors duration-200"
4448
>
45-
<i className="fa-brands fa-github mr-2"></i>Github
49+
<i className="fa-brands fa-github mr-2"></i>GitHub
4650
</a>
4751
<a
4852
href="https://www.waveterm.dev/?ref=about"
4953
target="_blank"
5054
rel="noopener"
51-
className="inline-flex items-center px-4 py-2 rounded border border-border hover:bg-hoverbg transition-colors duration-200"
55+
className="inline-flex items-center justify-center px-4 py-2 rounded border border-border hover:bg-hoverbg transition-colors duration-200"
5256
>
5357
<i className="fa-sharp fa-light fa-globe mr-2"></i>Website
5458
</a>
5559
<a
5660
href="https://github.com/wavetermdev/waveterm/blob/main/ACKNOWLEDGEMENTS.md"
5761
target="_blank"
5862
rel="noopener"
59-
className="inline-flex items-center px-4 py-2 rounded border border-border hover:bg-hoverbg transition-colors duration-200"
63+
className="inline-flex items-center justify-center px-4 py-2 rounded border border-border hover:bg-hoverbg transition-colors duration-200"
6064
>
61-
<i className="fa-sharp fa-light fa-heart mr-2"></i>Acknowledgements
65+
<i className="fa-sharp fa-light fa-book mr-2"></i>Open Source
66+
</a>
67+
<a
68+
href="https://github.com/sponsors/wavetermdev"
69+
target="_blank"
70+
rel="noopener"
71+
className="inline-flex items-center justify-center px-4 py-2 rounded border border-border hover:bg-hoverbg transition-colors duration-200"
72+
>
73+
<i className="fa-sharp fa-light fa-heart mr-2"></i>Sponsor
6274
</a>
6375
</div>
6476
<div className="items-center gap-4 self-stretch w-full text-center">
@@ -76,6 +88,16 @@ const AboutModal = () => {
7688
const [updaterChannel] = useState(() => getApi().getUpdaterChannel());
7789
const versionString = `${details.version} (${isDev() ? "dev-" : ""}${details.buildTime})`;
7890

91+
useEffect(() => {
92+
fireAndForget(async () => {
93+
RpcApi.RecordTEventCommand(
94+
TabRpcClient,
95+
{ event: "action:other", props: { "action:type": "about" } },
96+
{ noresponse: true }
97+
);
98+
});
99+
}, []);
100+
79101
return (
80102
<AboutModalV
81103
versionString={versionString}

frontend/layout/lib/utils.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,6 @@ export function setTransform(
7474
top: 0,
7575
left: 0,
7676
transform: translate,
77-
WebkitTransform: translate,
78-
MozTransform: translate,
79-
msTransform: translate,
80-
OTransform: translate,
8177
width: setSize ? `${widthRounded}px` : undefined,
8278
height: setSize ? `${heightRounded}px` : undefined,
8379
position: "absolute",

frontend/layout/tests/layoutTree.test.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ import {
1313
import { newLayoutTreeState } from "./model";
1414

1515
test("layoutTreeStateReducer - compute move", () => {
16-
let treeState = newLayoutTreeState(newLayoutNode(undefined, undefined, undefined, { blockId: "root" }));
17-
assert(treeState.rootNode.data!.blockId === "root", "root should have no children and should have data");
18-
let node1 = newLayoutNode(undefined, undefined, undefined, { blockId: "node1" });
16+
const nodeA = newLayoutNode(undefined, undefined, undefined, { blockId: "nodeA" });
17+
const node1 = newLayoutNode(undefined, undefined, undefined, { blockId: "node1" });
18+
const node2 = newLayoutNode(undefined, undefined, undefined, { blockId: "node2" });
19+
const treeState = newLayoutTreeState(newLayoutNode(undefined, undefined, [nodeA, node1, node2]));
20+
assert(treeState.rootNode.children!.length === 3, "root should have three children");
1921
let pendingAction = computeMoveNode(treeState, {
2022
type: LayoutTreeActionType.ComputeMove,
2123
nodeId: treeState.rootNode.id,
@@ -29,12 +31,11 @@ test("layoutTreeStateReducer - compute move", () => {
2931
assert(insertOperation.insertAtRoot, "insert operation insertAtRoot should be true");
3032
moveNode(treeState, insertOperation);
3133
assert(
32-
treeState.rootNode.data === undefined && treeState.rootNode.children!.length === 2,
33-
"root node should now have no data and should have two children"
34+
treeState.rootNode.data === undefined && treeState.rootNode.children!.length === 3,
35+
"root node should still have three children"
3436
);
3537
assert(treeState.rootNode.children![1].data!.blockId === "node1", "root's second child should be node1");
3638

37-
let node2 = newLayoutNode(undefined, undefined, undefined, { blockId: "node2" });
3839
pendingAction = computeMoveNode(treeState, {
3940
type: LayoutTreeActionType.ComputeMove,
4041
nodeId: node1.id,
@@ -48,15 +49,15 @@ test("layoutTreeStateReducer - compute move", () => {
4849
assert(!insertOperation2.insertAtRoot, "insert operation insertAtRoot should be false");
4950
moveNode(treeState, insertOperation2);
5051
assert(
51-
treeState.rootNode.data === undefined && treeState.rootNode.children!.length === 2,
52-
"root node should still have three children"
52+
treeState.rootNode.data === undefined && (treeState.rootNode.children!.length as number) === 2,
53+
"root node should now have two children after node2 moved into node1"
5354
);
5455
assert(treeState.rootNode.children![1].children!.length === 2, "root's second child should now have two children");
5556
});
5657

5758
test("computeMove - noop action", () => {
58-
let nodeToMove = newLayoutNode(undefined, undefined, undefined, { blockId: "nodeToMove" });
59-
let treeState = newLayoutTreeState(
59+
const nodeToMove = newLayoutNode(undefined, undefined, undefined, { blockId: "nodeToMove" });
60+
const treeState = newLayoutTreeState(
6061
newLayoutNode(undefined, undefined, [
6162
nodeToMove,
6263
newLayoutNode(undefined, undefined, undefined, { blockId: "otherNode" }),

0 commit comments

Comments
 (0)