Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
c022a15
fix(timeline): tighten core editing interactions
lightninglu10 Aug 26, 2026
10f7758
fix(playback): harden preview and direct export
lightninglu10 Aug 26, 2026
3058b32
refactor(timeline): simplify click seeking
lightninglu10 Aug 26, 2026
86bb526
fix(timeline): clear committed hover previews
lightninglu10 Aug 27, 2026
4f64f8e
fix(timeline): ignore clicks during voiceover takes
lightninglu10 Aug 27, 2026
c85bf58
fix(timeline): source drilled export mixer from active composition
lightninglu10 Aug 27, 2026
f5adb69
Fix cross-sequence linked clipboard track mapping
lightninglu10 Aug 27, 2026
1e3535b
fix export render cancellation ownership
lightninglu10 Aug 27, 2026
9bd1f37
fix(preview): invalidate stale source presentations
lightninglu10 Aug 27, 2026
b011124
test direct export render lifecycle
lightninglu10 Aug 27, 2026
8e882eb
Complete atomic clipboard lane recovery
lightninglu10 Aug 27, 2026
7402738
test(preview): guard deferred source replacement work
lightninglu10 Aug 27, 2026
fadb8a6
fix(editor): close hardening review gaps
lightninglu10 Aug 27, 2026
7837f21
fix(editor): fence export and preview ownership races
lightninglu10 Aug 27, 2026
5cf81b4
fix(preview): retire invalidated source canvases
lightninglu10 Aug 27, 2026
b987c81
fix(preview): scope source binding generations
lightninglu10 Aug 27, 2026
a92c12f
fix(editor): repair timeline seek and clip boundary playback
lightninglu10 Aug 27, 2026
1f8df90
fix(timeline): invalidate stale hover previews
lightninglu10 Aug 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"routes": "tsr generate",
"test": "vp test",
"test:run": "vp test run",
"test:editor-hardening": "vp test run src/features/timeline/components/timeline-content.test.tsx src/features/timeline/components/timeline-item/use-timeline-item-pointer-handlers.test.tsx src/features/timeline/hooks/shortcuts/use-clipboard-shortcuts.test.tsx src/features/timeline/hooks/shortcuts/use-playback-shortcuts.test.tsx src/features/timeline/stores/export-snapshot.test.ts src/features/export/components/export-dialog.test.tsx src/features/export/hooks/client-render-source.test.ts src/features/export/hooks/use-client-render.test.tsx src/features/preview/workers/consume-video-samples.test.ts src/features/preview/utils/media-resolver.test.ts src/features/preview/hooks/use-preview-media-resolution.test.tsx src/features/preview/components/source-composition.generation.test.tsx src/features/preview/components/video-preview.sync.test.tsx src/infrastructure/browser/blob-url-manager.test.ts",
"test:preview-sync": "vp test run src/features/preview/components/video-preview.sync.test.tsx",
"test:preview-sync:stress": "node scripts/preview-sync-stress.mjs --runs 20",
"test:coverage": "vp test run --coverage",
Expand Down
81 changes: 81 additions & 0 deletions src/features/export/components/export-dialog.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,69 @@ const mockDownloadVideo = vi.fn()
const mockResetState = vi.fn()
const mockGetSupportedCodecs = vi.fn<(...args: unknown[]) => Promise<ClientCodec[]>>()

const { mainSequence, selectedSequence, mockGetExportableSequence } = vi.hoisted(() => {
const sequence = (id: string | null, name: string, itemId: string) => {
const trackId = `track-${itemId}`
const item = {
id: itemId,
trackId,
type: 'text' as const,
from: 0,
durationInFrames: 30,
label: name,
text: name,
color: '#ffffff',
}
return {
id,
name,
tracks: [
{
id: trackId,
name: 'V1',
kind: 'video' as const,
height: 60,
locked: false,
visible: true,
muted: false,
solo: false,
order: 0,
items: [item],
},
],
items: [item],
transitions: [],
keyframes: [],
fps: 30,
width: 1920,
height: 1080,
backgroundColor: '#000000',
masterBusDb: 0,
durationFrames: 30,
inPoint: null,
outPoint: null,
markers: [],
}
}

const main = sequence(null, 'Main Timeline', 'main-title')
const selected = sequence('agent-cut', 'Agent Cut', 'agent-title')
return {
mainSequence: main,
selectedSequence: selected,
mockGetExportableSequence: vi.fn((id: string | null) => (id === selected.id ? selected : main)),
}
})

vi.mock('@/features/export/deps/timeline-compositions', () => ({
getActiveExportSequenceId: () => null,
getExportableSequence: mockGetExportableSequence,
listExportableSequences: () => [
{ id: null, name: mainSequence.name },
{ id: selectedSequence.id, name: selectedSequence.name },
],
}))

vi.mock('../hooks/use-client-render', () => ({
useClientRender: () => ({
isExporting: false,
Expand Down Expand Up @@ -115,4 +178,22 @@ describe('ExportDialog', () => {
const h265Option = await screen.findByRole('option', { name: /H\.265/i })
expect(h265Option).toHaveAttribute('data-disabled')
})

it('passes the selected sequence snapshot to direct export', async () => {
mockGetSupportedCodecs.mockResolvedValue(['avc'])
mockStartExport.mockResolvedValue(undefined)

render(<ExportDialog open onClose={() => {}} />)

fireEvent.keyDown(screen.getByLabelText('Sequence'), { key: 'ArrowDown' })
fireEvent.click(await screen.findByRole('option', { name: selectedSequence.name }))

const exportButton = screen.getByRole('button', { name: 'Export Video' })
await waitFor(() => expect(exportButton).not.toBeDisabled())
fireEvent.click(exportButton)

await waitFor(() => {
expect(mockStartExport).toHaveBeenCalledWith(expect.any(Object), selectedSequence)
})
})
})
9 changes: 4 additions & 5 deletions src/features/export/components/export-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -394,8 +394,7 @@ export function ExportDialog({ open, onClose, onOpenRenderQueue }: ExportDialogP
const reversedClipIds = new Set(
items
.filter(
(item) =>
(item.type === 'video' || item.type === 'audio') && item.isReversed === true,
(item) => (item.type === 'video' || item.type === 'audio') && item.isReversed === true,
)
.map((item) => item.id),
)
Expand Down Expand Up @@ -643,8 +642,9 @@ export function ExportDialog({ open, onClose, onOpenRenderQueue }: ExportDialogP

// Start export
const handleStartExport = async () => {
const seq = captureSelection()
setView('progress')
await startExport(buildExtendedSettings())
await startExport(buildExtendedSettings(), seq)
}

// The active render range for a sequence (whole timeline unless in/out set).
Expand Down Expand Up @@ -1574,8 +1574,7 @@ export function ExportDialog({ open, onClose, onOpenRenderQueue }: ExportDialogP
</div>
<div className="flex items-center justify-between text-sm gap-2">
<span className="text-muted-foreground truncate">
{status === 'preparing' &&
(progressMessage ?? t('export.progress.preparing'))}
{status === 'preparing' && (progressMessage ?? t('export.progress.preparing'))}
{status === 'rendering' && t('export.progress.rendering')}
{status === 'encoding' && t('export.progress.encoding')}
{status === 'finalizing' && t('export.progress.finalizing')}
Expand Down
51 changes: 51 additions & 0 deletions src/features/export/hooks/client-render-source.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// @vitest-environment node

import { describe, expect, it } from 'vite-plus/test'
import type { ExportableSequence } from '@/features/export/deps/timeline-compositions'
import { resolveClientRenderSource } from './client-render-source'

function makeSequence(overrides: Partial<ExportableSequence> = {}): ExportableSequence {
return {
id: 'selected',
name: 'Selected',
tracks: [],
items: [],
transitions: [],
keyframes: [],
fps: 24,
width: 1280,
height: 720,
masterBusDb: -3,
durationFrames: 0,
inPoint: null,
outPoint: null,
markers: [],
...overrides,
}
}

describe('resolveClientRenderSource', () => {
it('preserves an explicitly unset selected-sequence range and EQ', () => {
const sequence = makeSequence({ busAudioEq: undefined, backgroundColor: undefined })
const result = resolveClientRenderSource(
sequence,
makeSequence({ id: null, inPoint: 30, outPoint: 90 }),
{
busAudioEq: { enabled: true, lowGainDb: 4, midGainDb: 2, highGainDb: 3 },
masterBusDb: 6,
},
{ width: 1920, height: 1080, backgroundColor: '#ff0000' },
)

expect(result).toMatchObject({
fps: 24,
inPoint: null,
outPoint: null,
busAudioEq: undefined,
masterBusDb: -3,
backgroundColor: undefined,
width: 1280,
height: 720,
})
})
})
43 changes: 43 additions & 0 deletions src/features/export/hooks/client-render-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { ExportableSequence } from '@/features/export/deps/timeline-compositions'
import { DEFAULT_PROJECT_HEIGHT, DEFAULT_PROJECT_WIDTH } from '@/shared/projects/defaults'

type TimelineRenderSource = Pick<
ExportableSequence,
'tracks' | 'items' | 'transitions' | 'fps' | 'inPoint' | 'outPoint' | 'keyframes'
>

type PlaybackRenderSource = Pick<ExportableSequence, 'busAudioEq' | 'masterBusDb'>

interface ProjectRenderMetadata {
width?: number
height?: number
backgroundColor?: string
}

/**
* Select one complete render source. Once a sequence snapshot is supplied,
* its nullable/optional values are authoritative too: an unset range or EQ
* must not inherit state from whichever timeline happens to be active.
*/
export function resolveClientRenderSource(
sequence: ExportableSequence | undefined,
timeline: TimelineRenderSource,
playback: PlaybackRenderSource,
projectMetadata: ProjectRenderMetadata | undefined,
) {
const source = sequence ?? timeline
return {
tracks: source.tracks,
items: source.items,
transitions: source.transitions,
fps: source.fps,
inPoint: source.inPoint,
outPoint: source.outPoint,
keyframes: source.keyframes,
busAudioEq: sequence ? sequence.busAudioEq : playback.busAudioEq,
masterBusDb: sequence ? sequence.masterBusDb : playback.masterBusDb,
backgroundColor: sequence ? sequence.backgroundColor : projectMetadata?.backgroundColor,
width: sequence?.width ?? projectMetadata?.width ?? DEFAULT_PROJECT_WIDTH,
height: sequence?.height ?? projectMetadata?.height ?? DEFAULT_PROJECT_HEIGHT,
}
}
Loading
Loading