From ac72901e42b8375ab5e604c3a6f9c0c4fb522134 Mon Sep 17 00:00:00 2001 From: Patrick Lu Date: Tue, 8 Sep 2026 13:35:11 -0700 Subject: [PATCH 01/22] test(host): construct readonly timeline fixtures without push --- src/features/editor/host/controller.test.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/features/editor/host/controller.test.ts b/src/features/editor/host/controller.test.ts index f75d73205..aa0bd9be7 100644 --- a/src/features/editor/host/controller.test.ts +++ b/src/features/editor/host/controller.test.ts @@ -1907,10 +1907,13 @@ describe('gesture authority and recovery regressions', () => { const initial = snapshot() const anchor = initial.timeline.tracks[0]!.items[0]! Object.assign(anchor, { from: 10, sourceStart: 20, sourceEnd: 140, speed: 2 }) - initial.timeline.tracks[0]!.items.push( - { ...anchor, id: 'next', from: 70, sourceStart: 0, sourceEnd: 120 }, - { ...anchor, id: 'detached', from: 130, rippleLinked: false }, - ) + Object.assign(initial.timeline.tracks[0]!, { + items: [ + ...initial.timeline.tracks[0]!.items, + { ...anchor, id: 'next', from: 70, sourceStart: 0, sourceEnd: 120 }, + { ...anchor, id: 'detached', from: 130, rippleLinked: false }, + ], + }) const batch = trimIntentBatch(initial.timeline, 'clip-1', { handle: 'start', deltaFrames: 5, @@ -1944,10 +1947,11 @@ describe('gesture authority and recovery regressions', () => { it('never interprets a new delete selection as a retry of an unknown earlier delete', async () => { const initial = snapshot() - initial.timeline.tracks[0]!.items.push({ - ...initial.timeline.tracks[0]!.items[0]!, - id: 'clip-2', - from: 60, + Object.assign(initial.timeline.tracks[0]!, { + items: [ + ...initial.timeline.tracks[0]!.items, + { ...initial.timeline.tracks[0]!.items[0]!, id: 'clip-2', from: 60 }, + ], }) const submitEdit = vi.fn(async () => { throw new Error('unknown outcome') From dfad1cf03ab889c791c176cc0b9a04a85419f530 Mon Sep 17 00:00:00 2001 From: Patrick Lu Date: Tue, 8 Sep 2026 13:41:02 -0700 Subject: [PATCH 02/22] fix(host): hide local media transcription actions --- .../components/media-card.test.tsx | 20 +++++++++++++++++++ .../media-library/components/media-card.tsx | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/features/media-library/components/media-card.test.tsx b/src/features/media-library/components/media-card.test.tsx index b6a22d1b6..261cc8160 100644 --- a/src/features/media-library/components/media-card.test.tsx +++ b/src/features/media-library/components/media-card.test.tsx @@ -3,6 +3,13 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react' import type { MouseEvent, ReactNode } from 'react' import type { MediaMetadata } from '@/types/storage' +const hostContextState = vi.hoisted(() => ({ hostMode: false })) + +vi.mock('../deps/editor', () => ({ + useEditorHostMode: () => hostContextState.hostMode, + useEditorCapability: () => true, +})) + const mediaLibraryServiceMocks = vi.hoisted(() => ({ getThumbnailBlobUrl: vi.fn(), getMediaFile: vi.fn(), @@ -365,6 +372,7 @@ describe('MediaCard', () => { beforeEach(() => { vi.clearAllMocks() vi.useRealTimers() + hostContextState.hostMode = false mediaStoreState.selectedMediaIds = [] mediaStoreState.mediaItems = [makeMedia()] mediaStoreState.importingIds = [] @@ -467,6 +475,18 @@ describe('MediaCard', () => { ) }) + it('hides local transcription actions in host mode even when transcription capability is enabled', () => { + hostContextState.hostMode = true + const { rerender } = render() + expect(screen.queryByText('Generate Transcript')).not.toBeInTheDocument() + mediaStoreState.transcriptStatus = new Map([['media-1', 'ready']]) + rerender() + expect(screen.queryByText('Refresh Transcript')).not.toBeInTheDocument() + expect(screen.queryByText('Delete Transcript')).not.toBeInTheDocument() + expect(screen.queryByTestId('transcribe-dialog')).not.toBeInTheDocument() + expect(mediaTranscriptionRunnerMocks.runMediaTranscriptionJob).not.toHaveBeenCalled() + }) + it('uses transcript wording in the media action menu', () => { const { rerender } = render() expect(screen.getByText('Generate Transcript')).toBeInTheDocument() diff --git a/src/features/media-library/components/media-card.tsx b/src/features/media-library/components/media-card.tsx index 9c60c68ad..f15865b94 100644 --- a/src/features/media-library/components/media-card.tsx +++ b/src/features/media-library/components/media-card.tsx @@ -710,7 +710,7 @@ const MediaCardInternal = memo(function MediaCardInternal({ const mediaType = getMediaType(media.mimeType) const isTranscribable = - canTranscribeCapability && (mediaType === 'video' || mediaType === 'audio') + !hostMode && canTranscribeCapability && (mediaType === 'video' || mediaType === 'audio') const canGenerateProxy = canGenerateProxyCapability && !hostMode && From f308ced62e44f64fd94e370ebb467ff83fe1324f Mon Sep 17 00:00:00 2001 From: Patrick Lu Date: Tue, 8 Sep 2026 13:57:41 -0700 Subject: [PATCH 03/22] fix(editor): preserve host focus and cancel hidden move gestures --- .../editor-workspace-shell.test.tsx | 28 ++++++++ .../components/editor-workspace-shell.tsx | 6 +- .../timeline/hooks/use-timeline-drag.ts | 12 ++++ tests/browser/layout-refresh.spec.ts | 46 ++++++++++++- tests/browser/layout-refresh.tsx | 67 +++++++++++++++++-- 5 files changed, 152 insertions(+), 7 deletions(-) diff --git a/src/features/editor/components/editor-workspace-shell.test.tsx b/src/features/editor/components/editor-workspace-shell.test.tsx index e9a8953d8..8328e10ab 100644 --- a/src/features/editor/components/editor-workspace-shell.test.tsx +++ b/src/features/editor/components/editor-workspace-shell.test.tsx @@ -109,6 +109,34 @@ describe('editor workspace columns', () => { screen.getByRole('button', { name: 'editor.refresh.canvasSettings' }), ) }) + it('closes stale settings without stealing focus from the host chat', () => { + const workspace = ( + <> +