diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 9c0ee568c74..1b695fe071a 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -53,7 +53,7 @@ allowBuilds: node-pty: true sherpa-onnx: true ssh2: false - "@orca/windows-registry": false + '@orca/windows-registry': false overrides: monaco-editor>dompurify: 3.4.14 diff --git a/src/main/runtime/runtime-client-settings-terminal-copy-projection.test.ts b/src/main/runtime/runtime-client-settings-terminal-copy-projection.test.ts new file mode 100644 index 00000000000..8a6fad5065c --- /dev/null +++ b/src/main/runtime/runtime-client-settings-terminal-copy-projection.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest' +import { RuntimeClientSettingsController } from './runtime-client-settings' +import { createGlobalSettingsFixture } from '../../shared/global-settings-test-fixture' +import type { GlobalSettings } from '../../shared/global-settings-types' + +// Why: `settings.get` is an explicit allowlist, not the whole settings object. +// Mobile's terminal Copy reads terminalCopyTrimsGutter from it (#19770), and a +// field missing here is indistinguishable on the client from an older host — +// so the opt-out would silently never arrive. +function projectionOf(settings: Partial) { + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: get() reads nothing but store.getSettings(); every other RuntimeStore member is unreachable from that path. + return new RuntimeClientSettingsController({ getSettings: () => settings } as never).get() +} + +function hostSettings(overrides: Partial): Partial { + return { ...createGlobalSettingsFixture({ workspaceDir: '/w' }), ...overrides } +} + +describe('RuntimeClientSettingsController terminal copy projection', () => { + it('publishes the gutter-trim opt-out to paired clients', () => { + expect( + projectionOf(hostSettings({ terminalCopyTrimsGutter: false })).terminalCopyTrimsGutter + ).toBe(false) + }) + + it('publishes the gutter-trim opt-in to paired clients', () => { + expect( + projectionOf(hostSettings({ terminalCopyTrimsGutter: true })).terminalCopyTrimsGutter + ).toBe(true) + }) + + it('reports on when the host has no persisted preference', () => { + const settings = hostSettings({}) + delete settings.terminalCopyTrimsGutter + expect(projectionOf(settings).terminalCopyTrimsGutter).toBe(true) + }) +}) diff --git a/src/main/runtime/runtime-client-settings.ts b/src/main/runtime/runtime-client-settings.ts index 900900700f3..4e36ee22024 100644 --- a/src/main/runtime/runtime-client-settings.ts +++ b/src/main/runtime/runtime-client-settings.ts @@ -27,6 +27,7 @@ export type RuntimeClientSettings = Pick< | 'agentDefaultArgs' | 'agentDefaultEnv' | 'agentStatusHooksEnabled' + | 'terminalCopyTrimsGutter' | 'defaultTaskSource' | 'defaultTaskViewPreset' | 'visibleTaskProviders' @@ -97,6 +98,9 @@ export class RuntimeClientSettingsController { agentDefaultArgs: settings.agentDefaultArgs ?? {}, agentDefaultEnv: settings.agentDefaultEnv ?? {}, agentStatusHooksEnabled: settings.agentStatusHooksEnabled !== false, + // Why projected: mobile's terminal Copy honours this, and a host predating + // the setting sends no key, which the client reads as on (#19770). + terminalCopyTrimsGutter: settings.terminalCopyTrimsGutter !== false, defaultTaskSource: settings.defaultTaskSource ?? 'github', defaultTaskViewPreset: settings.defaultTaskViewPreset ?? 'issues', visibleTaskProviders: settings.visibleTaskProviders ?? [...TASK_PROVIDERS], diff --git a/src/main/runtime/runtime-store-contract.ts b/src/main/runtime/runtime-store-contract.ts index f3f5d5a8f51..b1471ff8efe 100644 --- a/src/main/runtime/runtime-store-contract.ts +++ b/src/main/runtime/runtime-store-contract.ts @@ -87,6 +87,7 @@ export type RuntimeStore = { terminalWindowsShell?: GlobalSettings['terminalWindowsShell'] floatingTerminalEnabled?: GlobalSettings['floatingTerminalEnabled'] agentStatusHooksEnabled?: GlobalSettings['agentStatusHooksEnabled'] + terminalCopyTrimsGutter?: GlobalSettings['terminalCopyTrimsGutter'] experimentalNativeChat?: GlobalSettings['experimentalNativeChat'] openAgentTabsInChatByDefault?: GlobalSettings['openAgentTabsInChatByDefault'] experimentalStructuredNativeChat?: GlobalSettings['experimentalStructuredNativeChat'] diff --git a/src/shared/source-scan/source-tree-walk.test.ts b/src/shared/source-scan/source-tree-walk.test.ts index afc7c8dba88..4030fa2f37c 100644 --- a/src/shared/source-scan/source-tree-walk.test.ts +++ b/src/shared/source-scan/source-tree-walk.test.ts @@ -108,7 +108,6 @@ describe('scanSourceTree filesystem traversal', () => { expect(statSync).toHaveBeenCalledExactlyOnceWith(join(root, 'alias')) }) - it('still reports a broken link instead of silently dropping it', () => { const target = join(root, '.target') mkdirSync(target) diff --git a/src/shared/terminal-selection-gutter.ts b/src/shared/terminal-selection-gutter.ts index 30650ebb4e5..ad30ce0a839 100644 --- a/src/shared/terminal-selection-gutter.ts +++ b/src/shared/terminal-selection-gutter.ts @@ -8,8 +8,11 @@ // selection that starts mid-line, or that covers any column-0 line, shares a // run of zero and comes back untouched. -// Terminal cells never hold tabs (the emulator expands them) and xterm folds -// non-breaking spaces into plain ones, so spaces are the whole alphabet here. +// Spaces are the whole alphabet here: terminal cells never hold tabs (the +// emulator expands them), and xterm's selectionText getter already folds every +// NBSP cell to a plain space on its way out (SelectionService.ts, the +// ALL_NON_BREAKING_SPACE_REGEX replace) — that is the selection path, not the +// input path. const LEADING_SPACES = /^ */ type SelectionLine = { indent: number; text: string; terminator: string }