From 2265fce5912487f6a88ee0827dac14953b6a72c8 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 7 Sep 2026 14:54:18 -0700 Subject: [PATCH 01/10] chore(mobile): remove stale max-lines exceptions (#19366) --- config/max-lines-baseline.txt | 5 ----- mobile/.oxlintrc.json | 30 ------------------------------ 2 files changed, 35 deletions(-) diff --git a/config/max-lines-baseline.txt b/config/max-lines-baseline.txt index 4bc75a5a132..6d4e17066c8 100644 --- a/config/max-lines-baseline.txt +++ b/config/max-lines-baseline.txt @@ -9,8 +9,3 @@ inline src/main/ssh/ssh-relay-deploy.ts inline src/main/ssh/ssh-relay-session.ts inline src/relay/pty-handler.ts inline src/renderer/src/components/terminal-pane/remote-runtime-pty-transport.ts -mobile-config app/h/*/files/*.tsx -mobile-config app/h/*/source-control/*.tsx -mobile-config app/index.tsx -mobile-config scripts/mock-server.ts -mobile-config src/transport/rpc-client.ts diff --git a/mobile/.oxlintrc.json b/mobile/.oxlintrc.json index d58682609a3..aa73bfe8583 100644 --- a/mobile/.oxlintrc.json +++ b/mobile/.oxlintrc.json @@ -20,36 +20,6 @@ "rules": { "max-lines": ["error", { "max": 379, "skipBlankLines": true, "skipComments": true }] } - }, - { - "files": ["app/h/*/source-control/*.tsx"], - "rules": { - "max-lines": ["error", { "max": 2152, "skipBlankLines": true, "skipComments": true }] - } - }, - { - "files": ["app/index.tsx"], - "rules": { - "max-lines": ["error", { "max": 1422, "skipBlankLines": true, "skipComments": true }] - } - }, - { - "files": ["src/transport/rpc-client.ts"], - "rules": { - "max-lines": ["error", { "max": 1074, "skipBlankLines": true, "skipComments": true }] - } - }, - { - "files": ["scripts/mock-server.ts"], - "rules": { - "max-lines": ["error", { "max": 407, "skipBlankLines": true, "skipComments": true }] - } - }, - { - "files": ["app/h/*/files/*.tsx"], - "rules": { - "max-lines": ["error", { "max": 402, "skipBlankLines": true, "skipComments": true }] - } } ] } From 9f044031fc9e4985b855d984311fac1b943809eb Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 7 Sep 2026 16:21:47 -0700 Subject: [PATCH 02/10] fix(native-chat): render compaction notices, plan documents, and images (#19228) * fix(native-chat): render compaction notices, plan documents, and images * fix(native-chat): avoid repeating notice text in details * fix(native-chat): journal canonical and legacy compaction events * test: add digest to native chat notice payload fixture * chore(native-chat): drop the planning doc from the PR --------- Co-authored-by: Merge Sim --- .../codex/codex-image-item-translation.ts | 61 ++++++++ .../codex-notice-item-translation.test.ts | 114 +++++++++++++++ .../codex-structured-item-translation.ts | 28 ++++ ...dex-structured-journal-compactions.test.ts | 133 ++++++++++++++++++ .../codex-structured-journal-compactions.ts | 66 +++++++++ .../codex/codex-structured-journal-items.ts | 3 + ...ctured-journal-translation-streams.test.ts | 65 +++++++++ .../codex-structured-journal-translation.ts | 31 ++-- .../provider-frame-disposition.test.ts | 24 +++- .../provider-frame-disposition.ts | 7 +- .../structured-session-compaction.ts | 12 +- .../unhandled-provider-frame.test.ts | 38 +++++ .../unhandled-provider-frame.ts | 30 +++- .../native-chat/NativeChatMessageRow.tsx | 19 +++ .../native-chat/NativeChatNoticeRow.test.tsx | 110 +++++++++++++++ .../native-chat/NativeChatNoticeRow.tsx | 86 +++++++++++ .../NativeChatTranscriptChrome.tsx | 10 +- src/renderer/src/i18n/locales/en.json | 5 + .../agent-session-journal-schemas.test.ts | 28 ++++ src/shared/agent-session-journal-schemas.ts | 4 + src/shared/agent-session-journal-types.ts | 3 + src/shared/native-chat-types.ts | 3 + ...tructured-agent-session-projection.test.ts | 23 +++ .../structured-agent-session-projection.ts | 2 + 24 files changed, 878 insertions(+), 27 deletions(-) create mode 100644 src/main/codex/codex-image-item-translation.ts create mode 100644 src/main/codex/codex-notice-item-translation.test.ts create mode 100644 src/main/codex/codex-structured-journal-compactions.test.ts create mode 100644 src/main/codex/codex-structured-journal-compactions.ts create mode 100644 src/renderer/src/components/native-chat/NativeChatNoticeRow.test.tsx create mode 100644 src/renderer/src/components/native-chat/NativeChatNoticeRow.tsx diff --git a/src/main/codex/codex-image-item-translation.ts b/src/main/codex/codex-image-item-translation.ts new file mode 100644 index 00000000000..7d5e70a0c51 --- /dev/null +++ b/src/main/codex/codex-image-item-translation.ts @@ -0,0 +1,61 @@ +import type { AgentJournalMessageItem } from '../../shared/agent-session-journal-types' +import type { NativeChatBlock } from '../../shared/native-chat-types' +import { buildImageDataUri } from '../../shared/image-data-uri' +import { DEFAULT_JOURNAL_PAYLOAD_LIMITS } from '../native-chat/agent-session-journal/journal-payload-bounds' +import { readString } from './codex-item-field-readers' +import type { CodexThreadItem } from './codex-thread-item-identity' + +// Leave room for operation text and the journal envelope beside inline image bytes. +const MAX_IMAGE_REFERENCE_BYTES = DEFAULT_JOURNAL_PAYLOAD_LIMITS.inlineHeadBytes / 2 + +function imagePath(item: CodexThreadItem, key: string): string | null { + const value = readString(item, key) + return value?.trim() && Buffer.byteLength(value, 'utf8') <= MAX_IMAGE_REFERENCE_BYTES + ? value + : null +} + +function generatedImageUrl(item: CodexThreadItem): string | null { + const result = readString(item, 'result') + if (!result || result.length > MAX_IMAGE_REFERENCE_BYTES) { + return null + } + const match = /^data:(image\/(?:png|jpeg|webp));base64,(.*)$/s.exec(result) + const base64 = (match?.[2] ?? result).replace(/\s/g, '') + if (!base64 || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(base64)) { + return null + } + const url = buildImageDataUri(match?.[1] ?? 'image/png', base64) + return url && url.length <= MAX_IMAGE_REFERENCE_BYTES ? url : null +} + +export function codexImageItemBody(item: CodexThreadItem): AgentJournalMessageItem { + let image: Extract | null = null + let text: string + if (item.type === 'imageView') { + const path = imagePath(item, 'path') + text = path ? 'Viewed image' : 'Image view: preview unavailable' + image = path ? { type: 'image-ref', path } : null + } else { + const status = readString(item, 'status') + if (item.failure || status === 'failed') { + text = 'Image generation failed' + } else if (status !== 'completed') { + text = status === 'inProgress' ? 'Generating image…' : 'Image generation: preview unavailable' + } else { + const path = imagePath(item, 'savedPath') + const url = path ? null : generatedImageUrl(item) + image = path + ? { type: 'image-ref', path } + : url + ? { type: 'image-ref', url, alt: 'Generated image' } + : null + text = image ? 'Generated image' : 'Image generated: preview unavailable' + } + } + return { + kind: 'message', + role: 'assistant', + blocks: [{ type: 'text', text }, ...(image ? [image] : [])] + } +} diff --git a/src/main/codex/codex-notice-item-translation.test.ts b/src/main/codex/codex-notice-item-translation.test.ts new file mode 100644 index 00000000000..15f638c4015 --- /dev/null +++ b/src/main/codex/codex-notice-item-translation.test.ts @@ -0,0 +1,114 @@ +import { describe, expect, it } from 'vitest' +import { codexItemBody, codexStreamingJournalItem } from './codex-structured-item-translation' +import { AgentJournalItemBodySchema } from '../../shared/agent-session-journal-schemas' +import { projectStructuredItemsToNativeChat } from '../../shared/structured-agent-session-projection' + +describe('plan document translation', () => { + it('marks both complete documents and streaming snapshots', () => { + const item = { id: 'plan-1', type: 'plan', text: '# Plan\n\nReadable prose.' } + expect(codexItemBody(item)).toEqual({ + kind: 'status', + text: item.text, + presentation: 'plan-document' + }) + expect(codexStreamingJournalItem(item, '# Plan\n\nPartial').body).toEqual({ + kind: 'status', + text: '# Plan\n\nPartial', + presentation: 'plan-document' + }) + expect(codexItemBody({ id: 'plan-1', type: 'plan' })).toBeNull() + }) + it('preserves the full existing reasoning body byte for byte', () => { + expect( + codexItemBody({ id: 'r', type: 'reasoning', summary: ['Thinking through the problem.'] }) + ).toEqual({ + kind: 'status', + text: 'Thinking through the problem.' + }) + expect(codexStreamingJournalItem({ id: 'r', type: 'reasoning' }, 'Thinking…')).toEqual({ + body: { kind: 'status', text: 'Thinking…' }, + handled: true + }) + }) +}) + +describe('image item translation', () => { + it.each(['/remote/work/image.png', 'C:\\work\\image.png'])( + 'preserves the execution-host path %s and old-reader operation text', + (path) => { + expect(codexItemBody({ id: 'view', type: 'imageView', path })).toEqual({ + kind: 'message', + role: 'assistant', + blocks: [ + { type: 'text', text: 'Viewed image' }, + { type: 'image-ref', path } + ] + }) + } + ) + it('prefers saved paths over unbounded inline image data', () => { + const body = codexItemBody({ + id: 'gen', + type: 'imageGeneration', + status: 'completed', + savedPath: '/remote/image.png', + result: 'A'.repeat(100_000) + }) + expect(body).toEqual({ + kind: 'message', + role: 'assistant', + blocks: [ + { type: 'text', text: 'Generated image' }, + { type: 'image-ref', path: '/remote/image.png' } + ] + }) + expect(AgentJournalItemBodySchema.safeParse(body).success).toBe(true) + const [message] = projectStructuredItemsToNativeChat([ + { itemId: 'gen', revision: 1, sequence: 1, observedAt: 1, body: body! } + ]) + expect(message?.blocks).toEqual(body?.kind === 'message' ? body.blocks : []) + }) + it.each(['AAAA', 'data:image/png;base64,AAAA'])( + 'maps bounded image data onto a meaningful existing image-ref: %s', + (result) => { + expect( + codexItemBody({ id: 'gen', type: 'imageGeneration', status: 'completed', result }) + ).toMatchObject({ + kind: 'message', + role: 'assistant', + blocks: [ + { type: 'text', text: 'Generated image' }, + { type: 'image-ref', url: 'data:image/png;base64,AAAA', alt: 'Generated image' } + ] + }) + } + ) + it.each(['A'.repeat(20_000), 'not valid image bytes', 'data:text/html;base64,AAAA', ''])( + 'keeps unavailable results bounded and readable', + (result) => { + const body = codexItemBody({ + id: 'gen', + type: 'imageGeneration', + status: 'completed', + result + }) + expect(body).toEqual({ + kind: 'message', + role: 'assistant', + blocks: [{ type: 'text', text: 'Image generated: preview unavailable' }] + }) + } + ) + it.each([ + [{ status: 'inProgress' }, 'Generating image…'], + [{ status: 'failed' }, 'Image generation failed'], + [{ status: 'completed', failure: { type: 'usageLimitExceeded' } }, 'Image generation failed'], + [{ status: 'future-state' }, 'Image generation: preview unavailable'] + ])('does not invent completed output for %j', (fields, text) => { + expect(codexItemBody({ id: 'gen', type: 'imageGeneration', ...fields })).toEqual({ + kind: 'message', + role: 'assistant', + blocks: [{ type: 'text', text }] + }) + }) +}) diff --git a/src/main/codex/codex-structured-item-translation.ts b/src/main/codex/codex-structured-item-translation.ts index fca66257636..576f3fb19ec 100644 --- a/src/main/codex/codex-structured-item-translation.ts +++ b/src/main/codex/codex-structured-item-translation.ts @@ -7,6 +7,7 @@ import { DEFAULT_JOURNAL_PAYLOAD_LIMITS } from '../native-chat/agent-session-journal/journal-payload-bounds' import { unhandledProviderFrameJournalItem } from '../native-chat/agent-session-wire/unhandled-provider-frame' +import { codexImageItemBody } from './codex-image-item-translation' import { commandActionFacts } from './codex-command-action-class' import { readFirstString, @@ -250,6 +251,23 @@ export function codexJournalItem(item: CodexThreadItem): CodexJournalItem { if (item.type === 'webSearch') { return webSearchItem(item) } + if (item.type === 'imageView' || item.type === 'imageGeneration') { + return { body: codexImageItemBody(item), handled: true } + } + if (item.type === 'plan') { + const text = readTextContent(item, 'text') + return { + body: + text === null + ? null + : { + kind: 'status', + text: boundInlineText(text, DEFAULT_JOURNAL_PAYLOAD_LIMITS).text, + presentation: 'plan-document' + }, + handled: true + } + } if (item.type === 'reasoning' || item.type === 'plan') { const text = readTextContent(item, 'text') ?? @@ -298,6 +316,16 @@ export function codexStreamingJournalItem(item: CodexThreadItem, text: string): handled: true } } + if (item.type === 'plan') { + return { + body: { + kind: 'status', + text: boundInlineText(text, DEFAULT_JOURNAL_PAYLOAD_LIMITS).text, + presentation: 'plan-document' + }, + handled: true + } + } const bounded = boundInlineText(text, DEFAULT_JOURNAL_PAYLOAD_LIMITS) return { body: { kind: 'status', text: bounded.text }, handled: true } } diff --git a/src/main/codex/codex-structured-journal-compactions.test.ts b/src/main/codex/codex-structured-journal-compactions.test.ts new file mode 100644 index 00000000000..b86cea2725d --- /dev/null +++ b/src/main/codex/codex-structured-journal-compactions.test.ts @@ -0,0 +1,133 @@ +import { describe, expect, it } from 'vitest' +import { agentJournalItemKey } from '../../shared/agent-session-journal-item-key' +import type { AgentJournalItemBody } from '../../shared/agent-session-journal-types' +import { projectStructuredItemsToNativeChat } from '../../shared/structured-agent-session-projection' +import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink' +import { + createCodexJournalTranslator, + MAX_CODEX_GENERIC_TURN_BUCKETS +} from './codex-structured-journal-translation' + +function setup() { + const rows = new Map() + let writes = 0 + const sink: StructuredAgentSessionEventSink = { + appendItem: (identity, body) => { + writes += 1 + rows.set(agentJournalItemKey(identity), body) + }, + appendTombstone: (identity) => rows.delete(agentJournalItemKey(identity)), + publish: () => {} + } + const translator = createCodexJournalTranslator({ sink }) + const send = (method: string, turnId = 'turn', threadId = 'thread') => + translator.handle({ + type: 'notification', + sessionId: 'session', + threadId, + method, + params: { turnId, item: { id: `compact-${turnId}`, type: 'contextCompaction' } } + }) + return { rows, sink, translator, send, writes: () => writes } +} + +describe('compaction provider generation compatibility', () => { + it.each([ + ['item/completed'], + ['thread/compacted'], + ['item/completed', 'thread/compacted'], + ['thread/compacted', 'item/completed'] + ])('projects one readable divider for %j', (...methods) => { + const { rows, translator, send } = setup() + expect(send('item/started')).toEqual({ accepted: true }) + expect(rows.size).toBe(0) + for (const method of methods) { + expect(send(method)).toEqual({ accepted: true }) + } + const messages = projectStructuredItemsToNativeChat( + [...rows].map(([itemId, body], index) => ({ + itemId, + body, + sequence: index + 1, + revision: 1, + observedAt: 1 + })) + ) + expect(messages).toHaveLength(1) + expect(messages[0]?.blocks).toEqual([ + { type: 'text', text: 'Context compacted', presentation: 'compaction' } + ]) + translator.dispose() + }) + + it('keeps the canonical item authoritative and scopes deduplication by thread and turn', () => { + const { rows, translator, send, writes } = setup() + send('thread/compacted') + send('item/completed') + expect(writes()).toBe(2) + send('thread/compacted') + send('item/completed') + expect(writes()).toBe(2) + send('item/completed', 'turn-2') + send('item/completed', 'turn', 'other-thread') + expect(rows.size).toBe(3) + translator.dispose() + }) + + it.each(['append', 'publish'])('does not suppress the retry after rejected %s', (stage) => { + const { rows, sink, translator, send } = setup() + let reject = true + let publishes = 0 + sink.tryAppendItem = (identity, body) => { + if (stage === 'append' && reject) { + return { accepted: false, reason: 'backpressure' } + } + sink.appendItem(identity, body) + return { accepted: true } + } + sink.tryPublish = () => { + publishes += 1 + return stage === 'publish' && reject + ? { accepted: false, reason: 'backpressure' } + : { accepted: true } + } + expect(send('item/completed')).toEqual({ accepted: false, reason: 'backpressure' }) + reject = false + expect(send('item/completed')).toEqual({ accepted: true }) + expect(rows.size).toBe(1) + expect(publishes).toBe(stage === 'publish' ? 2 : 1) + translator.dispose() + }) + + it('bounds retained turns and keeps the same journal identity after eviction', () => { + const { rows, translator, send, writes } = setup() + send('item/completed', 'oldest') + for (let index = 0; index < MAX_CODEX_GENERIC_TURN_BUCKETS; index += 1) { + send('item/completed', `turn-${index}`) + } + const before = writes() + send('thread/compacted', 'oldest') + expect(writes()).toBe(before + 1) + expect(rows.size).toBe(MAX_CODEX_GENERIC_TURN_BUCKETS + 1) + translator.dispose() + }) + + it('restores canonical compaction history over a previously journaled legacy fallback', () => { + const { rows, sink, translator, send } = setup() + send('thread/compacted') + translator.dispose() + const restored = createCodexJournalTranslator({ sink }) + expect( + restored.restoreThread('thread', { + turns: [{ id: 'turn', items: [{ id: 'renumbered', type: 'contextCompaction' }] }] + }) + ).toEqual({ accepted: true }) + expect(rows.size).toBe(1) + expect([...rows.values()][0]).toEqual({ + kind: 'status', + text: 'Context compacted', + presentation: 'compaction' + }) + restored.dispose() + }) +}) diff --git a/src/main/codex/codex-structured-journal-compactions.ts b/src/main/codex/codex-structured-journal-compactions.ts new file mode 100644 index 00000000000..d2d6aeca464 --- /dev/null +++ b/src/main/codex/codex-structured-journal-compactions.ts @@ -0,0 +1,66 @@ +import { createHash } from 'node:crypto' +import { isCodexCompactionComplete } from '../native-chat/agent-session-wire/structured-session-compaction' +import type { StructuredAgentSessionEventSink } from '../native-chat/agent-session-wire/structured-agent-session-event-sink' +import { + CODEX_JOURNAL_ADMITTED, + type CodexJournalTranslationAdmission +} from './codex-structured-journal-contracts' +import { MAX_CODEX_GENERIC_TURN_BUCKETS } from './codex-structured-journal-limits' +import { appendCodexLifecycleItem, publishCodexLifecycle } from './codex-structured-journal-sink' +import { readCodexTurnId } from './codex-structured-thread-facts' + +export class CodexJournalCompactions { + private readonly turns = new Map() + + constructor( + private readonly sink: StructuredAgentSessionEventSink, + private readonly activeTurn: (threadId: string) => string | null + ) {} + + handle(event: { + threadId: string + method: string + params: unknown + }): CodexJournalTranslationAdmission | null { + if (!isCodexCompactionComplete(event.method, event.params)) { + return null + } + const turnId = readCodexTurnId(event.params) ?? this.activeTurn(event.threadId) + if (!turnId) { + return null + } + // Collapse compactions within a thread/turn; the canonical item replaces its legacy fallback. + const key = createHash('sha256') + .update(JSON.stringify([event.threadId, turnId])) + .digest('hex') + const source = event.method === 'item/completed' ? 'item' : 'legacy' + const previous = this.turns.get(key) + if (previous === 'item' || previous === source) { + return CODEX_JOURNAL_ADMITTED + } + const admission = appendCodexLifecycleItem( + this.sink, + { provider: 'orca', clientMessageId: `codex-compaction:${key}` }, + { kind: 'status', text: 'Context compacted', presentation: 'compaction' } + ) + if (!admission.accepted) { + return admission + } + const published = publishCodexLifecycle(this.sink) + if (!published.accepted) { + return published + } + this.turns.set(key, source) + while (this.turns.size > MAX_CODEX_GENERIC_TURN_BUCKETS) { + const oldest = this.turns.keys().next().value + if (oldest !== undefined) { + this.turns.delete(oldest) + } + } + return CODEX_JOURNAL_ADMITTED + } + + clear(): void { + this.turns.clear() + } +} diff --git a/src/main/codex/codex-structured-journal-items.ts b/src/main/codex/codex-structured-journal-items.ts index fd8fbf558a8..f984bc1d9bf 100644 --- a/src/main/codex/codex-structured-journal-items.ts +++ b/src/main/codex/codex-structured-journal-items.ts @@ -78,6 +78,9 @@ export class CodexJournalItems { if (source === 'live' && item.type === 'userMessage') { return { handled: true, admission: CODEX_JOURNAL_ADMITTED } } + if (item.type === 'contextCompaction' && event.method === 'item/started') { + return { handled: true, admission: CODEX_JOURNAL_ADMITTED } + } const translated = codexJournalItem(item) const command = readCodexJournalString(item, 'command') if (command) { diff --git a/src/main/codex/codex-structured-journal-translation-streams.test.ts b/src/main/codex/codex-structured-journal-translation-streams.test.ts index 86eb94fef07..ea01f88e4c9 100644 --- a/src/main/codex/codex-structured-journal-translation-streams.test.ts +++ b/src/main/codex/codex-structured-journal-translation-streams.test.ts @@ -603,3 +603,68 @@ describe('codex journal translation', () => { expect(tap.rows).toEqual([]) }) }) + +describe('notice journal pipeline', () => { + it('replaces a legacy compaction divider with its canonical item at the same journal key', () => { + const { translator, tap } = translatorWith() + translator.handle(notification('thread/compacted', { threadId: THREAD_ID, turnId: TURN_ID })) + translator.handle( + notification('item/completed', { + turnId: TURN_ID, + item: { id: 'compact', type: 'contextCompaction' } + }) + ) + expect(tap.rows).toHaveLength(2) + expect([...new Map(tap.rows.map((row) => [row.key, row.body])).values()]).toEqual([ + expect.objectContaining({ + kind: 'status', + text: 'Context compacted', + presentation: 'compaction' + }) + ]) + translator.dispose() + }) + it('preserves every notice after generic traffic reaches its cap', () => { + const { translator, tap, window } = translatorWith() + translator.handle(TURN_STARTED) + for (let index = 0; index < MAX_CODEX_GENERIC_ROWS_PER_TURN; index += 1) { + translator.handle(notification('future/notification', { value: index })) + } + for (const method of ['warning', 'guardianWarning', 'configWarning', 'deprecationNotice']) { + translator.handle(notification(method, { message: method, summary: method })) + } + window.fire() + expect(tap.rows.slice(-4).map((row) => row.body)).toEqual([ + expect.objectContaining({ text: 'warning', tone: 'warning' }), + expect.objectContaining({ text: 'guardianWarning', tone: 'warning' }), + expect.objectContaining({ text: 'configWarning', tone: 'warning' }), + expect.objectContaining({ text: 'deprecationNotice', tone: 'notice' }) + ]) + translator.dispose() + }) + it('keeps the plan document marker during streamed updates and completion', () => { + const { translator, tap, window } = translatorWith() + translator.handle( + notification('item/started', { + turnId: TURN_ID, + item: { id: 'plan', type: 'plan', text: '' } + }) + ) + translator.handle( + notification('item/plan/delta', { turnId: TURN_ID, itemId: 'plan', delta: '# Plan' }) + ) + window.fire() + expect(tap.rows.at(-1)?.body).toMatchObject({ text: '# Plan', presentation: 'plan-document' }) + translator.handle( + notification('item/completed', { + turnId: TURN_ID, + item: { id: 'plan', type: 'plan', text: '# Plan\n\nComplete' } + }) + ) + expect(tap.rows.at(-1)?.body).toMatchObject({ + text: '# Plan\n\nComplete', + presentation: 'plan-document' + }) + translator.dispose() + }) +}) diff --git a/src/main/codex/codex-structured-journal-translation.ts b/src/main/codex/codex-structured-journal-translation.ts index c8a6fe9f158..00b90d7ffc8 100644 --- a/src/main/codex/codex-structured-journal-translation.ts +++ b/src/main/codex/codex-structured-journal-translation.ts @@ -6,6 +6,7 @@ import { import { CodexSubagentRoster } from './codex-subagent-roster' import { readCodexThreadItem } from './codex-structured-item-translation' import { CodexJournalGenericFrames } from './codex-structured-journal-generic-frames' +import { CodexJournalCompactions } from './codex-structured-journal-compactions' import { CodexJournalItems } from './codex-structured-journal-items' import { CodexJournalPrompts } from './codex-structured-journal-prompts' import { @@ -46,6 +47,9 @@ export function createCodexJournalTranslator( deps: CodexJournalTranslatorDeps ): CodexJournalTranslator { const activeTurns = new CodexJournalActiveTurns() + const compactions = new CodexJournalCompactions(deps.sink, (threadId) => + activeTurns.current(threadId) + ) const genericFrames = new CodexJournalGenericFrames(deps, (threadId) => activeTurns.current(threadId) ) @@ -94,6 +98,10 @@ export function createCodexJournalTranslator( currentTurnIds: activeTurns.byThread, ordinals: items.ordinals, handleItem: (event) => { + const compaction = compactions.handle(event) + if (compaction) { + return compaction + } const translated = items.handle(event, 'history') return translated.handled ? translated.admission @@ -135,6 +143,7 @@ export function createCodexJournalTranslator( items.activeItems.clear() prompts.pending.clear() activeTurns.clear() + compactions.clear() return CODEX_JOURNAL_ADMITTED } if (event.type === 'notification') { @@ -168,6 +177,10 @@ export function createCodexJournalTranslator( if (event.method === 'turn/started') { return startTurn(event) } + const compaction = compactions.handle(event) + if (compaction) { + return publishActivity(event, compaction) + } if (event.method === 'turn/completed') { return completeTurn(event) } @@ -223,17 +236,15 @@ export function createCodexJournalTranslator( genericFrames.dispose() subagents.dispose() activeTurns.clear() + compactions.clear() } } /** Settles the item a notification the transport refused to carry left * mid-flight; null when the frame is not one. */ - function settleOversizedNotification(event: { - sessionId: string - threadId: string - kind: string - payload: unknown - }): CodexJournalTranslationAdmission | null { + function settleOversizedNotification( + event: Extract + ): CodexJournalTranslationAdmission | null { return settleCodexOversizedNotificationFrame({ ...event, sink: deps.sink, @@ -242,11 +253,9 @@ export function createCodexJournalTranslator( }) } - function startTurn(event: { - sessionId: string - threadId: string - params: unknown - }): CodexJournalTranslationAdmission { + function startTurn( + event: Extract + ): CodexJournalTranslationAdmission { const turnId = readCodexTurnId(event.params) if (!turnId) { return CODEX_JOURNAL_ADMITTED diff --git a/src/main/native-chat/agent-session-wire/provider-frame-disposition.test.ts b/src/main/native-chat/agent-session-wire/provider-frame-disposition.test.ts index d4726a9b602..a57aa5d9ed1 100644 --- a/src/main/native-chat/agent-session-wire/provider-frame-disposition.test.ts +++ b/src/main/native-chat/agent-session-wire/provider-frame-disposition.test.ts @@ -104,11 +104,12 @@ describe('provider frame classification catalog', () => { }) it('dispositions codex item-form frames, which the method catalog never matches', () => { - // `thread/compacted` is already chrome; its item form is the same event and - // must not leak `codex · item:contextCompaction` into the transcript. - expect(classifyProviderFrame('codex', 'item:contextCompaction', {})).toBe('status-chrome') + // Both provider generations reach the journal's compaction deduplication. + expect(classifyProviderFrame('codex', 'item:contextCompaction', {})).toBe( + 'timeline-substantive' + ) expect(classifyProviderFrame('codex', 'notification:thread/compacted', {})).toBe( - 'status-chrome' + 'timeline-substantive' ) // An item type nobody has dispositioned still falls through visibly. expect(classifyProviderFrame('codex', 'item:futureThing', {})).toBe('timeline-substantive') @@ -165,6 +166,21 @@ describe('provider frame classification catalog', () => { }) }) +describe('notice disposition boundaries', () => { + it.each(['warning', 'guardianWarning', 'deprecationNotice', 'configWarning'])( + 'retains the error-surface cap exemption for %s', + (method) => { + expect(classifyProviderFrame('codex', `notification:${method}`, {})).toBe('error-surface') + } + ) + it('does not change usage or rate-limit classifications', () => { + expect(classifyProviderFrame('codex', 'thread/tokenUsage/updated', {})).toBe('status-chrome') + expect(classifyProviderFrame('codex', 'account/rateLimits/updated', {})).toBe( + 'suppressed-benign' + ) + }) +}) + describe('codex subagent item disposition', () => { it('keeps subagent lifecycle out of the transcript now that it renders as a roster row', () => { expect( diff --git a/src/main/native-chat/agent-session-wire/provider-frame-disposition.ts b/src/main/native-chat/agent-session-wire/provider-frame-disposition.ts index 35223a1971f..d1bc7d0e7d3 100644 --- a/src/main/native-chat/agent-session-wire/provider-frame-disposition.ts +++ b/src/main/native-chat/agent-session-wire/provider-frame-disposition.ts @@ -66,7 +66,7 @@ export const PROVIDER_FRAME_CLASSIFICATIONS = { 'item/reasoning/summaryTextDelta': 'stream-into-item', 'item/reasoning/summaryPartAdded': 'stream-into-item', 'item/reasoning/textDelta': 'stream-into-item', - 'thread/compacted': 'status-chrome', + 'thread/compacted': 'timeline-substantive', 'model/rerouted': 'status-chrome', 'model/verification': 'status-chrome', 'turn/moderationMetadata': 'suppressed-benign', @@ -196,9 +196,8 @@ function hasProviderError(payload: unknown): boolean { * new item type cannot leak `codex · item:` into the transcript. The * notification catalog above is keyed by METHOD and never matches these. */ const CODEX_ITEM_CLASSIFICATIONS: Record = { - // The `thread/compacted` notification is already chrome; its item form is the - // same event and must not read as a mysterious opcode row. - contextCompaction: 'status-chrome', + // The journal coalesces this canonical completion with the legacy notification. + contextCompaction: 'timeline-substantive', // Subagent lifecycle renders as the spawn-group roster row, so its raw items // must not print a gray `codex · item:` row beside it. The live // notification path intercepts them before this catalog is reached; diff --git a/src/main/native-chat/agent-session-wire/structured-session-compaction.ts b/src/main/native-chat/agent-session-wire/structured-session-compaction.ts index 69d5bfffc14..328c283dc11 100644 --- a/src/main/native-chat/agent-session-wire/structured-session-compaction.ts +++ b/src/main/native-chat/agent-session-wire/structured-session-compaction.ts @@ -11,6 +11,13 @@ function record(value: unknown): Record { return value && typeof value === 'object' ? (value as Record) : {} } +export function isCodexCompactionComplete(method: string, params: unknown): boolean { + return ( + method === 'thread/compacted' || + (method === 'item/completed' && record(record(params).item).type === 'contextCompaction') + ) +} + /** A receipt is not completion; keep listening through the provider's terminal frame. */ export class StructuredSessionCompaction { private readonly pending = new Map() @@ -95,10 +102,7 @@ export class StructuredSessionCompaction { if (method === 'turn/started' && typeof turn.id === 'string') { pending.turnId = turn.id } - if ( - method === 'thread/compacted' || - (method === 'item/completed' && record(params.item).type === 'contextCompaction') - ) { + if (isCodexCompactionComplete(method, params)) { pending.compacted = true } if (method === 'turn/completed' && turn.id === pending.turnId) { diff --git a/src/main/native-chat/agent-session-wire/unhandled-provider-frame.test.ts b/src/main/native-chat/agent-session-wire/unhandled-provider-frame.test.ts index e389b30aba2..61544fd56ab 100644 --- a/src/main/native-chat/agent-session-wire/unhandled-provider-frame.test.ts +++ b/src/main/native-chat/agent-session-wire/unhandled-provider-frame.test.ts @@ -235,3 +235,41 @@ describe('a failed provider dependency', () => { ).toBeNull() }) }) + +describe('typed notice metadata', () => { + it('publishes readable compaction statuses for both provider forms', () => { + expect( + unhandledProviderFrameJournalItem('codex', 'notification:thread/compacted', {}) + ).toMatchObject({ + classification: 'timeline-substantive', + body: { kind: 'status', text: 'Context compacted', presentation: 'compaction' } + }) + expect(unhandledProviderFrameJournalItem('codex', 'item:contextCompaction', {})).toMatchObject({ + body: { kind: 'status', text: 'Context compacted', presentation: 'compaction' } + }) + }) + it.each([ + ['warning', { message: 'Check this' }, 'warning', 'Check this'], + ['guardianWarning', { message: 'Review required' }, 'warning', 'Review required'], + [ + 'configWarning', + { summary: 'Invalid option', details: 'Remove the option' }, + 'warning', + 'Invalid option\n\nRemove the option' + ], + [ + 'deprecationNotice', + { summary: 'Old option', details: 'Use its replacement' }, + 'notice', + 'Old option\n\nUse its replacement' + ], + ['error', { error: { message: 'Connection failed' } }, 'error', 'Connection failed'] + ])('assigns the tone and readable text for %s', (method, payload, tone, text) => { + expect( + unhandledProviderFrameJournalItem('codex', `notification:${method}`, payload) + ).toMatchObject({ + classification: 'error-surface', + body: { kind: 'status', text, tone } + }) + }) +}) diff --git a/src/main/native-chat/agent-session-wire/unhandled-provider-frame.ts b/src/main/native-chat/agent-session-wire/unhandled-provider-frame.ts index 60f34707ac9..272e960b838 100644 --- a/src/main/native-chat/agent-session-wire/unhandled-provider-frame.ts +++ b/src/main/native-chat/agent-session-wire/unhandled-provider-frame.ts @@ -90,12 +90,38 @@ export function unhandledProviderFrameJournalItem( // Why: the opcode alone ("codex · notification:warning") tells the user nothing // and reads as protocol noise. Lead with the provider's own sentence when it has // one; the raw frame stays behind the row's disclosure either way. - const message = readableProviderFrameText(payload) + const method = kind.startsWith('notification:') ? kind.slice('notification:'.length) : kind + const compaction = + provider === 'codex' && (method === 'thread/compacted' || method === 'item:contextCompaction') + const noticeTone = + provider === 'codex' + ? method === 'deprecationNotice' + ? 'notice' + : ['warning', 'guardianWarning', 'configWarning'].includes(method) + ? 'warning' + : undefined + : undefined + const tone = noticeTone ?? (classification === 'error-surface' ? 'error' : undefined) + let message = readableProviderFrameText(payload) + if ( + provider === 'codex' && + (method === 'configWarning' || method === 'deprecationNotice') && + typeof payload === 'object' && + payload !== null + ) { + const record = payload as Record + message = + [record.summary, record.details] + .filter((part): part is string => typeof part === 'string' && part.trim().length > 0) + .join('\n\n') || message + } const display = message ? boundInlineText(message, limits) : null return { body: { kind: 'status', - text: display?.text ?? `${provider} · ${kind}`, + text: compaction ? 'Context compacted' : (display?.text ?? `${provider} · ${kind}`), + ...(compaction ? { presentation: 'compaction' } : {}), + ...(tone ? { tone } : {}), providerFrame: { provider, kind, payload: bounded } }, classification: classification === 'error-surface' ? 'error-surface' : 'timeline-substantive' diff --git a/src/renderer/src/components/native-chat/NativeChatMessageRow.tsx b/src/renderer/src/components/native-chat/NativeChatMessageRow.tsx index c6d0ce6c3ac..e30b46e594d 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageRow.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageRow.tsx @@ -11,6 +11,7 @@ import { import { isSubagentGroupBlock, type NativeChatMessage } from '../../../../shared/native-chat-types' import { splitNativeChatBlocks } from './native-chat-tool-fold' import { NativeChatToolRun } from './NativeChatToolRun' +import { NativeChatNoticeRow } from './NativeChatNoticeRow' import { NativeChatMessageTimestamp } from './NativeChatMessageTimestamp' import { nativeChatProseToMarkdown } from './native-chat-prose' import { @@ -94,6 +95,24 @@ export const MessageRow = memo(function MessageRow({ return null } + const notice = isSystem + ? message.blocks.find( + (block) => + block.type === 'text' && (block.presentation !== undefined || block.tone !== undefined) + ) + : undefined + if (notice?.type === 'text') { + return ( +
+ +
+ ) + } + if (providerFrame) { return (
diff --git a/src/renderer/src/components/native-chat/NativeChatNoticeRow.test.tsx b/src/renderer/src/components/native-chat/NativeChatNoticeRow.test.tsx new file mode 100644 index 00000000000..ef4364b9cb1 --- /dev/null +++ b/src/renderer/src/components/native-chat/NativeChatNoticeRow.test.tsx @@ -0,0 +1,110 @@ +// @vitest-environment happy-dom +import '@testing-library/jest-dom/vitest' +import { cleanup, render, screen } from '@testing-library/react' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { AgentJournalItemBodySchema } from '../../../../shared/agent-session-journal-schemas' +import { projectStructuredItemsToNativeChat } from '../../../../shared/structured-agent-session-projection' +import type { AgentJournalStatusItem } from '../../../../shared/agent-session-journal-types' +import { MessageRow } from './NativeChatMessageRow' + +afterEach(cleanup) + +function renderStatus(body: AgentJournalStatusItem) { + const [message] = projectStructuredItemsToNativeChat([ + { itemId: 'notice', sequence: 1, revision: 1, observedAt: 1, body } + ]) + return render( + + ) +} + +describe('notice rows', () => { + it('renders compaction as a centered separator', () => { + renderStatus({ kind: 'status', text: 'Context compacted', presentation: 'compaction' }) + expect(screen.getByRole('separator', { name: 'Context compacted' })).toHaveClass( + 'text-muted-foreground' + ) + expect( + screen.getByText('Context compacted').parentElement?.querySelectorAll('.bg-border') + ).toHaveLength(2) + }) + it.each([ + ['warning', 'text-[color:var(--warning,#f59e0b)]'], + ['error', 'text-destructive'], + ['notice', 'text-muted-foreground'] + ])('renders %s using its existing color treatment', (tone, className) => { + renderStatus({ kind: 'status', text: 'Readable notice', tone }) + expect(screen.getByText('Readable notice').parentElement?.parentElement).toHaveClass(className) + }) + it('renders a plan as readable markdown in the card primitive', () => { + renderStatus({ + kind: 'status', + text: '# Steps\n\nA **readable** document.', + presentation: 'plan-document' + }) + expect(screen.getByText('Plan').closest('[data-slot="card"]')).toBeInTheDocument() + expect(screen.getByRole('heading', { name: 'Steps' })).toBeInTheDocument() + expect(screen.getByText('readable').tagName).toBe('STRONG') + expect(screen.getByText('readable').closest('[data-slot="card-content"]')).toHaveClass( + 'text-sm', + 'text-foreground' + ) + }) + it('shows provider notice text once while retaining its diagnostic disclosure', () => { + renderStatus({ + kind: 'status', + text: 'Check the configuration', + tone: 'warning', + providerFrame: { + provider: 'codex', + kind: 'notification:warning', + payload: { + head: '{"message":"Check the configuration"}', + byteLength: 37, + digest: 'digest', + truncated: false + } + } + }) + expect(screen.getAllByText('Check the configuration')).toHaveLength(1) + const disclosure = screen.getByText('Details').closest('details') + expect(disclosure?.querySelector('summary')).not.toHaveTextContent('Check the configuration') + expect(disclosure?.querySelector('pre')).toHaveTextContent('Check the configuration') + }) + it('renders future presentation and tone values as untinted text', () => { + renderStatus({ + kind: 'status', + text: 'Future readable text', + tone: 'future-tone', + presentation: 'future-presentation' + }) + expect(screen.getByText('Future readable text').parentElement?.parentElement).toHaveClass( + 'text-foreground' + ) + expect(screen.getByText('Future readable text').parentElement?.querySelector('svg')).toBeNull() + }) +}) + +describe('old-reader compatibility', () => { + // Derive the prior status shape without its new optional hints. + const statusSchema = AgentJournalItemBodySchema.options.find( + (schema): schema is (typeof AgentJournalItemBodySchema.options)[5] => + schema.shape.kind.value === 'status' + )! + const oldStatusSchema = statusSchema.omit({ tone: true, presentation: true }) + it.each([ + { presentation: 'compaction' }, + { presentation: 'plan-document' }, + { tone: 'warning' }, + { tone: 'error' }, + { tone: 'notice' }, + { tone: 'future-tone', presentation: 'future-presentation' } + ])('accepts new metadata and still renders text with an old reader: %j', (metadata) => { + const body = { kind: 'status', text: 'Text survives version skew', ...metadata } + expect(AgentJournalItemBodySchema.safeParse(body).success).toBe(true) + const oldBody = oldStatusSchema.parse(body) as AgentJournalStatusItem + expect(oldBody).toEqual({ kind: 'status', text: body.text }) + renderStatus(oldBody) + expect(screen.getByText(body.text)).toBeInTheDocument() + }) +}) diff --git a/src/renderer/src/components/native-chat/NativeChatNoticeRow.tsx b/src/renderer/src/components/native-chat/NativeChatNoticeRow.tsx new file mode 100644 index 00000000000..8b05f0d2d61 --- /dev/null +++ b/src/renderer/src/components/native-chat/NativeChatNoticeRow.tsx @@ -0,0 +1,86 @@ +import { AlertCircle, AlertTriangle, Info } from 'lucide-react' +import CommentMarkdown, { + type CommentMarkdownLinkClickHandler +} from '@/components/sidebar/CommentMarkdown' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { translate } from '@/i18n/i18n' +import { cn } from '@/lib/utils' +import type { NativeChatTextBlock } from '../../../../shared/native-chat-types' +import { ProviderFrameRow } from './NativeChatTranscriptChrome' + +export function NativeChatNoticeRow({ + block, + onLinkClick, + allowFileUriLinks = false +}: { + block: NativeChatTextBlock + onLinkClick?: CommentMarkdownLinkClickHandler + allowFileUriLinks?: boolean +}): React.JSX.Element { + if (block.presentation === 'compaction') { + const label = translate('components.native-chat.notices.compaction', 'Context compacted') + return ( +
+ + {label} + +
+ ) + } + if (block.presentation === 'plan-document') { + return ( + + + + {translate('components.native-chat.notices.plan', 'Plan')} + + + + + + + ) + } + const tone = block.tone + const Icon = + tone === 'warning' + ? AlertTriangle + : tone === 'error' + ? AlertCircle + : tone === 'notice' + ? Info + : null + return ( +
+
+ {Icon ?
+ {block.providerFrame ? ( + + ) : null} +
+ ) +} diff --git a/src/renderer/src/components/native-chat/NativeChatTranscriptChrome.tsx b/src/renderer/src/components/native-chat/NativeChatTranscriptChrome.tsx index 31566c5ebbb..4dea3e2d617 100644 --- a/src/renderer/src/components/native-chat/NativeChatTranscriptChrome.tsx +++ b/src/renderer/src/components/native-chat/NativeChatTranscriptChrome.tsx @@ -274,7 +274,13 @@ export function NativeChatAgentControls({ ) } -export function ProviderFrameRow({ block }: { block: NativeChatBlock }): React.JSX.Element | null { +export function ProviderFrameRow({ + block, + summary +}: { + block: NativeChatBlock + summary?: string +}): React.JSX.Element | null { if (block.type !== 'text' || !block.providerFrame) { return null } @@ -284,7 +290,7 @@ export function ProviderFrameRow({ block }: { block: NativeChatBlock }): React.J › {frame.provider} - {nativeChatProviderFrameSummary(block)} + {summary ?? nativeChatProviderFrameSummary(block)} {frame.payload.truncated ? ( ·{' '} diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index 7a205ba816b..c8f61feaab3 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -16968,6 +16968,11 @@ "empty": "No users found" }, "native-chat": { + "notices": { + "compaction": "Context compacted", + "details": "Details", + "plan": "Plan" + }, "composer": { "imageUnsupported": "Image paste is not supported for this agent.", "send": "Send", diff --git a/src/shared/agent-session-journal-schemas.test.ts b/src/shared/agent-session-journal-schemas.test.ts index 2348c4f6706..ac7dafacfd2 100644 --- a/src/shared/agent-session-journal-schemas.test.ts +++ b/src/shared/agent-session-journal-schemas.test.ts @@ -190,6 +190,34 @@ describe('forward tolerance', () => { }) }) +describe('optional notice metadata', () => { + it.each([ + {}, + { presentation: 'compaction' }, + { presentation: 'plan-document' }, + { tone: 'warning' }, + { tone: 'error' }, + { tone: 'notice' }, + { presentation: 'future-presentation', tone: 'future-tone' } + ])('admits existing status and text kinds with %j', (metadata) => { + expect( + isAdmissibleAgentJournalItemBody({ kind: 'status', text: 'Readable fallback', ...metadata }) + ).toBe(true) + expect( + isAdmissibleAgentJournalItemBody({ + kind: 'message', + role: 'system', + blocks: [{ type: 'text', text: 'Readable fallback', ...metadata }] + }) + ).toBe(true) + }) + it.each([{ tone: false }, { presentation: {} }])('rejects malformed metadata: %j', (metadata) => { + expect(isAdmissibleAgentJournalItemBody({ kind: 'status', text: 'Text', ...metadata })).toBe( + false + ) + }) +}) + describe('optional tool annotations', () => { const body = { kind: 'tool-call', name: 'shell', input: null, state: 'completed' } it('admits old rows and rows with optional annotations without a new kind', () => { diff --git a/src/shared/agent-session-journal-schemas.ts b/src/shared/agent-session-journal-schemas.ts index 7906377c057..89c2d9f7293 100644 --- a/src/shared/agent-session-journal-schemas.ts +++ b/src/shared/agent-session-journal-schemas.ts @@ -68,6 +68,8 @@ const Block = z.union([ z.object({ type: z.literal('text'), text: z.string(), + presentation: z.string().optional(), + tone: z.string().optional(), providerFrame: ProviderFrame.optional() }), // `input: undefined` loses its key under JSON.stringify, so a persisted @@ -160,6 +162,8 @@ export const AgentJournalItemBodySchema = z.discriminatedUnion('kind', [ z.object({ kind: z.literal('status'), text: z.string(), + presentation: z.string().optional(), + tone: z.string().optional(), turnLifecycle: z.object({ turnId: z.string(), state: z.string().min(1) }).optional(), providerFrame: ProviderFrame.optional() }) diff --git a/src/shared/agent-session-journal-types.ts b/src/shared/agent-session-journal-types.ts index 03950b9d242..c0074e015f5 100644 --- a/src/shared/agent-session-journal-types.ts +++ b/src/shared/agent-session-journal-types.ts @@ -148,6 +148,9 @@ export type AgentJournalQuestionItem = { export type AgentJournalStatusItem = { kind: 'status' text: string + /** Optional display hints; unknown values retain the ordinary text fallback. */ + presentation?: string + tone?: string /** Durable root-turn lifecycle used by clients to expose cancellation only * while the provider can still accept it. */ turnLifecycle?: { turnId: string; state: 'running' | 'completed' } diff --git a/src/shared/native-chat-types.ts b/src/shared/native-chat-types.ts index b07c182d750..3a018d75e6b 100644 --- a/src/shared/native-chat-types.ts +++ b/src/shared/native-chat-types.ts @@ -32,6 +32,9 @@ export type NativeChatRole = (typeof NATIVE_CHAT_ROLES)[number] export type NativeChatTextBlock = { type: 'text' text: string + /** Optional journal display hints; readers narrow only the values they know. */ + presentation?: string + tone?: string /** Optional structured detail for an otherwise ordinary fallback line. */ providerFrame?: { provider: string diff --git a/src/shared/structured-agent-session-projection.test.ts b/src/shared/structured-agent-session-projection.test.ts index f5c39feea20..9b68e4fcdd8 100644 --- a/src/shared/structured-agent-session-projection.test.ts +++ b/src/shared/structured-agent-session-projection.test.ts @@ -284,6 +284,29 @@ describe('structured agent session status projection', () => { }) }) +describe('notice projection for desktop and mobile consumers', () => { + it.each([ + { presentation: 'compaction' }, + { presentation: 'plan-document' }, + { tone: 'warning' }, + { tone: 'error' }, + { tone: 'notice' }, + { presentation: 'future-presentation', tone: 'future-tone' } + ])('preserves readable text alongside optional metadata: %j', (metadata) => { + const projected = projectStructuredItemToNativeChat( + item('notice', 1, { + kind: 'status', + text: 'A readable document or notice', + ...metadata + }) + ) + expect(projected).toMatchObject({ + role: 'system', + blocks: [{ type: 'text', text: 'A readable document or notice', ...metadata }] + }) + }) +}) + it('preserves optional tool annotations for desktop and mobile projection', () => { const metadata = { exitCode: 127, diff --git a/src/shared/structured-agent-session-projection.ts b/src/shared/structured-agent-session-projection.ts index c80afbe3437..5bb142b2949 100644 --- a/src/shared/structured-agent-session-projection.ts +++ b/src/shared/structured-agent-session-projection.ts @@ -113,6 +113,8 @@ function itemBlocks(item: AgentJournalRenderItem): { { type: 'text', text: body.text, + ...(body.presentation !== undefined ? { presentation: body.presentation } : {}), + ...(body.tone !== undefined ? { tone: body.tone } : {}), ...(body.providerFrame ? { providerFrame: body.providerFrame } : {}) } ] From 588240043e84b1f8e58013298e692e7a57ba8ad5 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 7 Sep 2026 17:14:54 -0700 Subject: [PATCH 03/10] fix(sidebar): reveal collapsed workspaces without clearing filters (#19398) --- .../src/components/sidebar/WorktreeList.tsx | 3 +- .../navigation/use-reveal-requests.test.tsx | 116 ++++++++++++------ .../navigation/use-reveal-requests.ts | 43 +++++-- 3 files changed, 115 insertions(+), 47 deletions(-) diff --git a/src/renderer/src/components/sidebar/WorktreeList.tsx b/src/renderer/src/components/sidebar/WorktreeList.tsx index 9153ff4e39e..ce6813d229f 100644 --- a/src/renderer/src/components/sidebar/WorktreeList.tsx +++ b/src/renderer/src/components/sidebar/WorktreeList.tsx @@ -236,7 +236,8 @@ const WorktreeList = React.memo(function WorktreeList({ useSidebarRevealRequests({ groupBy, renderedSidebarRowKeys: rowModel.renderedSidebarRowKeys, - renderedWorktreeIdentities: selection.renderedWorktreeIdentities, + visibleWorktrees, + visibleFolderWorkspaces: visibleScope.visibleFolderWorkspacesForRows, currentSidebarWorktreeId, currentSidebarExecutionHostId: activeWorkspaceExecutionHostId, worktreeMap, diff --git a/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.test.tsx b/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.test.tsx index 7f78d3d011a..f31b2bc9437 100644 --- a/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.test.tsx +++ b/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.test.tsx @@ -82,7 +82,8 @@ beforeEach(() => { args = { groupBy: 'repo', renderedSidebarRowKeys: new Set(), - renderedWorktreeIdentities: [], + visibleWorktrees: [], + visibleFolderWorkspaces: [], currentSidebarWorktreeId: worktree.id, currentSidebarExecutionHostId: 'ssh:dev', worktreeMap: new Map([[worktree.id, worktree]]), @@ -133,7 +134,7 @@ describe('revealing a filtered workspace', () => { args = { ...args, hasFilters: visible, - renderedWorktreeIdentities: visible ? ['ssh:dev|wt-1'] : [] + visibleWorktrees: visible ? args.worktrees : [] } await render() await act(async () => requestScrollToCurrentWorkspaceReveal()) @@ -143,6 +144,41 @@ describe('revealing a filtered workspace', () => { } ) + it.each(['ssh:dev', null] as const)( + 'reveals a collapsed workspace that passes filters with active host %s', + async (executionHostId) => { + args = { + ...args, + currentSidebarExecutionHostId: executionHostId, + visibleWorktrees: args.worktrees + } + await render() + await act(async () => requestScrollToCurrentWorkspaceReveal()) + expect(document.querySelector('[role="dialog"]')).toBeNull() + expect(args.clearFilters).not.toHaveBeenCalled() + expect(state.revealWorktreeInSidebar).toHaveBeenCalledTimes(1) + } + ) + + it('does not let a visible same-id workspace on another host bypass confirmation', async () => { + args = { ...args, visibleWorktrees: [{ ...args.worktrees[0], hostId: 'local' }] } + await render() + await act(async () => requestScrollToCurrentWorkspaceReveal()) + expect(document.querySelector('[role="dialog"]')).not.toBeNull() + expect(state.revealWorktreeInSidebar).not.toHaveBeenCalled() + await click('Keep filters') + }) + + it('preserves filters when the target becomes included while confirmation is open', async () => { + await render() + await act(async () => requestScrollToCurrentWorkspaceReveal()) + args = { ...args, visibleWorktrees: args.worktrees } + await render() + await click('Clear filters and reveal') + expect(args.clearFilters).not.toHaveBeenCalled() + expect(state.revealWorktreeInSidebar).toHaveBeenCalledTimes(1) + }) + it('does not apply a stale confirmation after switching workspaces', async () => { await render() await act(async () => requestScrollToCurrentWorkspaceReveal()) @@ -153,39 +189,47 @@ describe('revealing a filtered workspace', () => { expect(state.revealWorktreeInSidebar).not.toHaveBeenCalled() }) - it('confirms filtered folder workspaces and preserves the rename request', async () => { - args = { - ...args, - currentSidebarWorktreeId: folderWorkspaceKey('folder-1'), - currentSidebarExecutionHostId: null, - folderWorkspaces: [ - { - id: 'folder-1', - projectGroupId: 'project-1', - name: 'Notes', - folderPath: '/notes', - linkedTask: null, - comment: '', - isArchived: false, - isUnread: false, - isPinned: false, - sortOrder: 1, - lastActivityAt: 1, - createdAt: 1, - updatedAt: 1 - } - ] + it.each([true, false])( + 'reveals folder workspaces and preserves rename (filtered: %s)', + async (filtered) => { + args = { + ...args, + currentSidebarWorktreeId: folderWorkspaceKey('folder-1'), + currentSidebarExecutionHostId: null, + folderWorkspaces: [ + { + id: 'folder-1', + projectGroupId: 'project-1', + name: 'Notes', + folderPath: '/notes', + linkedTask: null, + comment: '', + isArchived: false, + isUnread: false, + isPinned: false, + sortOrder: 1, + lastActivityAt: 1, + createdAt: 1, + updatedAt: 1 + } + ] + } + args.visibleFolderWorkspaces = filtered ? [] : args.folderWorkspaces + await render() + await act(async () => requestScrollToCurrentWorkspaceRevealAndRename()) + expect(args.clearFilters).not.toHaveBeenCalled() + if (filtered) { + await click('Clear filters and reveal') + expect(args.clearFilters).toHaveBeenCalledTimes(1) + } else { + expect(document.querySelector('[role="dialog"]')).toBeNull() + } + expect(state.revealWorktreeInSidebar).toHaveBeenCalledWith(folderWorkspaceKey('folder-1'), { + behavior: 'smooth', + highlight: true, + beginRename: true, + executionHostId: undefined + }) } - await render() - await act(async () => requestScrollToCurrentWorkspaceRevealAndRename()) - expect(args.clearFilters).not.toHaveBeenCalled() - await click('Clear filters and reveal') - expect(args.clearFilters).toHaveBeenCalledTimes(1) - expect(state.revealWorktreeInSidebar).toHaveBeenCalledWith(folderWorkspaceKey('folder-1'), { - behavior: 'smooth', - highlight: true, - beginRename: true, - executionHostId: undefined - }) - }) + ) }) diff --git a/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.ts b/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.ts index 27a8ff5c3fa..33841f51b6c 100644 --- a/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.ts +++ b/src/renderer/src/components/sidebar/worktree-list/navigation/use-reveal-requests.ts @@ -10,16 +10,30 @@ import { import type { FolderWorkspace } from '../../../../../../shared/folder-workspace-types' import type { Worktree } from '../../../../../../shared/worktree/types' import type { ExecutionHostId } from '../../../../../../shared/execution-host' -import { composeWorktreeHostIdentity } from '../../../../../../shared/worktree/host-qualified-identity' +import { getWorktreeHostIdentity } from '../../../../../../shared/worktree/host-qualified-identity' +import { folderWorkspaceKey } from '../../../../../../shared/workspace-scope' import type { WorktreeGroupBy } from '../grouping/row-types' import { getKnownSidebarWorktreeById } from './folder-reveal' +function workspacePassesFilters( + worktree: Worktree, + worktrees: readonly Worktree[], + folderWorkspaces: readonly FolderWorkspace[] +): boolean { + const identity = getWorktreeHostIdentity(worktree) + return ( + worktrees.some((candidate) => getWorktreeHostIdentity(candidate) === identity) || + folderWorkspaces.some((workspace) => folderWorkspaceKey(workspace.id) === worktree.id) + ) +} + // Turns a "show me the current workspace" request into whatever the sidebar must change // first — grouping mode, active filters — before the viewport can scroll to it. export function useSidebarRevealRequests(args: { groupBy: WorktreeGroupBy renderedSidebarRowKeys: ReadonlySet - renderedWorktreeIdentities: readonly string[] + visibleWorktrees: readonly Worktree[] + visibleFolderWorkspaces: readonly FolderWorkspace[] currentSidebarWorktreeId: string | null currentSidebarExecutionHostId: ExecutionHostId | null worktreeMap: Map @@ -31,7 +45,8 @@ export function useSidebarRevealRequests(args: { const { groupBy, renderedSidebarRowKeys, - renderedWorktreeIdentities, + visibleWorktrees, + visibleFolderWorkspaces, currentSidebarWorktreeId, currentSidebarExecutionHostId, worktreeMap, @@ -106,11 +121,11 @@ export function useSidebarRevealRequests(args: { if (!activeWorktree || activeWorktree.isArchived) { return } - const currentIdentity = composeWorktreeHostIdentity( - currentSidebarExecutionHostId ?? undefined, - currentSidebarWorktreeId - ) - if (hasFilters && !renderedWorktreeIdentities.includes(currentIdentity)) { + // Collapsed groups hide rows without excluding their workspaces from the filter results. + if ( + hasFilters && + !workspacePassesFilters(activeWorktree, visibleWorktrees, visibleFolderWorkspaces) + ) { if (confirmationPending.current) { return } @@ -141,7 +156,14 @@ export function useSidebarRevealRequests(args: { ) { return } - if (latest.hasFilters && !latest.renderedWorktreeIdentities.includes(currentIdentity)) { + if ( + latest.hasFilters && + !workspacePassesFilters( + activeWorktree, + latest.visibleWorktrees, + latest.visibleFolderWorkspaces + ) + ) { latest.clearFilters() } } @@ -159,7 +181,8 @@ export function useSidebarRevealRequests(args: { currentSidebarExecutionHostId, folderWorkspaces, revealSidebarRow, - renderedWorktreeIdentities, + visibleWorktrees, + visibleFolderWorkspaces, revealWorktreeInSidebar, worktreeMap, worktrees From 3b8128df041c2625f446c9a45676a79fe4d13719 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 7 Sep 2026 17:42:23 -0700 Subject: [PATCH 04/10] fix(orchestration): index per-PTY mailbox reservation cleanup (#19390) Co-authored-by: Merge Sim --- .../orchestration/db/schema/migrate.ts | 1 + .../db/schema/schema-column-probes.ts | 7 ++ .../mailbox-pointer-release-query.test.ts | 91 +++++++++++++++++++ ...chestration-version-skew-migration.test.ts | 4 +- 4 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 src/main/runtime/orchestration/mailbox-pointer-release-query.test.ts diff --git a/src/main/runtime/orchestration/db/schema/migrate.ts b/src/main/runtime/orchestration/db/schema/migrate.ts index 9cdc9544da1..b8da910722d 100644 --- a/src/main/runtime/orchestration/db/schema/migrate.ts +++ b/src/main/runtime/orchestration/db/schema/migrate.ts @@ -30,6 +30,7 @@ export function migrate(this: OrchestrationDb): void { migrateV37.call(this, current) migrateV38.call(this, current) migrateV39.call(this, current) + this.createMailboxDeliveryIndexesIfPossible() this.db.pragma(`user_version = ${SCHEMA_VERSION}`) this.db.exec('COMMIT') } catch (err) { diff --git a/src/main/runtime/orchestration/db/schema/schema-column-probes.ts b/src/main/runtime/orchestration/db/schema/schema-column-probes.ts index fefe9421bfc..07e71e9e9d3 100644 --- a/src/main/runtime/orchestration/db/schema/schema-column-probes.ts +++ b/src/main/runtime/orchestration/db/schema/schema-column-probes.ts @@ -27,6 +27,13 @@ export function createMailboxDeliveryIndexesIfPossible(this: OrchestrationDb): v ON messages(to_handle, sequence) WHERE read = 0 AND pointer_enter_pending > 0; `) + if (this.hasColumn('messages', 'pointer_pty_id')) { + // Working-title frames release one PTY's reservations, including already-read rows. + this.db.exec(` + CREATE INDEX IF NOT EXISTS idx_messages_pending_pointer_pty + ON messages(pointer_pty_id) WHERE pointer_enter_pending > 0; + `) + } } if ( diff --git a/src/main/runtime/orchestration/mailbox-pointer-release-query.test.ts b/src/main/runtime/orchestration/mailbox-pointer-release-query.test.ts new file mode 100644 index 00000000000..90cbe1e8eec --- /dev/null +++ b/src/main/runtime/orchestration/mailbox-pointer-release-query.test.ts @@ -0,0 +1,91 @@ +import { mkdtempSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { describe, expect, it, vi } from 'vitest' +import Database from '../../sqlite/sync-database' +import { OrchestrationDb } from './db' + +function assertTargetedRelease(db: OrchestrationDb): void { + const prepare = vi.spyOn(db.db, 'prepare') + db.releasePendingMailboxPointerForPty('absent-pty') + const sql = prepare.mock.calls.find(([query]) => query.startsWith('UPDATE messages'))?.[0] + prepare.mockRestore() + expect(sql).toBeDefined() + const plan = db.db.prepare(`EXPLAIN QUERY PLAN ${sql}`).all(1, 'absent-pty') as { + detail: string + }[] + expect(plan.some(({ detail }) => detail.includes('SCAN messages'))).toBe(false) + expect(plan.some(({ detail }) => detail.includes('idx_messages_pending_pointer_pty'))).toBe(true) +} + +describe('PTY mailbox reservation cleanup', () => { + it('uses a PTY lookup and preserves reservation settlement semantics', () => { + const db = new OrchestrationDb(':memory:') + try { + const seed = db.db.prepare(`INSERT INTO messages + (id, from_handle, to_handle, subject, read, pointer_enter_pending, + pointer_pty_id, pointer_process_incarnation, delivered_at) + VALUES (?, 'sender', 'recipient', 'test', ?, ?, ?, 'incarnation', ?)`) + db.db.exec('BEGIN') + for (let i = 0; i < 1000; i++) { + seed.run(`history-${i}`, 1, 0, null, null) + } + seed.run('reserved', 0, 1, 'target', '2026-01-01 00:00:00') + seed.run('written', 0, 2, 'target', null) + seed.run('entered', 0, 3, 'target', null) + seed.run('read', 1, 2, 'target', '2026-01-01 00:00:00') + seed.run('other', 0, 1, 'other-pty', null) + db.db.exec('COMMIT') + assertTargetedRelease(db) + db.releasePendingMailboxPointerForPty('target') + const read = (id: string) => db.db.prepare('SELECT * FROM messages WHERE id = ?').get(id) + for (const id of ['reserved', 'written', 'entered', 'read']) { + expect(read(id)).toMatchObject({ + pointer_enter_pending: 0, + pointer_pty_id: null, + pointer_process_incarnation: null + }) + } + expect(read('reserved')).toMatchObject({ delivered_at: null, read: 0 }) + expect(read('written')).toMatchObject({ delivered_at: expect.any(String), read: 0 }) + expect(read('entered')).toMatchObject({ delivered_at: expect.any(String), read: 0 }) + expect(read('read')).toMatchObject({ delivered_at: '2026-01-01 00:00:00', read: 1 }) + expect(read('other')).toMatchObject({ pointer_enter_pending: 1, pointer_pty_id: 'other-pty' }) + expect(read('history-0')).toMatchObject({ read: 1, delivered_at: null }) + } finally { + db.close() + } + }) + + it.each(['current', 'before-pointer-columns'])( + 'installs the lookup on first open of an existing %s database', + (version) => { + const dir = mkdtempSync(join(tmpdir(), 'orca-pointer-release-')) + const path = join(dir, 'orchestration.db') + try { + new OrchestrationDb(path).close() + const raw = new Database(path) + try { + raw.exec('DROP INDEX idx_messages_pending_pointer_pty') + if (version === 'before-pointer-columns') { + raw.exec(`DROP INDEX idx_messages_pending_pointer_enter; + ALTER TABLE messages DROP COLUMN pointer_enter_pending; + ALTER TABLE messages DROP COLUMN pointer_pty_id; + ALTER TABLE messages DROP COLUMN pointer_process_incarnation;`) + raw.pragma('user_version = 32') + } + } finally { + raw.close() + } + const reopened = new OrchestrationDb(path) + try { + assertTargetedRelease(reopened) + } finally { + reopened.close() + } + } finally { + rmSync(dir, { recursive: true, force: true }) + } + } + ) +}) diff --git a/src/main/runtime/orchestration/orchestration-version-skew-migration.test.ts b/src/main/runtime/orchestration/orchestration-version-skew-migration.test.ts index 7a58e81920d..12ccf7303ca 100644 --- a/src/main/runtime/orchestration/orchestration-version-skew-migration.test.ts +++ b/src/main/runtime/orchestration/orchestration-version-skew-migration.test.ts @@ -431,7 +431,9 @@ describe('OrchestrationDb version-skew migration', () => { const raw = new Database(dbPath) raw.exec( - 'DROP INDEX IF EXISTS idx_messages_pending_pointer_enter; ALTER TABLE messages DROP COLUMN pointer_enter_pending;' + `DROP INDEX IF EXISTS idx_messages_pending_pointer_enter; + DROP INDEX IF EXISTS idx_messages_pending_pointer_pty; + ALTER TABLE messages DROP COLUMN pointer_enter_pending;` ) raw.pragma('user_version = 33') expect(resolveOrchestrationMigrationStartVersion(raw, 33, SCHEMA_VERSION)).toBe(6) From cfd59f2ca08e757c64f37fbeb9b578f0f126d6c7 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Mon, 7 Sep 2026 17:42:29 -0700 Subject: [PATCH 05/10] fix(deps): update desktop parser security dependencies (#19361) Co-authored-by: m4air --- pnpm-lock.yaml | 110 +++++++++++++++++++++++++------------------------ 1 file changed, 56 insertions(+), 54 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e4a40c0fe47..1868a9453c9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3513,8 +3513,8 @@ packages: '@vscode/windows-process-tree@0.8.0': resolution: {integrity: sha512-TI+h2GRwX+igD/YYJMQQVAFcsCNSg7Te2yYxQpKMzwto5RsJ8d2KKgOeur/p/6sAOQwZvopiTDOClyTHEn9MhQ==} - '@xmldom/xmldom@0.8.13': - resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} + '@xmldom/xmldom@0.8.15': + resolution: {integrity: sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==} engines: {node: '>=10.0.0'} '@xterm/addon-fit@0.12.0-beta.300': @@ -3677,8 +3677,8 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - baseline-browser-mapping@2.10.27: - resolution: {integrity: sha512-zEs/ufmZoUd7WftKpKyXaT6RFxpQ5Qm9xytKRHvJfxFV9DFJkZph9RvJ1LcOUi0Z1ZVijMte65JbILeV+8QQEA==} + baseline-browser-mapping@2.11.21: + resolution: {integrity: sha512-uh8vpY/1/YyFkunIDFH/12p7/7VdPKA1hejMVEbdkEaWnUz0Hesvx5EbiU6XxjyHZIOju+ZMbQJkRh+es3/spQ==} engines: {node: '>=6.0.0'} hasBin: true @@ -3715,8 +3715,8 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} - browserslist@4.28.2: - resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} + browserslist@4.28.9: + resolution: {integrity: sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -3779,8 +3779,8 @@ packages: resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} engines: {node: '>=6'} - caniuse-lite@1.0.30001791: - resolution: {integrity: sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ==} + caniuse-lite@1.0.30001810: + resolution: {integrity: sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==} ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -4306,8 +4306,8 @@ packages: electron-publish@26.15.3: resolution: {integrity: sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==} - electron-to-chromium@1.5.351: - resolution: {integrity: sha512-9D7Iqx8RImSvCnOsj86rCH6eQjZFQoM04Jn6HnZVM0Nu/G58/gmKYQ1d12MZTbjQbQSTGI8nwEy07ErsA2slLA==} + electron-to-chromium@1.5.422: + resolution: {integrity: sha512-UvA/32XqrLDdZSn7Jllo1AYNcWji/G0d5M0GTViE7KoGBiMunw3a34Sb2KO4ZZyrSEhqsxFoVhWWJshdyfKqJA==} electron-updater@6.8.9: resolution: {integrity: sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig==} @@ -4525,8 +4525,8 @@ packages: fast-string-width@3.0.2: resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} - fast-uri@3.1.5: - resolution: {integrity: sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==} + fast-uri@3.1.7: + resolution: {integrity: sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg==} fast-wrap-ansi@0.2.2: resolution: {integrity: sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==} @@ -5563,8 +5563,8 @@ packages: nan@2.26.2: resolution: {integrity: sha512-0tTvBTYkt3tdGw22nrAy50x7gpbGCCFH3AFcyS5WiUu7Eu4vWlri1woE6qHBSfy11vksDqkiwjOnlR7WV8G1Hw==} - nanoid@3.3.17: - resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==} + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -5600,8 +5600,9 @@ packages: node-pty@1.1.0: resolution: {integrity: sha512-20JqtutY6JPXTUnL0ij1uad7Qe1baT46lyolh2sSENDd4sTzKZ4nmAFkeAARDKwmlLjPx6XKRlwRUxwjOy+lUg==} - node-releases@2.0.38: - resolution: {integrity: sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw==} + node-releases@2.0.54: + resolution: {integrity: sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==} + engines: {node: '>=18'} nopt@9.0.0: resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==} @@ -5856,8 +5857,8 @@ packages: points-on-path@0.2.1: resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} - postcss-selector-parser@7.1.1: - resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + postcss-selector-parser@7.1.6: + resolution: {integrity: sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==} engines: {node: '>=4'} postcss@8.5.25: @@ -5977,8 +5978,8 @@ packages: engines: {node: '>=10.13.0'} hasBin: true - qs@6.15.2: - resolution: {integrity: sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==} + qs@6.16.0: + resolution: {integrity: sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==} engines: {node: '>=0.6'} queue-microtask@1.2.3: @@ -6392,8 +6393,8 @@ packages: resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} engines: {node: '>= 0.4'} - side-channel@1.1.0: - resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + side-channel@1.1.1: + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} engines: {node: '>= 0.4'} siginfo@2.0.0: @@ -6755,8 +6756,8 @@ packages: unzipper@0.12.5: resolution: {integrity: sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==} - update-browserslist-db@1.2.3: - resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + update-browserslist-db@1.3.2: + resolution: {integrity: sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -7119,7 +7120,7 @@ snapshots: dependencies: '@babel/compat-data': 7.29.7 '@babel/helper-validator-option': 7.29.7 - browserslist: 4.28.2 + browserslist: 4.28.9 lru-cache: 5.1.1 semver: 6.3.1 @@ -7739,7 +7740,7 @@ snapshots: eventsource: 3.0.7 eventsource-parser: 3.0.8 express: 5.2.1(supports-color@7.2.0) - express-rate-limit: 8.5.2(express@5.2.1(supports-color@7.2.0)) + express-rate-limit: 8.5.2(express@5.2.1) hono: 4.13.0 jose: 6.2.3 json-schema-typed: 8.0.2 @@ -7761,7 +7762,7 @@ snapshots: eventsource: 3.0.7 eventsource-parser: 3.0.8 express: 5.2.1(supports-color@7.2.0) - express-rate-limit: 8.5.2(express@5.2.1(supports-color@7.2.0)) + express-rate-limit: 8.5.2(express@5.2.1) hono: 4.13.0 jose: 6.2.3 json-schema-typed: 8.0.2 @@ -9826,7 +9827,7 @@ snapshots: node-addon-api: 7.1.0 optional: true - '@xmldom/xmldom@0.8.13': {} + '@xmldom/xmldom@0.8.15': {} '@xterm/addon-fit@0.12.0-beta.300(@xterm/xterm@6.1.0-beta.303(patch_hash=98756bcedc402bcdb7c6ab7b015d2e59cd18e97b03a2c06a27e95bb3ba429d9d))': dependencies: @@ -9882,7 +9883,7 @@ snapshots: ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 - fast-uri: 3.1.5 + fast-uri: 3.1.7 json-schema-traverse: 1.0.0 require-from-string: 2.0.2 @@ -9994,7 +9995,7 @@ snapshots: base64-js@1.5.1: {} - baseline-browser-mapping@2.10.27: {} + baseline-browser-mapping@2.11.21: {} bcrypt-pbkdf@1.0.2: dependencies: @@ -10014,7 +10015,7 @@ snapshots: http-errors: 2.0.1 iconv-lite: 0.7.2 on-finished: 2.4.1 - qs: 6.15.2 + qs: 6.16.0 raw-body: 3.0.2 type-is: 2.1.0 transitivePeerDependencies: @@ -10040,13 +10041,13 @@ snapshots: dependencies: fill-range: 7.1.1 - browserslist@4.28.2: + browserslist@4.28.9: dependencies: - baseline-browser-mapping: 2.10.27 - caniuse-lite: 1.0.30001791 - electron-to-chromium: 1.5.351 - node-releases: 2.0.38 - update-browserslist-db: 1.2.3(browserslist@4.28.2) + baseline-browser-mapping: 2.11.21 + caniuse-lite: 1.0.30001810 + electron-to-chromium: 1.5.422 + node-releases: 2.0.54 + update-browserslist-db: 1.3.2(browserslist@4.28.9) buffer-from@1.1.2: {} @@ -10119,7 +10120,7 @@ snapshots: camelcase@5.3.1: {} - caniuse-lite@1.0.30001791: {} + caniuse-lite@1.0.30001810: {} ccount@2.0.1: {} @@ -10647,7 +10648,7 @@ snapshots: transitivePeerDependencies: - supports-color - electron-to-chromium@1.5.351: {} + electron-to-chromium@1.5.422: {} electron-updater@6.8.9(supports-color@7.2.0): dependencies: @@ -10862,7 +10863,7 @@ snapshots: exponential-backoff@3.1.3: {} - express-rate-limit@8.5.2(express@5.2.1(supports-color@7.2.0)): + express-rate-limit@8.5.2(express@5.2.1): dependencies: express: 5.2.1(supports-color@7.2.0) ip-address: 10.4.0 @@ -10889,7 +10890,7 @@ snapshots: once: 1.4.0 parseurl: 1.3.3 proxy-addr: 2.0.7 - qs: 6.15.2 + qs: 6.16.0 range-parser: 1.2.1 router: 2.2.0(supports-color@7.2.0) send: 1.2.1(supports-color@7.2.0) @@ -10922,7 +10923,7 @@ snapshots: dependencies: fast-string-truncated-width: 3.0.3 - fast-uri@3.1.5: {} + fast-uri@3.1.7: {} fast-wrap-ansi@0.2.2: dependencies: @@ -12250,7 +12251,7 @@ snapshots: nan@2.26.2: optional: true - nanoid@3.3.17: {} + nanoid@3.3.18: {} negotiator@1.0.0: {} @@ -12289,7 +12290,7 @@ snapshots: dependencies: node-addon-api: 7.1.1 - node-releases@2.0.38: {} + node-releases@2.0.54: {} nopt@9.0.0: dependencies: @@ -12575,7 +12576,7 @@ snapshots: plist@3.1.0: dependencies: - '@xmldom/xmldom': 0.8.13 + '@xmldom/xmldom': 0.8.15 base64-js: 1.5.1 xmlbuilder: 15.1.1 @@ -12590,14 +12591,14 @@ snapshots: path-data-parser: 0.1.0 points-on-curve: 0.2.0 - postcss-selector-parser@7.1.1: + postcss-selector-parser@7.1.6: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 postcss@8.5.25: dependencies: - nanoid: 3.3.17 + nanoid: 3.3.18 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -12743,9 +12744,10 @@ snapshots: pngjs: 5.0.0 yargs: 15.4.1 - qs@6.15.2: + qs@6.16.0: dependencies: - side-channel: 1.1.0 + es-define-property: 1.0.1 + side-channel: 1.1.1 queue-microtask@1.2.3: {} @@ -13222,7 +13224,7 @@ snapshots: '@dotenvx/dotenvx': 1.66.0 '@modelcontextprotocol/sdk': 1.30.0(zod@3.25.76) '@types/validate-npm-package-name': 4.0.2 - browserslist: 4.28.2 + browserslist: 4.28.9 commander: 14.0.3 cosmiconfig: 9.0.1(typescript@7.0.2) dedent: 1.7.2 @@ -13236,7 +13238,7 @@ snapshots: open: 11.0.0 ora: 8.2.0 postcss: 8.5.25 - postcss-selector-parser: 7.1.1 + postcss-selector-parser: 7.1.6 prompts: 2.4.2 recast: 0.23.11 stringify-object: 5.0.0 @@ -13296,7 +13298,7 @@ snapshots: object-inspect: 1.13.4 side-channel-map: 1.0.1 - side-channel@1.1.0: + side-channel@1.1.1: dependencies: es-errors: 1.3.0 object-inspect: 1.13.4 @@ -13668,9 +13670,9 @@ snapshots: graceful-fs: 4.2.11 node-int64: 0.4.0 - update-browserslist-db@1.2.3(browserslist@4.28.2): + update-browserslist-db@1.3.2(browserslist@4.28.9): dependencies: - browserslist: 4.28.2 + browserslist: 4.28.9 escalade: 3.2.0 picocolors: 1.1.1 From 91fbc1529ad3f2914c7904a037a1a8e69790bfd3 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Mon, 7 Sep 2026 17:42:37 -0700 Subject: [PATCH 06/10] fix(deps): harden cloud HTTP and WebSocket dependencies (#19362) Co-authored-by: m4air --- cloud/apps/relay-fence-broker/package.json | 4 +- cloud/apps/relay-ops/package.json | 4 +- cloud/apps/relay/package.json | 6 +- cloud/pnpm-lock.yaml | 64 +++++++++++----------- 4 files changed, 39 insertions(+), 39 deletions(-) diff --git a/cloud/apps/relay-fence-broker/package.json b/cloud/apps/relay-fence-broker/package.json index c9bf65c2cf3..06379184f13 100644 --- a/cloud/apps/relay-fence-broker/package.json +++ b/cloud/apps/relay-fence-broker/package.json @@ -14,8 +14,8 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@hono/node-server": "^1.19.14", - "hono": "^4.12.27", + "@hono/node-server": "^1.19.17", + "hono": "^4.13.7", "zod": "^3.25.76" }, "devDependencies": { diff --git a/cloud/apps/relay-ops/package.json b/cloud/apps/relay-ops/package.json index da4f73f8672..2881a5c09f3 100644 --- a/cloud/apps/relay-ops/package.json +++ b/cloud/apps/relay-ops/package.json @@ -16,8 +16,8 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@hono/node-server": "^1.19.14", - "hono": "^4.12.27", + "@hono/node-server": "^1.19.17", + "hono": "^4.13.7", "zod": "^3.25.76" }, "devDependencies": { diff --git a/cloud/apps/relay/package.json b/cloud/apps/relay/package.json index 4c2b2e4269c..de30d66e413 100644 --- a/cloud/apps/relay/package.json +++ b/cloud/apps/relay/package.json @@ -15,13 +15,13 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@hono/node-server": "^1.19.14", + "@hono/node-server": "^1.19.17", "@orca-cloud/relay-contract": "workspace:*", - "hono": "^4.12.27", + "hono": "^4.13.7", "jose": "^6.1.3", "pg": "^8.22.0", "tweetnacl": "^1.0.3", - "ws": "^8.18.3", + "ws": "^8.21.3", "zod": "^3.25.76" }, "devDependencies": { diff --git a/cloud/pnpm-lock.yaml b/cloud/pnpm-lock.yaml index 27fdd29071a..3598ae54930 100644 --- a/cloud/pnpm-lock.yaml +++ b/cloud/pnpm-lock.yaml @@ -24,14 +24,14 @@ importers: apps/relay: dependencies: '@hono/node-server': - specifier: ^1.19.14 - version: 1.19.14(hono@4.12.27) + specifier: ^1.19.17 + version: 1.19.17(hono@4.13.7) '@orca-cloud/relay-contract': specifier: workspace:* version: link:../../packages/relay-contract hono: - specifier: ^4.12.27 - version: 4.12.27 + specifier: ^4.13.7 + version: 4.13.7 jose: specifier: ^6.1.3 version: 6.2.3 @@ -42,8 +42,8 @@ importers: specifier: ^1.0.3 version: 1.0.3 ws: - specifier: ^8.18.3 - version: 8.21.0 + specifier: ^8.21.3 + version: 8.21.3 zod: specifier: ^3.25.76 version: 3.25.76 @@ -70,11 +70,11 @@ importers: apps/relay-fence-broker: dependencies: '@hono/node-server': - specifier: ^1.19.14 - version: 1.19.14(hono@4.12.27) + specifier: ^1.19.17 + version: 1.19.17(hono@4.13.7) hono: - specifier: ^4.12.27 - version: 4.12.27 + specifier: ^4.13.7 + version: 4.13.7 zod: specifier: ^3.25.76 version: 3.25.76 @@ -95,11 +95,11 @@ importers: apps/relay-ops: dependencies: '@hono/node-server': - specifier: ^1.19.14 - version: 1.19.14(hono@4.12.27) + specifier: ^1.19.17 + version: 1.19.17(hono@4.13.7) hono: - specifier: ^4.12.27 - version: 4.12.27 + specifier: ^4.13.7 + version: 4.13.7 zod: specifier: ^3.25.76 version: 3.25.76 @@ -300,8 +300,8 @@ packages: cpu: [x64] os: [win32] - '@hono/node-server@1.19.14': - resolution: {integrity: sha512-GwtvgtXxnWsucXvbQXkRgqksiH2Qed37H9xHZocE5sA3N8O8O8/8FA3uclQXxXVzc9XBZuEOMK7+r02FmSpHtw==} + '@hono/node-server@1.19.17': + resolution: {integrity: sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==} engines: {node: '>=18.14.1'} peerDependencies: hono: ^4 @@ -507,8 +507,8 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - hono@4.12.27: - resolution: {integrity: sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==} + hono@4.13.7: + resolution: {integrity: sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ==} engines: {node: '>=16.9.0'} jose@6.2.3: @@ -587,8 +587,8 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} - nanoid@3.3.13: - resolution: {integrity: sha512-sPdqC6ByMVVGvF1ynvvMo0/o+oD1VX7DaHhijt1bFgjvBkHBib4t49GoNDhf2NDta4oeUNlaGbSt5K7qjZ955Q==} + nanoid@3.3.18: + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true @@ -640,8 +640,8 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} - postcss@8.5.15: - resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} + postcss@8.5.28: + resolution: {integrity: sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==} engines: {node: ^10 || ^12 || >=14} postgres-array@2.0.0: @@ -805,8 +805,8 @@ packages: engines: {node: '>=8'} hasBin: true - ws@8.21.0: - resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -920,9 +920,9 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true - '@hono/node-server@1.19.14(hono@4.12.27)': + '@hono/node-server@1.19.17(hono@4.13.7)': dependencies: - hono: 4.12.27 + hono: 4.13.7 '@jridgewell/sourcemap-codec@1.5.5': {} @@ -1109,7 +1109,7 @@ snapshots: fsevents@2.3.3: optional: true - hono@4.12.27: {} + hono@4.13.7: {} jose@6.2.3: {} @@ -1166,7 +1166,7 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - nanoid@3.3.13: {} + nanoid@3.3.18: {} obug@2.1.3: {} @@ -1211,9 +1211,9 @@ snapshots: picomatch@4.0.4: {} - postcss@8.5.15: + postcss@8.5.28: dependencies: - nanoid: 3.3.13 + nanoid: 3.3.18 picocolors: 1.1.1 source-map-js: 1.2.1 @@ -1288,7 +1288,7 @@ snapshots: dependencies: lightningcss: 1.32.0 picomatch: 4.0.4 - postcss: 8.5.15 + postcss: 8.5.28 rolldown: 1.0.3 tinyglobby: 0.2.17 optionalDependencies: @@ -1329,7 +1329,7 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 - ws@8.21.0: {} + ws@8.21.3: {} xtend@4.0.2: {} From 1dae024ab2c07fe48c3cbc8c969ba7956eec4652 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Mon, 7 Sep 2026 17:42:48 -0700 Subject: [PATCH 07/10] chore(mobile): patch xmldom security fixes in plist tooling (#19380) Co-authored-by: m4air --- mobile/pnpm-lock.yaml | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/mobile/pnpm-lock.yaml b/mobile/pnpm-lock.yaml index 60ccee97d2f..d44e612bb73 100644 --- a/mobile/pnpm-lock.yaml +++ b/mobile/pnpm-lock.yaml @@ -3075,12 +3075,12 @@ packages: '@vitest/utils@4.1.11': resolution: {integrity: sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==} - '@xmldom/xmldom@0.8.13': - resolution: {integrity: sha512-KRYzxepc14G/CEpEGc3Yn+JKaAeT63smlDr+vjB8jRfgTBBI9wRj/nkQEO+ucV8p8I9bfKLWp37uHgFrbntPvw==} + '@xmldom/xmldom@0.8.15': + resolution: {integrity: sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==} engines: {node: '>=10.0.0'} - '@xmldom/xmldom@0.9.10': - resolution: {integrity: sha512-A9gOqLdi6cV4ibazAjcQufGj0B1y/vDqYrcuP6d/6x8P27gRS8643Dj9o1dEKtB6O7fwxb2FgBmJS2mX7gpvdw==} + '@xmldom/xmldom@0.9.12': + resolution: {integrity: sha512-5AXjrcMClTryPe9LgZrygpB1lj7s0S9E0+W+AHaVKAVyHanafK86iPSvG5xHVSp/jC+VH1UXu0TAEmY279xH7A==} engines: {node: '>=14.6'} '@xterm/addon-unicode11@0.10.0-beta.300': @@ -9072,13 +9072,13 @@ snapshots: '@expo/plist@0.5.3': dependencies: - '@xmldom/xmldom': 0.8.13 + '@xmldom/xmldom': 0.8.15 base64-js: 1.5.1 xmlbuilder: 15.1.1 '@expo/plist@0.5.4': dependencies: - '@xmldom/xmldom': 0.8.13 + '@xmldom/xmldom': 0.8.15 base64-js: 1.5.1 xmlbuilder: 15.1.1 @@ -10574,9 +10574,9 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 - '@xmldom/xmldom@0.8.13': {} + '@xmldom/xmldom@0.8.15': {} - '@xmldom/xmldom@0.9.10': {} + '@xmldom/xmldom@0.9.12': {} '@xterm/addon-unicode11@0.10.0-beta.300(@xterm/xterm@6.1.0-beta.303)': dependencies: @@ -14451,7 +14451,7 @@ snapshots: plist@3.1.1: dependencies: - '@xmldom/xmldom': 0.9.10 + '@xmldom/xmldom': 0.9.12 base64-js: 1.5.1 xmlbuilder: 15.1.1 From a4e5106e1483288632d4195dfa70934be5a759ea Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Mon, 7 Sep 2026 17:42:55 -0700 Subject: [PATCH 08/10] chore(docs): patch brace expansion resource exhaustion fixes (#19382) Co-authored-by: m4air --- docs/site/pnpm-lock.yaml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/site/pnpm-lock.yaml b/docs/site/pnpm-lock.yaml index 4320f1bd617..9840b484b10 100644 --- a/docs/site/pnpm-lock.yaml +++ b/docs/site/pnpm-lock.yaml @@ -2321,11 +2321,11 @@ packages: bindings@1.5.0: resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} - brace-expansion@1.1.16: - resolution: {integrity: sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==} + brace-expansion@1.1.18: + resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} - brace-expansion@5.0.8: - resolution: {integrity: sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==} + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} engines: {node: 20 || >=22} braces@3.0.3: @@ -6854,12 +6854,12 @@ snapshots: dependencies: file-uri-to-path: 1.0.0 - brace-expansion@1.1.16: + brace-expansion@1.1.18: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@5.0.8: + brace-expansion@5.0.9: dependencies: balanced-match: 4.0.4 @@ -8764,11 +8764,11 @@ snapshots: minimatch@10.2.6: dependencies: - brace-expansion: 5.0.8 + brace-expansion: 5.0.9 minimatch@3.1.5: dependencies: - brace-expansion: 1.1.16 + brace-expansion: 1.1.18 minimist@1.2.8: {} From b8f6c7cabe09986597a12d19be7532845290cf75 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Mon, 7 Sep 2026 17:43:18 -0700 Subject: [PATCH 09/10] fix(deps): update react-i18next for TypeScript 7 and parser fixes (#19378) Co-authored-by: m4air --- package.json | 2 +- pnpm-lock.yaml | 30 +++++++++++------------------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/package.json b/package.json index 25152e84849..871e742255d 100644 --- a/package.json +++ b/package.json @@ -176,7 +176,7 @@ "proper-lockfile": "4.1.2", "psl": "1.15.0", "qrcode": "^1.5.4", - "react-i18next": "^17.0.8", + "react-i18next": "17.0.13", "serve-sim": "^0.1.40", "sherpa-onnx": "1.12.37", "ssh2": "^1.17.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1868a9453c9..a2d57717540 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -174,8 +174,8 @@ importers: specifier: ^1.5.4 version: 1.5.4 react-i18next: - specifier: ^17.0.8 - version: 17.0.8(i18next@26.3.1(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2) + specifier: 17.0.13 + version: 17.0.13(i18next@26.3.1(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2) serve-sim: specifier: ^0.1.40 version: 0.1.40(typescript@7.0.2) @@ -4794,8 +4794,8 @@ packages: resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} engines: {node: '>=10'} - html-parse-stringify@3.0.1: - resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + html-parse-stringify@4.0.1: + resolution: {integrity: sha512-0zHsZJrK7S3K2aucXWL6ycoYJ/iNtIcFHC/nYQgFklPtrv5LpJctIiSCroWZWeuoXvuyFdzp6KzjJQ+OT5MfFw==} html-to-image@1.11.13: resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==} @@ -6030,14 +6030,14 @@ packages: react: optional: true - react-i18next@17.0.8: - resolution: {integrity: sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==} + react-i18next@17.0.13: + resolution: {integrity: sha512-Cc1PscmblIHA1kljTqDwrcVMI21ydgmUzw0UAeQBe7pAOgfuRLfzXze4EUBQoeDiICzFIXXhHFoZxuetNg5D0Q==} peerDependencies: i18next: '>= 26.2.0' react: '>= 16.8.0' react-dom: '*' react-native: '*' - typescript: ^5 || ^6 + typescript: ^5 || ^6 || ^7 peerDependenciesMeta: react-dom: optional: true @@ -6855,10 +6855,6 @@ packages: jsdom: optional: true - void-elements@3.1.0: - resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} - engines: {node: '>=0.10.0'} - vscode-oniguruma@2.0.1: resolution: {integrity: sha512-poJU8iHIWnC3vgphJnrLZyI3YdqRlR27xzqDmpPXYzA93R4Gk8z7T6oqDzDoHjoikA2aS82crdXFkjELCdJsjQ==} @@ -11300,9 +11296,7 @@ snapshots: dependencies: lru-cache: 6.0.0 - html-parse-stringify@3.0.1: - dependencies: - void-elements: 3.1.0 + html-parse-stringify@4.0.1: {} html-to-image@1.11.13: {} @@ -11362,7 +11356,7 @@ snapshots: minimatch: 10.2.5 ora: 9.4.0 react: 19.2.8 - react-i18next: 17.0.8(i18next@26.3.1(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2) + react-i18next: 17.0.13(i18next@26.3.1(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2) yaml: 2.9.0 transitivePeerDependencies: - '@swc/helpers' @@ -12842,10 +12836,10 @@ snapshots: optionalDependencies: react: 19.2.8 - react-i18next@17.0.8(i18next@26.3.1(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2): + react-i18next@17.0.13(i18next@26.3.1(typescript@7.0.2))(react-dom@19.2.8(react@19.2.8))(react@19.2.8)(typescript@7.0.2): dependencies: '@babel/runtime': 7.29.7 - html-parse-stringify: 3.0.1 + html-parse-stringify: 4.0.1 i18next: 26.3.1(typescript@7.0.2) react: 19.2.8 use-sync-external-store: 1.6.0(react@19.2.8) @@ -13749,8 +13743,6 @@ snapshots: transitivePeerDependencies: - msw - void-elements@3.1.0: {} - vscode-oniguruma@2.0.1: {} vscode-textmate@9.3.2: {} From 102402e41e55c1bbcd59a3dcfca085920f9c7acf Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Mon, 7 Sep 2026 17:43:28 -0700 Subject: [PATCH 10/10] fix(deps): update DOMPurify sanitizer hardening (#19377) Co-authored-by: m4air --- package.json | 2 +- pnpm-lock.yaml | 16 ++++++++-------- pnpm-workspace.yaml | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 871e742255d..9d42e149ad4 100644 --- a/package.json +++ b/package.json @@ -235,7 +235,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", - "dompurify": "^3.4.13", + "dompurify": "3.4.14", "electron": "^43.4.1", "electron-builder": "^26.15.3", "electron-builder-squirrel-windows": "^26.15.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2d57717540..0a196b80a90 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -106,7 +106,7 @@ settings: excludeLinksFromLockfile: false overrides: - monaco-editor>dompurify: 3.4.13 + monaco-editor>dompurify: 3.4.14 patchedDependencies: '@vscode/windows-process-tree@0.8.0': e66202cc623996d02040c93449eb9ae353fddadf426cb53202a59ee710ee6fe7 @@ -346,8 +346,8 @@ importers: specifier: ^1.1.1 version: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.17))(@types/react@19.2.17)(react-dom@19.2.8(react@19.2.8))(react@19.2.8) dompurify: - specifier: ^3.4.13 - version: 3.4.13 + specifier: 3.4.14 + version: 3.4.14 electron: specifier: ^43.4.1 version: 43.4.1(supports-color@7.2.0) @@ -4261,8 +4261,8 @@ packages: dom-accessibility-api@0.6.3: resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} - dompurify@3.4.13: - resolution: {integrity: sha512-2vmYIoqjze2d+kakP8S/nS5shfsl587kzwEjcGlTdiksUVgFHnFCsLYDVj/JNqJVOQZGSYBTmuycv0PodwmnMQ==} + dompurify@3.4.14: + resolution: {integrity: sha512-dVoH9z+MY+C9IilgGCk3YfFqjLi3fChm2OiKJMzh6axrJ5qwxqWaZamgmHrpv22CN/KdbZJuGEGgfQoL00LTdg==} dotenv-expand@11.0.7: resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} @@ -10570,7 +10570,7 @@ snapshots: dom-accessibility-api@0.6.3: {} - dompurify@3.4.13: + dompurify@3.4.14: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -11924,7 +11924,7 @@ snapshots: d3-sankey: 0.12.3 dagre-d3-es: 7.0.14 dayjs: 1.11.23 - dompurify: 3.4.13 + dompurify: 3.4.14 es-toolkit: 1.46.1 fastdom: 1.0.12 katex: 0.16.47 @@ -12209,7 +12209,7 @@ snapshots: monaco-editor@0.55.1: dependencies: - dompurify: 3.4.13 + dompurify: 3.4.14 marked: 14.0.0 ms@2.1.3: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 97920f087c5..30fdf0f16b9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -52,7 +52,7 @@ allowBuilds: windows-native-registry: false overrides: - monaco-editor>dompurify: 3.4.13 + monaco-editor>dompurify: 3.4.14 patchedDependencies: node-pty@1.1.0: config/patches/node-pty@1.1.0.patch