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
8 changes: 5 additions & 3 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -124,12 +124,12 @@ tasks:
package:
desc: Package the application for the current platform.
cmds:
- task: clean
- task: build:backend
- task: build:tsunamiscaffold
- npm run build:prod && npm exec electron-builder -- -c electron-builder.config.cjs -p never {{.CLI_ARGS}}
deps:
- clean
- npm:install
- build:backend
- build:tsunamiscaffold

build:frontend:dev:
desc: Build the frontend in development mode.
Expand Down Expand Up @@ -199,6 +199,7 @@ tasks:
- task: build:server:internal
vars:
ARCHS: arm64,amd64
GO_ENV_VARS: MACOSX_DEPLOYMENT_TARGET=12.0

build:server:quickdev:
desc: Build the wavesrv component for quickdev (arm64 macOS only, no generate).
Expand All @@ -207,6 +208,7 @@ tasks:
- task: build:server:internal
vars:
ARCHS: arm64
GO_ENV_VARS: MACOSX_DEPLOYMENT_TARGET=12.0
deps:
- go:mod:tidy
sources:
Expand Down
46 changes: 42 additions & 4 deletions electron-builder.config.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,44 @@ const fs = require("fs");
const path = require("path");

const windowsShouldSign = !!process.env.SM_CODE_SIGNING_CERT_SHA1_HASH;
const archNameByValue = {
[Arch.x64]: "x64",
[Arch.arm64]: "arm64",
[Arch.universal]: "universal",
};

function getPackageBinDir(context) {
if (context.electronPlatformName !== "darwin") {
return path.resolve(context.appOutDir, "resources/app.asar.unpacked/dist/bin");
}
return path.resolve(context.appOutDir, `${pkg.productName}.app/Contents/Resources/app.asar.unpacked/dist/bin`);
}

function getExpectedPackageBinaries(context) {
const platform = context.electronPlatformName;
const arch = archNameByValue[context.arch] ?? context.arch;
const exeExt = platform === "win32" ? ".exe" : "";

if (platform === "darwin" && arch === "universal") {
return ["wavesrv.arm64", "wavesrv.x64", `wsh-${pkg.version}-darwin.arm64`, `wsh-${pkg.version}-darwin.x64`];
}

return [
`wavesrv.${arch}${exeExt}`,
`wsh-${pkg.version}-${platform === "win32" ? "windows" : platform}.${arch}${exeExt}`,
];
}

function validatePackagedBinaries(context) {
const packageBinDir = getPackageBinDir(context);
const missing = getExpectedPackageBinaries(context).filter(
(file) => !fs.existsSync(path.resolve(packageBinDir, file))
);
if (missing.length === 0) {
return;
}
throw new Error(`Missing packaged Wave binaries in ${packageBinDir}: ${missing.join(", ")}`);
}

/**
* @type {import('electron-builder').Configuration}
Expand Down Expand Up @@ -61,6 +99,7 @@ const config = {
singleArchFiles: "**/dist/bin/wavesrv.*",
entitlements: "build/entitlements.mac.plist",
entitlementsInherit: "build/entitlements.mac.plist",
notarize: !!process.env.APPLE_API_KEY || !!process.env.APPLE_ID,
extendInfo: {
NSContactsUsageDescription: "A CLI application running in Wave wants to use your contacts.",
NSRemindersUsageDescription: "A CLI application running in Wave wants to use your reminders.",
Expand Down Expand Up @@ -122,12 +161,11 @@ const config = {
url: "https://dl.waveterm.dev/releases-w2",
},
afterPack: (context) => {
validatePackagedBinaries(context);

// This is a workaround to restore file permissions to the wavesrv binaries on macOS after packaging the universal binary.
if (context.electronPlatformName === "darwin" && context.arch === Arch.universal) {
const packageBinDir = path.resolve(
context.appOutDir,
`${pkg.productName}.app/Contents/Resources/app.asar.unpacked/dist/bin`
);
const packageBinDir = getPackageBinDir(context);

// Reapply file permissions to the wavesrv binaries in the final app package
fs.readdirSync(packageBinDir, {
Expand Down
54 changes: 54 additions & 0 deletions frontend/app/modals/closetabconfirmmodal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

import { Modal } from "@/app/modals/modal";
import { modalsModel } from "@/app/store/modalmodel";
import { useState } from "react";

type CloseTabConfirmModalProps = {
onCancel: () => void;
onConfirm: (dontAskAgain: boolean) => void | Promise<void>;
};

const CloseTabConfirmModal = ({ onCancel, onConfirm }: CloseTabConfirmModalProps) => {
const [dontAskAgain, setDontAskAgain] = useState(false);

const handleCancel = () => {
modalsModel.popModal();
onCancel();
};

const handleConfirm = () => {
modalsModel.popModal();
onConfirm(dontAskAgain);
};

return (
<Modal
className="pt-6 pb-4 px-5 w-[420px]"
onOk={handleConfirm}
onCancel={handleCancel}
onClose={handleCancel}
okLabel="Close Tab"
cancelLabel="Cancel"
>
<div className="mx-4 pb-2.5 font-bold text-primary">Close Tab?</div>
<div className="mx-4 mb-4 flex flex-col gap-4 text-primary">
<div className="text-sm text-secondary">Are you sure you want to close this tab?</div>
<label className="flex cursor-pointer items-center gap-2 text-sm">
<input
type="checkbox"
className="accent-accent cursor-pointer"
checked={dontAskAgain}
onChange={(e) => setDontAskAgain(e.target.checked)}
/>
<span>Do not ask me again</span>
</label>
</div>
</Modal>
);
};

CloseTabConfirmModal.displayName = "CloseTabConfirmModal";

export { CloseTabConfirmModal };
2 changes: 2 additions & 0 deletions frontend/app/modals/modalregistry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@ import { UpgradeOnboardingPatch } from "@/app/onboarding/onboarding-upgrade-patc
import { DeleteFileModal, PublishAppModal, RenameFileModal } from "@/builder/builder-apppanel";
import { SetSecretDialog } from "@/builder/tabs/builder-secrettab";
import { AboutModal } from "./about";
import { CloseTabConfirmModal } from "./closetabconfirmmodal";
import { UserInputModal } from "./userinputmodal";

const modalRegistry: { [key: string]: React.ComponentType<any> } = {
[NewInstallOnboardingModal.displayName || "NewInstallOnboardingModal"]: NewInstallOnboardingModal,
[UpgradeOnboardingModal.displayName || "UpgradeOnboardingModal"]: UpgradeOnboardingModal,
[UpgradeOnboardingPatch.displayName || "UpgradeOnboardingPatch"]: UpgradeOnboardingPatch,
[CloseTabConfirmModal.displayName || "CloseTabConfirmModal"]: CloseTabConfirmModal,
[UserInputModal.displayName || "UserInputModal"]: UserInputModal,
[AboutModal.displayName || "AboutModal"]: AboutModal,
[MessageModal.displayName || "MessageModal"]: MessageModal,
Expand Down
2 changes: 1 addition & 1 deletion frontend/app/onboarding/onboarding-common.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

export const CurrentOnboardingVersion = "v0.14.5";
export const CurrentOnboardingVersion = "v0.14.6";

export function OnboardingGradientBg() {
return (
Expand Down
22 changes: 10 additions & 12 deletions frontend/app/onboarding/onboarding-upgrade-patch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { UpgradeOnboardingModal_v0_14_1_Content } from "./onboarding-upgrade-v01
import { UpgradeOnboardingModal_v0_14_2_Content } from "./onboarding-upgrade-v0142";
import { UpgradeOnboardingModal_v0_14_4_Content } from "./onboarding-upgrade-v0144";
import { UpgradeOnboardingModal_v0_14_5_Content } from "./onboarding-upgrade-v0145";
import { UpgradeOnboardingModal_v0_14_6_Content } from "./onboarding-upgrade-v0146";

interface VersionConfig {
version: string;
Expand Down Expand Up @@ -64,10 +65,7 @@ export function UpgradeOnboardingFooter({
<div className="flex-1 flex justify-start">
{hasPrev && (
<div className="text-sm text-secondary">
<button
onClick={onPrev}
className="cursor-pointer hover:text-foreground transition-colors"
>
<button onClick={onPrev} className="cursor-pointer hover:text-foreground transition-colors">
&lt; {prevText}
</button>
</div>
Expand All @@ -81,10 +79,7 @@ export function UpgradeOnboardingFooter({
<div className="flex-1 flex justify-end">
{hasNext && (
<div className="text-sm text-secondary">
<button
onClick={onNext}
className="cursor-pointer hover:text-foreground transition-colors"
>
<button onClick={onNext} className="cursor-pointer hover:text-foreground transition-colors">
{nextText} &gt;
</button>
</div>
Expand Down Expand Up @@ -153,6 +148,12 @@ export const UpgradeOnboardingVersions: VersionConfig[] = [
version: "v0.14.5",
content: () => <UpgradeOnboardingModal_v0_14_5_Content />,
prevText: "Prev (v0.14.4)",
nextText: "Next (v0.14.6)",
},
{
version: "v0.14.6",
content: () => <UpgradeOnboardingModal_v0_14_6_Content />,
prevText: "Prev (v0.14.5)",
},
];

Expand Down Expand Up @@ -242,10 +243,7 @@ const UpgradeOnboardingPatch = ({ isReleaseNotes = false }: UpgradeOnboardingPat

if (showStarAsk) {
return (
<FlexiModal
className="w-[500px] rounded-[10px] !p-[30px] relative overflow-hidden bg-panel"
ref={modalRef}
>
<FlexiModal className="w-[500px] rounded-[10px] !p-[30px] relative overflow-hidden bg-panel" ref={modalRef}>
<OnboardingGradientBg />
<div className="relative z-10 flex flex-col w-full h-full">
<StarAskPage onClose={doClose} page="upgrade" />
Expand Down
45 changes: 45 additions & 0 deletions frontend/app/onboarding/onboarding-upgrade-v0146.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

const UpgradeOnboardingModal_v0_14_6_Content = () => {
return (
<div className="flex flex-col items-start gap-6 w-full mb-4 unselectable">
<div className="text-secondary leading-relaxed">
<p className="mb-0">
Wave v0.14.6 is a patch release focused on terminal input reliability and packaging safeguards.
</p>
</div>

<div className="flex w-full items-start gap-4">
<div className="flex-shrink-0">
<i className="text-[24px] text-accent fa-solid fa-keyboard"></i>
</div>
<div className="flex flex-col items-start gap-2 flex-1">
<div className="text-foreground text-base font-semibold leading-[18px]">IME Input Fixes</div>
<div className="text-secondary leading-5">
Korean IME composition now preserves the correct ordering when pressing Enter before a final
consonant is committed. This also avoids intermittent duplicated input when switching between
English and Korean input modes.
</div>
</div>
</div>

<div className="flex w-full items-start gap-4">
<div className="flex-shrink-0">
<i className="text-[24px] text-accent fa-sharp fa-solid fa-box"></i>
</div>
<div className="flex flex-col items-start gap-2 flex-1">
<div className="text-foreground text-base font-semibold leading-[18px]">Packaging Validation</div>
<div className="text-secondary leading-5">
Release packaging now fails early if required Wave backend binaries are missing from the
packaged app, preventing broken installers from being published.
</div>
</div>
</div>
</div>
);
};

UpgradeOnboardingModal_v0_14_6_Content.displayName = "UpgradeOnboardingModal_v0_14_6_Content";

export { UpgradeOnboardingModal_v0_14_6_Content };
21 changes: 11 additions & 10 deletions frontend/app/store/keymodel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
WOS,
} from "@/app/store/global";
import { getActiveTabModel } from "@/app/store/tab-model";
import { closeTabWithConfirmation } from "@/app/tab/closetab";
import { WorkspaceLayoutModel } from "@/app/workspace/workspace-layout-model";
import { deleteLayoutModelForTab, getLayoutModelForStaticTab, NavigateDirection } from "@/layout/index";
import * as keyutil from "@/util/keyutil";
Expand Down Expand Up @@ -132,16 +133,13 @@ function simpleCloseStaticTab() {
const workspaceId = globalStore.get(atoms.workspaceId);
const tabId = globalStore.get(atoms.staticTabId);
const confirmClose = globalStore.get(getSettingsKeyAtom("tab:confirmclose")) ?? false;
getApi()
.closeTab(workspaceId, tabId, confirmClose)
.then((didClose) => {
if (didClose) {
deleteLayoutModelForTab(tabId);
}
})
.catch((e) => {
console.log("error closing tab", e);
});
closeTabWithConfirmation({
confirmClose,
closeTab: () => getApi().closeTab(workspaceId, tabId, false),
onDidClose: () => deleteLayoutModelForTab(tabId),
}).catch((e) => {
console.log("error closing tab", e);
});
}

function uxCloseBlock(blockId: string) {
Expand Down Expand Up @@ -417,6 +415,9 @@ function appHandleKeyDown(waveEvent: WaveKeyboardEvent): boolean {
if (globalKeybindingsDisabled) {
return false;
}
if (waveEvent.isComposing) {
return false;
}
const nativeEvent = (waveEvent as any).nativeEvent;
if (lastHandledEvent != null && nativeEvent != null && lastHandledEvent === nativeEvent) {
return false;
Expand Down
48 changes: 48 additions & 0 deletions frontend/app/tab/closetab.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

import { modalsModel } from "@/app/store/modalmodel";
import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";

type CloseTabFn = () => Promise<boolean>;

type CloseTabWithConfirmationOpts = {
confirmClose: boolean;
closeTab: CloseTabFn;
onDidClose?: () => void;
};

export async function closeTabWithConfirmation({
confirmClose,
closeTab,
onDidClose,
}: CloseTabWithConfirmationOpts): Promise<boolean> {
if (!confirmClose) {
return await runClose(closeTab, onDidClose);
}
return await new Promise<boolean>((resolve, reject) => {
modalsModel.pushModal("CloseTabConfirmModal", {
onCancel: () => resolve(false),
onConfirm: async (dontAskAgain: boolean) => {
try {
if (dontAskAgain) {
await RpcApi.SetConfigCommand(TabRpcClient, { "tab:confirmclose": false });
}
const didClose = await runClose(closeTab, onDidClose);
resolve(didClose);
} catch (e) {
reject(e);
}
},
});
});
}

async function runClose(closeTab: CloseTabFn, onDidClose?: () => void): Promise<boolean> {
const didClose = await closeTab();
if (didClose) {
onDidClose?.();
}
return didClose;
}
Loading