Skip to content

Latest commit

 

History

History
167 lines (132 loc) · 39.9 KB

File metadata and controls

167 lines (132 loc) · 39.9 KB

OpenScene Design System

OpenScene is a compact local studio command desk for arranging recordings, imported media, generated local assets, and narration in one persistent desktop shell. The renderer follows a restrained desktop-tool design language: flat neutral surfaces, hairline borders, a strong/weak/weaker text hierarchy, one interactive accent, soft single-source elevation, and dense tactile controls. The warm command desk survives as the default light preset (daylight-glass); the neutral zinc foundation is the default dark preset (dark-zinc). It must not read as a cloud video generator, generic SaaS dashboard, provider console, or marketing page.

Core Principles

  • Navigation follows the stage flow Projects → workspace. Projects is the initial page; the workspace requires an active project. There is no menu page: the workspace tab strip switches between Editing, Voice Generation, and Video Generation.
  • Keep every renderer claim local. Projects, recordings, imports, generated results, and timeline edits stay on the user's machine unless a future reviewed provider operation explicitly says otherwise.
  • Ollama is the only local engine, and it serves the Edit Agent. Media generation (voice and video) runs exclusively against connected cloud provider APIs — there is no local TTS or local video runner, and no voice-profile capture. Never present a local media model, a bundled runtime, or a local runner path as available.
  • Projects are folder-backed behind a single folder-plus picker. One directory-only dialog (openDirectory + createDirectory) drives the whole flow: a picked folder that holds a readable project.json re-attaches, any folder without a project.json is initialized as a new project named after the folder (its existing files are untouched — only project.json is written), and a folder whose project file cannot be read is rejected without overwriting it, with the failure shown as a status line on the Projects page. Selecting a folder therefore always routes to the Menu on success. The project folder holds project.json and the assets/ copies. A private userData registry maps project ids to their folders, the renderer only ever sees folder names (never full paths), directory confinement and symlink checks anchor on each project folder, and deleting an externally stored project only removes it from the list — the user's real folder is never deleted recursively. Legacy internal projects keep working unchanged.
  • Never expose raw local filesystem paths in renderer UI for imported timeline assets. Show names, durations, media kind, status, and secure playback URLs only when playback needs them.
  • Use dense but readable panels with mono section kickers, strong desk labels, clear control copy, and compact metadata.
  • Do not present cloud generation, provider integrations, unsupported formats, bundled model setup, or frame-perfect mastering guarantees as implemented until the code supports them.

Layout Contract

  • product-chrome is a compact top bar with open-project tabs and icon-only Projects and Settings buttons. The tabs replace the page label and the single project pill: several projects can be open, a click switches, and a close control appears on hover, on the active tab, and on keyboard focus. They follow opencode's titlebar tabs: no borders, a recessed fill on the active tab only, dim titles that brighten on it, and a hairline divider between adjacent inactive tabs that disappears beside the active one. The strip sits on a faint surface of its own, and only the tab element carries the selected fill: the body inside it is role="tab" too, so the shared tab styling would otherwise paint a second fill around the label and shout the project name in uppercase — a project name is user text and is shown as typed. The tab body is inert under the pointer — no background or colour changes on the selection target — so nothing repaints while crossing the strip. The close control is always visible rather than revealed, and keeps its own hover feedback. A tab remembers a project, not a separate editor session — the editor holds one in-memory timeline — so switching or closing persists unsaved work first rather than dropping it, and a project removed from the list is pruned from the tabs. The window title, About panel, and app.setName all read one APP_NAME constant so they cannot drift; macOS titles the application menu from the running bundle, so a dev run still reads Electron while a packaged build reads OpenScene. The icon lives in resources/ as SVG plus rendered PNGs. It carries no Local indicator: everything the app does is local, so the tag stated the default and only spent chrome width. It reads as the desk toolbar, not the product billboard; the Edit workspace keeps its own branding visually hidden for accessibility only. Projects and Settings stay reachable at all times.
  • product-chrome is also the window titlebar: the native macOS titlebar is hidden (hiddenInset, repositioned traffic lights) and the bar is a full-width -webkit-app-region: drag surface at the very top of the shell, above the workspace/chat body. Interactive clusters opt out with no-drag, macOS reserves left padding for the traffic lights via the root data-platform flag, and the bar sits outside the agent workspace lock but goes inert while an agent turn runs so navigation stays locked. Other platforms keep their native frame.
  • The right AgentChatPanel is the only user-facing Edit Agent UI. It is the rightmost flex child beside the mounted workspace stack and owns Edit Agent model/connection status, the project scope chip, conversation and tool stream, approval queue, reset/status, and prompt controls. The sidebar is collapsible: a header control collapses it to a slim labeled rail with an expand button, the collapsed state persists with the chat layout preference, and conversation state lives in AgentChatProvider so collapsing never loses the chat.
  • The Edit Agent operates at project scope. The renderer passes a safe active-project context (projectId, name, asset and track counts — never filesystem paths) with every send; the agent system prompt instructs the model to operate on that project by default. There is no per-asset attach flow in the panel.
  • Do not render a global agent model selector in product-chrome, add a separate EditAgentWorkspace, or move Edit Agent controls into the left workspace navigation. Direct AI Video and AI Voice studios keep their own domain-specific controls, and Settings keeps provider credentials and primary model configuration separate from the Edit Agent chat surface.
  • app-page-stack owns top-level page visibility for Home, mounted workspaces, and Settings. Active page state is separate from active workspace state.
  • The editor gives the timeline the larger half of the vertical split: the program/timeline splitter defaults to 45% program (min 25%, so the timeline can grow further) and timeline lanes are 72px tall. Layout schema 4 carries a stored v3 layout's docks and floating panels across but adopts the new split, because a stored v3 percentage would otherwise mask it.
  • local-edit-bay owns the active workspace panel while a workspace page is active. The panel fills the space between product chrome and the persistent right AgentChatPanel; do not reintroduce a left workspace sidebar.
  • Workspace panels for Editing, Voice Generation, and Video Generation stay mounted while inactive. Hide inactive panels with the platform hidden state rather than unmounting them, so video generation jobs, sample capture, local TTS job state, and timeline editor state survive navigation.
  • Settings is a top-level page opened from product chrome, not a workspace navigation item.
  • Home is the direct workspace chooser and lists Editing, Voice Generation, and Video Generation as entry cards. MP4 export lives inside the Editing workspace because it acts on the saved local timeline.
  • editor-workspace--nle uses a desktop NLE grid: tabbed project/media dock on the left, persistent program monitor in the center, tabbed inspector on the right, and persistent timeline across the bottom. Timeline commands are routed through the native application menu, not an in-workspace command bar.
  • Project/media dock tabs and inspector tabs exist only inside the Edit workspace. Never use those tabs for switching to Screen Recording or Voice Generation, and never tab the monitor or timeline.
  • On narrow screens, preserve the same order in a single column: program monitor, timeline, project/media dock tabs, inspector tabs.

Workspace Entry

  • Editing is the only workspace page: voice and video generation are tabs within it, not pages of their own. The workspace tab line carries a settings button at its right end that opens the project settings dialog — name (renaming stores the new label and leaves the folder on disk where the user put it), storage kind, created/updated, asset and track counts, and removal worded for the storage kind. The Inspector no longer has a project tab: it is for whatever is under the cursor, and the only destructive project action no longer sits one stray tab away from a clip. A tab strip in the workspace's top-left corner switches between Editing, Voice Generation, and Video Generation, so generating a clip and placing it on the timeline never leaves the workspace and the Edit Agent chat stays open beside all three. The chosen tab persists under its own storage key and falls back to Editing for anything unrecognised, so an id from another build cannot blank the area. Both studios read like the chat panel: a compact header (title, a short mono scope line, and the model picker), a scrolling body of fields, chips, and job rows, and a composer pinned to the bottom where the prompt and the primary action live. Model selection is DomainModelPicker, the Edit Agent picker's rule and popover parameterised by domain — models grouped by provider with a per-provider connection tag, listed only once their provider is connected, active selection always kept, rendered through a body portal. It replaces the old selector's badges, status block, and cosmetic context/precision select, which showed a value nothing consumed. Video generation accepts an optional reference image for image-to-video: the picker runs in the main process and returns the bytes inline as base64, so the renderer never learns the file path. Google Veo receives it as the instance image; the Sora adapter refuses a reference image outright rather than silently generating without it, because it would need a multipart input_reference this build does not send. The Edit Agent drives generation end to end with the same tools the UI uses — createVideoJob/createSpeechJob, getJobStatus, then importGeneratedResult to bring the finished job into the project as an asset (broadcasting the project change so an open editor reloads) and addClipToTimeline to place it — so a generated clip can reach the timeline without touching the studios by hand.
  • Status labels are fixed product labels, not live progress text. Live recording, sample, TTS, save, or error status stays inside the active workspace panel.
  • When users activate a workspace card, focus moves to the newly active region. Product chrome Home returns focus to the Home page region when it is already active.

Home And Settings Pages

  • Projects is the first page after launch: a left Projects rail (muted heading with the folder-plus picker button beside it, compact project rows with the active project highlighted) next to a Chats column listing persisted Edit Agent conversations grouped by day (Today / Yesterday / date). Selecting a chat opens its project, enters the editor workspace, expands the chat sidebar, and restores the transcript. Chat history lives in a path-free chats.json inside each project folder, written by the main process after successful agent turns (atomic writes, hostile files degrade to empty) — never from the renderer. Creating or opening a project routes straight to the workspace on success. Navigation to a project-required page without a project (including losing the active project mid-session) falls back to Projects. Each project and chat row carries a hover-revealed remove control that also appears on keyboard focus. Removing a project reuses the store's own rule — a folder the user chose is only unregistered, never deleted recursively; only projects living in app storage have their files removed — and removing the open one clears the editor. Deleting a conversation drops it from chats.json and resets the in-memory thread, so a panel still pointing at that id cannot resume it.
  • Home copy must stay truthful to the product boundary: it can describe local editing, connected provider generation, and local result import, but must not imply account, analytics, hidden upload, or bundled model/runtime setup.
  • Product chrome opens Home and Settings with native icon-only Button controls: inline SVG icons with aria-label and title tooltips, aria-current="page" when active, and aria-controls for each page region.
  • Settings owns theme preferences, provider connections, endpoints, primary chat-model configuration, and local AI engine preferences. Chat models are managed in Settings; voice-generation and video-generation models are managed inside their own workspace pages, where the selected model decides which provider API runs the job. Settings → Providers additionally lists media providers (ElevenLabs, Runway, Kling, Luma, MiniMax Hailuo — registered as minimax_hailuo so it does not shadow the catalog's minimax chat provider, while sharing its credential slot) with the same safe-storage connect/disconnect flow. Implemented media adapters: ElevenLabs + OpenAI TTS for voice, Google Veo (Gemini API) + OpenAI Sora for video — keys travel in headers only, outputs land in local ai_generations storage for the existing import flow, and providers without an adapter stay honestly unavailable. It is not part of APP_WORKSPACES and must not appear in AppWorkspaceNavigation.
  • Model access follows a provider → model concept. The provider/model registry derives from a generated snapshot of the models.dev catalog (llmCatalog.generated.ts, regenerated with scripts/generateLlmCatalog.mjs): every provider that speaks the OpenAI or Anthropic wire format on a public https endpoint, plus the native Anthropic and Google Gemini adapters (~153 providers, ~4,950 models). Anthropic-compatible gateways (MiniMax, Kimi For Coding, FreeModel, Subconscious, Thinking Machines…) route to their own base URL rather than api.anthropic.com. Providers needing special auth (Azure, Bedrock, Vertex, OAuth gateways) or an unresolved endpoint placeholder are excluded rather than listed as fake options. Model identity is the canonical providerId/modelId key; each cloud provider stores its API key under its own safe-storage slot (the original four keep their legacy slots). OpenAI is one provider with two login methods: an API key, or a ChatGPT sign-in (PKCE OAuth, tokens in main-process safe storage). With an API key, regular text models use the public chat-completions route and Codex-family models use the public Responses API. The ChatGPT sign-in instead talks to the private Codex backend, which is not the public Responses API: it serves only a fixed model set, and every request must carry the originator/User-Agent/session-id client identity — this app identifies itself as openvideo from the single CHATGPT_CLIENT_ORIGINATOR constant the authorize URL and the request headers both read, while the OAuth client id stays Codex's because ChatGPT-subscription sign-in has no public app registration — and send stream: true with store: false — omitting any of these returns a bare 400 with no body, so both the completions adapter (which parses the SSE stream itself) and the Edit Agent LangChain client (streaming + zdrEnabled) send them. Models the backend does not serve fall back to the API-key path rather than failing. Settings → Providers is laid out as a Connected providers list (hairline-separated rows with a source tag — Local for Ollama with its endpoint field inline, API key for cloud — and a ghost Disconnect) above a Popular providers list of unconnected rows with an "Unlocks …" note and a + Connect button that opens a modal connect dialog (Connect {Provider}, write-only password field into main-process safe storage, required-key validation, Cancel/Connect). Settings → Models is a model manager: a search field over the catalog grouped by provider, each row with a role="switch" visibility toggle persisted locally; hidden models disappear from the Edit Agent picker and the primary model select while the active selection always stays listed. Agent timeline edits obey the editor's own rules and are visible immediately: removeTimelineClip takes a clip back off the timeline through the same shared deleteClip the editor uses, dropping transitions that referenced it and refusing rather than reporting success when the id is not on the timeline. addClipToTimeline resolves the target track from the asset kind when the caller names none (the agent cannot see track ids), appends after the last clip on that track when given no offset, uses the whole asset when given no duration, and routes the clip through the shared placeClip so an agent can never persist an overlap the UI would reject — a rejected placement returns an error naming the tracks that exist. Because those tools write the project store directly from the main process, every successful timeline write broadcasts project-timeline:changed to open windows; an editor showing that project reloads it. The reload always applies — the edit was asked for — and unsaved local work is pushed onto the undo stack rather than discarded or used as a reason to skip the reload, since refusing to load left the agent's result invisible after any local edit. Chat runs in sessions: conversations are already persisted per project in chats.json, and the panel exposes them through AgentChatSessionPicker — a switcher listing this project's conversations newest first (the open one included even before its first turn is recorded, labelled New session) plus a that starts a fresh session without discarding the current one. Switching reloads the stored transcript and re-seeds the main-process thread through the same path the home screen uses. Reset still clears the open session's thread. Approval is opencode's three-way reply rather than a binary: Run once, Always allow — which records the tool for the rest of the conversation so repeats stop asking — and Deny, which may carry a short reason that reaches the model as a correction instead of a bare refusal. The panel also shows context-window usage from the newest turn that reported tokens (provider prompt tokens already cover the whole conversation, so the latest total is the current context, not a running sum) against the catalog's context size for the active model, and hides the meter entirely when neither is known. The window is also managed, not just measured: a conversation may fill the window minus a reserve kept free for the next reply, and crossing that line means the next turn no longer fits. The panel warns at 80% of that budget and offers Compact, which summarizes the older turns with the conversation's own model into a fixed set of sections — objective, important details, work state, next move — restarts the thread from that summary, and keeps the most recent turns verbatim. Ids and timings are preserved literally in the summary because they are how the tools address things. A model with no published window never reports pressure and is never asked to compact. Sending echoes the user's turn into the transcript immediately — the main-process round trip lasts as long as the model does — and a failed turn keeps the conversation: the session reports the error over the transcript the thread already holds instead of an empty one, and the panel re-appends the just-sent turn if the graph never recorded it. Transcript rows render through AgentChatMessageView: user and assistant turns parse a small markdown subset (paragraphs keeping the agent's own line breaks, -/1. lists, fenced code, **bold**, inline code) so the panel never shows literal asterisks, and tool turns collapse into a single <details> status row — a ✓/✕ mark, the tool name, and a one-line summary (the payload's field names, or its error when it failed) — with the pretty-printed payload behind the disclosure instead of a raw JSON dump in the conversation. The Edit Agent prompt bar uses AgentModelPicker: models grouped by provider with per-provider connection state, cloud models listed only once their provider is connected, and the selection persisted per domain. The popover renders through a body portal anchored to its trigger, because the chat panel and prompt card both clip overflow. Model rows show the label only — no context size. Beside the trigger sits the model "variant" control: a thinking-effort select offered only for models that list effort levels, with default plus the model's own values (from the generated catalog), stored per model and sent as the reasoning effort for OpenAI-family models. Chat sending gates on "selected model's provider is connected", not "local only" — OpenAI, Anthropic, Google Gemini, and DeepSeek have real main-process adapters for both completions and Edit Agent tool calling, and API keys never enter the renderer.

Program Header And Command Surface

  • The Edit workspace keeps Local studio, OpenScene, and the Timeline editor subtitle as visually hidden region labels for accessibility; no visible branding header renders inside the workspace, and app chrome must not duplicate it.
  • Timeline commands such as Play, Rewind, Undo, Redo, add track actions, layout changes, and Save timeline live in the native Timeline menu bridge. The timeline toolbar additionally exposes reference-style selected-clip tools — split at playhead, split left/right (trim to playhead), duplicate, and delete — as icon buttons that disable without a selection, plus a snapping magnet toggle; unimplemented tools (separate audio, freeze frame) render disabled with coming-soon tooltips.
  • Keyboard shortcut customization lives in Settings → Shortcuts, which renders the Shortcut map disclosure (TimelineShortcutMap). It must support remap, disable, reset, validation, persistence, and role="status" feedback; the Editing workspace consumes the stored preferences without rendering the remap surface. useTimelineShortcuts matches per binding, not per chord, so a disabled binding stays disabled and a default binding can answer to alternate chords for keys that differ by keyboard — Delete also fires on Backspace, which is what the main delete key reports on Apple keyboards. Bound actions cover playback (Space), history (Meta+Z / Meta+Shift+Z), editing (Delete, S, Meta+D), Meta+S to save, Escape to clear the selection, playhead motion (/ step, Home/End), clip nudging (Alt+←/Alt+→), and the layout toggles. Meta+A selects every clip on the timeline: selection carries a clip id list alongside the single primary selection that drives the Inspector, the canvas marks every selected clip, and Delete removes the whole selection.
  • Save state must remain explicit. Local timeline mutations are unsaved until saveTimeline succeeds.

Project And Media Dock

  • The left dock is labeled Project and media and contains two tabs: Project for ProjectRail and Media for AssetBin.
  • Default to Project unless an asset is selected, then reveal Media. Keep both tabs compact and dock scoped.
  • Project cards should prioritize project name, local status, save state, and selection.
  • The media view offers grid and list presentations. Grid tiles use a 16:9 preview well with an uppercase kind label and a mono duration badge, with the asset name and size below; list rows use a small kind thumb, name, and right-aligned mono duration. Both must show media kind, asset name, duration or metadata status, and selection, and video/audio stay distinguished with text labels and subtle tint, not hue alone.
  • An empty media library renders as a dashed import drop-zone CTA; a missing project keeps the existing empty slate.
  • Assets must show Reading metadata until browser metadata has been persisted through updateAssetMetadata.
  • AssetMetadataProbeHost is hidden infrastructure outside the dock panels. It may request secure playback URLs and probe browser metadata, but it must not become visible UI or imply analysis beyond local duration and video dimensions.

Program Monitor

  • The central program region is the largest panel. It contains the ProgramMonitor preview surface with a visually hidden heading; no visible header row sits above the monitor.
  • The monitor is for local timeline review. It is a best-effort v3 evaluator surface for keyframes, transitions, and audio mix, not final mastering, frame-perfect export, cloud preview, or AI generation.
  • MP4 export is a compact control in the monitor transport row: a primary Export trigger that opens a small popover with job state, progress, and Export/Cancel/Open/Reveal actions. It may show job state and result actions, but never local output paths, FFmpeg executable paths, or FFmpeg argv.
  • Empty states may be expressive, but they must guide users toward creating a project, importing assets, or selecting timeline media.
  • The monitor remains visible while users switch side dock tabs. It is the stable review surface for the current playhead and active timeline media.

Inspector

  • The right inspector is tabbed with Selection, Asset, and Project sections.
  • Default to Selection when a timeline clip is selected, Asset when an asset is selected, and Project otherwise.
  • Disable inspector tabs that cannot produce useful content. With no project, only Project stays available. Keep Asset unavailable while imported metadata is still pending.
  • Selection owns selected clip controls. Asset owns imported media metadata. Project owns current project metadata and project deletion.
  • Inspector content uses collapsible property groups separated by hairline borders: an xs medium group title with a −/+ toggle, and label/value property rows (caption muted label on the left, mono value or quiet compact input on the right). Selected-clip trim/nudge actions render as a compact two-column button grid above the groups.
  • Clip controls belong here when they affect the selected clip. Timeline wide commands belong in the native Timeline menu bridge.
  • Keep destructive actions visually distinct with the danger color and clear labels.
  • The inspector status card renders below the tab panels only while a status message exists; routine project opening emits no status. Status messages use role="status" and must stay readable after any tab switch.

Bottom Timeline

  • The timeline is the bottom anchor of the editor. It spans the full workspace width on desktop.
  • Track lanes are flat rounded inset wells separated by small gaps, under a slim adaptive ruler: tick and label intervals come from a time ladder chosen by real pixel density (labels ~90px apart, denser ticks that always divide the label interval), labels are mono times that gain tenths only at sub-second intervals and hours for hour-long timelines, and the right-pinned total suppresses colliding tick labels. Track mute/solo/lock render as icon toggles (speaker, headphones, padlock) with aria-pressed state. The playhead is a neutral foreground hairline with a round scrub-dot handle in the ruler. Clip blocks encode media kind with text labels plus tinted fills and borders, and reveal primary-colored grip trim handles on hover/selection.
  • Clip blocks show asset name and duration. Trim handles stay visible enough to discover, but should not dominate the clip label.
  • Timeline interactions must read as local edits. Do not suggest non-existent cloud sync, unsupported render formats, or frame-perfect mastering guarantees.
  • Timeline mouse tools: the select tool drags/trims clips and scrubs; the razor tool splits a clip at the clicked time (undoable, crosshair cursor, dragging disabled); the hand tool pans the track area horizontally (grab cursor). Ctrl/Cmd + wheel (trackpad pinch) zooms around the cursor with the time under the pointer anchored; plain and shift wheel keep native scrolling.
  • The timeline remains visible while users switch side dock tabs. It is not part of the left dock or inspector tab systems.

Local UI Primitives

  • Keep shared UI primitives lightweight and renderer local: Button, Panel, PanelHeading, MetadataList, StatusCard, Tabs, TabPanel, and classNames.
  • Prefer composition over broad component APIs. Primitives should wrap semantics, class names, and small variants, not own editor business rules.
  • Button variants are limited to default, primary, record, stop, and ghost. Add new variants only when a repeated local workflow needs a distinct semantic tone.
  • MetadataList is for compact term and value facts. Use it for project, asset, clip, runtime, and result metadata rather than ad hoc grids.
  • StatusCard is for user visible status with success, warning, danger, or default tone. Do not hide busy, warning, or error states in copy outside the visible card.
  • Tabs mark the selected item quietly — brighter text on a light foreground wash rather than a filled primary button, which reads as a call to action rather than "you are here". The wash is mixed from --foreground, so it lifts against a dark theme and darkens against a light one. No accent rule underlines the selected tab; the wash carries the state on its own, and it is rounded so it reads as a selected cell inside the strip rather than a full-bleed block. The unselected rule is declared before the selected override so the override wins at equal specificity.
  • Tabs and TabPanel are the only tab primitives. Use them for Edit workspace dock scoped navigation, not for major workspace regions. Application workspace switching belongs to the workspace tab strip.

Theme System

  • The renderer uses semantic theme tokens: --background, --foreground, --card, --primary, --secondary, --muted, --accent, --destructive, --success, --warning, --info, --border, --input, and --ring.
  • The Issue #63 foundation is a neutral near-white/near-black surface stack: light #f8f8f8 canvas with #fcfcfc/#ffffff panels, dark #101010 canvas with #161616/#1c1c1c panels, hairline borders, and hex text tiers (--text-strong / --text-weak / --text-weaker, mirrored by --color-faint) that all hold WCAG AA against the surfaces they sit on.
  • Presets only swap accent and surface temperature on top of that foundation. daylight-glass keeps the warm command desk light identity (ivory canvas, copper primary, cyan review accent); dark-zinc is the neutral dark identity; midnight-neon and obsidian-pro stay expressive alternates with light and dark variants.
  • The typography scale is pixel-locked by role: 11px micro, 12px caption, 13px small, 14px body, 15px subhead, 17px title, and a clamped hero. Headings sit at weight 500–600 with tight (−0.01em to −0.02em) tracking; weight 700 is reserved for tiny uppercase labels. Uppercase mono metadata uses restrained letter-spacing (0.04–0.08em).
  • Elevation is layered and low-alpha with a single top light source (--shadow-control, --shadow-panel, --shadow-editorial). Panels are flat cards — no glass blur, canvas grids, or decorative gradients.
  • Light and dark themes must read as distinct operating environments at the root token level. --background, --card, --muted, --border, --input, --shadow-panel, and --surface-control should differ enough that Home, Settings, Program Monitor, timeline, inspector, tabs, and Agent Chat remain visually distinguishable without component-specific overrides.
  • Theme-specific presentation belongs in renderer CSS. React theme controls should expose semantic state with class names, aria-*, data-*, and fixed preset identifiers; they should not carry layout, panel, or control styling inline. Fixed visual marks such as preset swatches may use CSS classes keyed by stable preset ids.
  • The persisted preset id daylight-glass remains a compatibility identifier only. Do not rename it. User-facing copy may describe that light preset as Command Desk, but compatibility identifiers listed in AGENTS.md must remain stable.
  • dark-zinc remains the dark fallback preset and daylight-glass remains the light fallback preset. Missing, invalid, or mode-incompatible presets must continue to resolve to those compatibility ids.
  • Legacy --editor-* and --color-* names are aliases over the semantic tokens. Keep new styling on semantic tokens and only use aliases to match existing renderer classes.
  • Atmosphere, panels, timeline grids, controls, tabs, and selected states use color for semantic hierarchy and interaction, not decoration. Pair hue with contrast, opacity, borders, shadows, labels, and subtle patterns so media kind is never signaled by color alone.

Reference Design Refinement Contract

  • Keep the product direction original, restrained, and local first. The refinement may borrow from generic public design principles such as hierarchy, rhythm, contrast, editorial spacing, and neutral surface systems common to modern desktop agent tools, but it must not borrow third party brand expression.
  • Preserve OpenScene as an expressive light and dark desktop NLE. Do not change the product into a cloud editor, provider console, marketing dashboard, or generative media suite.
  • Strengthen hierarchy through scale, weight, placement, and concise labels. Program monitor, timeline, dock tabs, inspector tabs, Agent Chat, and Home workspace entry cards should each have one clear job and a visible reading order.
  • Use measured whitespace, not empty decoration. Give the monitor and timeline enough breathing room to feel primary, keep command clusters tight, and separate dock, inspector, chat, and status content with consistent gaps.
  • Build surface depth with semantic theme tokens only: flat card panels, hairline borders, inset wells, and the layered low-alpha shadow scale. Dark surfaces remain cool and readable without reducing NLE contrast.
  • Keep navigation clarity accessible. Home workspace entry cards need visible labels, static local status, aria-controls, and focus movement to the active mounted region.
  • Preserve mounted workspace state retention. Visual refinements must keep inactive Editing, Voice Generation, and Video Generation regions hidden rather than unmounted.
  • Keep compact controls and focus treatment intact. Shortcut map controls stay dense, tabs stay compact, and the 2px focus outline with 2px offset and 4px halo remains visible in both themes. Default controls use the 36px minimum height; compact controls use 30px.
  • Respect reduced motion. Any hover lift, reveal, or panel transition must stay short and must not override prefers-reduced-motion.
  • Third party brand elements are excluded: no borrowed names, color values, logos, typography, copy, layouts, gradients, or token names.

Theme Switching

  • bootstrapRendererTheme applies the resolved mode before React mounts by setting document.documentElement.dataset.theme and document.documentElement.style.colorScheme.
  • bootstrapRendererTheme also applies the resolved preset through document.documentElement.dataset.preset; visual CSS may branch from data-theme and data-preset, but JavaScript behavior must keep the same parser/resolver path.
  • Missing, invalid, or unreadable stored preferences resolve to system. The system mode comes from window.matchMedia('(prefers-color-scheme: dark)'), with light as the non-browser fallback.
  • The persisted key is window-loom-theme. Only explicit light or dark values are stored. Toggling from a system-resolved mode stores the opposite explicit mode.
  • The persisted preset key is window-loom-theme-preset. Keep dark-zinc, daylight-glass, midnight-neon, and obsidian-pro as the complete stable preset id set until a separate migration is designed.
  • ThemeProvider listens for system preference changes while the current preference is system, then reapplies the resolved mode through the same root data-theme path.
  • Product chrome carries no theme control. Theme mode (light/dark/system) and preset selection live only in Settings → Appearance, using CSS-classed controls with aria-pressed state.

Type, Status, And Motion

  • Titles use the display stack. Controls use the body stack. Metadata, section kickers, timers, command labels, media badges, tab labels, and theme switch text use monospace.
  • Status semantics stay restrained: default is muted graphite, success is green, warning or busy is amber, and danger or destructive states are red. Use these only for state, not decoration.
  • Home workspace entry icons are decorative inline SVGs with aria-hidden="true"; accessible names come from visible labels and existing button semantics, not from icon-only controls.
  • Video and audio clips use flat tinted fills with solid semantic borders (primary tint for video, success tint for audio). Both must keep visible Video or Audio text labels, so media kind never depends on hue alone.
  • Controls should feel tactile with subtle background and border changes on hover. Respect prefers-reduced-motion and keep transitions short.
  • Compact controls use the 30px minimum. Default controls use the 36px minimum where space allows. Tabs are compact underline tabs; the selected tab carries the accent underline and an Active marker.

Accessibility Rules

  • Preserve semantic regions and labels: product-chrome, Home, Settings, OpenScene workspaces, Project and media, Timeline editor, inspector, Voice Generation, and Video Generation. Timeline commands are available through the native menu bridge rather than as a renderer toolbar landmark.
  • Keep keyboard focus visible with the semantic focus ring on Home workspace cards, workspace regions, buttons, timeline lanes, clips, inputs, and dock tabs. The implemented rule is a 2px --focus-ring outline, 2px offset, and 4px --focus-shadow halo.
  • Maintain 36px default control height where space allows. Compact shortcut map buttons may be 30px because the customization grid is dense.
  • Do not rely on color alone. Pair tones with labels such as video, audio, selected, saved, and metadata status, plus border or pattern changes where media kind differs.
  • Dock and inspector tabs inside the Edit workspace must use the ARIA tab contract: role="tablist", role="tab", role="tabpanel", aria-selected, aria-controls, and aria-labelledby.
  • Use roving tab focus. The selected tab has tabIndex=0; inactive tabs have tabIndex=-1; disabled tabs are skipped.
  • Tab keyboard support must include Left, Right, Up, Down, Home, and End. Arrow keys wrap through enabled tabs. Home and End jump to the first or last enabled tab.

Local First Constraints

  • No cloud upload, analytics, accounts, crash reporting, provider calls, or hidden network work may be implied by renderer copy.
  • Voice Generation and Video Generation can import configured local results into the active project, but they remain local workspace regions with no cloud upload, account, analytics, or hidden provider implication.
  • Voice and video generation depend on a connected cloud provider. The renderer must not claim a local media runner, bundled models, or automatic model setup.
  • Local MP4 export depends on user provided FFmpeg availability through VIDEO_TOOL_FFMPEG_PATH or absolute PATH discovery. The renderer must not claim bundled FFmpeg, cloud export, multiple formats, or access to filesystem paths.
  • Provider seams for future Gemini Veo, OpenAI Sora, and ElevenLabs support are interfaces only unless implementation changes prove otherwise.

OpenCut Reference Boundary

OpenCut is high level inspiration for local asset and timeline UX only. OpenScene must not copy or claim OpenCut code, assets, branding, exact visual identity, interaction details, or unsupported feature scope. Any future reference to OpenCut must state that no OpenCut source, dependency, artwork, logo, name treatment, or branded design system is used in this renderer.