diff --git a/apps/desktop/src/app/chat/perf-probe.tsx b/apps/desktop/src/app/chat/perf-probe.tsx index 475925823875..56b540f8a496 100644 --- a/apps/desktop/src/app/chat/perf-probe.tsx +++ b/apps/desktop/src/app/chat/perf-probe.tsx @@ -119,15 +119,13 @@ if (typeof window !== 'undefined' && !window.__PERF_DRIVE__) { let baseline: ReturnType | null = null let activeHandle: SyntheticDriverHandle | null = null - let rightPaneBaseline: - | null - | { - activeTerminalId: null | string - cwd: string - repoStatusByCwd: ReturnType - takeover: boolean - terminals: readonly TerminalEntry[] - } = null + let rightPaneBaseline: null | { + activeTerminalId: null | string + cwd: string + repoStatusByCwd: ReturnType + takeover: boolean + terminals: readonly TerminalEntry[] + } = null const stop = () => { activeHandle = null diff --git a/apps/desktop/src/app/chat/session-tile.tsx b/apps/desktop/src/app/chat/session-tile.tsx index d279aea439e7..9c30a632f979 100644 --- a/apps/desktop/src/app/chat/session-tile.tsx +++ b/apps/desktop/src/app/chat/session-tile.tsx @@ -162,10 +162,12 @@ function TileChat({ (url: string) => addContextRefAttachment(`@url:${formatRefValue(url)}`, url), [addContextRefAttachment] ) + const onPasteClipboardImage = useCallback( (opts?: { silent?: boolean }) => pasteClipboardImage(opts), [pasteClipboardImage] ) + const onPickFiles = useCallback(() => void pickContextPaths('file'), [pickContextPaths]) const onPickFolders = useCallback(() => void pickContextPaths('folder'), [pickContextPaths]) const onPickImages = useCallback(() => void pickImages(), [pickImages]) diff --git a/apps/desktop/src/app/chat/sidebar/sessions-section.tsx b/apps/desktop/src/app/chat/sidebar/sessions-section.tsx index 95cbe9bf0daa..684d217856fd 100644 --- a/apps/desktop/src/app/chat/sidebar/sessions-section.tsx +++ b/apps/desktop/src/app/chat/sidebar/sessions-section.tsx @@ -286,7 +286,9 @@ export function SidebarSessionsSection({ (items: SessionInfo[]) => { const entries = flattenSessionsWithBranches(items) - return (dateGrouped ? groupEntriesByRecency(entries) : toSessionRows(entries)).map(row => renderListRow(row, false)) + return (dateGrouped ? groupEntriesByRecency(entries) : toSessionRows(entries)).map(row => + renderListRow(row, false) + ) }, [dateGrouped, renderListRow] ) diff --git a/apps/desktop/src/app/session/hooks/use-message-stream/index.ts b/apps/desktop/src/app/session/hooks/use-message-stream/index.ts index 22f405bf7f7a..a73852f6e923 100644 --- a/apps/desktop/src/app/session/hooks/use-message-stream/index.ts +++ b/apps/desktop/src/app/session/hooks/use-message-stream/index.ts @@ -287,6 +287,7 @@ export function useMessageStream({ // stays as the fallback. const writeCost = performance.now() - startedAt lastFlushCostRef.current = writeCost + // At most one measurement rAF may be pending: only the newest flush's // measurement matters (the guard below discards stale frames), and a // hidden renderer parks rAF callbacks — without cancellation a long @@ -295,8 +296,10 @@ export function useMessageStream({ if (measureRafRef.current !== null) { window.cancelAnimationFrame(measureRafRef.current) } + measureRafRef.current = window.requestAnimationFrame(frameStart => { measureRafRef.current = null + // A newer flush already started; its own measurement wins. if (lastFlushAtRef.current !== startedAt) { return diff --git a/apps/desktop/src/app/session/hooks/use-session-actions/resume-structural-parts.test.ts b/apps/desktop/src/app/session/hooks/use-session-actions/resume-structural-parts.test.ts index b2c8a8e7976f..2dff424a9f0a 100644 --- a/apps/desktop/src/app/session/hooks/use-session-actions/resume-structural-parts.test.ts +++ b/apps/desktop/src/app/session/hooks/use-session-actions/resume-structural-parts.test.ts @@ -103,9 +103,9 @@ describe('reconcileResumeMessages — structural parts on a mid-turn switch', () expect(assistant.parts.some(part => part.type === 'reasoning')).toBe(true) expect(assistant.parts.some(part => part.type === 'tool-call')).toBe(true) - expect(assistant.parts.filter(part => part.type === 'text').map(part => ('text' in part ? part.text : ''))).toEqual([ - 'partial' - ]) + expect(assistant.parts.filter(part => part.type === 'text').map(part => ('text' in part ? part.text : ''))).toEqual( + ['partial'] + ) }) it('does not graft historical structure onto a live text-only row after compression rewrote ordinals', () => { diff --git a/apps/desktop/src/app/session/hooks/use-session-actions/utils.test.ts b/apps/desktop/src/app/session/hooks/use-session-actions/utils.test.ts index 0c430330507f..ed61eda70de0 100644 --- a/apps/desktop/src/app/session/hooks/use-session-actions/utils.test.ts +++ b/apps/desktop/src/app/session/hooks/use-session-actions/utils.test.ts @@ -733,6 +733,7 @@ describe('preserveLocalPendingTurnMessages', () => { 'does not duplicate the optimistic %s turn when the persisted turn carries its directive', kind => { const ref = `@${kind}:X` + const previous = [ msg('1-user', 'user', 'first'), msg('2-assistant', 'assistant', 'first answer'), @@ -771,6 +772,7 @@ describe('preserveLocalPendingTurnMessages', () => { it('does not duplicate a turn with multiple CRLF directives and Unicode payloads', () => { const refs = ['@file:`資料/über notes.md`', '@url:`https://example.com/café?q=✓`'] + const previous = [ msg('1-user', 'user', 'first'), msg('2-assistant', 'assistant', 'first answer'), @@ -1153,9 +1155,9 @@ describe('appendLiveSessionProjection', () => { expect(assistants[0].parts.some(part => part.type === 'reasoning')).toBe(true) expect(assistants[0].parts.some(part => part.type === 'tool-call')).toBe(true) // Answer text stays the structured row's text, not the dump. - expect(assistants[0].parts.filter(part => part.type === 'text').map(part => ('text' in part ? part.text : ''))).toEqual([ - 'partial' - ]) + expect( + assistants[0].parts.filter(part => part.type === 'text').map(part => ('text' in part ? part.text : '')) + ).toEqual(['partial']) }) it('still projects inflight when only a completed historical tool reply has structure', () => { diff --git a/apps/desktop/src/components/pet/floating-pet.tsx b/apps/desktop/src/components/pet/floating-pet.tsx index 3e800e8ff1e7..62acd28b4e94 100644 --- a/apps/desktop/src/components/pet/floating-pet.tsx +++ b/apps/desktop/src/components/pet/floating-pet.tsx @@ -172,6 +172,7 @@ export function FloatingPet() { if (hasPetSpriteForMeta(current, meta)) { const merged = mergePetInfoMeta(current, meta) + if (merged !== current) { setPetInfo(merged) } @@ -214,11 +215,14 @@ export function FloatingPet() { // so no timer. Legacy backend: the historical poll. const timer = changeEventsAvailable ? null - : window.setInterval(() => { - if (document.visibilityState === 'visible') { - void pull() - } - }, active ? PET_ACTIVE_REFRESH_MS : PET_POLL_MS) + : window.setInterval( + () => { + if (document.visibilityState === 'visible') { + void pull() + } + }, + active ? PET_ACTIVE_REFRESH_MS : PET_POLL_MS + ) return () => { cancelled = true diff --git a/apps/desktop/src/components/ui/status-pulse.tsx b/apps/desktop/src/components/ui/status-pulse.tsx index 1b2186d70ad1..e162263564d7 100644 --- a/apps/desktop/src/components/ui/status-pulse.tsx +++ b/apps/desktop/src/components/ui/status-pulse.tsx @@ -34,6 +34,7 @@ const beat = () => { for (const subscriber of pulseSubscribers) { subscriber.play() } + sharedTimer = window.setTimeout(beat, PULSE_PERIOD_MS) } @@ -46,6 +47,7 @@ const handleSharedPauseChange = () => { for (const subscriber of pulseSubscribers) { subscriber.cancel() } + return } diff --git a/apps/desktop/src/debug/right-pane-events.ts b/apps/desktop/src/debug/right-pane-events.ts index 9d76f3475f57..39a86906cdf6 100644 --- a/apps/desktop/src/debug/right-pane-events.ts +++ b/apps/desktop/src/debug/right-pane-events.ts @@ -1,9 +1,5 @@ export type RightPanePerfEvent = - | 'project-tree-render' - | 'project-tree-row-render' - | 'terminal-fit-active' - | 'terminal-fit-hidden' - | 'terminal-measure' + 'project-tree-render' | 'project-tree-row-render' | 'terminal-fit-active' | 'terminal-fit-hidden' | 'terminal-measure' export interface RightPanePerfSnapshot { counts: Record diff --git a/apps/desktop/src/debug/right-pane-probe.ts b/apps/desktop/src/debug/right-pane-probe.ts index d9e660078b84..6204c2ec7c5e 100644 --- a/apps/desktop/src/debug/right-pane-probe.ts +++ b/apps/desktop/src/debug/right-pane-probe.ts @@ -42,9 +42,7 @@ if (typeof window !== 'undefined' && !window.__RIGHT_PANE_PERF__) { }, snapshot: (): RightPanePerfSnapshot => ({ counts: { ...counts }, - details: Object.fromEntries( - Object.entries(details).map(([event, eventDetails]) => [event, { ...eventDetails }]) - ), + details: Object.fromEntries(Object.entries(details).map(([event, eventDetails]) => [event, { ...eventDetails }])), rows: { ...rows } }), start: () => { diff --git a/apps/desktop/src/lib/inflight-turn-journal.test.ts b/apps/desktop/src/lib/inflight-turn-journal.test.ts index 74338585fa61..573d5f82276e 100644 --- a/apps/desktop/src/lib/inflight-turn-journal.test.ts +++ b/apps/desktop/src/lib/inflight-turn-journal.test.ts @@ -210,10 +210,7 @@ describe('recoverInFlightTurnJournal', () => { }) it('keeps journal answer text when a longer flat dump is not a strict extension (#76444)', () => { - journalEntry([ - user('u1', 'do the thing'), - assistantWithTool('assistant-stream-old', 'partial', { pending: true }) - ]) + journalEntry([user('u1', 'do the thing'), assistantWithTool('assistant-stream-old', 'partial', { pending: true })]) const base = [ user('db-u1', 'do the thing'), diff --git a/apps/desktop/src/store/pet-gallery.test.ts b/apps/desktop/src/store/pet-gallery.test.ts index 407a3bdd4c98..5a5f494a8a25 100644 --- a/apps/desktop/src/store/pet-gallery.test.ts +++ b/apps/desktop/src/store/pet-gallery.test.ts @@ -56,6 +56,7 @@ describe('pet gallery pet.info sync', () => { throw new Error(`unexpected method: ${method}`) }) + const request = requestMock as unknown as GatewayRequest await loadPetGallery(request) @@ -113,6 +114,7 @@ describe('pet gallery pet.info sync', () => { throw new Error(`unexpected method: ${method}`) }) + const request = requestMock as unknown as GatewayRequest await loadPetGallery(request) @@ -147,6 +149,7 @@ describe('pet gallery pet.info sync', () => { throw new Error(`unexpected method: ${method}`) }) + const request = requestMock as unknown as GatewayRequest await loadPetGallery(request) @@ -195,6 +198,7 @@ describe('pet gallery pet.info sync', () => { throw new Error(`unexpected method: ${method}`) }) + const request = requestMock as unknown as GatewayRequest await expect(adoptPet(request, 'boba', 'Could not adopt pet.')).resolves.toBe(true) diff --git a/apps/desktop/src/store/pet-gallery.ts b/apps/desktop/src/store/pet-gallery.ts index b44e22bd3ecb..60322bcc1fd1 100644 --- a/apps/desktop/src/store/pet-gallery.ts +++ b/apps/desktop/src/store/pet-gallery.ts @@ -147,7 +147,6 @@ export function loadPetGallery(request: GatewayRequest, options: { force?: boole $petGalleryError.set(null) localOk = true } - } catch (e) { if (isMissingMethod(e)) { $petGalleryStatus.set('stale') @@ -216,6 +215,7 @@ async function syncInfo(request: GatewayRequest): Promise { if (hasPetSpriteForMeta(current, meta)) { const merged = mergePetInfoMeta(current, meta) + if (merged !== current) { setPetInfo(merged) } diff --git a/apps/desktop/src/store/pet.test.ts b/apps/desktop/src/store/pet.test.ts index 9d7f83add13a..c3fa2962f39b 100644 --- a/apps/desktop/src/store/pet.test.ts +++ b/apps/desktop/src/store/pet.test.ts @@ -88,6 +88,7 @@ describe('pet info metadata cache helpers', () => { spritesheetBase64: 'large-sprite-payload', spritesheetRevision: '100:2048' } + const meta = { enabled: true, slug: 'boba', @@ -116,6 +117,7 @@ describe('pet info metadata cache helpers', () => { spritesheetBase64: 'large-sprite-payload', spritesheetRevision: '100:2048' } + const meta = { enabled: true, slug: 'boba', diff --git a/ui-tui/src/__tests__/scrollBoxRendererBounds.test.ts b/ui-tui/src/__tests__/scrollBoxRendererBounds.test.ts index 25ed017e48e2..5a413400cbac 100644 --- a/ui-tui/src/__tests__/scrollBoxRendererBounds.test.ts +++ b/ui-tui/src/__tests__/scrollBoxRendererBounds.test.ts @@ -383,8 +383,7 @@ describe('ScrollBox renderer bounds', () => { expect(nestedTextWrapper?.yogaNode).toBeDefined() const nestedText = nestedTextWrapper!.childNodes.find(child => child.nodeName === 'ink-text') as - | DOMElement - | undefined + DOMElement | undefined const nestedTextChild = nestedText?.childNodes[0] diff --git a/ui-tui/src/app/overlayStore.ts b/ui-tui/src/app/overlayStore.ts index 056b06d4cf51..92d52656545d 100644 --- a/ui-tui/src/app/overlayStore.ts +++ b/ui-tui/src/app/overlayStore.ts @@ -120,11 +120,11 @@ export const $isBlocked = computed( export const hasFloatingPanel = (overlay: OverlayState): boolean => Boolean( overlay.modelPicker || - overlay.pager || - overlay.petPicker || - overlay.pluginsHub || - overlay.sessions || - overlay.skillsHub + overlay.pager || + overlay.petPicker || + overlay.pluginsHub || + overlay.sessions || + overlay.skillsHub ) export const $isStatusRuleOccluded = computed([$overlayState, $uiState], (overlay, ui) =>