From f85ec455472294a95292786f28f4af2d94899e4a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 4 Mar 2026 06:34:53 +0000
Subject: [PATCH 1/4] Initial plan
From 107b294bfb5f1e6ce6050b2dbc3dee7669c81fb9 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 4 Mar 2026 06:41:29 +0000
Subject: [PATCH 2/4] feat: add term:osc52 focus/always config override
Co-authored-by: sawka <2722291+sawka@users.noreply.github.com>
---
docs/docs/config.mdx | 2 +
frontend/app/view/term/osc-handlers.test.ts | 71 +++++++++++++++++++++
frontend/app/view/term/osc-handlers.ts | 13 +++-
frontend/types/gotypes.d.ts | 2 +
pkg/waveobj/metaconsts.go | 1 +
pkg/waveobj/wtypemeta.go | 1 +
pkg/wconfig/defaultconfig/settings.json | 1 +
pkg/wconfig/metaconsts.go | 1 +
pkg/wconfig/settingsconfig.go | 1 +
schema/settings.json | 7 ++
10 files changed, 98 insertions(+), 2 deletions(-)
create mode 100644 frontend/app/view/term/osc-handlers.test.ts
diff --git a/docs/docs/config.mdx b/docs/docs/config.mdx
index 6cdd59cdfa..7465261f4f 100644
--- a/docs/docs/config.mdx
+++ b/docs/docs/config.mdx
@@ -73,6 +73,7 @@ wsh editconfig
| term:cursorblink | bool | when enabled, terminal cursor blinks (default false) |
| term:bellsound | bool | when enabled, plays the system beep sound when the terminal bell (BEL character) is received (default false) |
| term:bellindicator | bool | when enabled, shows a visual indicator in the tab when the terminal bell is received (default false) |
+| term:osc52 | string | controls OSC 52 clipboard behavior: `focus` (default, requires focused block) or `always` (allows OSC 52 even when block is not focused) |
| term:durable | bool | makes remote terminal sessions durable across network disconnects (defaults to false) |
| editor:minimapenabled | bool | set to false to disable editor minimap |
| editor:stickyscrollenabled | bool | enables monaco editor's stickyScroll feature (pinning headers of current context, e.g. class names, method names, etc.), defaults to false |
@@ -147,6 +148,7 @@ For reference, this is the current default configuration (v0.14.0):
"telemetry:enabled": true,
"term:bellsound": false,
"term:bellindicator": false,
+ "term:osc52": "focus",
"term:cursor": "block",
"term:cursorblink": false,
"term:copyonselect": true,
diff --git a/frontend/app/view/term/osc-handlers.test.ts b/frontend/app/view/term/osc-handlers.test.ts
new file mode 100644
index 0000000000..c7edc7b5dd
--- /dev/null
+++ b/frontend/app/view/term/osc-handlers.test.ts
@@ -0,0 +1,71 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+const { mockWriteText, mockGet } = vi.hoisted(() => ({
+ mockWriteText: vi.fn(),
+ mockGet: vi.fn(),
+}));
+
+vi.mock("@/app/store/wshclientapi", () => ({ RpcApi: {} }));
+vi.mock("@/app/store/wshrpcutil", () => ({ TabRpcClient: {} }));
+vi.mock("@/store/services", () => ({}));
+vi.mock("@/store/global", () => ({
+ getApi: vi.fn(),
+ getBlockMetaKeyAtom: vi.fn(),
+ getBlockTermDurableAtom: vi.fn(),
+ getOverrideConfigAtom: vi.fn((_blockId: string, key: string) => ({ key })),
+ globalStore: { get: mockGet },
+ recordTEvent: vi.fn(),
+ WOS: {},
+}));
+vi.mock("@/util/util", () => ({
+ base64ToString: (data: string) => Buffer.from(data, "base64").toString("utf8"),
+ fireAndForget: (fn: () => Promise) => {
+ void fn();
+ },
+ isSshConnName: vi.fn(),
+ isWslConnName: vi.fn(),
+}));
+
+import { handleOsc52Command } from "./osc-handlers";
+
+describe("handleOsc52Command", () => {
+ beforeEach(() => {
+ vi.clearAllMocks();
+ mockWriteText.mockResolvedValue(undefined);
+ Object.defineProperty(globalThis, "navigator", {
+ configurable: true,
+ value: { clipboard: { writeText: mockWriteText } },
+ });
+ Object.defineProperty(globalThis, "document", {
+ configurable: true,
+ value: { hasFocus: () => true },
+ });
+ });
+
+ it("rejects unfocused block when term:osc52 is focus", () => {
+ mockGet.mockImplementation((atom: { key?: string } | undefined) => {
+ if (atom?.key === "term:osc52") {
+ return "focus";
+ }
+ return false;
+ });
+
+ handleOsc52Command("c;SGVsbG8=", "block-1", true, { nodeModel: { isFocused: {} } } as any);
+
+ expect(mockWriteText).not.toHaveBeenCalled();
+ });
+
+ it("allows unfocused block when term:osc52 is always", async () => {
+ mockGet.mockImplementation((atom: { key?: string } | undefined) => {
+ if (atom?.key === "term:osc52") {
+ return "always";
+ }
+ return false;
+ });
+
+ handleOsc52Command("c;SGVsbG8=", "block-1", true, { nodeModel: { isFocused: {} } } as any);
+ await Promise.resolve();
+
+ expect(mockWriteText).toHaveBeenCalledWith("Hello");
+ });
+});
diff --git a/frontend/app/view/term/osc-handlers.ts b/frontend/app/view/term/osc-handlers.ts
index dd8021ae1d..fde76095b5 100644
--- a/frontend/app/view/term/osc-handlers.ts
+++ b/frontend/app/view/term/osc-handlers.ts
@@ -3,7 +3,15 @@
import { RpcApi } from "@/app/store/wshclientapi";
import { TabRpcClient } from "@/app/store/wshrpcutil";
-import { getApi, getBlockMetaKeyAtom, getBlockTermDurableAtom, globalStore, recordTEvent, WOS } from "@/store/global";
+import {
+ getApi,
+ getBlockMetaKeyAtom,
+ getBlockTermDurableAtom,
+ getOverrideConfigAtom,
+ globalStore,
+ recordTEvent,
+ WOS,
+} from "@/store/global";
import * as services from "@/store/services";
import { base64ToString, fireAndForget, isSshConnName, isWslConnName } from "@/util/util";
import debug from "debug";
@@ -114,8 +122,9 @@ export function handleOsc52Command(data: string, blockId: string, loaded: boolea
if (!loaded) {
return true;
}
+ const osc52Mode = globalStore.get(getOverrideConfigAtom(blockId, "term:osc52")) ?? "focus";
const isBlockFocused = termWrap.nodeModel ? globalStore.get(termWrap.nodeModel.isFocused) : false;
- if (!document.hasFocus() || !isBlockFocused) {
+ if (!document.hasFocus() || (osc52Mode !== "always" && !isBlockFocused)) {
console.log("OSC 52: rejected, window or block not focused");
return true;
}
diff --git a/frontend/types/gotypes.d.ts b/frontend/types/gotypes.d.ts
index 2d155a919c..6070f46bef 100644
--- a/frontend/types/gotypes.d.ts
+++ b/frontend/types/gotypes.d.ts
@@ -1119,6 +1119,7 @@ declare global {
"term:conndebug"?: string;
"term:bellsound"?: boolean;
"term:bellindicator"?: boolean;
+ "term:osc52"?: string;
"term:durable"?: boolean;
"web:zoom"?: number;
"web:hidenav"?: boolean;
@@ -1313,6 +1314,7 @@ declare global {
"term:cursorblink"?: boolean;
"term:bellsound"?: boolean;
"term:bellindicator"?: boolean;
+ "term:osc52"?: string;
"term:durable"?: boolean;
"editor:minimapenabled"?: boolean;
"editor:stickyscrollenabled"?: boolean;
diff --git a/pkg/waveobj/metaconsts.go b/pkg/waveobj/metaconsts.go
index 8399340cfe..873732de9d 100644
--- a/pkg/waveobj/metaconsts.go
+++ b/pkg/waveobj/metaconsts.go
@@ -121,6 +121,7 @@ const (
MetaKey_TermConnDebug = "term:conndebug"
MetaKey_TermBellSound = "term:bellsound"
MetaKey_TermBellIndicator = "term:bellindicator"
+ MetaKey_TermOsc52 = "term:osc52"
MetaKey_TermDurable = "term:durable"
MetaKey_WebZoom = "web:zoom"
diff --git a/pkg/waveobj/wtypemeta.go b/pkg/waveobj/wtypemeta.go
index da73892365..8cd5ed1d9b 100644
--- a/pkg/waveobj/wtypemeta.go
+++ b/pkg/waveobj/wtypemeta.go
@@ -125,6 +125,7 @@ type MetaTSType struct {
TermConnDebug string `json:"term:conndebug,omitempty"` // null, info, debug
TermBellSound *bool `json:"term:bellsound,omitempty"`
TermBellIndicator *bool `json:"term:bellindicator,omitempty"`
+ TermOsc52 string `json:"term:osc52,omitempty"`
TermDurable *bool `json:"term:durable,omitempty"`
WebZoom float64 `json:"web:zoom,omitempty"`
diff --git a/pkg/wconfig/defaultconfig/settings.json b/pkg/wconfig/defaultconfig/settings.json
index aead19efbe..5c81bb9291 100644
--- a/pkg/wconfig/defaultconfig/settings.json
+++ b/pkg/wconfig/defaultconfig/settings.json
@@ -30,6 +30,7 @@
"telemetry:enabled": true,
"term:bellsound": false,
"term:bellindicator": false,
+ "term:osc52": "focus",
"term:cursor": "block",
"term:cursorblink": false,
"term:copyonselect": true,
diff --git a/pkg/wconfig/metaconsts.go b/pkg/wconfig/metaconsts.go
index 52dfa4514c..e031a493ea 100644
--- a/pkg/wconfig/metaconsts.go
+++ b/pkg/wconfig/metaconsts.go
@@ -56,6 +56,7 @@ const (
ConfigKey_TermCursorBlink = "term:cursorblink"
ConfigKey_TermBellSound = "term:bellsound"
ConfigKey_TermBellIndicator = "term:bellindicator"
+ ConfigKey_TermOsc52 = "term:osc52"
ConfigKey_TermDurable = "term:durable"
ConfigKey_EditorMinimapEnabled = "editor:minimapenabled"
diff --git a/pkg/wconfig/settingsconfig.go b/pkg/wconfig/settingsconfig.go
index 387598e899..69c531eb77 100644
--- a/pkg/wconfig/settingsconfig.go
+++ b/pkg/wconfig/settingsconfig.go
@@ -107,6 +107,7 @@ type SettingsType struct {
TermCursorBlink *bool `json:"term:cursorblink,omitempty"`
TermBellSound *bool `json:"term:bellsound,omitempty"`
TermBellIndicator *bool `json:"term:bellindicator,omitempty"`
+ TermOsc52 string `json:"term:osc52,omitempty" jsonschema:"enum=focus,enum=always"`
TermDurable *bool `json:"term:durable,omitempty"`
EditorMinimapEnabled bool `json:"editor:minimapenabled,omitempty"`
diff --git a/schema/settings.json b/schema/settings.json
index ad4cd83155..d60367bea3 100644
--- a/schema/settings.json
+++ b/schema/settings.json
@@ -151,6 +151,13 @@
"term:bellindicator": {
"type": "boolean"
},
+ "term:osc52": {
+ "type": "string",
+ "enum": [
+ "focus",
+ "always"
+ ]
+ },
"term:durable": {
"type": "boolean"
},
From ac855da93ee1506b6c7ae0c24a4a8b6c24f1ae15 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Wed, 4 Mar 2026 18:49:35 +0000
Subject: [PATCH 3/4] feat: default osc52 to always and skip focus checks in
always mode
Co-authored-by: sawka <2722291+sawka@users.noreply.github.com>
---
docs/docs/config.mdx | 4 +--
frontend/app/view/term/osc-handlers.test.ts | 36 +++++++++++++++++++++
frontend/app/view/term/osc-handlers.ts | 12 ++++---
pkg/wconfig/defaultconfig/settings.json | 2 +-
4 files changed, 46 insertions(+), 8 deletions(-)
diff --git a/docs/docs/config.mdx b/docs/docs/config.mdx
index 7465261f4f..cd29f8a7f4 100644
--- a/docs/docs/config.mdx
+++ b/docs/docs/config.mdx
@@ -73,7 +73,7 @@ wsh editconfig
| term:cursorblink | bool | when enabled, terminal cursor blinks (default false) |
| term:bellsound | bool | when enabled, plays the system beep sound when the terminal bell (BEL character) is received (default false) |
| term:bellindicator | bool | when enabled, shows a visual indicator in the tab when the terminal bell is received (default false) |
-| term:osc52 | string | controls OSC 52 clipboard behavior: `focus` (default, requires focused block) or `always` (allows OSC 52 even when block is not focused) |
+| term:osc52 | string | controls OSC 52 clipboard behavior: `always` (default, allows OSC 52 at any time) or `focus` (requires focused window and focused block) |
| term:durable | bool | makes remote terminal sessions durable across network disconnects (defaults to false) |
| editor:minimapenabled | bool | set to false to disable editor minimap |
| editor:stickyscrollenabled | bool | enables monaco editor's stickyScroll feature (pinning headers of current context, e.g. class names, method names, etc.), defaults to false |
@@ -148,7 +148,7 @@ For reference, this is the current default configuration (v0.14.0):
"telemetry:enabled": true,
"term:bellsound": false,
"term:bellindicator": false,
- "term:osc52": "focus",
+ "term:osc52": "always",
"term:cursor": "block",
"term:cursorblink": false,
"term:copyonselect": true,
diff --git a/frontend/app/view/term/osc-handlers.test.ts b/frontend/app/view/term/osc-handlers.test.ts
index c7edc7b5dd..42101a12c8 100644
--- a/frontend/app/view/term/osc-handlers.test.ts
+++ b/frontend/app/view/term/osc-handlers.test.ts
@@ -68,4 +68,40 @@ describe("handleOsc52Command", () => {
expect(mockWriteText).toHaveBeenCalledWith("Hello");
});
+
+ it("allows write when term:osc52 is always and window is unfocused", async () => {
+ Object.defineProperty(globalThis, "document", {
+ configurable: true,
+ value: { hasFocus: () => false },
+ });
+ mockGet.mockImplementation((atom: { key?: string } | undefined) => {
+ if (atom?.key === "term:osc52") {
+ return "always";
+ }
+ return false;
+ });
+
+ handleOsc52Command("c;SGVsbG8=", "block-1", true, { nodeModel: { isFocused: {} } } as any);
+ await Promise.resolve();
+
+ expect(mockWriteText).toHaveBeenCalledWith("Hello");
+ });
+
+ it("defaults term:osc52 to always when unset", async () => {
+ Object.defineProperty(globalThis, "document", {
+ configurable: true,
+ value: { hasFocus: () => false },
+ });
+ mockGet.mockImplementation((atom: { key?: string } | undefined) => {
+ if (atom?.key === "term:osc52") {
+ return undefined;
+ }
+ return false;
+ });
+
+ handleOsc52Command("c;SGVsbG8=", "block-1", true, { nodeModel: { isFocused: {} } } as any);
+ await Promise.resolve();
+
+ expect(mockWriteText).toHaveBeenCalledWith("Hello");
+ });
});
diff --git a/frontend/app/view/term/osc-handlers.ts b/frontend/app/view/term/osc-handlers.ts
index fde76095b5..25fdf0e89b 100644
--- a/frontend/app/view/term/osc-handlers.ts
+++ b/frontend/app/view/term/osc-handlers.ts
@@ -122,11 +122,13 @@ export function handleOsc52Command(data: string, blockId: string, loaded: boolea
if (!loaded) {
return true;
}
- const osc52Mode = globalStore.get(getOverrideConfigAtom(blockId, "term:osc52")) ?? "focus";
- const isBlockFocused = termWrap.nodeModel ? globalStore.get(termWrap.nodeModel.isFocused) : false;
- if (!document.hasFocus() || (osc52Mode !== "always" && !isBlockFocused)) {
- console.log("OSC 52: rejected, window or block not focused");
- return true;
+ const osc52Mode = globalStore.get(getOverrideConfigAtom(blockId, "term:osc52")) ?? "always";
+ if (osc52Mode === "focus") {
+ const isBlockFocused = termWrap.nodeModel ? globalStore.get(termWrap.nodeModel.isFocused) : false;
+ if (!document.hasFocus() || !isBlockFocused) {
+ console.log("OSC 52: rejected, window or block not focused");
+ return true;
+ }
}
if (!data || data.length === 0) {
console.log("OSC 52: empty data received");
diff --git a/pkg/wconfig/defaultconfig/settings.json b/pkg/wconfig/defaultconfig/settings.json
index 5c81bb9291..2de1974716 100644
--- a/pkg/wconfig/defaultconfig/settings.json
+++ b/pkg/wconfig/defaultconfig/settings.json
@@ -30,7 +30,7 @@
"telemetry:enabled": true,
"term:bellsound": false,
"term:bellindicator": false,
- "term:osc52": "focus",
+ "term:osc52": "always",
"term:cursor": "block",
"term:cursorblink": false,
"term:copyonselect": true,
From e70a9e9a23b793e47a9aa85665706f134108d32a Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Thu, 5 Mar 2026 00:22:17 +0000
Subject: [PATCH 4/4] test: remove osc52 vitest coverage
Co-authored-by: sawka <2722291+sawka@users.noreply.github.com>
---
frontend/app/view/term/osc-handlers.test.ts | 107 --------------------
1 file changed, 107 deletions(-)
delete mode 100644 frontend/app/view/term/osc-handlers.test.ts
diff --git a/frontend/app/view/term/osc-handlers.test.ts b/frontend/app/view/term/osc-handlers.test.ts
deleted file mode 100644
index 42101a12c8..0000000000
--- a/frontend/app/view/term/osc-handlers.test.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-import { beforeEach, describe, expect, it, vi } from "vitest";
-
-const { mockWriteText, mockGet } = vi.hoisted(() => ({
- mockWriteText: vi.fn(),
- mockGet: vi.fn(),
-}));
-
-vi.mock("@/app/store/wshclientapi", () => ({ RpcApi: {} }));
-vi.mock("@/app/store/wshrpcutil", () => ({ TabRpcClient: {} }));
-vi.mock("@/store/services", () => ({}));
-vi.mock("@/store/global", () => ({
- getApi: vi.fn(),
- getBlockMetaKeyAtom: vi.fn(),
- getBlockTermDurableAtom: vi.fn(),
- getOverrideConfigAtom: vi.fn((_blockId: string, key: string) => ({ key })),
- globalStore: { get: mockGet },
- recordTEvent: vi.fn(),
- WOS: {},
-}));
-vi.mock("@/util/util", () => ({
- base64ToString: (data: string) => Buffer.from(data, "base64").toString("utf8"),
- fireAndForget: (fn: () => Promise) => {
- void fn();
- },
- isSshConnName: vi.fn(),
- isWslConnName: vi.fn(),
-}));
-
-import { handleOsc52Command } from "./osc-handlers";
-
-describe("handleOsc52Command", () => {
- beforeEach(() => {
- vi.clearAllMocks();
- mockWriteText.mockResolvedValue(undefined);
- Object.defineProperty(globalThis, "navigator", {
- configurable: true,
- value: { clipboard: { writeText: mockWriteText } },
- });
- Object.defineProperty(globalThis, "document", {
- configurable: true,
- value: { hasFocus: () => true },
- });
- });
-
- it("rejects unfocused block when term:osc52 is focus", () => {
- mockGet.mockImplementation((atom: { key?: string } | undefined) => {
- if (atom?.key === "term:osc52") {
- return "focus";
- }
- return false;
- });
-
- handleOsc52Command("c;SGVsbG8=", "block-1", true, { nodeModel: { isFocused: {} } } as any);
-
- expect(mockWriteText).not.toHaveBeenCalled();
- });
-
- it("allows unfocused block when term:osc52 is always", async () => {
- mockGet.mockImplementation((atom: { key?: string } | undefined) => {
- if (atom?.key === "term:osc52") {
- return "always";
- }
- return false;
- });
-
- handleOsc52Command("c;SGVsbG8=", "block-1", true, { nodeModel: { isFocused: {} } } as any);
- await Promise.resolve();
-
- expect(mockWriteText).toHaveBeenCalledWith("Hello");
- });
-
- it("allows write when term:osc52 is always and window is unfocused", async () => {
- Object.defineProperty(globalThis, "document", {
- configurable: true,
- value: { hasFocus: () => false },
- });
- mockGet.mockImplementation((atom: { key?: string } | undefined) => {
- if (atom?.key === "term:osc52") {
- return "always";
- }
- return false;
- });
-
- handleOsc52Command("c;SGVsbG8=", "block-1", true, { nodeModel: { isFocused: {} } } as any);
- await Promise.resolve();
-
- expect(mockWriteText).toHaveBeenCalledWith("Hello");
- });
-
- it("defaults term:osc52 to always when unset", async () => {
- Object.defineProperty(globalThis, "document", {
- configurable: true,
- value: { hasFocus: () => false },
- });
- mockGet.mockImplementation((atom: { key?: string } | undefined) => {
- if (atom?.key === "term:osc52") {
- return undefined;
- }
- return false;
- });
-
- handleOsc52Command("c;SGVsbG8=", "block-1", true, { nodeModel: { isFocused: {} } } as any);
- await Promise.resolve();
-
- expect(mockWriteText).toHaveBeenCalledWith("Hello");
- });
-});