diff --git a/Taskfile.yml b/Taskfile.yml index bf37a83e45..991b968f80 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -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. @@ -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). @@ -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: diff --git a/electron-builder.config.cjs b/electron-builder.config.cjs index d49f2da616..b94da50ad2 100644 --- a/electron-builder.config.cjs +++ b/electron-builder.config.cjs @@ -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} @@ -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.", @@ -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, { diff --git a/frontend/app/onboarding/onboarding-common.tsx b/frontend/app/onboarding/onboarding-common.tsx index 41f05e1f43..2641604f60 100644 --- a/frontend/app/onboarding/onboarding-common.tsx +++ b/frontend/app/onboarding/onboarding-common.tsx @@ -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 ( diff --git a/frontend/app/onboarding/onboarding-upgrade-patch.tsx b/frontend/app/onboarding/onboarding-upgrade-patch.tsx index 87ffadb1e4..4e12b68fb4 100644 --- a/frontend/app/onboarding/onboarding-upgrade-patch.tsx +++ b/frontend/app/onboarding/onboarding-upgrade-patch.tsx @@ -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; @@ -64,10 +65,7 @@ export function UpgradeOnboardingFooter({
{hasPrev && (
-
@@ -81,10 +79,7 @@ export function UpgradeOnboardingFooter({
{hasNext && (
-
@@ -153,6 +148,12 @@ export const UpgradeOnboardingVersions: VersionConfig[] = [ version: "v0.14.5", content: () => , prevText: "Prev (v0.14.4)", + nextText: "Next (v0.14.6)", + }, + { + version: "v0.14.6", + content: () => , + prevText: "Prev (v0.14.5)", }, ]; @@ -242,10 +243,7 @@ const UpgradeOnboardingPatch = ({ isReleaseNotes = false }: UpgradeOnboardingPat if (showStarAsk) { return ( - +
diff --git a/frontend/app/onboarding/onboarding-upgrade-v0146.tsx b/frontend/app/onboarding/onboarding-upgrade-v0146.tsx new file mode 100644 index 0000000000..d365def604 --- /dev/null +++ b/frontend/app/onboarding/onboarding-upgrade-v0146.tsx @@ -0,0 +1,45 @@ +// Copyright 2026, Command Line Inc. +// SPDX-License-Identifier: Apache-2.0 + +const UpgradeOnboardingModal_v0_14_6_Content = () => { + return ( +
+
+

+ Wave v0.14.6 is a patch release focused on terminal input reliability and packaging safeguards. +

+
+ +
+
+ +
+
+
IME Input Fixes
+
+ 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. +
+
+
+ +
+
+ +
+
+
Packaging Validation
+
+ Release packaging now fails early if required Wave backend binaries are missing from the + packaged app, preventing broken installers from being published. +
+
+
+
+ ); +}; + +UpgradeOnboardingModal_v0_14_6_Content.displayName = "UpgradeOnboardingModal_v0_14_6_Content"; + +export { UpgradeOnboardingModal_v0_14_6_Content }; diff --git a/frontend/app/store/keymodel.ts b/frontend/app/store/keymodel.ts index cca01753bb..690f80ec20 100644 --- a/frontend/app/store/keymodel.ts +++ b/frontend/app/store/keymodel.ts @@ -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; diff --git a/frontend/app/view/term/ijson.tsx b/frontend/app/view/term/ijson.tsx index 617a6e094d..472c4c0cfd 100644 --- a/frontend/app/view/term/ijson.tsx +++ b/frontend/app/view/term/ijson.tsx @@ -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; } `} diff --git a/frontend/app/view/term/term-model.ts b/frontend/app/view/term/term-model.ts index a256929e7d..3af9f23950 100644 --- a/frontend/app/view/term/term-model.ts +++ b/frontend/app/view/term/term-model.ts @@ -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; diff --git a/frontend/app/view/term/term.tsx b/frontend/app/view/term/term.tsx index 67eb5737c6..085c16d91e 100644 --- a/frontend/app/view/term/term.tsx +++ b/frontend/app/view/term/term.tsx @@ -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; @@ -292,6 +296,7 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => 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; const wasFocused = model.termRef.current != null && globalStore.get(model.nodeModel.isFocused); const termWrap = new TermWrap( tabModel.tabId, @@ -300,7 +305,7 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => { theme: termTheme, fontSize: termFontSize, - fontFamily: termSettings?.["term:fontfamily"] ?? connFontFamily ?? "Hack", + fontFamily: termSettings?.["term:fontfamily"] ?? connFontFamily ?? DefaultTermFontFamily, drawBoldTextInBrightColors: false, fontWeight: "normal", fontWeightBold: "bold", @@ -315,7 +320,7 @@ const TerminalView = ({ blockId, model }: ViewComponentProps) => }, { keydownHandler: model.handleTerminalKeydown.bind(model), - useWebGl: !termSettings?.["term:disablewebgl"], + useWebGl: !termDisableWebGl, sendDataHandler: model.sendDataToController.bind(model), nodeModel: model.nodeModel, } diff --git a/frontend/app/view/term/termwrap.test.ts b/frontend/app/view/term/termwrap.test.ts new file mode 100644 index 0000000000..214f0e5b30 --- /dev/null +++ b/frontend/app/view/term/termwrap.test.ts @@ -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; + + 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"]); + }); +}); diff --git a/frontend/app/view/term/termwrap.ts b/frontend/app/view/term/termwrap.ts index d10b600459..062c718e9b 100644 --- a/frontend/app/view/term/termwrap.ts +++ b/frontend/app/view/term/termwrap.ts @@ -52,6 +52,7 @@ const TermCacheFileName = "cache:term:full"; const MinDataProcessedForCache = 100 * 1024; export const SupportsImageInput = true; const MaxRepaintTransactionMs = 2000; +const MaxCompositionSuffixLength = 4; // detect webgl support function detectWebGLSupport(): boolean { @@ -111,6 +112,12 @@ export class TermWrap { lastPasteData: string = ""; lastPasteTime: number = 0; + // IME composition ordering + compositionActive: boolean = false; + compositionRecentlyEndedUntil: number = 0; + pendingCompositionSuffix: { data: string; timeout: ReturnType | null } | null = null; + disposed: boolean = false; + // dev only (for debugging) recentWrites: { idx: number; data: string; ts: number }[] = []; recentWritesCounter: number = 0; @@ -272,6 +279,12 @@ export class TermWrap { }) ); this.terminal.attachCustomKeyEventHandler((e: KeyboardEvent) => { + if (e.isComposing || e.keyCode == 229) { + return true; + } + if (this.shouldBypassWaveKeydownForComposition(e)) { + return true; + } if (!waveOptions.keydownHandler) { return true; } @@ -282,6 +295,7 @@ export class TermWrap { this.heldData = []; this.handleResize_debounced = debounce(50, this.handleResize.bind(this)); this.terminal.open(this.connectElem); + this.registerCompositionEventHandlers(); const dragoverHandler = (e: DragEvent) => { e.preventDefault(); @@ -442,6 +456,7 @@ export class TermWrap { } dispose() { + this.disposed = true; this.promptMarkers.forEach((marker) => { try { marker.dispose(); @@ -450,6 +465,7 @@ export class TermWrap { } }); this.promptMarkers = []; + this.cancelPendingCompositionSuffix(); this.webglContextLossDisposable?.dispose(); this.webglContextLossDisposable = null; this.terminal.dispose(); @@ -463,15 +479,168 @@ export class TermWrap { this.mainFileSubject.release(); } - handleTermData(data: string) { - if (!this.loaded) { + registerCompositionEventHandlers() { + const textarea = this.terminal.textarea; + if (textarea == null) { return; } + const compositionStartHandler = () => { + this.compositionActive = true; + this.compositionRecentlyEndedUntil = 0; + this.flushPendingCompositionSuffix(); + }; + const compositionEndHandler = () => { + this.compositionActive = false; + this.compositionRecentlyEndedUntil = Date.now() + 75; + this.schedulePendingCompositionSuffixFlush(); + }; + textarea.addEventListener("compositionstart", compositionStartHandler); + textarea.addEventListener("compositionend", compositionEndHandler); + this.toDispose.push({ + dispose: () => { + textarea.removeEventListener("compositionstart", compositionStartHandler); + textarea.removeEventListener("compositionend", compositionEndHandler); + this.cancelPendingCompositionSuffix(); + }, + }); + } + + shouldBypassWaveKeydownForComposition(event: KeyboardEvent): boolean { + if (this.compositionActive) { + return true; + } + if (Date.now() > this.compositionRecentlyEndedUntil) { + return false; + } + return !event.ctrlKey && !event.metaKey && !event.altKey && this.isCompositionSuffixData(event.key); + } + sendTermData(data: string) { this.sendDataHandler?.(data); this.multiInputCallback?.(data); } + flushPendingCompositionSuffix() { + if (this.pendingCompositionSuffix == null) { + return; + } + const pendingData = this.pendingCompositionSuffix.data; + if (this.pendingCompositionSuffix.timeout != null) { + clearTimeout(this.pendingCompositionSuffix.timeout); + } + this.pendingCompositionSuffix = null; + if (!this.loaded || this.disposed) { + return; + } + this.sendTermData(pendingData); + } + + cancelPendingCompositionSuffix() { + if (this.pendingCompositionSuffix == null) { + return; + } + if (this.pendingCompositionSuffix.timeout != null) { + clearTimeout(this.pendingCompositionSuffix.timeout); + } + this.pendingCompositionSuffix = null; + } + + schedulePendingCompositionSuffixFlush() { + if (this.pendingCompositionSuffix == null) { + return; + } + if (this.pendingCompositionSuffix.timeout != null) { + clearTimeout(this.pendingCompositionSuffix.timeout); + this.pendingCompositionSuffix.timeout = null; + } + if (this.compositionActive) { + return; + } + this.pendingCompositionSuffix.timeout = setTimeout(() => { + this.flushPendingCompositionSuffix(); + }, 30); + } + + isLikelyCompositionText(data: string): boolean { + if (data.length === 0) { + return false; + } + let hasNonAscii = false; + for (const ch of data) { + const codePoint = ch.codePointAt(0); + if (codePoint == null || codePoint <= 0x1f || codePoint === 0x7f) { + return false; + } + if (codePoint > 0x7f) { + hasNonAscii = true; + } + } + return hasNonAscii; + } + + isCompositionSuffixData(data: string): boolean { + if (data.length === 0 || data.length > MaxCompositionSuffixLength) { + return false; + } + for (const ch of data) { + const codePoint = ch.codePointAt(0); + if (codePoint == null || codePoint < 0x20 || codePoint > 0x7e) { + return false; + } + } + return true; + } + + shouldDeferCompositionData(data: string): boolean { + if (this.compositionActive) { + return data === "\r"; + } + if (Date.now() > this.compositionRecentlyEndedUntil) { + return false; + } + return data === "\r" || this.isCompositionSuffixData(data); + } + + handleTermData(data: string) { + if (!this.loaded) { + return; + } + + if (this.pendingCompositionSuffix != null) { + if (this.isLikelyCompositionText(data)) { + const pendingData = this.pendingCompositionSuffix.data; + if (this.pendingCompositionSuffix.timeout != null) { + clearTimeout(this.pendingCompositionSuffix.timeout); + } + this.pendingCompositionSuffix = null; + this.sendTermData(data); + this.sendTermData(pendingData); + return; + } + if (this.shouldDeferCompositionData(data)) { + if (this.pendingCompositionSuffix.timeout != null) { + clearTimeout(this.pendingCompositionSuffix.timeout); + this.pendingCompositionSuffix.timeout = null; + } + this.pendingCompositionSuffix.data += data; + this.schedulePendingCompositionSuffixFlush(); + return; + } + this.flushPendingCompositionSuffix(); + } + + if (this.shouldDeferCompositionData(data)) { + this.pendingCompositionSuffix = { + data, + timeout: null, + }; + this.schedulePendingCompositionSuffixFlush(); + return; + } + + this.sendTermData(data); + } + addFocusListener(focusFn: () => void) { this.terminal.textarea.addEventListener("focus", focusFn); } diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts index c5b870d7ed..49f99b2a16 100644 --- a/frontend/types/gotypes.d.ts +++ b/frontend/types/gotypes.d.ts @@ -2052,6 +2052,7 @@ declare global { code: string; repeat?: boolean; location?: number; + isComposing?: boolean; shift?: boolean; control?: boolean; alt?: boolean; diff --git a/frontend/util/keyutil.ts b/frontend/util/keyutil.ts index 867dfcb4e2..4a5a789388 100644 --- a/frontend/util/keyutil.ts +++ b/frontend/util/keyutil.ts @@ -240,6 +240,7 @@ function adaptFromReactOrNativeKeyEvent(event: React.KeyboardEvent | KeyboardEve rtn.key = event.key; rtn.location = event.location; (rtn as any).nativeEvent = event; + rtn.isComposing = event.isComposing || (event as any).keyCode == 229; if (event.type == "keydown" || event.type == "keyup" || event.type == "keypress") { rtn.type = event.type; } else { @@ -268,6 +269,7 @@ function adaptFromElectronKeyEvent(event: any): WaveKeyboardEvent { rtn.location = event.location; rtn.code = event.code; rtn.key = event.key; + rtn.isComposing = event.isComposing || event.keyCode == 229; return rtn; } diff --git a/package-lock.json b/package-lock.json index 1798a0fa38..43bf74f03a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "waveterm", - "version": "0.14.5", + "version": "0.14.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "waveterm", - "version": "0.14.5", + "version": "0.14.6", "hasInstallScript": true, "license": "Apache-2.0", "workspaces": [ diff --git a/package.json b/package.json index 4c1d56798f..ab8daee87a 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "productName": "Wave", "description": "Open-Source AI-Native Terminal Built for Seamless Workflows", "license": "Apache-2.0", - "version": "0.14.5", + "version": "0.14.6", "homepage": "https://waveterm.dev", "build": { "appId": "dev.commandline.waveterm" diff --git a/pkg/vdom/vdom_types.go b/pkg/vdom/vdom_types.go index 9ff5a4157e..3833aa3926 100644 --- a/pkg/vdom/vdom_types.go +++ b/pkg/vdom/vdom_types.go @@ -236,6 +236,8 @@ type WaveKeyboardEvent struct { Code string `json:"code"` // KeyboardEvent.code Repeat bool `json:"repeat,omitempty"` Location int `json:"location,omitempty"` // KeyboardEvent.location + // True while an IME composition is active. These key events should not trigger app shortcuts. + IsComposing bool `json:"isComposing,omitempty"` // modifiers Shift bool `json:"shift,omitempty"` diff --git a/pkg/wconfig/defaultconfig/settings.json b/pkg/wconfig/defaultconfig/settings.json index d8847cabf2..f9ed4b5957 100644 --- a/pkg/wconfig/defaultconfig/settings.json +++ b/pkg/wconfig/defaultconfig/settings.json @@ -32,6 +32,8 @@ "telemetry:enabled": true, "term:bellsound": false, "term:bellindicator": true, + "term:disablewebgl": true, + "term:fontfamily": "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", "term:osc52": "always", "term:cursor": "block", "term:cursorblink": false,