Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
20 changes: 20 additions & 0 deletions CONTEXT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# OpenScene editing

OpenScene organizes local media into projects and lets people make cuts through an editor workbench.

## Language

**Project**:
A named collection of media assets, a cut, and related production material that can be reopened for further work.

**Media asset**:
A source video, audio recording, or still image available to use in a project.
_Avoid_: Timeline clip, file path

**Cut**:
The timed arrangement of clips, titles, and transitions that determines a project's finished video.
_Avoid_: Editor screen

**Editor workbench**:
The visual surface where a person previews and changes a cut.
_Avoid_: Project, cut
3 changes: 3 additions & 0 deletions docs/adr/0001-opencut-presentation-boundary.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Keep the OpenCut workbench separate from OpenScene project authority

OpenScene uses OpenCut components for the editor's layout and controls, with host-supplied identity, theme, and action callbacks. OpenScene owns the persisted project, shared desktop/mobile editing rules, local media access, native export, and Agent tools. We chose this boundary over embedding OpenCut's standalone browser editor because both editors would otherwise hold separate timeline and media state for one visible project, letting preview, Agent edits, and export disagree. The standalone OpenCut web route remains a browser editor with its own local session; its project data is not an OpenScene project.
22 changes: 22 additions & 0 deletions docs/opencut-integration.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,28 @@ The pinned commit is a reviewed merge on the independent `Theorvane/OpenCut`

## Runtime boundary

| Responsibility | Owner |
|---|---|
| Workbench layout, shared header, tool rail, media list, and edit-point controls | OpenCut |
| Project identity, media and timeline state, editing decisions, and desktop/mobile parity | OpenScene shared model and host adapter |
| Local project files, asset URLs, FFmpeg MP4 export, and Agent tools | OpenScene main process |
| Theme preference and save/export actions in the desktop workbench | OpenScene host, passed to OpenCut as values and callbacks |

The host adapter in `src/renderer/src/editor/` converts one live OpenScene
project into OpenCut presentation props and routes UI intent back into the
OpenScene editor controller. OpenCut's `EditorHeader` renders the same project
and theme controls in its web route and in the desktop host; it receives state
and callbacks and does not save projects or run exports itself. The web route
supplies its own browser session state and WebM action. It does not synchronize
that session with an OpenScene desktop project.

A desktop media import enters through OpenScene's local project store, appears
in OpenCut's media list, and reaches the shared placement rule through the
host callback. Selection and playback use that same project in the Program
Monitor; saving and Agent tools then read or update the local project. The
mobile editor uses the same shared editing rules while rendering its own
phone-sized interface.

OpenCut's web `/editor` route supports a local Classic-inspired workflow.
OpenScene renders its editor through OpenCut's `EditorWorkspace` component,
which now owns the visible header, panel frames, responsive workbench grid, and
Expand Down
25 changes: 14 additions & 11 deletions src/renderer/src/editor/TimelineEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useEffect, useState, type CSSProperties, type ReactElement, type ReactNode } from 'react';
import { EditorWorkspace } from '../../../../external/opencut/apps/web/src/components/editor/editor-workspace';
import { EditorHeader } from '../../../../external/opencut/apps/web/src/components/editor/editor-header';

import { AssetMetadataProbeHost } from './AssetMetadataProbeHost';
import { EDITOR_INSPECTOR_DOCK_TAB_IDS, getDefaultEditorDockTabs, getDefaultEditorLeftDockTabId } from './dockTabs';
Expand Down Expand Up @@ -217,17 +218,19 @@ export function TimelineEditor({ editor }: TimelineEditorProps): ReactElement {
className={workspaceClassName}
style={workspaceStyle}
labelledBy="timeline-editor-title"
header={<>
<strong className="oc-editor-host__brand">OpenScene</strong>
<span className="oc-editor-host__project" title={editor.project?.name ?? 'No project'}>{editor.project?.name ?? 'No project'}</span>
<span className="oc-editor-host__header-spacer" />
<select className="oc-editor-theme-picker" aria-label="Editor theme preset" value={preset} onChange={(event) => setPreset(event.currentTarget.value as ThemePresetId)}>
{THEME_PRESETS.map((item) => <option key={item.id} value={item.id}>{item.label}</option>)}
</select>
<button className="oc-editor-host__save" type="button" onClick={toggleTheme} aria-label={`Switch to ${themeMode === 'dark' ? 'light' : 'dark'} theme`} title={`Switch to ${themeMode === 'dark' ? 'light' : 'dark'} theme`}>{themeMode === 'dark' ? '☀' : '☾'}</button>
<button className="oc-editor-host__save" type="button" onClick={() => { void editor.saveTimeline(); }} disabled={editor.project === null || !editor.hasUnsavedTimeline}>Save{editor.hasUnsavedTimeline ? ' *' : ''}</button>
{!floatingExportVisible && <ExportPanel editor={editor} />}
</>}
header={<EditorHeader
brand="OpenScene"
projectName={editor.project?.name ?? 'No project'}
themeMode={themeMode}
themePreset={preset}
themeOptions={THEME_PRESETS}
onThemeModeToggle={toggleTheme}
onThemePresetChange={(next) => setPreset(next as ThemePresetId)}
actions={<>
<button className="oc-editor-host__save" type="button" onClick={() => { void editor.saveTimeline(); }} disabled={editor.project === null || !editor.hasUnsavedTimeline}>Save{editor.hasUnsavedTimeline ? ' *' : ''}</button>
{!floatingExportVisible && <ExportPanel editor={editor} />}
</>}
/>}
left={<TimelineEditorLeftDock
activeTabId={leftDockTabId}
editor={editor}
Expand Down
12 changes: 9 additions & 3 deletions tests/timelineEditorLayoutSource.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const SHORTCUT_PREFERENCE_HOOK_SOURCE_URL = new URL('../src/renderer/src/editor/
const TIMELINE_SHORTCUTS_SOURCE_URL = new URL('../src/renderer/src/editor/useTimelineShortcuts.ts', import.meta.url);
const STYLES_SOURCE_URL = new URL('../src/renderer/src/styles.css', import.meta.url);
const OPENCUT_HOST_STYLES_SOURCE_URL = new URL('../external/opencut/apps/web/src/components/editor/editor-workspace-host.css', import.meta.url);
const OPENCUT_HEADER_SOURCE_URL = new URL('../external/opencut/apps/web/src/components/editor/editor-header.tsx', import.meta.url);

const normalizedSource = (source: string): string => source.replace(/\r\n/g, '\n');

Expand Down Expand Up @@ -74,9 +75,14 @@ describe('timeline editor layout source contract', () => {
it('Given product identity, When the app shell and editor render, Then OpenScene branding belongs to the OpenCut workbench header', async () => {
const source = await readTimelineEditorSource();
const appShellSource = await readAppShellSource();

expect(source).toContain('<strong className="oc-editor-host__brand">OpenScene</strong>');
expect(source).toContain('className="oc-editor-host__project"');
const headerSource = normalizedSource(await readFile(OPENCUT_HEADER_SOURCE_URL, 'utf8'));

expect(source).toContain('<EditorHeader');
expect(source).toContain('brand="OpenScene"');
expect(source).toContain('projectName={editor.project?.name ?? \'No project\'}');
expect(source).toContain('onThemePresetChange={(next) => setPreset(next as ThemePresetId)}');
expect(headerSource).toContain('className="oc-editor-header__brand"');
expect(headerSource).toContain('className="oc-editor-header__project"');
expect(source).toContain('<h1 id="timeline-editor-title">OpenScene</h1>');
expect(source).toContain('<span className="editor-program-region__subtitle">Timeline editor</span>');
expect(appShellSource).not.toContain('product-chrome__eyebrow');
Expand Down
Loading