Conversation
- add varg export CLI command with placeholder and rendered modes - support OTIO format (DaVinci, Nuke, Premiere via plugin) - support FCP XML format (Premiere, DaVinci, FCPX) - placeholder mode generates colored bars with prompt text for planning - rendered mode generates AI content before export - includes tree walker to extract timeline structure from JSX
📝 WalkthroughWalkthroughadds a new export cli command that dynamically loads react components and converts them to industry-standard timeline formats (otio and fcpxml). includes a tree walker that transforms render elements into timeline structures with assets, clips, transitions, and text items. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant CLI as Export CLI
participant Loader as Component Loader
participant Walker as Tree Walker
participant Exporters as Format Exporters<br/>(OTIO/FCPXML)
participant FS as File System
User->>CLI: run export command
CLI->>Loader: loadComponent(filePath)
Loader->>FS: read & parse tsx
Loader->>Loader: inject auto imports if needed
Loader-->>CLI: VargElement<render>
CLI->>Walker: walkTree(element, mode, cache)
Walker->>FS: generate placeholder assets
Walker->>Walker: traverse render tree
Walker->>Walker: extract clips, transitions, text
Walker-->>CLI: Timeline structure
alt format === "otio"
CLI->>Exporters: exportOTIO(timeline)
else format === "fcpxml"
CLI->>Exporters: exportFCPXML(timeline)
end
Exporters->>Exporters: serialize timeline
Exporters-->>CLI: format string
CLI->>FS: write to output path
FS-->>CLI: success
CLI-->>User: summary (clips, duration, path)
Estimated code review effort🎯 4 (complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 6
🤖 Fix all issues with AI agents
In `@src/cli/commands/export.tsx`:
- Around line 130-131: The code uses unsafe double assertions ("const format =
args.format as string as TimelineFormat" and similarly for mode) which bypasses
type checking; replace these with runtime validation: read args.format and
args.mode as strings, validate them against the allowed values of TimelineFormat
and ExportMode (e.g., check membership in the enum or array of permitted
strings), and if invalid either throw a descriptive error or fallback to a safe
default before proceeding; update the code paths that use format and mode (the
consts named format and mode in export.tsx) to only accept the validated/parsed
values.
In `@src/react/timeline/fcpxml.ts`:
- Line 93: The sequence element currently hardcodes format="r2" (coupling to the
first asset's generated format id), which breaks when there are no assets;
change this by introducing a dedicated sequence format id (e.g., seqFormatId)
and emit a corresponding <format id="..."> definition for that id independent of
asset formats, then set <sequence format="{seqFormatId}">; update the code that
builds formats (the block that generates asset format ids and the sequence
rendering code in fcpxml.ts) so the sequence format is always defined even when
assets array is empty and no asset-generated id like "r2" exists.
In `@src/react/timeline/index.ts`:
- Around line 34-39: The current logic silently treats any non-"otio" value as
FCPXML; add an explicit guard to validate options.format and fail fast on
invalid input: check options.format and call exportOTIO(timeline) only when it's
"otio" and exportFCPXML(timeline) only when it's "fcpxml"; for any other value
throw a clear Error (or return a rejected promise) mentioning the invalid format
so callers fail fast. Reference the variables/functions options.format,
exportOTIO, exportFCPXML, and timeline in src/react/timeline/index.ts when
implementing the check.
In `@src/react/timeline/walker.ts`:
- Around line 339-351: The speech item currently hardcodes startTime: 0 and uses
currentTime for duration, causing incorrect zero-length clips; update the code
in the speech handling block (where processMediaElement is called and
speechProps is read) to set startTime to speechProps.start ?? 0 (instead of 0)
and compute duration from available data — prefer speechProps.duration if
present, fall back to asset.duration, then finally fall back to currentTime
(e.g. duration = speechProps.duration ?? asset.duration ?? currentTime) when
pushing into audioTrack.items.
- Line 153: Replace the fragile id construction that uses Date.now() + a 4-char
random suffix with a deterministic counter like assets do: add a numeric
textCounter field to WalkerContext (initialize to 0), increment it when
producing new text ids, and use it in the id template (e.g., include
ctx.textCounter in the id string generated where id:
`text_${Date.now()}_${Math.random().toString(36).slice(2,6)}` currently appears)
so ids are unique even for rapid consecutive calls; update any code that creates
or types WalkerContext to include textCounter.
- Around line 325-338: The music/speech track items are incorrectly using
currentTime for duration and calling processMediaElement without a duration, so
asset.duration stays undefined; update the branch that handles childElement.type
=== "music" (and similarly "speech" if present) to pass a proper duration into
processMediaElement (e.g., musicProps.duration or a sensible default) and then
set the track item duration to asset.duration (falling back to
musicProps.duration or a default) instead of currentTime; adjust creation of the
musicTrack.items entry (id, assetId, startTime, duration, trimStart, trimEnd,
volume) to reference asset.duration for the duration field.
🧹 Nitpick comments (4)
src/react/timeline/index.ts (1)
17-20: add jsdoc for exporttimelinepublic api without jsdoc. keep it short and focused.
proposed patch
-export async function exportTimeline( +/** + * export a render tree to a timeline file (otio or fcpxml). + */ +export async function exportTimeline( element: VargElement<"render">, options: TimelineExportOptions, ): Promise<TimelineExportResult> {as per coding guidelines ensure all public functions and classes have jsdoc comments.
src/react/timeline/fcpxml.ts (1)
52-52: dead code: ternary always returns same valuethe
audioRoleassignment is"dialogue"in both branches of the ternary - this conditional does nothing.- const audioRole = asset.type === "audio" ? "dialogue" : "dialogue"; + const audioRole = "dialogue";src/cli/commands/export.tsx (2)
50-51: temp file cleanup could be cleanerwriting an empty string instead of deleting the file leaves orphaned empty files in the cache. also the
&&short-circuit for control flow is a bit unusual.suggested cleanup approach
} finally { - (await Bun.file(tmpFile).exists()) && (await Bun.write(tmpFile, "")); + try { + const { unlink } = await import("node:fs/promises"); + await unlink(tmpFile); + } catch {} }
66-68: same cleanup pattern repeatedthis is the same temp file cleanup issue as line 50. consider extracting a helper function for temp file management.
| const format = args.format as string as TimelineFormat; | ||
| const mode = args.mode as string as ExportMode; |
There was a problem hiding this comment.
double type assertion is a code smell
args.format as string as TimelineFormat bypasses type checking. if someone passes an invalid format string, it won't be caught until runtime (or maybe never).
consider validating the format
- const format = args.format as string as TimelineFormat;
- const mode = args.mode as string as ExportMode;
+ const format = args.format as string;
+ const mode = args.mode as string;
+ if (format !== "otio" && format !== "fcpxml") {
+ console.error("error: format must be 'otio' or 'fcpxml'");
+ process.exit(1);
+ }
+ if (mode !== "placeholders" && mode !== "rendered") {
+ console.error("error: mode must be 'placeholders' or 'rendered'");
+ process.exit(1);
+ }🤖 Prompt for AI Agents
In `@src/cli/commands/export.tsx` around lines 130 - 131, The code uses unsafe
double assertions ("const format = args.format as string as TimelineFormat" and
similarly for mode) which bypasses type checking; replace these with runtime
validation: read args.format and args.mode as strings, validate them against the
allowed values of TimelineFormat and ExportMode (e.g., check membership in the
enum or array of permitted strings), and if invalid either throw a descriptive
error or fallback to a safe default before proceeding; update the code paths
that use format and mode (the consts named format and mode in export.tsx) to
only accept the validated/parsed values.
| <library> | ||
| <event name="${escapeXml(timeline.name)}"> | ||
| <project name="${escapeXml(timeline.name)}"> | ||
| <sequence format="r2"> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check if file exists and examine context around line 93
if [ -f "src/react/timeline/fcpxml.ts" ]; then
echo "=== File found, examining context around line 93 ==="
sed -n '85,105p' "src/react/timeline/fcpxml.ts"
else
echo "=== File not found at expected path ==="
find . -name "fcpxml.ts" -type f 2>/dev/null | head -5
fiRepository: vargHQ/sdk
Length of output: 456
🏁 Script executed:
#!/bin/bash
# Search for generateAssetXml function to understand format id generation
rg -n "generateAssetXml" --type=ts -A 20 | head -100Repository: vargHQ/sdk
Length of output: 2817
🏁 Script executed:
#!/bin/bash
# Search for format id generation logic with r$ pattern
rg -n "r\$\{" --type=ts -B 2 -A 2Repository: vargHQ/sdk
Length of output: 36
🏁 Script executed:
#!/bin/bash
# check if sequence format is defined elsewhere or if there's handling for zero assets
rg -n "sequence" --type=ts -B 3 -A 3 src/react/timeline/fcpxml.tsRepository: vargHQ/sdk
Length of output: 340
🏁 Script executed:
#!/bin/bash
# check if there are tests for this that reveal intended behavior
find . -name "*.test.*" -o -name "*.spec.*" | xargs grep -l "fcpxml\|sequence" 2>/dev/null | head -5Repository: vargHQ/sdk
Length of output: 36
sequence format should not hardcode asset format reference
the sequence format id is hardcoded to r2, which happens to match the first asset's generated format id when there's at least one asset. if there are zero assets, r2 would be referenced but never defined in the xml, causing invalid output. the sequence needs its own dedicated format definition rather than coupling to the first asset's id.
🤖 Prompt for AI Agents
In `@src/react/timeline/fcpxml.ts` at line 93, The sequence element currently
hardcodes format="r2" (coupling to the first asset's generated format id), which
breaks when there are no assets; change this by introducing a dedicated sequence
format id (e.g., seqFormatId) and emit a corresponding <format id="...">
definition for that id independent of asset formats, then set <sequence
format="{seqFormatId}">; update the code that builds formats (the block that
generates asset format ids and the sequence rendering code in fcpxml.ts) so the
sequence format is always defined even when assets array is empty and no
asset-generated id like "r2" exists.
| let content: string; | ||
| if (options.format === "otio") { | ||
| content = exportOTIO(timeline); | ||
| } else { | ||
| content = exportFCPXML(timeline); | ||
| } |
There was a problem hiding this comment.
validate format before falling back to fcpxml
any non-"otio" value currently becomes fcpxml, so invalid input silently exports the wrong format. add a guard and fail fast, meow.
proposed patch
- if (options.format === "otio") {
- content = exportOTIO(timeline);
- } else {
- content = exportFCPXML(timeline);
- }
+ if (options.format === "otio") {
+ content = exportOTIO(timeline);
+ } else if (options.format === "fcpxml") {
+ content = exportFCPXML(timeline);
+ } else {
+ throw new Error(`unsupported timeline format: ${options.format}`);
+ }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let content: string; | |
| if (options.format === "otio") { | |
| content = exportOTIO(timeline); | |
| } else { | |
| content = exportFCPXML(timeline); | |
| } | |
| let content: string; | |
| if (options.format === "otio") { | |
| content = exportOTIO(timeline); | |
| } else if (options.format === "fcpxml") { | |
| content = exportFCPXML(timeline); | |
| } else { | |
| throw new Error(`unsupported timeline format: ${options.format}`); | |
| } |
🤖 Prompt for AI Agents
In `@src/react/timeline/index.ts` around lines 34 - 39, The current logic silently
treats any non-"otio" value as FCPXML; add an explicit guard to validate
options.format and fail fast on invalid input: check options.format and call
exportOTIO(timeline) only when it's "otio" and exportFCPXML(timeline) only when
it's "fcpxml"; for any other value throw a clear Error (or return a rejected
promise) mentioning the invalid format so callers fail fast. Reference the
variables/functions options.format, exportOTIO, exportFCPXML, and timeline in
src/react/timeline/index.ts when implementing the check.
| : ""; | ||
|
|
||
| return { | ||
| id: `text_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`, |
There was a problem hiding this comment.
potential id collision with rapid calls
using Date.now() + short random string could collide if multiple text elements are processed in the same millisecond. the random part is only 4 chars.
consider using a counter like assets do
- id: `text_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`,
+ id: `text_${++ctx.textCounter}`,this would require adding textCounter: number to WalkerContext.
🤖 Prompt for AI Agents
In `@src/react/timeline/walker.ts` at line 153, Replace the fragile id
construction that uses Date.now() + a 4-char random suffix with a deterministic
counter like assets do: add a numeric textCounter field to WalkerContext
(initialize to 0), increment it when producing new text ids, and use it in the
id template (e.g., include ctx.textCounter in the id string generated where id:
`text_${Date.now()}_${Math.random().toString(36).slice(2,6)}` currently appears)
so ids are unique even for rapid consecutive calls; update any code that creates
or types WalkerContext to include textCounter.
| } else if (childElement.type === "music") { | ||
| const musicProps = childElement.props as MusicProps; | ||
| const asset = await processMediaElement(ctx, childElement); | ||
| if (asset) { | ||
| musicTrack.items.push({ | ||
| id: `music_${asset.id}`, | ||
| assetId: asset.id, | ||
| startTime: musicProps.start ?? 0, | ||
| duration: currentTime, | ||
| trimStart: musicProps.cutFrom, | ||
| trimEnd: musicProps.cutTo, | ||
| volume: musicProps.volume, | ||
| }); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
fd -type f -name "walker.ts" | head -1Repository: vargHQ/sdk
Length of output: 224
🏁 Script executed:
wc -l src/react/timeline/walker.tsRepository: vargHQ/sdk
Length of output: 86
🏁 Script executed:
sed -n '300,360p' src/react/timeline/walker.tsRepository: vargHQ/sdk
Length of output: 1890
🏁 Script executed:
# also check for currentTime initialization and usage
rg "currentTime" src/react/timeline/walker.ts -B 3 -A 3Repository: vargHQ/sdk
Length of output: 1304
🏁 Script executed:
rg "MusicProps|processMediaElement" src/react/timeline/walker.ts -B 2 -A 5Repository: vargHQ/sdk
Length of output: 2173
🏁 Script executed:
# check if asset has duration property
rg "type.*Asset|interface.*Asset" src/ --include="*.ts" --include="*.tsx" | head -20Repository: vargHQ/sdk
Length of output: 495
🏁 Script executed:
# look for test files or examples showing expected usage
fd -type f \( -name "*.test.ts" -o -name "*.spec.ts" -o -name "*walker*" \) | grep -E "(test|spec|example)" | head -5Repository: vargHQ/sdk
Length of output: 224
🏁 Script executed:
# correct syntax for rg
rg "interface.*Asset|type.*Asset" src/ -t ts | head -20Repository: vargHQ/sdk
Length of output: 318
🏁 Script executed:
# look for TimelineAsset definition
rg "TimelineAsset" src/ -t ts -A 5 | head -30Repository: vargHQ/sdk
Length of output: 1530
🏁 Script executed:
# check the function signature of processMediaElement to see what defaultDuration is used for
rg "function processMediaElement" src/ -t ts -A 15Repository: vargHQ/sdk
Length of output: 935
🏁 Script executed:
# get the full TimelineAsset interface definition
sed -n '/^export interface TimelineAsset/,/^}/p' src/react/timeline/types.tsRepository: vargHQ/sdk
Length of output: 258
🏁 Script executed:
# check if TimelineAsset objects have a duration property stored
rg "duration" src/react/timeline/types.ts -B 2 -A 2Repository: vargHQ/sdk
Length of output: 670
🏁 Script executed:
# check if asset.duration is being used anywhere in timeline processing
rg "asset.duration" src/react/timeline/ -t tsRepository: vargHQ/sdk
Length of output: 263
🏁 Script executed:
# look at how music/speech is handled in clip processing for comparison
sed -n '230,270p' src/react/timeline/walker.tsRepository: vargHQ/sdk
Length of output: 966
🏁 Script executed:
# check if there are any tests or examples
fd -name "*.test.ts" -o -name "*.spec.ts" | head -5Repository: vargHQ/sdk
Length of output: 284
🏁 Script executed:
# check if asset duration is set in processMediaElement
sed -n '175,215p' src/react/timeline/walker.tsRepository: vargHQ/sdk
Length of output: 1586
🏁 Script executed:
# look at the full processMediaElement function
rg "async function processMediaElement" src/react/timeline/walker.ts -A 40Repository: vargHQ/sdk
Length of output: 1085
music/speech track items should use asset duration, not accumulated currentTime
the code at lines 325-338 sets duration: currentTime for music and speech track items. currentTime starts at 0 and only increments when processing clips, so if music/speech elements appear before any clips in the children array, they'll get zero duration. more critically, using currentTime (the accumulated timeline duration) as the track item's duration is wrong — it should use the asset's actual duration. when processMediaElement is called here without a duration parameter, asset.duration remains undefined.
compared to clip processing (which passes duration to processMediaElement), standalone music/speech elements never get their duration set. pass musicProps.duration or a sensible default to processMediaElement, and use asset.duration for the track item instead of currentTime. meow 🐱
🤖 Prompt for AI Agents
In `@src/react/timeline/walker.ts` around lines 325 - 338, The music/speech track
items are incorrectly using currentTime for duration and calling
processMediaElement without a duration, so asset.duration stays undefined;
update the branch that handles childElement.type === "music" (and similarly
"speech" if present) to pass a proper duration into processMediaElement (e.g.,
musicProps.duration or a sensible default) and then set the track item duration
to asset.duration (falling back to musicProps.duration or a default) instead of
currentTime; adjust creation of the musicTrack.items entry (id, assetId,
startTime, duration, trimStart, trimEnd, volume) to reference asset.duration for
the duration field.
| } else if (childElement.type === "speech") { | ||
| const asset = await processMediaElement(ctx, childElement); | ||
| if (asset) { | ||
| const speechProps = childElement.props as SpeechProps; | ||
| audioTrack.items.push({ | ||
| id: `speech_${asset.id}`, | ||
| assetId: asset.id, | ||
| startTime: 0, | ||
| duration: currentTime, | ||
| volume: speechProps.volume, | ||
| }); | ||
| } | ||
| } |
There was a problem hiding this comment.
speech at top level always starts at time 0
unlike music which uses musicProps.start ?? 0, speech hardcodes startTime: 0. also uses currentTime for duration which has the same zero-duration issue as music.
🤖 Prompt for AI Agents
In `@src/react/timeline/walker.ts` around lines 339 - 351, The speech item
currently hardcodes startTime: 0 and uses currentTime for duration, causing
incorrect zero-length clips; update the code in the speech handling block (where
processMediaElement is called and speechProps is read) to set startTime to
speechProps.start ?? 0 (instead of 0) and compute duration from available data —
prefer speechProps.duration if present, fall back to asset.duration, then
finally fall back to currentTime (e.g. duration = speechProps.duration ??
asset.duration ?? currentTime) when pushing into audioTrack.items.
Summary
Export JSX video compositions to NLE timeline formats for editing in Premiere, DaVinci Resolve, and Final Cut Pro.
Usage
Test Actionables
1. Basic Export Test
2. Import in DaVinci Resolve
output/test.fcpxml.cache/timeline/3. Import in Premiere Pro
4. Help Command
bun run src/cli/index.ts export --help5. Test with Existing Examples
Files Changed
src/react/timeline/- new timeline export modulesrc/cli/commands/export.tsx- CLI commandsrc/react/index.ts- exportssrc/cli/index.ts- command registration