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
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 };
3 changes: 3 additions & 0 deletions frontend/app/store/keymodel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,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
2 changes: 1 addition & 1 deletion frontend/app/view/term/ijson.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ body {
}

.fixed-font {
normal 12px / normal "Hack", monospace;
font: normal 12px / normal "Hack", "Noto Sans Mono CJK KR", "Noto Sans Mono CJK JP", "Noto Sans Mono CJK SC", "Noto Sans Mono CJK TC", "Noto Sans CJK KR", "Noto Sans CJK JP", "Noto Sans CJK SC", "Noto Sans CJK TC", "Apple SD Gothic Neo", "Hiragino Sans", "PingFang SC", "PingFang TC", "Microsoft YaHei", "Malgun Gothic", monospace;
}
`}
</style>
Expand Down
3 changes: 3 additions & 0 deletions frontend/app/view/term/term-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,9 @@ export class TermViewModel implements ViewModel {
}

handleTerminalKeydown(event: KeyboardEvent): boolean {
if (event.isComposing || event.keyCode == 229) {
return true;
}
const waveEvent = keyutil.adaptFromReactOrNativeKeyEvent(event);
if (waveEvent.type != "keydown") {
return true;
Expand Down
9 changes: 7 additions & 2 deletions frontend/app/view/term/term.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ import { TermWrap } from "./termwrap";
import "./xterm.css";

const dlog = debug("wave:term");
const DefaultTermFontFamily =
"Hack, 'Noto Sans Mono CJK KR', 'Noto Sans Mono CJK JP', 'Noto Sans Mono CJK SC', 'Noto Sans Mono CJK TC', " +
"'Noto Sans CJK KR', 'Noto Sans CJK JP', 'Noto Sans CJK SC', 'Noto Sans CJK TC', " +
"'Apple SD Gothic Neo', 'Hiragino Sans', 'PingFang SC', 'PingFang TC', 'Microsoft YaHei', 'Malgun Gothic', monospace";

interface TerminalViewProps {
blockId: string;
Expand Down Expand Up @@ -292,6 +296,7 @@ const TerminalView = ({ blockId, model }: ViewComponentProps<TermViewModel>) =>
const termMacOptionIsMeta = globalStore.get(termMacOptionIsMetaAtom) ?? false;
const termCursorStyle = normalizeCursorStyle(globalStore.get(getOverrideConfigAtom(blockId, "term:cursor")));
const termCursorBlink = globalStore.get(getOverrideConfigAtom(blockId, "term:cursorblink")) ?? false;
const termDisableWebGl = termSettings?.["term:disablewebgl"] ?? true;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for documentation or comments about WebGL default change

# Check for WebGL-related documentation
rg -n -C3 --type=md 'webgl|WebGL' | head -50

# Check for any TODO/FIXME about WebGL defaults
rg -n 'TODO|FIXME' --iglob '*webgl*' --iglob '*term*'

# Look for changelog or migration guide entries
fd -e md -e txt | xargs rg -l 'changelog|CHANGELOG|migration|breaking'

Repository: wavetermdev/waveterm

Length of output: 242


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect the toggle logic around the referenced lines
sed -n '250,360p' frontend/app/view/term/term.tsx | nl -ba | sed -n '40,140p'

# 2) Search for the setting key and related strings
rg -n --hidden --no-ignore -S 'term:disablewebgl|disablewebgl|Default terminal rendering to non-WebGL|non-WebGL|useWebGl' frontend/app | head -200

# 3) Search docs/releases/changelog/migration/breaking notes
rg -n -S --hidden --no-ignore '(WebGL|webgl|non-WebGL|nonwebgl|disablewebgl|term:disablewebgl|breaking|migration|changelog|RELEASES\.md|Release note|release notes)' \
  . | head -200

# 4) Specifically inspect RELEASES.md for any WebGL-related entry
if [ -f RELEASES.md ]; then
  rg -n -S 'WebGL|webgl|non-WebGL|disablewebgl|term:disablewebgl|breaking|migration' RELEASES.md | head -200 || true
  # show nearby sections if any match lines
  MATCH_LINES=$(rg -n -S 'WebGL|webgl|non-WebGL|disablewebgl|term:disablewebgl|breaking|migration' RELEASES.md | cut -d: -f1 | head -20 | tr '\n' ' ')
  if [ -n "${MATCH_LINES:-}" ]; then
    for ln in $MATCH_LINES; do
      echo "---- RELEASES.md around line $ln ----"
      sed -n "$((ln-20)),$((ln+20))p" RELEASES.md
    done
  fi
else
  echo "RELEASES.md not found at repo root."
fi

Repository: wavetermdev/waveterm

Length of output: 107


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# 1) Inspect toggle logic around referenced lines (with line numbers)
sed -n '260,340p' frontend/app/view/term/term.tsx | cat -n | sed -n '1,120p'

# 2) Search for the setting key and related strings (scoped to term code + docs)
rg -n -S --hidden --no-ignore 'term:disablewebgl|disablewebgl|Default terminal rendering to non-WebGL|non-WebGL|useWebGl|useWebGL' frontend/app . | head -200

# 3) Search docs/releases/changelog/migration/breaking notes
rg -n -S --hidden --no-ignore '(WebGL|webgl|non-WebGL|nonwebgl|disablewebgl|term:disablewebgl|breaking|migration|changelog|RELEASES\.md)' . | head -200

# 4) Specifically inspect RELEASES.md
if [ -f RELEASES.md ]; then
  echo "---- RELEASES.md matches ----"
  rg -n -S 'WebGL|webgl|non-WebGL|nonwebgl|disablewebgl|term:disablewebgl|breaking|migration|non-WebGL' RELEASES.md | head -200 || true
else
  echo "RELEASES.md not found at repo root."
fi

Repository: wavetermdev/waveterm

Length of output: 50377


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Show termdisablewebgl docs/defaults/schema with tight context (exclude node_modules)
rg -n -S 'term:disablewebgl' pkg/wconfig/defaultconfig/settings.json schema/settings.json docs/docs/config.mdx frontend/types/gotypes.d.ts frontend/app/view/term/termwrap.ts frontend/app/view/term/term.tsx

echo "---- docs/docs/config.mdx ----"
sed -n '40,90p' docs/docs/config.mdx

echo "---- pkg/wconfig/defaultconfig/settings.json ----"
sed -n '1,80p' pkg/wconfig/defaultconfig/settings.json

echo "---- schema/settings.json (term:disablewebgl section) ----"
python3 - <<'PY'
import json
from pathlib import Path

p = Path("schema/settings.json")
data = json.loads(p.read_text())
# brute-force search for key in nested structures
def find_key(obj, key):
    if isinstance(obj, dict):
        for k,v in obj.items():
            if k == key:
                return v
            r = find_key(v, key)
            if r is not None:
                return r
    elif isinstance(obj, list):
        for it in obj:
            r = find_key(it, key)
            if r is not None:
                return r
    return None

v = find_key(data, "term:disablewebgl")
print(v)
PY

echo "---- frontend/app/view/term/termwrap.ts (renderer selection) ----"
sed -n '140,230p' frontend/app/view/term/termwrap.ts

# Search RELEASES.md for WebGL/non-WebGL without blowing up output (exclude node_modules)
if [ -f RELEASES.md ]; then
  rg -n -S 'WebGL|webgl|non-WebGL|nonwebgl|disablewebgl|term:disablewebgl' RELEASES.md | head -200 || true
fi

# Also search docs/releases folder if exists (avoid node_modules)
if [ -d docs ]; then
  rg -n -S '(WebGL|webgl|non-WebGL|nonwebgl|disablewebgl|term:disablewebgl)' docs | head -200 || true
fi

Repository: wavetermdev/waveterm

Length of output: 23101


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Look for release/note mentions (exclude node_modules)
for f in RELEASES.md docs/RELEASES.md CHANGELOG.md docs/CHANGELOG.md; do
  if [ -f "$f" ]; then
    echo "---- $f ----"
    rg -n -S 'non-WebGL|nonwebgl|WebGL|webgl|term:disablewebgl|disablewebgl|disable WebGL' "$f" || true
  fi
done

# Search docs + repo for the specific phrase the review comment mentioned
rg -n -S --hidden --no-ignore --glob '!**/node_modules/**' \
  'Default terminal rendering to non-WebGL|non-WebGL|term:disablewebgl|disablewebgl|useWebGl' . | head -200

# Check config default description inconsistency
echo "---- docs/docs/config.mdx line with term:disablewebgl ----"
rg -n -S 'term:disablewebgl' docs/docs/config.mdx

Repository: wavetermdev/waveterm

Length of output: 1621


🏁 Script executed:

#!/bin/bash
set -euo pipefail
rg -n "DefaultTermFontFamily" frontend/app/view/term/term.tsx

Repository: wavetermdev/waveterm

Length of output: 207


Fix WebGL default inconsistency for term:disablewebgl (and document it)

  • frontend/app/view/term/term.tsx (299): termDisableWebGl = termSettings?.["term:disablewebgl"] ?? true => WebGL is disabled when the setting is unset; (323) useWebGl: !termDisableWebGl => defaults to false.
  • pkg/wconfig/defaultconfig/settings.json (35) also sets "term:disablewebgl": true.
  • docs/docs/config.mdx (63) states term:disablewebgl default is false, which conflicts with the code/config defaults above.
  • No release/changelog/migration note mentioning term:disablewebgl / WebGL (or “non-WebGL”) was found in RELEASES.md / docs release files.

Update the docs (and/or add release/migration notes if this default changed for existing users).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@frontend/app/view/term/term.tsx` at line 299, The code currently sets
termDisableWebGl via termSettings?.["term:disablewebgl"] ?? true which makes
termDisableWebGl true (WebGL disabled) by default and useWebGl is computed as
!termDisableWebGl; this conflicts with the docs that state the default is false;
update the documentation to reflect the actual default (term:disablewebgl
defaults to true) or change the default in code/config to false to match the
docs, and add a short migration/release note documenting the default behavior
change for the setting (referencing the termDisableWebGl variable and the
useWebGl computation) so users are informed of the change.

const wasFocused = model.termRef.current != null && globalStore.get(model.nodeModel.isFocused);
const termWrap = new TermWrap(
tabModel.tabId,
Expand All @@ -300,7 +305,7 @@ const TerminalView = ({ blockId, model }: ViewComponentProps<TermViewModel>) =>
{
theme: termTheme,
fontSize: termFontSize,
fontFamily: termSettings?.["term:fontfamily"] ?? connFontFamily ?? "Hack",
fontFamily: termSettings?.["term:fontfamily"] ?? connFontFamily ?? DefaultTermFontFamily,
drawBoldTextInBrightColors: false,
fontWeight: "normal",
fontWeightBold: "bold",
Expand All @@ -315,7 +320,7 @@ const TerminalView = ({ blockId, model }: ViewComponentProps<TermViewModel>) =>
},
{
keydownHandler: model.handleTerminalKeydown.bind(model),
useWebGl: !termSettings?.["term:disablewebgl"],
useWebGl: !termDisableWebGl,
sendDataHandler: model.sendDataToController.bind(model),
nodeModel: model.nodeModel,
}
Expand Down
88 changes: 88 additions & 0 deletions frontend/app/view/term/termwrap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright 2026, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

describe("TermWrap IME data ordering", () => {
let originalDocument: Document;

beforeEach(() => {
vi.useFakeTimers();
originalDocument = globalThis.document;
vi.stubGlobal("document", {
createElement: () => ({
getContext: () => null,
}),
});
});

afterEach(() => {
vi.useRealTimers();
vi.unstubAllGlobals();
if (originalDocument != null) {
vi.stubGlobal("document", originalDocument);
}
vi.resetModules();
});

async function makeTermWrapHarness() {
const { TermWrap } = await import("./termwrap");
const sent: string[] = [];
const termWrap = Object.create(TermWrap.prototype) as InstanceType<typeof TermWrap>;

termWrap.loaded = true;
termWrap.disposed = false;
termWrap.compositionActive = false;
termWrap.compositionRecentlyEndedUntil = 0;
termWrap.pendingCompositionSuffix = null;
termWrap.sendDataHandler = (data: string) => sent.push(data);
termWrap.multiInputCallback = null;

return { sent, termWrap };
}

it("sends Enter after committed IME text when Enter arrives during composition", async () => {
const { sent, termWrap } = await makeTermWrapHarness();

termWrap.compositionActive = true;
termWrap.handleTermData("\r");
expect(sent).toEqual([]);

termWrap.compositionActive = false;
termWrap.compositionRecentlyEndedUntil = Date.now() + 75;
termWrap.handleTermData("가");

expect(sent).toEqual(["가", "\r"]);
});

it("flushes a deferred Enter if composition ends without committed text", async () => {
const { sent, termWrap } = await makeTermWrapHarness();

termWrap.compositionActive = true;
termWrap.handleTermData("\r");
termWrap.compositionActive = false;
termWrap.schedulePendingCompositionSuffixFlush();

vi.advanceTimersByTime(30);

expect(sent).toEqual(["\r"]);
});

it("does not defer ordinary ASCII data while composition is active", async () => {
const { sent, termWrap } = await makeTermWrapHarness();

termWrap.compositionActive = true;
termWrap.handleTermData("hello");

expect(sent).toEqual(["hello"]);
});

it("does not treat a full ASCII line as a composition suffix", async () => {
const { sent, termWrap } = await makeTermWrapHarness();

termWrap.compositionRecentlyEndedUntil = Date.now() + 75;
termWrap.handleTermData("previous sentence");

expect(sent).toEqual(["previous sentence"]);
});
});
Loading