fix(terminal): answer live OSC 10/11 queries on the PTY host

gh auth login probes background color with OSC 11, then survey-reads
stdin. A renderer-side reply over SSH arrives after that read starts,
so survey treats ESC ] as an unexpected key and aborts. Answer those
queries on the POSIX PTY host for the whole session, strip them from
forwarded output so the renderer cannot send a late duplicate, and
leave Windows ConPTY on its existing consume-only path.
This commit is contained in:
Brennan Benson
2026-08-12 19:39:09 -07:00
parent bc2e30000b
commit 8dc662fd8d
19 changed files with 273 additions and 38 deletions
+3
View File
@@ -533,6 +533,9 @@ export class DaemonPtyAdapter implements IPtyProvider {
...(this.supportsStartupIngress && !attachOnly && opts.startupIngress
? { startupIngress: opts.startupIngress }
: {}),
...(!attachOnly && opts.oscColorQueryReplies
? { oscColorQueryReplies: opts.oscColorQueryReplies }
: {}),
...(!attachOnly && opts.agentSessionEnsure
? { agentSessionEnsure: opts.agentSessionEnsure }
: {})
+2
View File
@@ -22,6 +22,7 @@ import { checkPtySpawnHealth } from './pty-subprocess'
import { createNoopDaemonFileLog, type DaemonFileLog } from './daemon-file-log'
import { isTuiAgent } from '../../shared/tui-agent-config'
import { parsePtyStartupIngressIntent } from '../../shared/pty-startup-ingress'
import { parseTerminalOscColorQueryReplyColors } from '../../shared/terminal-osc-color-reply'
import { unlinkOwnedDaemonPidFile, unlinkOwnedDaemonTokenFile } from './daemon-spawner'
import {
DAEMON_ENDPOINT_LOST_MESSAGE,
@@ -1028,6 +1029,7 @@ export class DaemonServer {
shellReadySupported: p.shellReadySupported,
historySeedChunks,
startupIngress: parsePtyStartupIngressIntent(p.startupIngress),
oscColorQueryReplies: parseTerminalOscColorQueryReplyColors(p.oscColorQueryReplies),
...(p.shellReadyTimeoutMs !== undefined
? { shellReadyTimeoutMs: p.shellReadyTimeoutMs }
: {}),
+3
View File
@@ -34,6 +34,7 @@ import {
type PtyStartupIngressIntent
} from '../../shared/pty-startup-ingress'
import { extractOnlyCookedEchoSafeQueryReplies } from '../../shared/terminal-query-reply'
import type { TerminalOscColorQueryReplyColors } from '../../shared/terminal-osc-color-reply'
import type {
PendingOutputRecord,
SessionState,
@@ -109,6 +110,7 @@ export type SessionOptions = {
// a reaper, dead sessions and their scrollback emulators accumulate for the daemon's lifetime.
onExit?: (code: number) => void
startupIngress?: PtyStartupIngressIntent
oscColorQueryReplies?: TerminalOscColorQueryReplyColors
ownerBackend?: PtyOwnerBackend
}
@@ -206,6 +208,7 @@ export class Session {
const echoProbe = createPtySlaveEchoProbe(this.subprocess.slavePath)
this.startupIngress = new PtyStartupIngress({
...(opts.startupIngress ? { intent: opts.startupIngress } : {}),
...(opts.oscColorQueryReplies ? { liveOscColors: opts.oscColorQueryReplies } : {}),
...(opts.ownerBackend ? { ownerBackend: opts.ownerBackend } : {}),
write: (data) => this.subprocess.write(data),
onEmission: (emission) => this.emitSubprocessOutput(emission),
@@ -2,6 +2,7 @@ import type { StartupCommandDelivery } from '../../shared/codex-startup-delivery
import type { TuiAgent } from '../../shared/types'
import type { ShellReadyState, TerminalSnapshot } from './types'
import type { PtyStartupIngressIntent } from '../../shared/pty-startup-ingress'
import type { TerminalOscColorQueryReplyColors } from '../../shared/terminal-osc-color-reply'
import type {
AgentSessionClaimedSpawnResult,
AgentSessionExecutionClaim,
@@ -29,6 +30,7 @@ export type CreateOrAttachOptions = {
shellReadyTimeoutMs?: number
historySeedChunks?: readonly string[]
startupIngress?: PtyStartupIngressIntent
oscColorQueryReplies?: TerminalOscColorQueryReplyColors
agentSessionEnsure?: {
claim: AgentSessionExecutionClaim
surface: AgentSessionSurfaceBinding
@@ -109,6 +109,7 @@ export async function createOrAttachTerminalSession(
shellReadySupported,
historySeedChunks: opts.historySeedChunks,
...(opts.startupIngress ? { startupIngress: opts.startupIngress } : {}),
...(opts.oscColorQueryReplies ? { oscColorQueryReplies: opts.oscColorQueryReplies } : {}),
wslDistro,
onExit: () => deps.onSessionExit(opts.sessionId, opts.agentSessionGeneration),
...(opts.shellReadyTimeoutMs !== undefined
+4
View File
@@ -86,6 +86,10 @@ export type CreateOrAttachRequest = {
shellReadySupported?: boolean
shellReadyTimeoutMs?: number
startupIngress?: PtyStartupIngressIntent
oscColorQueryReplies?: {
foreground?: string
background?: string
}
agentSessionEnsure?: {
claim: AgentSessionExecutionClaim
surface: AgentSessionSurfaceBinding
+12 -1
View File
@@ -226,7 +226,10 @@ import {
} from '../../shared/setup-agent-sequencing'
import { dropAgentResumeArgvFromCommand } from '../../shared/agent-resume-argv-drop'
import { parseWorkspaceKey } from '../../shared/workspace-scope'
import { getStartupTerminalColorQueryReplyColors } from './terminal-startup-color-query-replies'
import {
getLiveTerminalOscColorQueryReplyColors,
getStartupTerminalColorQueryReplyColors
} from './terminal-startup-color-query-replies'
import {
assertFolderWorkspacePathUsable,
getFolderWorkspacePathStatus
@@ -4704,6 +4707,10 @@ export function registerPtyHandlers(
deadlineMs: 5_000
}
}
const liveOscColorQueryReplyColors = getLiveTerminalOscColorQueryReplyColors(args)
if (liveOscColorQueryReplyColors) {
spawnOptions.oscColorQueryReplies = liveOscColorQueryReplyColors
}
let ptySpawnCommitReported = false
const reportPtySpawnCommitted = (): void => {
if (ptySpawnCommitReported) {
@@ -6432,6 +6439,10 @@ export function registerPtyHandlers(
deadlineMs: 5_000
}
}
const liveOscColorQueryReplyColors = getLiveTerminalOscColorQueryReplyColors(args)
if (liveOscColorQueryReplyColors) {
spawnOptions.oscColorQueryReplies = liveOscColorQueryReplyColors
}
const resolvedPaneSpawnReservationKey = makePaneSpawnReservationKey(
args.worktreeId,
args.connectionId,
@@ -3,6 +3,7 @@ import { isTuiAgent } from '../../shared/tui-agent-config'
import { agentKindSchema } from '../../shared/telemetry-events'
import type { SleepingAgentLaunchConfig } from '../../shared/agent-session-resume'
import {
parseTerminalOscColorQueryReplyColors,
terminalOscColorQueryReply,
type TerminalOscColorQueryReplyColors
} from '../../shared/terminal-osc-color-reply'
@@ -56,3 +57,9 @@ export function getStartupTerminalColorQueryReplyColors(args: {
}
return normalizeTerminalColorQueryReplyColors(args.terminalColorQueryReplies)
}
export function getLiveTerminalOscColorQueryReplyColors(args: {
terminalColorQueryReplies?: unknown
}): TerminalOscColorQueryReplyColors | null {
return parseTerminalOscColorQueryReplyColors(args.terminalColorQueryReplies) ?? null
}
+1
View File
@@ -945,6 +945,7 @@ export class LocalPtyProvider implements IPtyProvider {
const startupEchoProbe = createPtySlaveEchoProbe(readPtySlavePath(proc))
const startupIngress = new PtyStartupIngress({
...(args.startupIngress ? { intent: args.startupIngress } : {}),
...(args.oscColorQueryReplies ? { liveOscColors: args.oscColorQueryReplies } : {}),
ownerBackend: resolvePtyOwnerBackend({
platform: process.platform,
shellPath,
@@ -1,5 +1,6 @@
import type { TuiAgent } from '../../shared/types'
import type { PtyStartupIngressIntent } from '../../shared/pty-startup-ingress'
import type { TerminalOscColorQueryReplyColors } from '../../shared/terminal-osc-color-reply'
import type { StartupCommandDelivery } from '../../shared/codex-startup-delivery'
import type { TerminalOscLinkRange } from '../../shared/terminal-osc-link-ranges'
import type { PtyBackgroundStreamEvent, PtyDataEvent } from './pty-provider-events'
@@ -92,6 +93,8 @@ export type PtySpawnOptions = {
terminalWindowsPowerShellImplementation?: 'auto' | 'powershell.exe' | 'pwsh.exe'
/** Fresh-spawn-only source authority installed before any PTY output is released. */
startupIngress?: PtyStartupIngressIntent
/** Theme colors for live OSC 10/11 answers after startup query authority closes. */
oscColorQueryReplies?: TerminalOscColorQueryReplyColors
agentSessionEnsure?: {
claim: AgentSessionExecutionClaim
surface: AgentSessionSurfaceBinding
@@ -39,6 +39,7 @@ export function buildSshPtySpawnRequest(args: {
startupIngress: options.startupIngress
}
: {}),
...(options.oscColorQueryReplies ? { oscColorQueryReplies: options.oscColorQueryReplies } : {}),
...(options.agentSessionEnsure ? { agentSessionEnsure: options.agentSessionEnsure } : {}),
...(args.supportsCreateOperation
? { agentSessionCreateOperationId: options.agentSessionCreateOperationId }
+34
View File
@@ -1866,6 +1866,40 @@ describe('PtyHandler', () => {
}
})
it('answers live OSC 11 on a POSIX SSH relay and strips the query', async () => {
vi.useFakeTimers()
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'linux' })
try {
let dataCallback: ((data: string) => void) | undefined
const term = {
...mockPtyInstance,
onData: vi.fn((cb: (data: string) => void) => {
dataCallback = cb
}),
onExit: vi.fn()
}
mockPtySpawn.mockReturnValue(term)
await dispatcher.callRequest('pty.spawn', {
shellOverride: '/bin/bash',
oscColorQueryReplies: { foreground: '#2e3434', background: '#ffffff' }
})
dataCallback!('\x1b]11;?\x1b\\\x1b[6n')
await vi.advanceTimersByTimeAsync(8)
expect(term.write).toHaveBeenCalledWith('\x1b]11;rgb:ffff/ffff/ffff\x1b\\')
const dataNotifies = dispatcher.notify.mock.calls
.filter((call) => call[0] === 'pty.data')
.map((call) => call[1] as { data: string })
expect(dataNotifies.map((frame) => frame.data).join('')).toBe('\x1b[6n')
} finally {
if (originalPlatform) {
Object.defineProperty(process, 'platform', originalPlatform)
}
}
})
it('forwards color queries from a POSIX SSH relay owner', async () => {
const originalPlatform = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { configurable: true, value: 'linux' })
+8
View File
@@ -59,6 +59,10 @@ import {
parsePtyStartupIngressIntent,
type PtyIngressEmission
} from '../shared/pty-startup-ingress'
import {
parseTerminalOscColorQueryReplyColors,
type TerminalOscColorQueryReplyColors
} from '../shared/terminal-osc-color-reply'
import { extractOnlyCookedEchoSafeQueryReplies } from '../shared/terminal-query-reply'
import { resolvePtyOwnerBackend, type PtyOwnerBackend } from '../shared/pty-owner-backend'
import { RecentPtyOutputBuffer } from '../main/runtime/recent-pty-output-buffer'
@@ -169,6 +173,7 @@ type ManagedPty = {
gracefulKillSent?: boolean
startupIngress?: PtyStartupIngress
startupIngressIntent?: ReturnType<typeof parsePtyStartupIngressIntent>
liveOscColors?: TerminalOscColorQueryReplyColors
ownerBackend: PtyOwnerBackend
agentSessionOwners?: AgentSessionOwnerBinding[]
}
@@ -743,6 +748,7 @@ export class PtyHandler {
const echoProbe = createPtySlaveEchoProbe(readPtySlavePath(managed.pty))
managed.startupIngress ??= new PtyStartupIngress({
...(managed.startupIngressIntent ? { intent: managed.startupIngressIntent } : {}),
...(managed.liveOscColors ? { liveOscColors: managed.liveOscColors } : {}),
ownerBackend: managed.ownerBackend,
write: (data) => managed.pty.write(data),
onEmission: emitIngressData,
@@ -1611,6 +1617,7 @@ export class PtyHandler {
params.startupIngressVersion === PTY_STARTUP_INGRESS_VERSION
? parsePtyStartupIngressIntent(params.startupIngress)
: undefined
const liveOscColors = parseTerminalOscColorQueryReplyColors(params.oscColorQueryReplies)
const managed: ManagedPty = {
id,
incarnationId: randomUUID(),
@@ -1636,6 +1643,7 @@ export class PtyHandler {
wslDistro: terminalWindowsWslDistro
}),
...(startupIngressIntent ? { startupIngressIntent } : {}),
...(liveOscColors ? { liveOscColors } : {}),
...(terminalHandle ? { terminalHandle } : {}),
...(managedStartupCommand && (shouldProviderDeliverCommand || rendererShellReadySupported)
? {
@@ -0,0 +1,69 @@
import {
terminalOscColorQueryReplies,
type TerminalOscColorQueryReplyColors,
type TerminalOscColorQuerySlot
} from './terminal-osc-color-reply'
import type { PtyOwnerBackend } from './pty-owner-backend'
import type { PtyStartupIngressIntent } from './pty-startup-ingress-intent'
type ColorReplyDelivery = {
answer: (reply: string, onFailed?: () => void) => boolean
}
export function answerLiveOscColorQuery(args: {
slots: readonly TerminalOscColorQuerySlot[]
colors: TerminalOscColorQueryReplyColors | undefined
ownerBackend: PtyOwnerBackend
delivery: ColorReplyDelivery
}): boolean {
if (!args.colors || args.ownerBackend === 'windows-conpty') {
return false
}
const replies = terminalOscColorQueryReplies(args.colors, args.slots)
if (!replies) {
return false
}
for (const reply of replies) {
args.delivery.answer(reply)
}
return true
}
export function answerStartupOscColorQuery(args: {
slots: readonly TerminalOscColorQuerySlot[]
intent: PtyStartupIngressIntent | undefined
answeredSlots: Set<TerminalOscColorQuerySlot>
delivery: ColorReplyDelivery
onBothSlotsAnswered: () => void
}): boolean {
if (args.slots.some((slot) => args.answeredSlots.has(slot)) || !args.intent) {
return false
}
const replies = terminalOscColorQueryReplies(args.intent.colors, args.slots)
if (!replies) {
return false
}
let wroteAny = false
for (const [index, reply] of replies.entries()) {
const slot = args.slots[index]
if (slot === undefined) {
return wroteAny
}
args.answeredSlots.add(slot)
// Why per slot: the replies to one query are written independently, so a
// deferred write that fails after reporting success invalidates only its own
// claim. Dropping every claim would let a slot that did land be answered a
// second time, and a duplicate reply corrupts a parser already mid-read.
if (!args.delivery.answer(reply, () => args.answeredSlots.delete(slot))) {
args.answeredSlots.delete(slot)
return wroteAny
}
wroteAny = true
}
if (args.answeredSlots.has(10) && args.answeredSlots.has(11)) {
args.onBothSlotsAnswered()
}
return wroteAny
}
@@ -1,6 +1,7 @@
import type { PtyOwnerBackend } from './pty-owner-backend'
import type { PtyStartupIngressIntent } from './pty-startup-ingress-intent'
import type { PtySlaveEchoProbe } from './pty-slave-line-discipline-echo'
import type { TerminalOscColorQueryReplyColors } from './terminal-osc-color-reply'
export type PtyIngressEmission = {
data: string
@@ -11,6 +12,13 @@ export type PtyIngressEmission = {
export type PtyStartupIngressOptions = {
intent?: PtyStartupIngressIntent
/**
* Live OSC 10/11 answers for the whole PTY lifetime. Distinct from startup
* `intent`: that path is once-per-slot and expires. Survey-style readers
* (gh auth login) query after that window; a late renderer reply is `\x1b]`
* on stdin.
*/
liveOscColors?: TerminalOscColorQueryReplyColors
ownerBackend?: PtyOwnerBackend
write: (data: string) => void
onEmission: (emission: PtyIngressEmission) => void
@@ -0,0 +1,57 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import { PtyStartupIngress, type PtyIngressEmission } from './pty-startup-ingress'
const COLORS = { foreground: '#2e3434', background: '#ffffff' }
const FOREGROUND_REPLY = '\x1b]10;rgb:2e2e/3434/3434\x1b\\'
const BACKGROUND_REPLY = '\x1b]11;rgb:ffff/ffff/ffff\x1b\\'
function visible(emissions: readonly PtyIngressEmission[]): string {
return emissions.map((emission) => emission.data).join('')
}
describe('PtyStartupIngress live OSC 10/11 answers', () => {
afterEach(() => vi.useRealTimers())
it('answers a post-startup OSC 11 query and strips it from forwarded output', async () => {
vi.useFakeTimers()
const emissions: PtyIngressEmission[] = []
const writes: string[] = []
const ingress = new PtyStartupIngress({
liveOscColors: COLORS,
write: (data) => writes.push(data),
onEmission: (emission) => emissions.push(emission)
})
// gh auth login writes OSC 11 (ST) plus CPR, then survey-reads stdin.
ingress.accept('\x1b]11;?\x1b\\\x1b[6n')
await vi.advanceTimersByTimeAsync(0)
expect(writes).toEqual([BACKGROUND_REPLY])
expect(visible(emissions)).toBe('\x1b[6n')
ingress.accept('\x1b]11;?\x07prompt')
await vi.advanceTimersByTimeAsync(0)
expect(writes).toEqual([BACKGROUND_REPLY, BACKGROUND_REPLY])
expect(visible(emissions)).toBe('\x1b[6nprompt')
ingress.drainAndClose()
})
it('still answers live OSC 11 after startup query authority expires', async () => {
vi.useFakeTimers()
const emissions: PtyIngressEmission[] = []
const writes: string[] = []
const ingress = new PtyStartupIngress({
intent: { colors: COLORS, deadlineMs: 5_000 },
liveOscColors: COLORS,
write: (data) => writes.push(data),
onEmission: (emission) => emissions.push(emission)
})
ingress.accept('\x1b]10;?\x07\x1b]11;?\x07')
await vi.advanceTimersByTimeAsync(0)
expect(writes).toEqual([FOREGROUND_REPLY, BACKGROUND_REPLY])
await vi.advanceTimersByTimeAsync(5_000)
writes.length = 0
ingress.accept('\x1b]11;?\x1b\\')
await vi.advanceTimersByTimeAsync(0)
expect(writes).toEqual([BACKGROUND_REPLY])
expect(visible(emissions)).toBe('')
ingress.drainAndClose()
})
})
+26 -36
View File
@@ -1,8 +1,12 @@
import {
parseTerminalOscColorQuery,
terminalOscColorQueryReplies,
type TerminalOscColorQueryReplyColors,
type TerminalOscColorQuerySlot
} from './terminal-osc-color-reply'
import {
answerLiveOscColorQuery,
answerStartupOscColorQuery
} from './pty-startup-ingress-color-answer'
import type { PtyStartupIngressIntent } from './pty-startup-ingress-intent'
import type { PtyOwnerBackend } from './pty-owner-backend'
import { PtyStartupReplyDelivery } from './pty-startup-reply-delivery'
@@ -38,6 +42,7 @@ const ECHO_CONTINUATION_HOLD_MS = 500
*/
export class PtyStartupIngress {
private readonly intent: PtyStartupIngressIntent | undefined
private readonly liveOscColors: TerminalOscColorQueryReplyColors | undefined
private readonly ownerBackend: PtyOwnerBackend
private readonly delivery: PtyStartupReplyDelivery
private readonly onEmission: (emission: PtyIngressEmission) => void
@@ -54,6 +59,7 @@ export class PtyStartupIngress {
constructor(options: PtyStartupIngressOptions) {
this.intent = options.intent
this.liveOscColors = options.liveOscColors
this.ownerBackend = options.ownerBackend ?? 'posix-pty'
this.delivery = new PtyStartupReplyDelivery(this.ownerBackend, options.write, options.echoProbe)
this.onEmission = options.onEmission
@@ -242,7 +248,8 @@ export class PtyStartupIngress {
const input = combinePtyIngressSourceSpans(this.queryPending, span)
this.queryPending = null
const suppressConptyQuery = this.ownerBackend === 'windows-conpty'
if ((!this.queryOpen || !this.intent) && !suppressConptyQuery) {
const canAnswerColorQuery = Boolean(this.liveOscColors || (this.queryOpen && this.intent))
if (!canAnswerColorQuery && !suppressConptyQuery) {
this.emit(input, false)
return
}
@@ -277,7 +284,23 @@ export class PtyStartupIngress {
this.emit(slicePtyIngressSourceSpan(input, emittedOffset, candidateIndex), false)
}
const querySpan = slicePtyIngressSourceSpan(input, candidateIndex, query.endIndex)
const answered = this.queryOpen && this.intent && this.answerQuery(query.slots)
const answered =
this.queryOpen && this.intent
? answerStartupOscColorQuery({
slots: query.slots,
intent: this.intent,
answeredSlots: this.answeredSlots,
delivery: this.delivery,
onBothSlotsAnswered: () => {
this.queryOpen = false
}
})
: answerLiveOscColorQuery({
slots: query.slots,
colors: this.liveOscColors,
ownerBackend: this.ownerBackend,
delivery: this.delivery
})
if (answered || suppressConptyQuery) {
this.emit(querySpan, true, '')
} else {
@@ -288,39 +311,6 @@ export class PtyStartupIngress {
}
}
private answerQuery(slots: readonly TerminalOscColorQuerySlot[]): boolean {
if (slots.some((slot) => this.answeredSlots.has(slot)) || !this.intent) {
return false
}
const replies = terminalOscColorQueryReplies(this.intent.colors, slots)
if (!replies) {
return false
}
let wroteAny = false
for (const [index, reply] of replies.entries()) {
const slot = slots[index]
if (slot === undefined) {
return wroteAny
}
this.answeredSlots.add(slot)
// Why per slot: the replies to one query are written independently, so a
// deferred write that fails after reporting success invalidates only its own
// claim. Dropping every claim would let a slot that did land be answered a
// second time, and a duplicate reply corrupts a parser already mid-read.
if (!this.delivery.answer(reply, () => this.answeredSlots.delete(slot))) {
this.answeredSlots.delete(slot)
return wroteAny
}
wroteAny = true
}
if (this.answeredSlots.has(10) && this.answeredSlots.has(11)) {
this.queryOpen = false
}
return wroteAny
}
private releaseQueryPending(): void {
if (!this.queryPending) {
return
+15 -1
View File
@@ -1,5 +1,8 @@
import { describe, expect, it } from 'vitest'
import { parseTerminalOscColorQuery } from './terminal-osc-color-reply'
import {
parseTerminalOscColorQuery,
parseTerminalOscColorQueryReplyColors
} from './terminal-osc-color-reply'
describe('parseTerminalOscColorQuery', () => {
it('matches exact OSC color queries terminated by ST or BEL', () => {
@@ -43,6 +46,17 @@ describe('parseTerminalOscColorQuery', () => {
expect(parseTerminalOscColorQuery('\x1b]11;?;?\x1b\\', 0)).toEqual({ kind: 'none' })
})
it('accepts theme colors that can produce OSC 10 and OSC 11 replies', () => {
expect(
parseTerminalOscColorQueryReplyColors({
foreground: '#2e3434',
background: '#ffffff'
})
).toEqual({ foreground: '#2e3434', background: '#ffffff' })
expect(parseTerminalOscColorQueryReplyColors({ foreground: '#2e3434' })).toBeUndefined()
expect(parseTerminalOscColorQueryReplyColors(null)).toBeUndefined()
})
it('rejects unsupported query-shaped bodies without waiting for a terminator', () => {
expect(parseTerminalOscColorQuery(`\x1b]10;?${'x'.repeat(10_000)}`, 0)).toEqual({
kind: 'none'
+17
View File
@@ -100,6 +100,23 @@ function clampByte(value: number): number {
return Math.min(255, Math.max(0, Math.round(value)))
}
export function parseTerminalOscColorQueryReplyColors(
value: unknown
): TerminalOscColorQueryReplyColors | undefined {
if (!value || typeof value !== 'object') {
return undefined
}
const record = value as { foreground?: unknown; background?: unknown }
const colors: TerminalOscColorQueryReplyColors = {
...(typeof record.foreground === 'string' ? { foreground: record.foreground } : {}),
...(typeof record.background === 'string' ? { background: record.background } : {})
}
if (!terminalOscColorQueryReply(colors, 10) || !terminalOscColorQueryReply(colors, 11)) {
return undefined
}
return colors
}
export function terminalOscColorQueryReply(
colors: TerminalOscColorQueryReplyColors,
slot: TerminalOscColorQuerySlot