The user interface
9. The user interface
Section titled “9. The user interface”9.1 Window
Section titled “9.1 Window”Dashboard: a 64-pixel rail (Watch, Project, Activity, Share, GPU — its icon carrying a badge where servers started from this app may still cost, and under its label two lines naming the one option a render will use (§9.7a) —, Settings, and the ? — nav.help — which opens the support dialog, screens/support_dialog.dart, anchored near the window’s top and as tall as its tab: a Get Support tab — its introduction, off the web, saying that the recording can be described and sent; Transcribe what I say (support.record.voice, off the web); Record an issue (support.record), which starts the issue recorder (§6.8); and, off the web, the recordings not sent, each with Send (support.unsent.<stamp>) — an About tab with the app’s name and the build’s stamp (§6.6a), selectable, the specification’s version and address and Open-source notices (support.licenses), and Close (support.close); while a recording runs the ? is a red stop button, disabled with a tooltip while one is starting or stopping, which off the web opens Describe the problem (§6.8), and, off the web, the rail shows the recording’s microphone (nav.record.mic, §6.8), and while it listens a caption near the bottom of the window, beside the rail, shows what is being said; every picture the recording takes is marked by a brief flash over the window; Settings keeps its own About card; and beneath the ?, the FilmOpen account’s profile — nav.profile, §6.6: a name and the account’s state, which opens the account’s popup — the rail scrolling, never cut, where the window is too short for all of it) beside an IndexedStack of eagerly built pages. The project’s page keeps its layout while another of the rail’s pages is shown, or while a route covers it — a dialog (ModalRoute.isCurrentOf) —, and takes the window’s as it is shown again: a change of layout moves its detail pane’s element, which the semantics cannot follow on a page the IndexedStack or a modal barrier hides from them. NavIndex numbers the pages — project 0, activity 1, settings 2, watch 3, share 4, gpu 5, numbers that never change — and the rail draws them in its own order; whatever reports a page names it (NavIndex.nameOf: watch, project, activity, share, gpu, settings), as the agent layer’s state, the issue recorder’s tape and a replay do. The app opens on Project. Watch (screens/watch_page.dart) holds two cards, Tutorials (watch.tutorials) and Featured works (watch.featured), each an empty state saying what will fill it and that it is a placeholder; it fetches nothing. Activity shows this device’s usage first, then the media being added (§9.7); Share the open film’s sends and receipts (§9.7); GPU the servers started for renders (§9.7a). Material 3, ColorScheme.fromSeed, compact density, light and dark themes, and an AppColors theme extension for the semantic colours the scheme lacks (the official star, a positive rating — also a plug-in that is ready —, the locked look, and caution, amber, for a plug-in that works less well than it could or whose turning off was not confirmed).
The frame has two layouts on the left and two on the right (screens/dashboard.dart). From 700 px of window the rail stands at the left and, on Project, the tree beside the page, resizable. Below 700 px the rail and, on Project, the tree are one drawer (SlideOut, filmopen_ui) behind a menu button floating at the window’s upper left (nav.tree): it slides in over the page, which the theme’s scrim (drawerTheme.scrimColor: black at 0.4 in light, white at 0.3 in dark) tones toward neutral, its text and pictures with it, and the button becomes a back arrow that closes it. A section chosen in the drawer keeps it open, since the person moves between sections while it is open; a tree row chosen closes it, as a tap on the scrim and the system’s back do. There is no tree outside Project. The drawer is as wide as the window less a hand’s width, between the rail with the tree’s minimum and 384 px, and the rail alone outside Project. It stays mounted while closed — moved out of view and out of the semantics, focus and taps — so that the rail’s account profile keeps listening for the request that opens the account’s popup; that request on a narrow window opens the drawer too (nav.drawer {action: open, cause: hamburger|account} and {action: close, cause: hamburger|back|scrim|row|crossing, ms}). From Breakpoints.assistantDocked (1000 px of window) the AI assistant is a full-height panel at the window’s right, resizable from 280 px to 45 % of the window, its width remembered (settingsProvider.assistantWidth); below it its portrait floats at the upper right (assistant.dock, a button) and slides the same panel in from the right over the scrim (§9.3). Neither floating control has a row of its own: the first visible row of each screen clears them by the room ChromeInsets gives (ChromeClearance), at the start below 700 px and at the end below 1000 — the entity page’s notices, stale-pick banner or header, the epoch overview, the project chooser and overview, the category, script, batch and other-files pages, Watch, Activity, Share, GPU’s tab bar, Settings’ title, its first card and the Plugins page’s top — and nothing else moves. The page keeps a floor of Breakpoints.pageMin (360 px): the docked assistant gives way to it first, down to its minimum, then the tree, down to 220; the widths the person chose are kept and come back as the window grows (SidebarSplitView.laidWidth, the one rule). layout.frame {wide, docked, clamped, cause: start|resize|section, window} is logged at start and at each change of layout, with the widths laid out where the floor bites.
The timeline dock (filmopen_timeline_ui, §19.15). A scene’s page — and no other — has its timeline docked beneath its tabs, across the pane: a handle (timeline.handle) to drag it up or tap it open and narrow, then one row; slid up, the tracks beneath that row. Its height is the person’s, kept in the settings (timelineHeight) as the assistant’s width is, and laid out between that one row (56 px) and half the page, so a stored height taller than half a smaller page is laid out at half and kept. Below 700 px of window it stays where it is, narrow by default, as the tree goes into its drawer. On Linux and Windows it plays the scene’s picture and sound and exports its MP4 through MLT (§19.16); on the web and on mobile the fake’s frame and block numbers stand in for the picture and play is disabled with its reason. Slid up, its picture takes the dock’s height at 16:9, capped at half the dock’s width, the tracks the rest: the person makes the picture large with the handle. The pop-out (timeline.popout, beside the export menu) opens the picture in a window of its own on Linux and Windows — a second view of the app’s one engine, drawing the same texture as the dock, with its own transport and a full-screen button (monitor.fullscreen); space plays or pauses, F fills its screen or leaves it, Escape leaves full screen, then closes the window. The keys are the window’s that has the keyboard: a key typed in the main window never reaches the pop-out, nor the reverse. The button closes the window again; elsewhere it is disabled, its tooltip saying why, and an open that fails is said in a message where it was asked. What the player refuses or fails is said in the pop-out too, in the dock’s words, beneath its picture. Closing the main window closes the pop-out first, so the open drafts are written as at any exit.
The browser page has two layouts (filmopen_browser/browser_page.dart). At 700 px of window width and above it is a SidebarSplitView (sidebar 220–560 px, seeded from settings, saved on drag end) with TreePane left and DetailPane right. Below it the detail pane has the page to itself, and the tree (BrowserTree, the page.tree region) is in the dashboard’s drawer. Crossing the breakpoint keeps the detail pane’s element — it is held by a GlobalKey — so a draft being typed is not written out from under the reader and the tab they are on survives the resize; the split is rebuilt rather than kept, because it is seeded once and would otherwise come back at whatever the layout last shrank it to.
Tab order. The rail, the tree and the page are each a Tab group of their own (FocusTraversalGroup, in screens/dashboard.dart and in both layouts of filmopen_browser/browser_page.dart): Tab walks one before it reaches the next, and reading order applies within each, so a page’s fields are never interleaved with tree rows that happen to sit level with them. Within an entity’s page the header, the tab bar and the body are groups of their own too (filmopen_browser/entity_tabs.dart), since the body scrolls on its own and a card scrolled under the header would otherwise share a line with the tabs. A scene’s block card is a group of its own, walked in the order it is built (§9.3).
The widths, and which axis each is on (filmopen_ui/theme.dart‘s Breakpoints). The drawer’s 700 px and the docked assistant’s 1000 px are measured on the window, because the side panels are what moves and the reader resizes a window; every other number is measured on the pane or the card that holds the thing, which the rail, the tree and the docked assistant beside it narrow, so a LayoutBuilder answers the second kind of question and MediaQuery the first. assistantDocked is never below the narrowest frame that docks uncrushed — the rail and its divider, the tree’s 220, the assistant’s 280, two 4 px splitters and the page’s 360, 933 in all. Below 560 px of pane a form puts one field on a row whatever width the field asked for; the scene grid gives up a column for every 300 px of pane it lacks (sceneCard), from six down to one; below 300 px a comparison gives away the page’s padding and the cards’, which is 72 px back into its two columns; below 240 px it scrolls sideways at the width where it last fitted; below 260 px a label sits above what it names rather than beside it — the version row’s chips, the epoch chips and a nested attribute row; below 360 px the tree’s header puts the viewer’s handle under the project’s name (§9.2); below 360 px of row New project’s Browse… shows its icon alone, its name its tooltip, so that the parent folder’s box keeps room for a path (§9.2); below 360 px of card a segmented button shows its labels alone, without the icons or the selected check, since every segment of one is as wide as the widest and three iconed ones want about 320 px that a phone’s card has not — a FittedBox then scales the control rather than let a label break mid-word, which a SegmentedButton does in silence; and 260 px is what a card in a grid of them asks for, which is a width rather than a threshold. The web build carries no viewport meta tag of its own, as Flutter’s template does not: the engine writes one at start-up and warns on every load if it finds another.
Heights matter too, and for one reason. A header above an Expanded takes the height it asks for and leaves the body the remainder — and the headers here grow as they wrap, so at a narrow pane the remainder was nothing. Two of them now yield: the entity header above the tab bar takes what is left once the tabs have kept 240 px and scrolls inside that, with a bar to say it is cut — keeping 80 px of its own on a page too short for both, so that it is cut rather than gone; and a comparison’s column headings scroll with its rows once the page is under 240 px tall, instead of standing above them.
What a narrow pane costs, and what says so. A Row reports that it overflowed; a Wrap does not, and the difference is not what it first appears. A Wrap does bound its children to its own width — a card that asks for 260 px inside a 215 px row is given 215 — so nothing paints outside. What it does instead is crush: at a 74 px row two chips that want 60 px each come out two pixels wide, one to a line, and no console line is printed. That is why the version row and the epoch chips put their label above their chips below 260 px rather than beside them: a 72 px label column beside a Wrap takes the last of its width, and what is left is unreadable rather than absent.
9.2 Tree pane
Section titled “9.2 Tree pane”Header with project name and subtitle (folder path, or “bundled sample”), a chip showing the viewer handle when one is set — beside the name where the pane is at least 360 px wide and on a line under it where it is not, since a 120 px chip and three buttons beside the name left the name eight characters at the sidebar’s 220 px minimum — expand-all / collapse-all in 32 px buttons rather than Material’s 48, and the project menu (⋮): New project… and Open project… (on every platform; its dialog, below, offers the folder picker only where a folder dialog exists), Close project (drafts written, then a blank page saying Start a new project or open an existing one, absent while nothing is open, as are the two items after it), Copy to a folder… (for a session source without a disk root, wherever a folder can be named: the desktops, not a phone; copies every file, including media and thumbnails, and opens the complete copy), Re-read files, then a Projects section listing the bundled sample and every folder opened before (name, path — on a phone the path after the app’s projects folder, …/<tag>, since every project’s begins the same), the open one ticked. Opening any of them, or another folder, lets the project on screen finish its writes first (§7): where a draft cannot be written the project stays, its form showing why, and a message, kept for 10 s, says the other was not opened — with the refusal’s own sentence, or, where the draft is still being written or failed without one, See the page’s save status, then try again. New project says it inside its dialog, Copy to a folder… as it says a draft it waits for. Choosing a remembered folder that no longer exists removes it from the list with a snackbar, and the project on screen stays. Each remembered folder carries, at its right, an × that takes it off the list and leaves the folder alone, and — where folders can be made — a red trash can that opens Delete this project. The row, its × and its trash are three controls to a screen reader and an agent as to a pointer — project.recent:<path> opens the project, project.recent-remove:<path> and project.recent-delete:<path> are the buttons’ —, the row being an entry of its own whose node holds neither button, where a menu item would merge all three into one node with a single tap. Delete this project asks one question naming the project folder and saying that it and everything inside it will be permanently deleted (deleteProjectFolder(path) in localization), with No (delete-project.no) and Yes (delete-project.yes) and no checkboxes. No or dismissing the dialog changes nothing. Yes checks isDeletableProjectFolder before closing or deleting anything: the target must be a real folder, not a link or filesystem root, with filmopen-project.json at its root; otherwise the app reports projectDeleteNotAProject. A project without the manifest still opens with a warning, but its trash action is refused before the open project is closed or any file is deleted. For the open project, identified by sameFolderPath (case and a trailing separator aside on Windows), drafts are written and the project closes before deleteProjectFolder(project.path) runs. Successful deletion removes its recent-project row and shows a message. A failure leaves the row and reopens what remains when the project was open. A separate media folder named by an existing manifest is always left alone; media inside the project folder is deleted with its contents. The loader reads media from the project’s media/ folder and ignores old manifest locators. Logged as project.removedFromMenu, project.deleted, project.deleteRefused or project.deleteFailed. Below the header, the filter field and the tree.
New project dialog (filmopen_tree/new_project_dialog.dart): the title first, with the focus; a little space; Folder for the project:; then, on a computer, the parent folder (typed or chosen with Browse) and the project’s own folder name, Folder — the short name, in a ShortNameField that follows the title (ShortName.normalise: lower case, dashes for spaces, what the grammar does not allow dropped, cut to its length) until the person types one of their own, and never afterwards; a name typed first is left alone. The box says the format’s rule only when it is broken, its typing dropping what the rule does not allow, and names a segment, never a path. Valid folder names receive no length advice, whether proposed from the title or typed by the person. Beside the parent folder, Browse… — its icon alone, its name the tooltip, where the row is under 360 px. The parent folder is proposed as the dialog opens (newProjectParentProvider, read afresh at each opening): the folder the last project was made in (Settings.newProjectParent), else, where none was saved, the folder that holds the last project opened (parentFolderOf; a drive’s root kept as C:\, never the drive’s current folder C:), else the person’s documents folder as the operating system names it (documentsDirectoryProvider, filmopen_platform‘s documentsDirectory through path_provider: Windows’ Documents, the XDG documents directory on Linux, Documents on macOS) — either folder only where it still exists, a saved one since deleted giving way to the documents folder, never to the last opened project’s. The answer may come late, and never writes over a box the person has typed in or browsed into; a documents folder the system will not name leaves the box empty and editable with Browse, and a line under it says so. The folder is remembered only once a project was made there and opened — never for a cancel, a refusal or a project not opened — and a write of it that fails stays for the session, said on Settings → General with Try again (§9.4). On a phone the parent row is absent: under Folder for the project: the dialog says the project goes into the app’s projects folder of §6.4 (while the folder is being found Create waits and its tooltip says so, and a folder that cannot be made is a line under the form, project.folderUnavailable in the log, and the same tooltip). On the web the dialog says the project lives in the browser session only, and the short name’s box is Short name, since no folder is made. It asks no kind and no media location, and shows no paragraph about folders and no preview of the files it will write: a new project has its manifest at the root, its project file and first scene in data/, and empty media/ and thumbnails/ folders, written as the director — the username in effect once the director’s block has made it so (below), read after any change of it still waiting on an open draft has landed (SettingsController.viewerSettled).
Open project dialog (filmopen_tree/open_project_dialog.dart), from Open project… on every platform, in four sections. Open a local folder: Choose a folder… (open.choose-folder) where a folder dialog exists (folderDialogsAvailableProvider, §6.4), then the remembered folders (open.recent:<path>); a remembered folder that is gone comes off the list, as the menu’s row does. Your projects on {domain}: signed out, Sign in to {domain} to see your projects here with Login / Signup (open.login), which closes the dialog — the account’s popup is on the rail behind it — and asks for the popup with that sentence as its reason; signed in, the account’s records (§6.6) grouped by username, each row (open.record:<id>) the project’s name, its provider’s mark (FilmOpen Cloud a cloud, Google Drive Drive’s, a folder a folder) and its location; a local record whose folder this device has not, and a provider this build does not open (Dropbox, iCloud, any other word), are greyed with a tooltip saying why; a failed read says Your projects could not be read ({code}) with Retry (open.retry), the local section still usable; no record, No projects on {domain} yet. The dialog follows the account’s snapshot, so a sign-in that lands while it is open lists the projects. Invited projects and Open an online project are disabled, Coming soon under them, their tooltips what they will be (open.invited, open.online). Opening a record (§6.14): a remembered folder whose manifest names the record — FilmOpen Cloud by sync.id; Drive by the id of the folder the read-only lookup finds, with the project’s tag, never by a folder’s name, which Drive does not keep unique, so a Drive record asks Drive (and its sign-in) first, its folder on this device being opened offline from the local section — is opened; otherwise its manifest is downloaded into a new folder of the parent — the app’s own folder on a phone, a folder the person picks on a computer — and that folder is opened. Opening… stands while it runs: Cancel, Back and the folder picker’s wait hold the dialog, and the barrier never closes it, so an open under way ends in the dialog that started it. A local open — the picker, the folder’s check, the open itself — ends every failure inline by a safe code. An open is proven by the project, not by the call: the project provider’s state is read after openDirectory, and an error or another folder is said inline — The project could not be opened ({code})., or the refused draft’s own sentence (open.failure) — and logged as failed. It logs project.openDialog {stage: shown | signIn | closed, signedIn, picker, remembered, records, outcome, ms on closing}, project.openDialog.records {stage: read | retry, outcome: start | read | refused | failed, count, local, code, status, type, error, ms}, project.openLocal.start | .done | .refused | .failed {how, path, code or stage: pick | check | open and type, cause, error, ms} and project.openRecord.start {provider, id, username, tag, location}, .step {stage: remembered | parent | lookup | manifest | bootstrap | open, outcome, code, ms} and one terminal .done | .refused | .failed {provider, tag, location, driveFolder, parent, stage, path, code or type, cause, error, ms} (error on an open that loaded another folder names that folder) — a parent-folder picker cancelled is .refused {stage: parent, code: noParent} and says nothing; each remembered folder whose manifest cannot be read or parsed is logged, project.openRecord.remembered {outcome: unreadable, stage: read | parse, path, reason: the type, error}; every stage logs its start; a local record whose folder this device has opens as a remembered folder does (how: record).
The downloading line. Under the tree’s header, while the open film’s files are fetched from its target, Downloading from {FilmOpen Cloud | Google Drive | the shared folder} — {n} files left and a thin bar (tree.downloading): a fetch queued, working or waiting for a retry is left to do; the bar is the done fetches and the progress of those left over every fetch of the film in the sync’s snapshot. A fetch waiting on a person — a sign-in, a pause, a form — needing attention or failed is blocked, and {n} files could not be downloaded — see Share is said under the files left and their bar, or alone where nothing is left; a cancelled one (a Stop) is neither, and the line goes when nothing is left or blocked. Media come down on demand (§6.14), so they are counted only once asked for. It logs tree.downloading {film, state: active | blocked | done, left, blocked, total, ms} at each change of a count or of its state, ms since the line began watching the film, the film as the first eight characters of its digest.
The cloud and the director (filmopen_sharing‘s CloudChoice, DirectorChoice and the alias dialog, under the folder name). Sync this project to the cloud so you can work on other devices and invite collaborators? is checked by default; checked, four radios, each with its mark: Use FilmOpen Cloud for $0.02 per GB per month (simplest) with an ⓘ that opens a paragraph about it — enabled signed in to the account with a known balance above zero; signed out, Login / Signup under it; at a known zero, a red You have no balance and Top up, which opens the account page in the browser (a page that does not open says The page could not be opened.); a balance not known yet says so and is never taken for a zero. Use your existing Google Drive account — enabled where this machine is signed in to Google and the build has a Google client; signed out, Sign in to Google (the Sharing model’s sign-in, its wait and Cancel; a sign-in that does not finish is said under it); without a client, the Sharing card’s reason as the tooltip. iCloud — coming soon and Dropbox — coming soon, never enabled. A disabled radio’s reason is its tooltip. The radio in effect is the one picked where it can take the project, else the first enabled (CloudOptions.selected). Checked with every radio disabled, Create is disabled with FilmOpen Cloud’s reason as its tooltip; unchecked, Create never waits on a cloud. Director role assigned to: signed out, a username box (ShortNameField(username: true)) prefilled with the username in effect, default where none was chosen; an empty box is default, and an invalid one says the username’s rule and leaves the last valid name in effect, so Create is never refused for want of a director; under it, If you will ever collaborate or work on multiple devices: and Login / Signup. Signed in, never typed: AccountUsernameChoice over the account’s names (§6.6, read once per account, at sign-in), a pulldown, the primary first, the director selected where it is one of them, else the primary; its last item, Add another username as an alias…, opens the alias dialog, and a claim reads the names again. While they are read the director is shown read-only with a still progress mark; a failed read shows it read-only with Your usernames could not be read ({code}) and Retry; an account with no username yet shows it read-only with Add another username as an alias…, which claims the account’s primary — the director then kept may not be one of the account’s. Both Login / Signup buttons ask the account’s popup (AccountOpener.open, with its reason), which shows Settings; the dialog follows the account’s snapshot and a source installed meanwhile, so that signing in at either removes both and shows the pulldown. The alias dialog (showAliasDialog): a username box checked with the website 500 ms after the last change (Checking…, Available, That name is taken., {domain} could not be reached., The name could not be checked ({code}).; an answer for a text typed over since is dropped); OK, enabled only for a name said to be available (its tooltip otherwise Only a name shown as available can be claimed.), claims it — the primary on an account with none, else an alias — and the pulldown selects it; a refusal is said inside the dialog by its code (Your account has its three aliases already., That name is taken., the username’s rule, {domain} could not be reached., else The name could not be claimed ({code}).), and OK then waits for a new check. Create, in order, with the director as Create was pressed — every stage uses that one, and while Create runs the cloud and director blocks take no pointer and no focus: the director becomes the username (setViewerHandle; a refusal is said in the dialog and nothing is made); the project is made as before; then, signed in — FilmOpen Cloud: SharingModel.shareOnFilmOpenCloud, which records the project itself; Drive: recordProject (provider drive, FilmOpen/<tag>) then shareOnDrive, which answers nothing — a notice says it failed, and the manifest’s sync naming Drive that it went through (shareNewProjectOnDrive; neither is notShared); the box unchecked: recordProject (provider local, the folder’s path; a project with no folder, the web’s, records nothing) — and signed out, only Drive’s shareOnDrive. Done, the dialog shows the Project page (NavIndex.project, which signing in from it had left on Settings) and answers the project. A stage after the project that fails is the dialog’s result, never undone or tried again: The project was created on this device., then each failed stage’s sentence and the action that finishes it — a record not written (Its record could not be written to {domain} ({code}). Settings → Sharing → Record on {domain}.), a record whose confirmation this device could not keep (The project is recorded on {domain}, but this device could not save that it is., or, for FilmOpen Cloud, FilmOpen Cloud setup stopped: …), FilmOpen Cloud not set up (FilmOpen Cloud could not be set up ({code}). Settings → Sharing → Share on FilmOpen Cloud.), Drive not set up (Google Drive could not be set up ({code}). Settings → Sharing → Share on Google Drive., the Sharing model’s notice code), an unexpected failure of either stage the same sentence with failed — and one button, Open the project (new-project.open), which lands as a create does; Create is not offered again and nothing dismisses the result. The log: project.create.start {cloud, signedIn, director, directorSource, parent, tag, name, onDisk}, project.create.stage {stage} before each of director, create, record, sharing, project.create.director {changed, outcome, source, from, to}, project.create.created {project, scene, author}, project.create.record {provider, outcome, code, username, location} (skipped for a project with no folder), project.create.sharing {provider, outcome: ready | failed | notShared, stage?, code?}, project.create.stageFailed {stage, type, error} for a failure nothing foresaw after the project exists, then one of project.create.done {result: ready | partial, failures}, project.create.refused {stage, code, said} or project.create.failed {stage, type, error}, each with ms; the choices’ own project.create.cloudChoice, the account’s names’ account.names (§6.6), project.create.directorChoice ({mode} as the block’s presentation changes, and {director, refused?, source: field | pulldown, outcome: accepted | defaulted | keptLastValid} (refused the invalid entry kept out) for each decision of the director, a typed one once the box is left rather than per keystroke), project.create.signIn, project.create.topUp and project.create.driveSignIn (each after its .start), project.create.alias (after .alias.open), and the alias dialog’s alias.check and alias.claim (each after its .start), with ms on every outcome and the values each worked on — the username, the tag, the path — never a token. A disabled Create says why in its tooltip: the phone’s folder, then the first box that is not good — the reason names its box: the empty parent folder on a computer (Choose the folder the project goes into first) or the folder’s name (Folder: …, ShortNameField.problemText). A name that followed the title is checked as a typed one is (ShortNameField marks a value that arrives as touched), so a title cleared again leaves the box saying a name is required. The dialog’s controls are new-project.folder, new-project.browse, new-project.title, new-project.tag, new-project.cloud (the box), the radios new-project.cloud.filmopen, .drive, .icloud and .dropbox, the buttons new-project.cloud.sign-in, .top-up, .info, .drive-sign-in and .drive-cancel, new-project.director and new-project.director.sign-in, .alias and .retry, new-project.cancel, new-project.create, the result’s new-project.result (read-only) and new-project.open, and the alias dialog’s alias.username, alias.status and alias.refusal (read-only), alias.ok and alias.cancel; the title and the folder name retain their dictation controls (§9.6). It refuses a non-empty folder with a message. The new folder is opened only once it has been read back whole (ProjectController.createProject): a folder written that will not list or load is ProjectNotOpenedException, said in the dialog with the folder it was written in — and that the project open behind the dialog stays open, where one is: after Close project none is — and the project on screen stays — its page, its viewer, no spinner. A failure said in the dialog goes once a box it was said of is changed. While it creates, nothing dismisses the dialog (a PopScope, Cancel disabled), since a dismissal would answer nothing while the project opens; a second press of Create while the first runs — a double click, Enter pressed twice — is ignored, so that the hold ends with the creation that set it and no other. Create answers a CreatedProject — the project file’s stem and the first scene’s — and the tree pane opens the new project’s own pj version on Edit, whatever tab the page before was on (the navigation asks its page for View/Edit, BrowserController.navigate(onEdit:), since the tabs keep the reader’s tab across pages where it stays open, as Compare does in development mode), its row highlighted and its ancestors expanded, the tree’s filter cleared so that the row shows, the drawer closed below 700 px; the first scene, Scene 1 in the person’s language, waits in the script. The project’s page has its Media card, so the landing writes the viewer’s commentary file for the project on its first visit (§13.4).
ShortNameField (filmopen_ui/short_name_field.dart) is the one input box for anything that must be a segment (§5.2): it filters as the user types (lower case, spaces and underscores to dashes, other characters dropped), explains the remaining problem under the box, and says nothing of a valid name’s length. With allowWorkspace it accepts one dot and validates both sides, for an author handle (§5.6): the owner a username of 3 to 32 characters, the workspace at most 20, counting up to 53. With username it is a person’s username alone (ShortName.validateUsername), counting up to 32, its helper the username’s rule where the caller gives none — A username is 3 to 32 lowercase letters, digits and dashes, and does not start with a dash. In either mode an owner too short, starting with a dash, of other characters or over 32 says that sentence; an over-long workspace names its 20. Any place that asks for a tag, an epoch token, a handle or a username reuses it.
A row of the script has a menu (filmopen_tree/script_actions.dart, ScriptActions): Add folder… and Add scene… (tree.add-folder, tree.add-scene) on the Script row and on every folder row — into the project file in use, or into the version of the folder the row draws — and Remove from folder (tree.remove; Remove from the script where the project file lists the row) on every row a list holds, a flagged entry that names nothing included, which is how a bad entry is cleaned out of a list from the app. The same identifiers on every row, one menu being open at a time. An item the viewer may not use is disabled with PageAccess.childrenLock‘s reason as its tooltip. Never a second write path: each item calls the one ProjectController method for it, and the pages’ Contents card calls the same ScriptActions. Add opens the new-entity dialog for a folder or a scene (showNewChildDialog): the name first, since the short name is filled in already — nextSceneTag, nextFolderTag (§5.11), overtypable, refused inline when taken — a folder’s kind (the vocabulary and None), no username — the file is written as the username in effect, as a character’s is — and no epoch question; a scene left unnamed is Scene 3, after its type and number — a numbered folder’s f is dropped (Folder 2), and a short name a person typed is kept whole (Scene flashback). The short name’s helper, which says why it is a number, may take as many lines as a phone’s dialog needs (ShortNameField.helperMaxLines; a decoration’s helper is cut at a number of lines, never wrapped freely). The identifiers stay new-entity.*. It settles the open drafts first: a draft pending on the parent’s page lands before the parent is written again. On Create the new page opens, the drawer closes below 700 px as it does when a row is chosen, and a message says Added … — or, where the parent could not be changed, that the file is under Not in the script and why. Remove says that nothing was deleted and where the row went.
Moving (§3.10). A row whose folder or scene has no retained version — every one discarded — is for inspection only: it offers no script action and cannot be dragged, since a move would list what the reader then drops. A row whose entry names a folder or a scene also offers Move up, Move down (tree.move-up, tree.move-down; disabled at the ends with It is the first already / the last already, and with the lock’s reason where the list is not the viewer’s) and Move to… (tree.move-to); a row under Not in the script, which no list holds, offers Move to… alone, which files it — disabled, with the reason, where nothing of the script can be changed by anybody here (no username, a read-only project), rather than opening a dialog with nothing in it. An entry a reader cannot read stays where it is until it is repaired or removed. Every move of one row is ProjectController.moveChild and nothing else — the menu’s items, the dialog and a drop (marked rows go together, below, through moveChildren): within one parent it is one write of that parent; between two parents the destination is written first and the origin second, so that a second write that fails leaves the entry listed twice — reported by every reader (childDuplicate), visible in both places, and said (… is in … now, but could not be taken out of …, so it is listed twice: …) — rather than nowhere; from Not in the script it is one write of the destination. Refused before any write: a folder into itself or into a folder beneath it (folderIsOrHolds, §5.12; ContentProblem.folderIntoItself), and, for both parents before either is written, everything an add is refused for — a draft that cannot be settled, a parent that is not the viewer’s, a read-only project, a save the writer would refuse. An entry moves as it is written, so a pinned full reference stays pinned; a destination that names the folder or scene already takes no second listing, and the move is then the removal alone — which is said (… was already in …: it was taken out of …), since the spelling that was moved, a pin included, goes with the listing that was taken out; a move to where the row already is writes nothing; nothing is deleted by any of it. The open drafts are settled first, and a change of a list whose page is open goes through that page’s draft, as an add’s does. A drop (ScriptActions.dragDrop: canDrag, placesOn, dropRefusal, drop; the view’s half is §8.3) is that same move and nothing else, so a drop writes what the same move by menu writes — a test compares the two folders’ files. A row of the script can be picked up where its entry names a folder or a scene and the list that holds it is the viewer’s to change, and a row beside the script whatever it is; a row that holds a list — Script, a folder drawn with what it holds, beside the script too — takes a drop into it, at its end, and a row a list of the script holds takes one before or after it, in that list, beside that row’s entry as the move’s turn finds the list (ChildBeside, read from the view, §7.5), never at the index the row was drawn at; Move up and Move down likewise move the entry one place from where that list then holds it. Refused, with the reason said on the drop: a list that is not the viewer’s to change (the lock’s own sentence), a folder into itself or beneath itself, and a row that is no place of the script (Drop it on Script, on a folder, or between two rows of the script). A refused drop is logged once, at the release (tree.drop.refused: the child and its list, the row and the place, the list it would go into, whether that list is locked and whether it would put a folder into itself, the reason and the time), and so is a drop whose row, or the row it was let go of over, the tree no longer holds at the release, or whose target no longer holds a list at that place (the same event, with draggedGone and targetGone in place of the lock and the cycle, and the code childrenChanged as its reason); a hover logs nothing. A Move to… whose chosen folder the film no longer holds when it is confirmed logs tree.move.refused (the child, both lists, toGone, the reason childrenChanged and the time); one whose opening row went while the dialog was open — a move published meanwhile, or the page changed under it — writes nothing and logs tree.move.refused with the reason openerGone and the time since the dialog opened; a cancel logs tree.move.cancelled {child, from, ms}, said by no message, since nothing it opened from is left to say it. Every refusal of a move past these is the controller’s, logged as folder.childMove.refused. A drop into a folder says where the row went, since it may go out of sight; one between rows lands under the person’s eyes. The drop reads both rows as the tree has them now (TreeModel.nodeOf): a drag carries the row it picked up, and a re-read under it would put the drop a place beside the line the person saw; a row that is gone is said (The script changed meanwhile…), not guessed. The move dialog (filmopen_tree/move_dialog.dart, showMoveDialog, moveDestinations) lists where the row may go: Script, and every folder the script draws whose version the viewer may change, in the tree’s order and indented as the tree indents them — read off the tree’s own model, so its rows are the tree’s rows (move.to:<the tree's row id>) — without the moved folder, wherever the script lists it, and what is beneath it, and without any folder the move itself would refuse (wouldCycle, which is the controller’s folderIsOrHolds, read on the film as the controller’s decisions read it, ProjectController.view — a move of the app’s own landed and not yet published counts, so a drop and the dialog refuse what the controller would; the dialog’s rows are the tree built from that view when it opens, so a place such a move has made is offered too): a folder listed a second time is drawn without what it holds, so a folder the moved one holds can stand outside its branch as the tree draws it, and the dialog offers nothing that Move would then refuse; what was left out because it is somebody else’s is said under the list — how many folders, and, since it is no folder, Script by name with the author of the project file in use — and with nowhere to go the dialog says what would give somewhere (fork the project file, or a folder). The file chosen is looked up again, as decisions read it now (the view), before anything is written to it. Choosing a row and pressing Move (move.confirm, disabled with Choose where it goes first until one is chosen; move.cancel) puts the entry at the end of the one chosen, and a message names where it went. The dialog writes nothing itself. The messages (ScriptActions): the project file’s list is called Script in every one, as the tree’s row, the menu’s item and the dialog call it (_listName), and a folder’s by its name; a message that says what state the files are in — a row now beside the script, a row listed twice, a destination that named it already, a refusal — stays ten seconds (kLongMessage, filmopen_ui/messages.dart), where a confirmation stays Material’s four; and a write development mode let through on somebody else’s list is said (§9.5): the tree has no page whose red line would say it, so the confirmation itself ends This changed ann’s version as bob: a release build would refuse it — one message, since a second would take the first one’s place before it was read, and one string (guardOtherAuthorDone, the confirmation its first placeholder), so that how the two sentences are joined is the language’s to say — and a move within one list, which otherwise shows by itself, then has a message too.
Moving marked rows (§8.1, §8.3). A drag of a marked row, or Move to… in a marked row’s menu, while more rows are marked, moves every marked row in one gesture: one call of ProjectController.moveChildren (§7) with the rows in the order the tree draws them — a folder, a row it holds and a row after it go as [F, X, Y], never grouped by list — so one write per list touched and one publication of its own. An unmarked row, or a single mark, moves as above. The rows, their lists and their order are read from the view (viewTreeModel, the tree built from ProjectController.view, as the move dialog builds its rows): the ids drawn are only the person’s choice, and a marked row the view no longer has where it was drawn — a move of the app’s own landed and not yet published — is left where it is, counted in the log and in the message (1 marked row had changed meanwhile and was left where it is); whatever remains, one row included, goes through moveChildren. Refused before anything is written: a drop on, before or after a row that is itself marked (Drop it on Script, on a folder…: once the marked rows are taken out it has no place); a destination any marked folder is or holds (the folder-into-itself sentence); the lock as for one row; and the door’s own refusals, an entry listed twice among them (A row being moved, or the row it goes beside, appears twice in the script…). A refused drop logs tree.drop.refused with the marked entries (marked), each by the list it comes from (byOrigin, as tree.move.marked gives it), whether the target is one of them (targetMarked) and which of them would hold the destination (cycleWith). Move to… on a marked row titles the dialog with their count (Move 3 items to…) and offers no folder any of them is or holds; its cancel logs the entries. What is said: Moved 4 items to Act II; nothing changed, It is already there; a list that could not give its rows up, Moved to Act II, but Act I could not give up 2 of the rows, so they are listed twice: …; written but not read back, the index’s own sentence, after the half-done one where there is one; refused, 3 items could not be moved: … — each a long message. The marks are cleared once the move is written, whole or not, unless a newer marking replaced them; a move refused whole leaves them. The gesture logs tree.move.marked {count, entries, byOrigin, to, beside, door: drop|menu, skipped: {id: reason}, marked} before the call and tree.move.marked.done {outcome: moved|unchanged|halfDone|notPublished|refused, to, entries, landed, originsLanded, notRemoved, alreadyListed, code, error, reason, skipped, ms} after it.
Import Script… appears beside New project in the project menu wherever projects can be created on
disk. Where a file can be chosen (the desktops) it opens the import dialog; where it cannot (a phone) it says
in a small dialog that a screenplay is imported in the desktop version only, with one OK
(import-script.desktop-only), and logs project.scriptImportDesktopOnly {platform}. The dialog has one
path box, a native Browse button offering one Screenplay group — fdx, pdf, fountain, txt, rtf,
docx and fadein, upper and lower case — OK and Cancel; the path box takes any file, since the file’s
content decides its format (Project Specification §16.6). Browse logs project.scriptImportPicker with its
outcome — started, selected with the path, cancelled or failed with the error — and its ms; a failure
the controller does not answer with a code is logged as the error project.scriptImportDialogFailed {path}. A helper
names the parent folder, using the same proposal as New project. The store gives each imported project a
fresh folder there. A missing destination disables OK with a reason. A refusal stays inside the dialog,
retaining the path and the current film: a file that is not a recognised screenplay (not_screenplay) says
FilmOpen could not recognise this file as a screenplay…, a Final Draft file that cannot be read keeps a Final
Draft sentence, and every other code its own. During browsing or importing, repeated activation and dismissal
are held. Success closes the dialog, clears the tree filter and opens the imported project on Edit through the
browser’s normal navigation. Identifiers are project.import-script and import-script.path, .browse,
.ok, .cancel.
The existing scene editor reads the imported dialogue, direction and action; source element types and
provenance remain available in JSON without adding another editor interaction.
9.3 Detail pane
Section titled “9.3 Detail pane”Each offered reference-card job action has a job:<id>:<action> semantics identifier. Starting another action clears its previous local acknowledgement refusal; a rejected acknowledgement displays its code through localization again.
A form’s saver is keyed by its rendered film and version, including both Compare columns. OpenDraft.apply accepts reseed and a DraftWriteContext: the latter belongs to the pending changed revision and stays with its snapshot through a failed save, a retry and disposal. An older in-flight snapshot keeps its original writer; incompatible pending contexts refuse before mutation. A successful snapshot releases its context only once every patch that shares it landed, leaving any later typing dirty by the existing generation rule. A bound writer’s returned entity is the base of the draft’s next write; a re-read is never offered to adopt. Discard releases the pending context. reseed compares the seed immediately before and after the mutation and updates only changed boxes synchronously, preserving unrelated text, caret and any later keystroke.
After the scene form writes a changed epoch or cast (compared canonically with the version before the write), it starts createCastVersions (§7.3) without awaiting it. The save succeeds independently of the companions and the helper never settles the saver’s in-flight write. The film, viewer and controller are captured before the form can go away. A write changing neither field logs scene.cast.versions.skipped with the before/after values, reason unchanged and time. While the form is still mounted, helper completion shows its outcomes together: Created {name} at {era}., or a localized refusal naming the character and era and why it could not be created. The era is its label, or its token where unlabelled; an ambiguous source says to choose a source in Create. A message containing any refusal stays for kLongMessage (ten seconds), including mixed creation/refusal outcomes; a creation-only confirmation stays four seconds. A disposed form shows nothing and writes no second scene save. scene.cast.versions.shown logs the scene, mounted state, outcomes, exact message, duration and its reason, and elapsed time, including completion after disposal.
Switches on the selection. When the project needs a choice, the Project chooser replaces everything: a card per pj candidate (name, stem, author, version, language, forked-from) that selects it.
-
The AI assistant’s dock (
AssistantDock,filmopen_assistant_ui) stands at the window’s right in every rail section — Watch, Project, Activity, Share, GPU and Settings (§9.1). FromBreakpoints.assistantDockedof window it is a full-height panel (assistant.dock, a container); narrower, the active assistant’s portrait floats at the upper right (assistant.dock, a button, hidden while the drawer is open) and slides the same body in from the right (assistant.slideout, closed byassistant.hide— a right arrow, as the left drawer closes with a left one —, the scrim or the system’s back); the slide-out closes as the window docks (assistant.panel {docked, width, cause},assistant.panel {action: open|close, cause, ms}). The body is three bands. At the top, frozen and at most 40 % of the panel’s height, scrolling itself where the actions are more: the active assistant’s portrait (assistant.picker), which opens a menu of the catalog assistants by portrait and name (assistant.picker.<tag>, an unavailable one disabled with its reason; choosing one leaves the chats held, each with the assistant it opened with), its name and where the person is (the held field’s label, the page and the rail), and, while any chat is held, the switcher among them (assistant.chats, §6.11a); then its actions where the person is (assistant.action.<id>), one group per level in the catalog’s order — the field’s, its form section’s, the page’s section, the rail’s, the always-offered — each headed by where it applies (the field’s label, the form section’s title as the form shows it, the page’s name, the rail’s), a heading the group above shows not repeated, a disabled chip carrying its reason:noModelonly where the action has no candidate model at all, else the first candidate’s refusal —credentialsfor a missing key, the offer carrying that provider’s tag —, andnoToolsfor a field’s action whose model’s provider declares no tools, since it could change nothing (The model chosen for this action cannot make changes. Choose one that can in Settings.); a provider no installed plug-in matched is never carried, and a diagnostic names a provider only as a safe word, elseinvalid. Under the where-line, fixed above the scrolling options so that scrolling to an action never hides what will pay, the first offer that lacks a key gives one readiness row (assistant.readiness): that sentence and Add a key (assistant.addKey), which closes the slide-out and opens Settings → Plugins on that provider’s page, where its key card is (assistant.dock {addKey, provider, ms}, after the slide-out’s close withcause: addKeyon a narrow window). Where no offer lacks a key, the same row names the offer’s decided plug-in / origin: its exact signed-in slot or program, your key, FilmOpen credit or this computer. While a chat is shown the row names the route that chat was admitted on and every turn of it uses — held by its session, never the fresh offer, which a route changed since would make another —; the offers speak only where no chat is shown. That line is the panel’s, not the guarantee: each control that starts a charge says its own route — every ready action chip carries its offer’s route words under its name (assistant.action.<id>.route, <plug-in> / <origin>, the decision its opening admits), so it is said before the press however fast the chat then opens, and the chat’s reply box carries the chat’s held route at the left of its buttons (assistant.chat.route), loggedassistant.chat.route.shown {session, action, plugin, route, line}as it changes: a chat still open says its own route while another action opens on another. Each change of the row is loggedassistant.readiness.shown {action, source: held | offer | none, plugin, route, funding, line, session}. An unavailable saved choice names the refused origin and Settings → Models; an opening failure retains that rejected identity even when settings change afterward. Under them, the conversation: Starting assistant and a refused open’s reason, then the chat, which fills the rest of the panel; with no chat open it says what would fill it (assistant.empty). A chip opens the action’s chat in the dock, beside the chats held, and nothing opens by itself; its first turn is under way at once, but for an action of idask, the stock assistant’s Ask, which waits for the person’s question (§6.11a). The AI icon on the form (assistant.shortcut.<id>, §9.3) presses the chip: the form holds the place — the field, or the section’s first field — and says the press (assistant.shortcut {action, from: field|section, serial}); the dock opens the slide-out where the panel is not docked (assistant.panel {action: open, cause: shortcut}), then starts the active assistant’s offer of that action where it is enabled — but an action offered on FilmOpen credit is only revealed: the panel opens where it is not docked and its options are scrolled to that chip (assistant.dock {shortcut.revealed, action}), whose route words say … / FilmOpen credit, and the person’s press of the chip starts it, so that nothing is charged to credit before its route is seen; on any other route (the person’s plan, their own key, this computer) it starts at once —, else refuses it with the reason, the chip saying why in the opened panel (assistant.dock {shortcut, action, from, field, plugin, route, outcome: started|revealed, reason: notCredit|credit, opened, ms}, a refusal{…, outcome: refused, reason, opened, ms}). A chip’s own press is said the same way (from: chip) for the icon to hear, and not run again. The icon and the chip animate as one: the chip shows its selection, and the icon of that action pulses — out and back, 200 ms each way — on its own tap and on the chip’s press, with no state beyond the running animation. The chat’s X ends it, and a close begun elsewhere — a Discard on its record, its film left, the quit; another chip opens another chat beside it, and a move, another chip or another assistant ends none: the dock holds them, one shown, the others reached by the switcher (assistant.chats) or their rows in the tree’s Chats, and a chat’s first recorded change opens its record’s page where the person is still where the chat was opened (§6.11a). Its voice conversation is offered on its own page alone — its project, rail, section and entity, whatever field is held and whichever version is shown —, disabled elsewhere with Conversation is offered on the page this chat opened on. The chat (assistant.chat) has an X fixed at its upper right (assistant.close,assistant.dock {close, cause: x}before itssession.ended), which ends the chat and not the panel; the messages (assistant.message.<n>) in a transcript that scrolls through its history and follows new turns while the reader is at its end — the person’s own scroll away stops it, their scroll back or their Send resumes it, and a change of the view’s size keeps a following transcript at its end; then the reply box pinned at the bottom (assistant.input), one bordered box whose text runs its whole width from one line up to eight — fewer where the chat is short, never the room its X, its buttons, a busy line and an error as it wraps need —, then scrolls inside, and under the text, at the right, the microphone and one button: a square Stop while a turn runs (assistant.cancel, which cancels it), Send, an up arrow, with text in the box (assistant.send), the conversation with the box empty where the assistant and the device share a voice provider, whatever the text chat’s state (assistant.conversation; the voice mode admits it itself), its request made at the press for the page as it is then — a field action’s field always the chat’s own: held where it is, else bound on the version the tabs show to that version’s file as read, as a View form binds it, in place of any other field held (assistant.dock {conversation.open, level, providers, field: none|held|bound|missing}), else Send greyed with why; a box locked with text in it (the chat stale) keeps its text, Send disabled with its reason and no conversation. Each enabled press is logged,assistant.composer.press {action: send|conversation|stop, state: busy|text|empty, text?}(a Send’s with the message sent), then its outcome once the session has answered it,assistant.composer {action, outcome: sent|refused|stopped|opened, code, ms}— a refusal with the session’s code (stale, say),msto that outcome, not the model’s reply. A chat has no Apply: a change is written at once by the assistant’s commands (§7.3). The transcript gives way first, down to nothing; where even a one-line box does not fit under the X, which stays fixed, the rest of the chat scrolls as a whole, held at its box, so the box is never cut. The microphone stands beside the box’s text, not in it: a press focuses the box, and a click on it or on the one button is no click outside the box. The chat, and its unsent text, survive the slide-out’s close and reopen and a change between docked and floating; a dictation under way in its reply box ends with the close — by its button, the scrim or the system’s back — through the dictation’s own stop, since a closed slide-out shows neither the capture nor its Stop (the panel’s tickers are off while it is closed). -
The activity indicator (
assistant.activity) stands right under the dock’s fixed header, so it is seen whichever chat is shown, an archived chat’s record or none, its lines scrolling in a region of their own of at most half the top band — every line kept, however many — the brain and Stop beside them always in view: one line for each held chat whose turn runs, named by its chat as the switcher names it (assistant.activity.turn.<session>, <chat>: thinking…), one for each piece of Auto-generate’s work, named by its entity, its place and its phase (assistant.activity.<stem>.<place>, Writing <name>’s prompt…, Rendering <name>…), and one after them for each other render job the insertion service runs — a producer’s job queued, working or waiting, a Workflow’s render among them, or held on a stop its producer could not confirm, which the installed service projects as needing attention — (assistant.activity.job.<job id>, Waiting to render <name>… while queued, Rendering <name>…), a job Auto-generate submitted being its line already and never a second (assistant.activity.dedup {jobs, reason: autoReferencesOwn}at each change); each Stopping <name>… while a Stop settles it and <name>: could not confirm the stop: <reason> where its stop was refused or not confirmed, the reason in the words Auto-generate gives it (§6.15), such a line neither pulsing nor counted for Stop, with Dismiss (<line's identifier>.dismiss) where its job offers it: the job’s own Dismiss, never Stop, the line going when the service drops the job and a refusal said on the line (That could not be done now…), which stays. A name is the job’s own, else the entity’s as the open film reads it where the job is that film’s, else its stem, else — a place key naming none — the job’s id; where the place is neitherreference_imagesnor unnamed it follows the name in its group’s own label (Bea · Character sheets, Scene one · First frames, · Clip, · Speech, · Takes). Before them, Material’spsychologybrain pulses (an opacity animation, standing still where the platform asks for reduced motion) while any line runs or stops, dimmed where only an unconfirmed stop is left; with no line the indicator is gone. Stop (assistant.activity.stop, Stop everything) stands at its end while anything runs that no Stop has reached: it stops every held chat’s running turn, each through its own stop, all of Auto-generate’s work, then every other render job that offers Stop, through the job’s own; the chat’s own square Stop still stops that chat’s turn alone. The lines come from the held chats, the engine’s own state and the insertion service’s job views (renderJobsProvider, which sees the jobs published after its first read); nothing is stored. Loggedassistant.activity.changed {lines, reason: started | changed | ended, source, ms}where each change is heard, never by a frame:source: chatsby the dock as a held chat’s turn starts, stops or ends (lines: [{kind: turn, session, chat, entity, callId, stopping}]),source: autoReferencesas the engine sets its lines (lines: [{kind: prompt | render, session, film, stem, place, phase, job_id, stopping, problem?}]),source: renderJobsas the kept jobs change (lines: [{kind: job, job_id, film, stem, place, name, phase, stateCode, stopping, stoppable, dismissable}]), each source with its ownreasonand itsmssince its first line appeared;assistant.activity.stop.requested {lines, source: header | chat}, the jobs asked among its lines, per jobassistant.activity.stop.outcome {kind: job, job_id, placeKey, outcome: accepted | refused | failed, code, error?, ms}, per Dismissassistant.activity.dismiss {job, stem, place, film, stateCode}andassistant.activity.dismiss.outcome {job, accepted, code, error?, ms}, and per turnassistant.activity.stop.outcome {kind: turn, session, callId, outcome: signalled, code, ms}— its control’s cancel, the turn’s end the session’s own lines. -
The box Auto-generate pictures (
assistant.autoReferences) stands in the dock’s top band, under the avatar and above the actions, where an assistant is active. Under it, the sentence naming the work it agrees to (assistant.autoReferences.line) and how the last automatic render went (assistant.autoReferences.last); both wrap, and the band scrolls within its cap. What it does is §6.15;settings.keep.start {key, value}andsettings.kept {key, value, outcome, code, cause, ms}as the other preferences. The picture names its decided plug-in and origin; an LLM prompt writer names its provider and origin too, while Basic is named as free. The writer estimate and picture quote remain; a rented destination supplies its own name. -
Field assistance is a field action of the dock, served by the plug-in host’s catalog service wherever the plug-in host runs; on the web the service is unavailable. A field keeps its dictation microphone; where the catalog in effect offers an action on the field itself, the AI icon (Material’s outlined
auto_awesome, the app’s one AI mark) stands after the microphone as that action’s shortcut, and where it offers one on the field’s form section, the icon stands on the section’s title row, after the badge and before the title (assistant.shortcut.<id>, the first action in catalog order where there are several, its tooltip the action’s label from the package’s strings) — only where the active assistant is available (its package without a refusal and its catalog loaded, the form building again as catalogs load); a field’s icon stands in View as in Edit, with or without a microphone — after it where there is one, at its place where there is none (the prose editor draws a read-only box’s suffix as its builder gives it) — and its press does what the chip’s does — but an action offered on FilmOpen credit is only revealed, its chip pressed by the person to start it, as §9.1’s AI icon says —, refused the same way, with the reason in the dock, where the chip is; a folded section has no title row for one. Where the slide-out covers the page (below the docked width), the chip’s selection is the feedback, and the icon’s pulse is seen where the panel is docked. The field’s own door to the dock is unchanged: focusing a field in Edit, or tapping it in either mode, publishes it to the dock — the innermost field where one holds another (a prompt inside the media card): the prompt for a tap on or focus in its boxes, the media card for its own controls, though focus never left it — (its stem, pair id, apply path and draft — on a View form the entity as read,FrozenAssistantDraft, current while its file’s text is unchanged), and Compare publishes nothing; building or focusing the field gathers no context and sends no request. A field’s action is an ordinary chat that writes: it opens with the field’s label and value, and the assistant changes the field withupdate-entityon the page’s entity, as any change, into the person’s draft — their own version labelledai: …, which they pick when done — and the page opens the version written with the box at its check (below); the chat goes on, and a follow-up changes the draft in place. There is no Finish, proposal or Apply in a chat; the catalog’sfinalizeis the voice conversation’s alone. A changed viewer, project, model, package, grant or credential makes the chat stale; the field’s form going, as its own write takes it away, does not. Credit in the dock and Settings. A provider paid by FilmOpen credit is shown as ordinary information, without a missing-key warning. Readiness follows the selected action. A credit admission failure names the failed step and distinguishes an unreachable server, a refusal and no credit; earlier work stays in the conversation. With an empty composer, Try again sends Continue from where you stopped. through the ordinary send. A drafted correction keeps its words and can be sent normally. Credit waits continue until an answer, a transport failure or Stop; the elapsed seconds stay visible. -
Synopsis voice is a separate mode of a field’s action, with a review of its own, the one the chat’s conversation button opens on Windows, Android and Linux wherever the chat offers a conversation, a talk-only action’s included. OpenAI uses WebRTC on Windows, Android and Linux; Grok retains its Windows WebSocket path. Settings can save keys normally. A catalog assistant is chosen in the dock’s picker (
assistant.picker); its chat’s conversation then opens the voice mode. A successful package Check, a text-model choice or an OpenRouter key is not a voice prerequisite. The provider/model selector lists the routes the assistant’s catalog and this device share, of those implemented here: Grokgrok-voice-think-fast-2.0and OpenAIgpt-realtime-2.1on Windows, and OpenAI alone on Android and Linux. It preselects the Speech service saved in Settings → General (Grok where nothing is saved) where it is among them, else the first, and says so where the saved service has no route on this device; the text-model choice is not read. The host also refuses an unsupported route before reading context or acquiring resources. Opening the dialog is inert: what Apply would do is read from the host without a key, an operation or a peer — a talk-only action says nothing of Apply — and Start is offered once a route is chosen while the person is still at the chat’s place; where the session Start opens would change another version than the one said (the pick moved meanwhile), nothing starts: its own sentence is shown and a second Start is asked for, and that sentence is shown thereafter. Start checks the admitted package and the funding, then begins one session and asks the opening question: on the person’s own key for the chosen provider where one is saved (a malformed one refused as invalid), else on FilmOpen credit where the account is eligible — OpenAI over its WebRTC peer, a chosen Grok without its key running as OpenAI —, else refused for want of a key. Provider voice activity detection handles later spoken turns without Send. Listening, Thinking, Speaking, elapsed time and muted state are visible. Mute releases the input device, including during Connecting; Unmute resumes it. End stops live resources and preserves the transcript, and spends Finish, which then says the conversation has ended. Finish goes through the host’s conversation: the drain, then the finalizer once where the drain finished, its outcome shown underassistant.proposalas Was: and the proposed text — the reviewed change’sbeforeandafter, never a live read — with Apply (assistant.apply,applyImportProposalonce per press) and Discard, or The proposal changes nothing.; closing while a proposal waits asks once — Discard the proposal?; a drain that timed out says the transcript is incomplete and nothing was proposed. Close releases resources before removing the dialog; while the finalizer runs after Finish, every way out waits for it, Close saying why. Errors and Finish feedback remain visible beside the controls, and the transcript follows new turns unless the person has scrolled back. Interrupted generated text is labelled as possibly unheard; OpenAI manages history truncation at its WebRTC output buffer, while Grok uses the measured local playback position.
On Linux an ordinary flutter run -d linux or Linux build, started from a graphical desktop, carries the voice mode. The bundled copy is on unless turned off and asks no consent (§6.11); a deliberate disable remains effective. Linux audio requires microphone, speaker and cleanup acceptance on the actual desktop.
Input captions appear as recognition arrives. Grok cumulative updates replace earlier words, while OpenAI live-caption fragments append within the provider’s stable input item; a completed transcript replaces the partial text and cannot be overwritten by a late fragment. An empty input bubble says Listening, partial words are labelled as subject to change, and a stopped turn awaiting recognition says Finishing transcript. End retains unfinished words as a partial transcript, or says the transcript is unavailable when no words arrived. Muting marks cleared live input as partial; a later final can still replace it. An empty completed input also says the transcript is unavailable. Recognition completion is independent of the assistant starting to speak. The transcript follows updates only while the reader is near its bottom.
Finish and the microphone owner in the dialog (§6.11’s session). Finish is the session’s two-phase finish: the status reads Finishing — waiting for the last words with the elapsed time while input is stopped and the last captions drain (at most five seconds), Mute, End and Finish are disabled, and Close stays enabled and ends the session at once; then the status reads Conversation ended, Finish’s outcome is shown, and a caption the drain did not complete is labelled Partial transcript. A Start the arbiter refuses, because a field’s dictation or the recording’s commentary holds the microphone, shows the sentence every microphone shares, naming the holder (The microphone is in use by another box’s dictation. Finish it, then try again.); while the conversation holds it, a field’s microphone and the commentary say the AI assistant’s conversation has it (§9.6); since nothing was taken, Start may be tried again in place, and Finish is enabled only for a conversation that began. The provider pull-down preselects the Speech service’s effective route (§9.6; Grok where nothing is chosen) where this device offers it, and OpenAI — with a line naming the service and where it is chosen — where the chosen service is unknown or has no route here; it writes nothing back. A Grok the host starts as OpenAI on credit (§6.11a) shows OpenAI in the pull-down and OpenAI’s billing line from then on.
The voice mode has no cap of the app’s on a conversation: it runs until the person finishes or ends it, or the provider or the connection ends it, and the dialog names no limit; a new conversation is a new Start, never automatic, and no allowance accumulates across conversations. Only an explicit Start gesture may create a paid conversation; a failed attempt never retries itself. Key and model access remain unchecked until Start, and the dialog says so. A missing key is saved through the normal FilmOpen Settings using the usual native credential store; Start refused by the key’s store says which state it found, in the sentences already in use — The speech service you chose has no key yet… for a missing record, The key saved for this service is not valid… for a malformed one, The key couldn’t be read from this device… for a store it cannot read. OpenAI input captions use gpt-live-transcribe within the WebRTC call; incoming deltas and the final transcript share the provider’s item identity. Caption errors stop the session. There is no separate OpenAI transcription socket. Usage retains reported units and labelled cost estimates (§6.7), without a price prompt. FilmOpen-funded voice is unavailable in this build. A local timer does not establish a provider charge. This mode creates no character and runs no finalizer of its own accord: its Finish writes no project file, and only Apply writes.
-
Use the LLM: is the one machine model override in Settings → General, independent of Dictation’s speech service. Its first enabled entry, Let the AI assistant decide, is selected by default and removes
assistant.textModel. Every text model offered by enabled providers follows once, local models included, named by the catalogue; usable models are enabled, unavailable ones disabled with their reason. A saved model no longer offered remains visible with its reason so it can be cleared. The menu (assistant.textModel) stays within the field’s width and a fixed scrolling height. Choosing a model stores its id under the same key; an old saved route is read by its model alone. Serialized select and clear writes retain the newest choice across restart. The choice overrides plug-in preferences for future LLM calls; current conversations keep their captured decision.assistant.textModel.select.start/.selectand.clear.start/.clearlog the old and new values, decision reason, outcome, applied state and elapsed time; a failure includes its stage, type and message. Inventory logs its reasons and trials only on change throughassistant.modelandassistant.models, without call-decision lines; an unavailable model asks for a provider key, credit or a local server. Model resolution and assistant route admission are §6.11/§6.11a’s. -
Project landing — opening a source shows the selected project version on its ordinary entity page. A folder needing a project choice asks which version to open; choosing one opens that exact version. A read-only session choice writes no commentary file; a writable choice with a viewer records its pick. Navigating to the project landing returns to the version in use and keeps following it when the viewer changes; an explicitly selected stem remains pinned. Its View/Edit scroll begins with Warnings, folded initially and showing the complete folder diagnostic count; opening it lists every diagnostic in the current language, including on a read-only project page. Zero warnings reads Nothing to report. A folder without a selected project version has a minimal landing with those diagnostics visible without a fold, its first card clearing the floating chrome. There is no separate project summary: General and Synopsis hold the title, kind, genres and text; Epochs holds the declarations and their editing; Other holds Release, the file attributes, contributors and tracks through the uncovered-attribute rule. Category navigation stays in the tree and library configuration in Settings. The warnings’ gesture logs
project.warnings.fold.startandproject.warnings.foldwith old/new state, full diagnostic text, count, reason, outcome and elapsed time. -
Script page (
filmopen_browser/script_page.dart,ScriptSelection) — a title with the count over three tabs, Contents, Script and Workflow: the entity pages’ own tabs (EntityTabs), so the tab shown is told to the assistant as an entity page’s is (§6.11a), and an agent opens them by name,contents,scriptandworkflow. Script is the whole film read as a folder’s Script tab reads it (below). Workflow is the chosen scriptwriting plug-in’s steps (below) under the Create Script card (filmopen_browser/workflow_tab.dart): a pull-down Scriptwriting plug-in (workflow.plugin) — None, then each enabled plug-in that declares a workflow, by its name, in the host’s order, those whose workflow was refused included; no plug-in is chosen for the person, and a chosen one no longer enabled stays chosen as its tag, the tab saying it is not installed or is turned off — and a box Stop a step at USD (workflow.ceiling, No limit while empty), kept as it is typed, so that leaving the page by any door — the tree, another tab, Enter or none — loses nothing: empty is no limit, an amount above 0 is kept (through the preferences’ own lines; an amount equal to the one kept writes nothing), and anything else — 0, a negative amount, words, a decimal comma — is said under the box (Enter an amount above 0, like 2.50, or leave it empty for no limit.) and nothing is kept, words loggedworkflow.ceiling.unread, a number not above 0 refused by the preferences with its reason. The two are this computer’s preferences (workflowPreferencesProvider, §7); on a phone’s width they go one to a row. The plug-ins are read when the tab opens and again whenever the host’s plug-ins change; in place of the steps the tab says what would fill it: no scriptwriting plug-in enabled, none chosen, the chosen one missing, its workflow refused (with the reader’s code and where), or the read failing (with the error, loggedworkflow.tab.failedwith the stage, the plug-in, the focus and the time). Contents is the script as a file manager shows a folder beside the tree: a Contents card (contents_card.dart) of what the project file in use holds, in order — each row an icon, the name, the detail and the warning where one applies, in the tree’s own words (ScriptRowText), opening its page on a tap — with Add folder… and Add scene… over the rows (contents.add-folder,contents.add-scene) and a ⋯ on each row (contents:<entry>, its menucontents:<entry>.menuwithcontents.remove; a second equal entry’s identifier carries#2, and a%, a/or a#the entry itself writes is written as its code,%25,%2For%23— the tree’s rule,scriptRowIdText—, so thatsc_1#2as written is never the secondsc_1). An empty script says what would fill it (EmptyState: Nothing in the script yet — add a scene, or a folder to keep scenes in), and an empty card that cannot be added to says why under that sentence (EmptyState.detail, the lock’s reason): the buttons’ tooltips need a hover, which a phone has not. A second card, Not in the script, lists what the tree’s row of that name lists and is absent when nothing is unfiled; its rows are flagged, as the tree’s are, and their menu’s Move to… files the entry, which no list holds. The two cards’ rows are marked as the tree’s are, as a file manager marks (§8.1): a mouse click marks the row and makes it the anchor without leaving the page, shift-click marks the range over both cards’ rows as drawn — Script’s, then Not in the script‘s, never a folder’s own contents — and control- or command-click adds or takes away; a second plain click on the same row within the double-click time, nothing clicked between, opens its page, timed from the pointer’s own events so a single click is never delayed; a touch, and a tap with no pointer (a screen reader’s, an agent’s), opens it as before. A marked row is tinted as the tree’s, and Move to… in a marked row’s menu moves every marked row (§9.2); the Contents has no drag. The marks’ ids are the tree’s (scriptRowIds), thecontents:<entry>identifiers unchanged; a row’s opening logscontents.row.opened {row, mark, by, outcome}. A folder’s page’s Contents is not marked. Both cards readscriptOutlineProvider’s walk, never one of the page’s own. The card is never a second write path: its buttons and its menu areScriptActions(§9.2), the same object the tree’s menu calls, and on a version that is not the viewer’s the buttons are disabled withPageAccess.childrenLock’s reason as their tooltip — inside the button’sSemantics(identifier:), so that the reason is the button’s own in the semantics tree and not the card’s. An add into a version of the viewer’s own that the script does not draw — an older one, chosen in the version pull-down — says so (…a version of … that is not the one in use, so it is under Not in the script), since the new row then turns up beside the script. -
A folder’s page is the entity page every type has — the header and the version row, View/Edit and Compare. On View/Edit it shows the same Contents card, for the version on screen — the list the script draws for it, the viewer’s assistant draft’s where there is one (§5.12), which its Add writes and is checked against, logged
contents.add {type, container}— above the form (EntityDataTab.leading,EntityForm.leading: the card is the pages’ and the form the forms’, which may import neither the tree nor the pages, so the page hands the card in as a widget), then the folder’s form through the one type door (folder_edit_tab.dart): Name; Kind, the vocabulary as chips (pathChoiceFieldoverfolderKinds, a token the vocabulary does not know shown as a chosen chip of its own, as written — chips and not a pull-down, the kind being a word the app makes nothing of, which a line under them says: A word for what the folder is. It decides nothing; tap the chosen one to have none,pathChoiceField‘shelper); Synopsis and Notes, multi-line; its pictures (the media card, Reference images) and its visual treatment (below); then the plug-ins’ cards forfo(pluginSectionsFor), then Other attributes.childrenis drawn once on the tab — as the card (hiddenKeys) — and as one row in Compare, where copy-left moves the list as it moves any list. The card’s rows arecontentsOf(outline, project, version): the script’s own rows for that version where the script draws it, so that a second listing in another list is flagged here as in the tree, else the file’s list alone (childRowsOf) — another author’s version on its own page. The form’s trailing Notes card is not drawn where a field has the key. -
The project’s kind is set and cleared on the project’s page, the one place it is asked: a pull-down of §8.1’s four words and None, which removes the key; a word this app does not know is an entry of its own, as written, with the sentence that says so, until another is chosen, as every pull-down of a token says it (above); a legacy spelling shows as the kind it means, on Edit as on View, and stays in the file until the kind is really changed — choosing the entry on show again writes nothing, and what is then written is the kind’s own name, never the legacy token (
franchisetyped by an agent is writtenseries: legacy tokens are read, never written); akindthat is no word — a number, an object — is drawn as it is, locked (ShapeLocked), and nothing writes over it; an emptykindreads as None, with no entry of its own, and choosing None removes the key. An agent types the word intofield:<stem>:kind, or nothing; a word that is no kind writes nothing. It is a word and chooses nothing:childrenis untouched. -
One token out of a list (
filmopen_forms/form_fields.dart,tokenChoiceField) — a scene’s Location and Epoch, a location’s Located within and a project’s kind, Aspect ratio and Resolution are one pull-down, drawn the same way wherever it is asked, each page keeping only its own rule (its words, what an agent may type, how it reads a legacy spelling); its key may be a dotted path (format.aspect), where None takes out an object it leaves empty, and an entry may carry an icon (an aspect ratio’s shape), which the box shows while that entry is chosen. Where a page keeps the shared setter (tokenSetter; not the project’s kind, whose rule is its own), an agent’s word the list does not have writes nothing and logsform.token.refused {key, reason: not_listed|shape}. The way: a key that holds nothing — absent, empty or blank — reads as None, with no entry of its own; a token the list does not have is an entry of its own, as written, and the sentence that says so stands under the box as an error while the field can be written and as the locked value’s helper when it cannot; the field’s own helper is said in both modes, which differ in what may be typed and in nothing else; helper and error wrap (kHelperMaxLines); choosing the entry already on show writes nothing — except where the key is there holding nothing, where None takes it out; and a value of another shape — a number, an object — is drawn as it is, locked (ShapeLocked), which no door writes over, an agent’s included. The Name box every form starts with is one field too (nameField): the type’s icon, Title on a project, and an empty box an error rather than a removal (§7). -
A scene’s form (
filmopen_forms/scene_edit_tab.dart) — a scene is a series of events at a time and a place, so its form names one location and one epoch and lists its characters, through the one type door and in two modes like every form: Name; Synopsis, multi-line; Location (sceneLocationField): the project’s locations in a menu, each by its name in use with its short name, and New location… last (field:<stem>:location.new), which opens the tree’s New location through the page (EntityCreation,EntityCreationScope: the forms may not import the tree, and Compare offers none), the scene’s epoch offered first, and puts the new location in the scene — its epoch aslocation_epochwhere it is not the scene’s and the project still declares it, none where it is the scene’s or the project declares it no more (attachedEpoch); with no location in the project the control opens New location at once; unset it says Choose where this scene happens; Clear location (field:<stem>:location.clear) removes thelocationkey alone — an unresolved one too, in a project with no location as in any — never a location’s file; choosing another location writes its short name and clearslocation_epoch— it resolves at the scene’s own epoch — while the one there already writes nothing; a box that can neither choose nor make one — Compare’s column, in a project with no location — says why in its tooltip; a press on the box takes the focus, so that the keyboard’s keys go where the person last pressed, and the box keeps it while a location is being made — a press meanwhile does nothing — so that the dialog gives it back when it closes; what a scene’s boxes said, and their focus, stay with that scene’s file and never show on another’s; a long name in the menu wraps and is cut on its second line; Enter or Space on the box opens its menu with the entry chosen, else the first, focused, so that the arrows move in it and Enter chooses, the box having the focus back as the menu closes — the Epoch’s alike —; where a press makes a location rather than opening a menu, the box shows a+, not the menu’s arrow; both boxes are buttons to assistive technology; what a box said after a creation stays only while it holds — the field’s next change ends it, and the sentence for a draft that could not be written first ends once the draft is the file’s again — and is said as a message where the field is gone by then; an agent’sset_fieldon Location writes as the menu does, another location clearinglocation_epoch; while empty the box’s words are its semantics’ value too; Epoch (sceneEpochField): Default while the scene names none — the project’s default, which is no entry of the menu — and the declared epochs by their labels, each once, a label two of them share followed by its token; choosing one writes its token; Delete or Backspace on the focused control, or Clear (field:<stem>:epoch.clear), removes the key and returns it to Default, never touching an epoch of the project; Characters, one row percastentry — the character’s name in use and short name, a remove button — and Add character, a menu of the project’s characters the scene does not list yet, each item about 56 px tall with the character’s 40 px picture (pictureInUse: at the scene’s epoch, else where its name is read; its icon where it has none), its name and, muted under it, its short name, and New character… last (cast:<stem>:add.new) — the menu’s one entry where every character is listed, and the button’s own press where the project has none — which opens the tree’s New character as New location… does and puts the character in the cast once, its epoch on the entry where it is not the scene’s and the project still declares it (Compare’s columns, which offer no creation, keep the menu of characters, disabled with the reason where there is none to add); Notes, multi-line. A creation from the scene keeps the scene (createForScene): the scene’s draft is written first, and one that cannot be stops it with nothing made (Nothing was made: …); the page stays on the scene; the answer goes only where it was asked for — the same project, the same username, the scene’s form still open for writing — else the new entity stays, made, and the scene says so (“…” was made, but not put in this scene: …); the attachment works on the draft as it is when the dialog answers, so what was typed meanwhile and every other entry’sepoch,outfitandnotesstay; once attached the draft saves as every change does, and a refused save is the form’s to say, with Retry, which writes the attachment again and never makes the entity again; Cancel writes nothing. Every way out is logged once,scene.create.decided {scene, type, key, outcome: noSaver|unsaved|noProject|unmounted|cancelled|attached|notAttached, reason, stem, ms}, the entity made named where there is one and the reason saying which change kept it from the scene. Where the project declares more than one epoch, a character’s row has an epoch pull-down of its own, labelled Character’s epoch so that the form has one box called Epoch — the scene’s, As the scene by default, which writes the entry’sepoch(the sample’s Kira is30yoin a scene set in the1990s); a project with one epoch shows none, neither as a pull-down nor as a locked value, and an entry that names one keeps it (kept, and shown only where a project declares more than one epoch). Where the character has two or more outfits at the entry’s epoch, else the scene’s (the project’s default where it names none), each counted, offered and named as its version in use there for the viewer — one deleted there left out, one nothing resolves for offered by its short name — the row has an Outfit pull-down beside the epoch’s (cast:<stem>:<index>.outfit): Default outfit first — the character’sdefault_outfit, which writes no key and removes one — then each outfit by its name and picture; a choice writes the entry’soutfit(castWithOutfitAt); the three parts share the row and stack below 560 px of pane. What an entry holds beside that —notes, anything else — is never touched (§5.13). Each use of Add character‘s menu, the speaker’s and the outfit’s is logged — its opening, however it opens (a press, the keyboard, a screen reader’s expand), then one outcome with its time, a collapse a cancellation; a press that makes a character at once, or finds no anchor for the menu, one line of its own (cast.picker.*,script.speaker.*,cast.outfit.*). A short name the project has nothing for — a location no file has, an epoch the project does not declare, a character nobody wrote — is shown as written and said, under the pull-down as an error on the viewer’s own version and as the locked value’s helper on another’s, as a location’swithinis; acastthat is not a list of entries that name a character is drawn as it is, locked (ShapeLocked), and never written. Then the plug-ins’ cards forsc(pluginSectionsFor: the Template plug-in’s note is one), then the Dialog and actions card (the keyblocks), which on the page is where a scene’s script is written (sceneBlocksSections,filmopen_forms/script_editor.dart‘sScriptEditor): one field,blocks, on the form’s own draft and its one saver, editable exactly where the form is (FormScope.enabled— the viewer’s own version, or anybody’s in development mode, with the page’s guard line), drawn whereverblocksis a script — absent or empty too — and as the key’s one locked value where it holds the ids a cue writes or is no list. A scene with no block shows one, local, where the form may write: the placeholder, drawn as a first block open on its fields, with no ⋮ and no handle, and named as one (script:<stem>:1), which the draft does not hold — a visit, a focus, a speaker chosen, a duration and blank typing write nothing, neither an entry nornext_block, and a speaker or a duration chosen there goes with the page; its first nonblank dialogue, direction or action makes it the scene’s first block in one change — the next number of the scene’s own count (Project Specification §10.3), with the speaker, the duration and every box’s text — the focus and the caret staying where they are; Add block makes it the block as it stands; a microphone on it takes no number, its accepted words being its first text; the registry names it nothing until it is a block, so that an agent types into it through its box. A placeholder that comes as the scene’s last block goes — deleted, moved to another scene, gone with a re-read — starts empty, never with that block’s words; a block another change brings to its place opens as any block does, only the placeholder’s own first words keeping their card open; while a speaker or a duration waits on it, a line under it says it is not kept yet; it keeps the room a block’s ⋮ and handle take, so that its first words change no box’s width under the caret; with no character in the film, its speaker says that Add character, above, makes one. Where the form may not write, the card says This scene has no dialog or actions yet. and that its author writes them, or a fork, which the Script tab of such a version says too, naming no Edit tab it does not have. A card per block, condensed to one line — the dialogue, moved right of its speaker’s mark (the first live reference image of the character the speaker resolves to, its thumbnail else its file, about 18 px, or the character’s icon where none resolves or there is none, over the speaker’s name in the smallest style, one line cut short, the whole name its tooltip; laid in the line’s own height, so the row is as tall as an action’s; a picture that cannot be read drawn as the icon and logged once per character and reference,script.speaker.picture {stem, reference, error}); or the action; or the direction in parentheses, after the speaker’s name in capitals and bold; or that name alone, a speaker chosen and nothing said yet; or (empty block) — until a field of it holds the focus, a menu of it is open, or it was just added or clicked: then the speaker’s pull-down with the Dialogue box beside it (under it in one column), Direction for character and Action (multi-line) — each saying what goes in it while empty, e.g. look into the camera and e.g. Cars are passing by under the boxes’ labels, wrapping rather than cut where the box is narrow, its label kept above it — the block’s box alone says Direction for character: any otherdirection, a shot’s key light, keeps its own word — and Duration with the hint auto; the block’soutput_duration_secis not shown here, but kept as it is. A click on the line opens the block with the focus in the field the line shows; Tab reaches the line and opens it on its first box; Shift+Tab reaches its ⋮ first and opens it there, so that it walks back through every box as Tab walks forward (a locked block, which has no ⋮, opens on its own Duration, Shift+Tab then passing over its beats’ and subtitles’ boxes). Tab then walks the open block’s controls in the order they are built — the speaker, Dialogue, Direction for character, Action, Duration, each beat’s boxes and its ⋮, Add beat, each subtitle’s boxes and its ⋮, Add subtitle, then the block’s ⋮, no microphone being a Tab stop (§9.6) — and the block stays open until the focus leaves the card (Tab past the ⋮, or another field) or a mouse’s click lands outside it, which also takes the focus from a button of the card; a click in the card keeps the focus, and nothing condenses it while a press is under way in the card or one of its menus is open. Where the focus went decides: a focus parked on a scope — the window losing its focus to another application, the app suspended, a box letting the focus go on a press outside — is not leaving, and the block stays (script.block.kept {stem, block, index, reason: focus_parked, primary}), its box given the focus back when the window has it again (kept {reason: focus_returned}), or kept where the focus lands on what does not count as leaving — a route, a menu (kept {reason: focus_excepted}); the next node to take the focus decides, so a finger’s tap on another block — which on the desktop parks the focus a frame before moving it — condenses the first block then (focusLeft). The timeline in charge — the editor’s side of the state’s script focus (§7), through which a timeline marks a press as its own (dockPressed), takes the charge (following) and asks for a block: a press so marked condenses nothing (kept {reason: timeline_press}); while the timeline is in charge, the block it asks for opens — found by its id where the list is now, so a block moved before the frame opens where it went and one gone opens nothing (script.focus.skipped {serial, reason: gone}) — without the keyboard’s focus, scrolled into view, and the one open before condenses (reasontimeline), each request answeredscript.focus.answered {stem, asked, block, index, serial, outcome, ms}—opened, naming the block opened and its place,missing(no such block here, ignored),not_following,other_scene,unmounted— and one overtaken by the next in its frame loggedscript.focus.skipped {reason: overtaken}; a press in the editor, the person’s focus arriving in a card (not the window’s focus coming back), or a press elsewhere on the page, which condenses the open block as before, take the charge back (script.follow), and the person’s focus arriving in a card — Tab, a finger’s tap — condenses any other open block (focusLeft), a block the timeline opened holding no focus to lose. The block open is published as that script focus after the frame that opened or condensed it; an opening whose card went or changed before then is not, loggedscript.focus.skipped {reason: unmounted | changed}, as is any publication whose scope is gone (scope_gone). Opening is always an act — a click, Tab or Shift+Tab into the block, a block just added, a speaker chosen: the focus coming to the ⋮ after a move leaves the moved block condensed, and a block that comes to an open block’s place — after a move to another scene, a delete, a re-read — is condensed there. A block stays open behind a popup above the page — its microphone’s, a dialog — and a click in the popup is not outside it, so dictated text lands in its box and the focus comes back to the box that had it. Each box that can be typed into has its microphone (§9.6) — Dialogue once a speaker opens it, Direction for character, Action and Duration — dictated text going in as typing does. Dictation is bound to the block, never to its place: a block with no id takes its number as dictation starts, as at its first change, and the words go in only where the block at that place is still that block — where another block came to its place (a re-read after another hand’s insert), or the block moved or went — told by the id at its place —, they are dropped, the popup closes saying Dictation discarded: the block changed, nothing is written, and no box takes the focus. Where the page itself went, the box went as any box goes: the popup stays, says so, and keeps the words to copy. The usage record names the block’s field asblocks.<id>.<field>where its id isbl_<n>, else asblocks.#<place>.<field>: a block’s id is the file’s free text, and nothing of a file’s free text goes to a log. The speaker’s pull-down lists No speaker (which removescharacter), then the characters the scene names — its cast’s and its blocks’ speakers — in the text colour, a line, then every other character of the project, muted, each group by the name shown (the Script tab’s naming rule,speakerName), each about 56 px tall by its picture as in Add character; a short name nobody wrote is shown as written and said under the box; choosing writescharacterand nothing else, never the cast, and puts the focus in the dialogue. The dialogue box is open exactly when a speaker is chosen; without one its text stays legible, locked, with Choose a character to write dialogue. Duration takes a number of seconds above zero and a day (86 400) at most — finite, so that hundreds of digits, which parse to infinity, are refused with the rest — with a dot for decimals (4., on the way to4.5, is written as 4, the box keeping its text); anything else is said inline and not written, the refusal kept with the block it was typed for, so that a re-read putting another block at its place shows that block’s own value; an emptied box removes the key (auto). A block’s beats (Project Specification §10.3) are listed under its duration and above its media, indented: each a row of Position — Before the line, During the line, After the line, shown only where the block has dialogue, a position outside the three shown as written —, Beat (prose, the block’s @-mentions), Duration (parsed and refused as the block’s own, hint auto), a ⋮ (Move up, Move down, Delete beat) and a handle that reorders the block’s beats only, the beat’s words travelling under the pointer; Add beat under them adds one at the end, During the line where the block has dialogue. Each edit is one change of the draft. A block’s boxes are made when its card first opens, or an agent types into it, and seeded then from the draft (script.box.made {stem, box, seed, live}), so that a block whose card never opened holds none and a change seeds only the boxes made; they are kept by block and beat place, so a beat added, removed or moved, and the block’s own move, seed them again from the draft and drop their refused durations. Beside the duration (under it in one column) Beats: 7 s, their seconds’ sum, where the block has beats; in the caution colour with longer than the block’s duration where the block’s duration is set and the beats’ seconds add up to more; under the list, where the block has dialogue, A beat before the line follows one during it. Neither warning refuses anything. None on the placeholder, which is no block yet; locked, the beats are read-only, with no ⋮, handle or Add beat. Every beat edit logsscript.beat.<op>.start(opadded, changed, removed or moved,sourceperson or set_field,blockby its log name,k,to,field, thevalueentered, thebeatas it was) and one outcome —.ok(outcomeapplied or unchanged, thebeatas written),.refused(reason: the helper’s code,durationfor seconds not given, or an agent’sbeatOutOfRange,unknownField,unknownPosition, with the key, the value and the beat it would have replaced) or.failed(stage,cause,error, the error’s message), rethrown — each withms; the beat’s menuscript.beat.menu.opened, thenchosen(action) orcancelled, and its position’s pull-downscript.beat.position.opened, thenchosenorunchanged(positionas chosen) orcancelled, each withms; a beat’s description is logged as written; a warningscript.beats.warned(reasonorder or total,total,duration) once as it comes andscript.beats.cleared(ms) as it goes; each of these lines names its block asblock(its log name) andblock_id(its own id), the clearing line naming the id the block had when it was warned. A block’s subtitles (Project Specification §10.3) are laid out exactly as its beats, under Add beat: each a row of Position (as a beat’s), Subtitle — plain text, a newline a line break, no @-mention, since the words are burnt over the picture as typed —, Duration, a ⋮ (Move up, Move down, Delete subtitle) and a handle that reorders the block’s subtitles only; Add subtitle under them, During the line where the block has dialogue. Subtitles: 5 s stands under Beats: 7 s beside the duration where the block has both, in the caution colour with longer than the block’s duration as the beats’ does; under the list A subtitle before the line follows one during it. Their boxes are kept by block and subtitle place, as the beats’ are. Beats and subtitles are one editor: every subtitle edit, its menu and its pull-down log as a beat’s, underscript.subtitle.*with thesubtitleandsubtitleOutOfRange; a warningscript.subtitles.warnedandscript.subtitles.cleared. A subtitle’s words are logged as written. Each card’s menu: Move up, Move down (disabled at an end, saying so; the focus follows the moved block), Insert block after, Move to scene…, Delete block (no confirmation, and the block’s number is never given out again in this file, Project Specification §10.3); a drag handle reorders, the block’s line travelling under the pointer. The ⋮ and the handle stand at the card’s right, and under an open block’s fields where the form is one column (below 560 px of pane, §9.1), so that the fields take the card’s whole width — Direction for character whole at a 700 px window; the placeholder keeps their room in the same place. A later block of an id the file uses twice, and an entry that is not a block, are drawn locked as JSON with what to do, and only Delete block; the first block of a doubled id stays editable by hand, while no agent’s setter and no move can address it. A speaker chosen stays on the condensed line while nothing is said yet (beside the direction, or alone), never beside an action; a film with no character yet says under the speaker’s box where one comes from. No block id is ever shown. An open block’s two frame places are always shown, each under its Prompt card, First-frame prompt above First frames and Last-frame prompt above Last frames (_FramePromptinscript_editor.dart, the place’s header; the writers offirst-frameandlast-frame, given the block’s id), whether or not a frame place holds anything: a prompt is written before its image. A Clip place, where shown, takes the block’s clip prompt as its header the same way (the writers ofclip, the block’sprompts.clip), yet is not always shown: Add a place… offers it, and it is shown where it holds a take or where the block’sprompts.clipis stored, a prompt being written before its clip; its@offers what the frames’ cards offer (the film’s characters’, locations’ and props’ pictures), and its writer is handed the block alone and no tokens, since the page knows no model to take them. Its boxes,field:<stem>:blocks.<id>.prompts.<kind>.positiveand.negative, are prose boxes in the block’s tap group whose@offers the film’s pictures (§9.8), and write the block’sprompts.<kind>throughwithBlockPromptas a keystroke does, a changed word taking the prompt’spluginaway (loggedprompt.pluginCleared); the holder whoseupdateda frame’s prompt is read against is the block; its Generate writes through the scene’s draft (ScriptEditor.binding, the form’s saver). Its media, under its fields (Project Specification §10.3;_BlockMediainscript_editor.dart): the two frame places, and a media place for each of the block’s other four groups that holds entries, in the table’s order — Clip, First frames, Last frames, Speech, Music, Sound effects — each aMediaPlaceon aBlockPlaceof the block’s id and the group, editing through the scene’s draft as any keystroke does, telling its observers no key; then, where the block may be written, Add a place… (media:<stem>:<block id>.place) naming those of the four not shown, which shows an empty place to add to and writes nothing, loggingblock.place.opened {block, offered}, thenblock.place.chosen {block, group, ms}orblock.place.cancelled {block, ms}. The condensed line shows no tile. A block without an id, or of an id the scene gives twice, has no media and no Add a place…, since no place could name it. Each block opened or condensed is loggedscript.block.opened/script.block.closed {stem, block, index, reason, ms},msthe time it spent in the state it leaves, the reason the act —tap,tab,shiftTab,added,typed,speaker,timeline;focusLeft,tapOutside,timeline,blockChanged(another block came to its place, named as the block that closed),listChanged,disposed(its card went while it was open) — the block by its id as it is, its index beside it. The places are drawn only while the block is open, so what a place says of itself — a failure, the archived fold, a group just shown and still empty — goes when the block condenses; the queue keeps every job and its failure, and a job comes back with its block. A block’s take wears no new mark. Move to scene… (filmopen_forms/script_move.dart,showMoveBlockDialog; its trailscript.blockMove.start {film, from, block, to, toName, viewer, forks},script.blockMove.stage {stage: settle | read | recheck | target | remove, outcome, ms}andscript.blockMove {film, from, block, to, toName, viewer, outcome: refused | copied | moved, reason, stage, ms}with the values that decided it — a coverage’sposition,first,last, thecoveringentry with its group and owning block, and the block’scontent, the target’s refusal, the versionwritten) lists every other scene of the film, one line a scene, in script order (scriptOutline: the script, then what is not in it), its folders as headings, a scene listed twice drawn once, an entry nothing resolves for left out, a scene whose version in use is in the recycle bin left out (Project Specification §7) — and a destination deleted after the list was drawn refused before any write (That scene is now in the recycle bin, so nothing was moved.) —, each named as the tree names it for the viewer; the target is the version in use for the viewer (pickAt) — written where it is theirs, forked where it is not, the line saying will fork {author}’s v{n} and the button Fork and move, else Move. On confirm, in this order: every open draft written (one that will not stops the move); the block read again from this scene’s draft as it now is, by its id; a coverage across it refused there, before anything is written — the block’s own entry whosespanslists another block, or another block’s entry listing it, read through the format’s readers, a deleted entry covering nothing — with Media of block {n} covers blocks {n} to {m}, so this block cannot move to another scene., the blocks named by position, while an extent (until) or an entry of its own block alone moves freely, the block’smediatravelling with it as content and the film’s records staying where they are; the target read afresh and its content built from that read, the block copied under the target’s next number with every other key (output_duration_secamong them); the target written first, once —WriteActions.trySaveOveron the viewer’s own version,tryForkWithholding the new content otherwise — a refusal (changed on disk since that read, the film gone) said in the dialog with Nothing was moved; the block compared again with what was copied, a change meanwhile stopping with the copy kept and nothing removed; then the block removed through the form’s draft as a change of the list, the draft settled, and the dialog closes with Moved to {scene} (script.blockMoved: the two stems and whether it forked) only where the removal’s save was not refused and the scene’s file, read back through the source after it, no longer holds the block — never the project’s index, which overlapping re-reads can leave older than the block’s own addition. A refused save — the saver left failed — is said as the copy kept, with the refusal, whatever any index says; the saver’s error alone decides nothing, since it keeps an older failure after a later success, and a write that landed after another’s failure holds the removal; a save that landed while more was typed is a move, since the file has the removal. The page’s draft must be the one open on the scene’s file (OpenDrafts.of): asked once the drafts are written and before the target is, where a page that went to another file, or away, refuses with Not moved: the page changed before the move. and nothing is written anywhere; and asked again before the removal, where the block stays in this scene and the dialog says so, naming it, since the page under the dialog may show another by then — The block was copied to {scene}, and {source} still holds it: delete it in {source}. — as it does wherever the file still holds the block after the settle. A save refused after the copy says so in the dialog — the block is already in {scene}; this scene’s file still holds it until this save goes through — and the card’s save status shows the failure. The dialog cannot be closed while a move is under way (neither Escape nor a click beside it), and an outcome reached after it has gone is said on the page; the list is drawn from the film as it is now, so after a move that stopped half-way a fork made for it is the version in use. Move is disabled, saying Choose a scene first, until a scene is chosen. A crash between the two writes leaves the block in both scenes, never in neither. Disabled, saying why, on a block the file gives no id yet (Type in this block once before moving it), on the first block of a doubled id, and on another author’s version open in development mode (Blocks are not moved out of another author’s version, even in development mode: fork the scene to move its blocks.): a move removes the block from this version, which must be the viewer’s own. The editor owns no draft and no writer: a keystroke reaches the saver as a change that tells the draft’s observers no key — the whole script never travels with a letter — and a change of the list tells themblocks. Its boxes are its own, by position and field, and re-seeded only where the draft changed under them, so a caret elsewhere stays.blocksis never under Other attributes (hiddenKeys, said outright and not asked of the file), and never copied as one value; nor is the scene’snext_block, the format’s own count, which the card’s field owns so that it is shown nowhere, and which copy-left never moves on a scene (uncopyableKeys), whatever either side holds — a version with noblockskey included: a copy could lower it and let a deleted number come back — then, folded in Other, Other attributes with everything else the file holds (story_day,time,props,location_epochand the rest). In Compare the rows pair — Location, Epoch and Characters are one row each, and copy-left movescastas it moves any list — and the blocks are a row per block, paired by id as before and labelled by their place in that version,#n, never by their id. An agent types into every field (FieldRegistry), a block’s included —blocks.<id>.character(a short name, or nothing),.dialogue,.direction,.action,.duration_sec(a number, or nothing), each through the editor’s own setter beside the form’s, false (the agent’snot_found) for an id the scene does not have or gives twice, and for a dialogue no speaker has opened: a short name intolocationandepoch, or nothing to remove the key, a name the project does not have writing nothing (the one the file names already may be typed back, which changes nothing); andcastone short name per line, a line that names neither a character of the project nor one the scene lists already refusing the whole text. Identifiers:field:<stem>:name|synopsis|location|epoch|cast|notes, a rowcast:<stem>:<index>with its.epoch,.outfitand.remove,cast:<stem>:addand the menu’scast:<stem>:add.<short name>; a blockscript:<stem>:<n>(the n-th as listed, 1 first, its label the condensed line) with, once opened,.character,.dialogue,.direction,.action,.duration, and on every block.menuand.handle;script:<stem>:add; Move to scene…’sscript.move:<short name>,script.move.confirmandscript.move.cancel. Locked — on another author’s version without development mode, the library, a read-only project — the blocks are the same cards, condensed, with no ⋮, no handle and no Add block; an opened one is read-only and legible, its speaker the box a pull-down leaves behind and no validation shown. -
Category view — a list of every entity of one type with tag, epoch and version counts and an “official set” chip. The character, location and prop pages carry Add character / Add location / Add prop (
category.add, its words the page’s type) beside the title and the count, and under the empty state when there are none; disabled with This project is read-only here on a project that cannot be written. It opens the new character / new location / new prop dialog (filmopen_tree/new_entity_dialog.dart— the tree’s row menu opens the same dialog for a folder and a scene, §9.2, and the tree may import neither the forms nor the pages, while the pages import the tree), laid out as the New project dialog is: a sentence saying the filename is permanent — The filename is permanent, so pick something generic like “main-hero” if the name isn’t decided yet. for a character, The filename is permanent. for a location and a prop; the Filename in aShortNameField— a segment of every file name of the entity, which its tooltip says, with main-hero as a character’s example while the box is empty and focused, never a value — refused inline when the type already has that tag here or in the library; the name (optional — an empty box writes the short name as the name, since §7 requires one); a character’s Role (the keykind: a pull-down of A.1’s vocabulary, nothing written when none is chosen) — a location is made without a kind, its form setting one later; a prop’s Kind, a pull-down of A.3’s ten in A.3’s order with None first and chosen, nothing written when none is chosen, since A.3 requires none (a prop is an ordinary library entity at an epoch: its file and its pick, no folder); and the epoch, only where the project declares several, the default first, or the scene’s where a scene asks (below). No username box: the file is written as the username in effect (§9.4),defaultwhere none is chosen, read once a change of it in flight has landed; a change made while the dialog was open is said inside it (Your username changed from … to …), what was typed kept, and the next Create writes as the new one. Create is disabled until the filename is valid and free, and says why in its tooltip, the reason naming its box as New project’s does (Filename: …; on the button’s own semantics node); the short names a list already names (takenChildTags, which walks every list of every version) are read once for an index and not at every keystroke; a refused write is shown inside the dialog — a file of that name written since the index was read as the same already taken sentence, the refusal reading that one file into the index (§7.5), so the name then shows as taken while it is typed. Once written the page moves to the new version, whose first tab is Edit — from the category page; a scene that asked stays on screen. -
Epoch overview (
detail/epoch_overview.dart) — what a library entity chosen as a whole shows (no epoch, no version: the group is not a file, §8.2): for a character or a location, one card per epoch the project declares, in its order (a hand-written token that is no filename segment excepted), and after them any epoch the entity has that the project does not declare; any other type, which the writer does not copy between epochs, one card per epoch it has. A card with a version has the picture (no type or tag chip under the heading) (the version in use’spreview, else itspicks.sheet, else the first image take at that epoch), the epoch named as everywhere — its label, else its year, else, for an unlabelled declaration, its token (epochNaminginfilmopen_state/tree_builder.dart) — with the year beside a label and no token chip, the age for a character (filmopen_format/character_age.dart’sageAt: the file’sappearance.age, a whole number or N of"N (YYYY)", none for a phrase), the version in use and the version count; it opens the epoch, and the epoch row has an All epochs chip back. A card with no version yet names the epoch and says so, and offers a copy: Based on (overview.<epoch>.basedOn), a pull-down of the epochs at which the entity resolves for the viewer (FilmProject.pickAt), the first in the project’s order chosen, and Create (overview.<epoch>.create), which makes the version in use there the viewer’s v1 at this epoch (ProjectWriter.createAtEpochthroughWriteActions.createAtEpochandProjectController.createAtEpoch): a complete independent copy naming its source inforked_from, picked where the pick can be written and otherwise in use by the ordinary rule, the card filling after the re-read and a message naming the epoch’s label. The source is bound at the click, by stem and by text, and checked again in the entity’s commentary queue: a pick completed meanwhile, the same version re-read with other bytes, the epoch no longer declared or a version made there meanwhile, another handle or another film refuses with nothing written — a changed binding says What this was based on changed meanwhile: check it and try again, changed bytes the stale-file sentence, and a source in use nowhere any more that nothing resolves; where another film is open by the time the copy is written, nothing is said over it, a refusal included, and a refusal’s Re-read files reads through the app, never the card, which may be gone by then. Where versions exist and none is in use for the viewer at any epoch, the card says No version of this character is in use at any epoch: pick one first and offers no copy. Create is disabled, saying why, wherePageAccess.canCreateVersionsdoes not hold or the version to copy was not read as text. A blank first version is not made here: that is Add character / Add location’s, since the overview exists only for an entity that has a version. A character’s outfits (outfit_list.dart): every card of a character, with a version or without, lists beneath what it holds the outfits at that epoch and no other — each by its picture (as a card’s), its name as its version in use there says it — its id where nothing is in use there — and that version, a tap (overview.<epoch>.outfit.<tag>) opening the outfit at that epoch — or says that none is yet, and ends with New outfit (overview.<epoch>.newOutfit), which asks a name only in a dialog naming the character and the epoch (outfit.new-name, Create — a target of standard density, 44 px at least — disabled until one is typed, a refusal cleared by retyping), bound to the film it was opened in: another film by the time Create is pressed refuses in the dialog before anything is allocated, Create then disabled with that refusal as its tooltip and Cancel the way out: the id is allocated by the writer (ProjectWriter.createOutfit,<character>.<owner>-<n>), never typed. It runs throughWriteActions.createOutfitandProjectController.createOutfit, in a queue of the character and the viewer’s owner (§7), bound at the click to the film and the viewer: the new outfit’s page opens at that epoch with a message, and where the id was taken meanwhile — creation writes no file the person has open, so a changed file there means another outfit took the place — the folder is re-read and the creation tried once more with a writer on the new index, a second collision being said in the dialog as that (Another outfit took that place meanwhile), never as a changed file; a character no longer there, a refusal of its own; an epoch no longer declared by then, the undeclared sentence below; any other failure the dialog’s own sentence, never an error’s text; a film closed or replaced by the time it answers hears nothing of it, and the dialog closes. The heading’s help says, for a character, that each card lists its outfits, and that New outfit adds one where one can be made. A declared epoch already has its card; one the project does not declare where only the character’s outfits are has a card of its own after the declared ones, its outfits listed; where the project declares epochs and not this one, the card is for reading — its line says so in place of No version at this epoch yet, and Based on, Create and New outfit are disabled, each with the same reason as its tooltip (The project does not declare this epoch: what is here opens, and nothing new is made at it), since the writer and the controller refuse an undeclared epoch and the project’s form declares only epochs it allocates; the heading’s help then promises no copy there. A project declaring none makes no version at a missing epoch either, since the controller copies one only to a declared epoch: Based on and Create are disabled with The project declares no epochs: a version is made at an epoch once the project declares it as the card’s line and their tooltip, and New outfit stays, as the writer makes an outfit at any epoch of such a project. New outfit is disabled, saying why, wherePageAccess.canCreateVersionsdoes not hold. Below 720 px of pane (Breakpoints.overviewTwoColumns) the cards are one to a row. On a character whose every version is discarded New outfit is disabled at every epoch, the overview passing the reason in as it passes its others: Every version of this character is discarded: no outfit can be added. An epoch whose every version is discarded keeps a card with a version — the card and Create read every version, the discarded included, so no Create the controller would refuse is offered there — whose line names the versions (Every version here is discarded: ann · v1, bob · v1), and counts them all, with no action: nothing brings a discarded version back. A missing card of an entity whose every version is discarded says so — there is nothing to copy to this epoch — rather than asking for a pick that cannot be made. -
Entity detail — header (icon and the name, nothing else: took the type, stem, library, forked-from, updated and kind chips away); an Epoch row when the entity has several, in the project’s order, each chip the epoch’s label with its year beside it, never its token; and the version row (
detail/version_row.dart), with no Version label beside it or inside its pull-down: the author of this version as text (other authors’ versions are one click away in the tree), a pull-down of that author’s versions (v2, orv2 · gothwhen labelled; star and/or check as trailing icons), the label button (right of the pull-down, left of the star, the author only, a small dialog writesversion_label, §7, removing an oldlabel), the official star (a toggle: grey, yellow when set; enabled for directors of a writable project; setting writes the pointer, clearing deletes it), the check — the version in use for the viewer (§6.1 step 2): on the version in use that nothing explicit chose (official with the star alone, your latest, the only author’s) a press records it as your own pick, the version itself and never the tracking value, so that it holds whatever another author or the assistant writes later and a draft of the assistant’s that you pick is no longer a draft, its next change making a new one (§7.3); on the version your pick, or one you follow official by, already names it only says why it is in use (your pick, you follow official), writing nothing; grey and clickable on every other version, where a click moves the pick here (on the official version a click removes your pick, since no pick means official — unless your own file still holds a pick naming no version here, a missing file, which the check on the official version then removes); enabled for any viewer of a writable project, library versions included, since the pick is the project’s own commentary; the decision is made in your commentary file’s queue, against the index the previous write left, and logged asversion.pick.start {kind: explicit-in-use|ordinary|already-picked, reason, ignoredPick?}(the stem of the viewer’s own pick where it named a discarded version and was ignored) thenversion.pick {outcome: picked|already|gone|stale|failed|refused, kind, reason, stage: decide|write|refresh, refresh, ms}, a failure with its error’s type, a stem only as a digest, the press itself aswrite.pickFromCheck {stem, inUse}— and Fork. Nothing follows Fork (No version in use, In use: and Official is chips are gone, the tree still flags an epoch where nothing is in use). Tooltips say why a control is disabled (no username, read-only, directors only). On a plug-in the star and the check are unlit and disabled (PageAccess.choosesNothing), whoever the viewer and in development mode too, the pull-down’s star and check are not drawn, and both say A plug-in runs the version installed; remove it and install another to change it. — one installed version of each plug-in runs whatever the film (§6.11), so a pick or a pointer there would choose nothing; the check on the version in use says nothing either. On a discarded version (Project Specification §7) the star and the check are disabled, each saying This version is discarded, since it resolves for nobody (PageAccess). Where your own pick names a discarded version it is ignored: the version in use — the fallback, or a pick your workspace inherits — is unchecked, its check saying The pick names a discarded version, “…”, and is ignored — Use this version (on official, — Use the official version: …), and a press writes your pick of it in place of the ignored one. Two boxes point at the check (check_popover.dart’sCheckPopover, in the overlay under the version row, tapering at its top into an arrow at the check, kept inside the page’s pane — never over the docked assistant — and its text wrapped, so that nothing overflows at 375 px): after an assistant’s verifiedupdate-entityorcreate-entity(the command’s observation,ok, names the stem written) the change is said with Open (assistant.write.open), whatever entity it changed, and the page moves by itself only to a chat’s record (§6.11a); Open opens that version as a click would, on View/Edit whatever tab was open, and the box says This version is a draft with ai changes. Pick it, go back to the prior version, or compare to pick which changes to keep. — on a version the assistant created, which has no prior version and so no Go back, This version is a draft with ai changes. Pick it, or compare to pick which changes to keep. —, bordered in the theme’s error colour, with Compare — the prior version’s page on Compare with the draft in the left column (openCompare), nothing written, loggedassistant.page {action: compare, ok}— and Go back — the person’s version in use opened, nothing written — where there is a prior version. The box shows whenever the page shows the person’s draft (§7.3’sassistantDraftOf), on every opening, not only after a write, until the person picks it. A verified change of any entity — the page’s own, a character the assistant made from the project page, another it changed — moves nothing by itself: a message names it, The assistant changed <name>., with Open (assistant.write.open), which opens that version with its box, so that the page and the chat on it stay where they are; each is loggedassistant.page.decision {command, decision: messaged, reason: no automatic move, stem, page}, the stem and the page’s tree key as they are. Wherever the version on screen is not the one in use and the viewer may pick, a smaller box says Use this version, never over the first. A box without a link — Use this version, and the first after a created entity — takes no taps, so a tap on it reaches what lies under it (the tabs at a narrow width), and dismisses Use this version. Either goes on the check’s activation (a click, a key or a screen reader’s tap), a move to another version, or its link, and Use this version also on a tap outside it or a wheel or trackpad scroll; the draft’s box waits for the person’s decision through a tap outside and a scroll. Gone, a box stays gone until the next write, or until such a version is shown again — the draft’s box included, which every opening of the draft shows again; nothing of it is kept across navigations (assistantWriteProviderholds the last write while its version is on screen). It is placed from where the check was laid out, measured after the frame while it is shown and again whenever a scrollable around the check scrolls, so that it follows the check as the header scrolls; where the check is scrolled out of the pane’s sight, the box keeps to the pane’s nearest edge, top or bottom, with no arrow, and the overlay holds it only while it shows, so that a page without it stands under no empty layer and the app is idle between frames. Logged asassistant.page {action: opened|shown|dismissed|back, kind: write|use, …}:shownwith the version and its reason (written,draft,not-in-use),dismissedwith itscause(check,outside,scroll,action,secondary,move,write) and how long it was shown,backwith the version opened and why it is in use; an observation whose label is no version’s stem isrefused {cause: not-a-stem}; a stem only as a digest. A failed or refused change shows nothing new: the chat says why. Then tabs: -
The media card (
filmopen_forms/media_section.dart,MediaCard) is a card within View/Edit on each entity type that has been given it — the second on a character, a location, an outfit and a prop, right below General; on the project file after General, Synopsis and Epochs, above the treatment’s prompt and Other, which holds the visual treatment its pictures are described into, and on a folder after Details, just above its visual treatment — part of the view, not above it. A type gains it with one line in its tab (characterFormSections,locationFormSections,outfitFormSections,propFormSections,projectFormSections,folderFormSections), and a type that has not been given it simply does not have it; what an upload is for that type is the format’s to say (uploadPrefixFor: a picture of a place is a concept sheet like a picture of a person,cs, and a sound of a place is ambience,am, where a character’s is a voice sample,vo— the prefix names the purpose, not the origin, §5.5), so a tab knows nothing of media beyond the line. The card ownsmedia,previewand the voice in use (voice.active_reference), which therefore leave Other attributes, and shows them as places, one media place per group (below): the type’s places of the format’s table (entityMediaPlaces), always and in its order, each taking its kinds through the file dialog, a drop or an address — a character’s Reference images, Character sheets, Voice samples with the voice panel under it (below), Inspirations and Motion clips; an outfit’s Reference images, Outfit sheets (the place its composite sheet will be rendered into) and Inspirations; a location’s Reference images, Location sheets and Inspirations; a prop’s Reference images, Prop sheets and Inspirations; the project’s and a folder’s Reference images alone, on the project Cover image; neither the outfit nor the prop offers Analyze; a group the file holds that its type does not list stays in the file and is not shown, logged once per display asmedia.places {stem, type, shown, hidden}, the stem as its digest and the two counts the places shown and the groups hidden; apreviewno entry names, as a tile of its own under Preview (media:<stem>:preview:1), opened as any tile is, its border its record’s stage and the status the draft gives it, with nothing to review; on its title row, always, the stage its renders are asked at (StageControl,media:<stem>.stage, over the sharedStageSelectorthe render dialog uses too, which writes and logs nothing of its own): Prototype, Draft, Final, left to right, each with its tooltip, written as the version’srender_stagethrough the form and read Prototype where it says none or one of another shape, held with its reason where the form may not write, the segments’ initials where the card is narrow, and the card’s field owning the key so that Other attributes and a comparison do not show it — the card’s title row wraps, the stage and the save status moving to a line of their own where the title leaves them no room; loggedmedia.stage.start {stem, from, to},media.stage.done {stem, outcome: changed|same, ms},media.stage.refused {stem, reason: readOnly}, the stem as its digest, and on a move upmedia.stage.outdatedfor each render it puts below the stage (§6.9). The card has no Add a place…: a type’s places are fixed. Every edit of the entries goes through the form’s draft (FormScope.change), so the autosave writes it as it writes a typed field. The card owns the display’s last look (§13.4): made when the page opens or the version on screen changes, it reads the viewer’svisitedat the epoch once, hands that instant to its places, which mark what arrived after it, and writes the look when the person opens one of its media — an entry’s tile or its menu’s Open (MediaPlace.onOpened), a sample’s tile in the voice panel (VoicePanel.onOpened), or the loose preview’s tile — throughWriteActions.visitand the state’s one door, where any medium the card shows (its places, the page’s chosen groups, the loose preview) is newer than the last look, or where there was none; nothing is written on the card’s first frame, on a landing or by scrolling; never without a handle, never on a project that cannot be written, and never shown; each open is logged, written or not,media.visit {stem, cause: open, opened, place, viewer, newest, since, decision: write|skip, reason: firstLook|newMedia|nothingNew|readOnly|noViewer, ms}; the marks stay until the look is written, then go. It owns the delete, too: a tile’s Delete writes the flag through the draft, thenWriteActions.deleteTakewrites every open draft (OpenDrafts.settleOpen), stops with The file was not deleted: this page’s changes, the entry’s deletion among them, could not be saved yet. where one did not land, reads the entity again and stops with the file changed meanwhile where the flag is not in it, and only then asks the state to delete the take’s file, saying what went — Deleted, and its file with it., Deleted from this version., or why the file stays (writeProblemText). The entry is gone from the page once its flag is in the draft, so where the file stays for a reason that may pass — the changes unsaved, the file held by another program — the message offers Retry, which runs the delete again; another author’s take and a take still named elsewhere stay by the rule, and are said without it. Where the page is gone once the drafts are written, the file stays, since no page would say what went; the flag is in the file andwrite.deleteTakeends refused aspage-gonewith its time. A tile opens the viewer (filmopen_media_ui/media_viewer.dart), a dialog as large as the window, 24 px from each edge, the picture taking the room the lines around it leave and the file’s name naming it for a screen reader; Escape closes it: the picture at full resolution in anInteractiveViewer, the file’s name, then the tile’s own status box (the sameMediaStateits caller built for the tile, the outdated sentence under an unreviewed box; none where the caller gives none), then its kind and size, the provenance strip’s words and the entry’s note under the picture (player.note), Show in folder where the file is on this machine (player.reveal), and Close (player.close). A clip or a sound plays through the engine the scenes play through (player/media_player*.dart): the app’smainsetsmediaEngineFactoryto the factory it installs for the dock (§19.16), and the player makes an engine of its own from it, probes the file, prepares its one-file timeline (fileTimeline, §19.2: a clip as one video item with its own sound, a sound as black and one speech lane at 0 dB) and shows the engine’s texture at the file’s aspect, a sound with its waveform icon over the black picture — and plays at once: every place that opens the viewer does so on a person’s gesture — a tile’s tap or the tile menu’s Open — soshowMediaViewerasks its player to play unless it is toldautoplay: false. The engine’s calls run one after another, and one the engine refuses is said under the controls by its code (player.failure) and never retried. The popup is modal, so a person cannot reach what is under it while it is open, and closing it releases the engine, after the calls already asked for —player.released {path, kind, why, ms}once it is; an engine still opening when the popup closes is released by the opening itself, once. The log says how far it got:player.open.start {path, kind, engine, autoplay}, the engine’s ownengine.*lines (§19.9),player.ready {path, kind, engine, duration_ms, fps, width, height, texture, playing, ms},player.<op>.startand.okfor each play, pause and seek,player.playingwhere the engine’s clock changes it, andplayer.failed {path, kind, stage, code, cause, ms}, the stageprobe,prepare,play,pause,seekorreleaseand the cause as the engine gave it. (The decoder is the engine’s; the controls row is a widget of its own (player/player_controls.dart, over aPlayerNowof position, length and whether it is playing), so what a person presses is pumped at 700 px rather than stood in for.player.play/player.pauseare one control named for what pressing it does next — a file that has run out reads Play, and plays from the start — andplayer.clockthe time it shows;player.seekfollows the finger and asks the engine for one seek when it lifts, holding the thumb where it was let go until the player arrives there or two seconds pass, and is disabled with its reason for a file that does not say how long it is — whose clock still counts up. The player is made when a tile is opened and never at start, so nothing constructs one in a widget test.) Where the engine plays no media — the fake, which refuses every probeunavailable: the web and a desktop without the native layer — the player says Coming soon (player.no_engine), as it does for an engine that gives no texture (player.no_texture), and as the viewer does itself for bytes a session holds only in memory, which no engine opens since the application writes no temporary copy, and in a build that sets no engine; Show in folder stays where the file is on disk. A file the engine cannot read is this file’s, not the engine’s absence: it says This file cannot be played here… with the engine’s code (player.failedat stageprobe) — the walk proves it with bytes that are not a clip under an.mp4name; the sample’s own placeholders are a clip with no tracks and a silent sound with no frames, and what an engine makes of each is its own to say. An address a file names is never fetched: the viewer shows it and says why, since a reference may be a URL (§6.3) and a file is somebody else’s work. The upload sheet’s primary button stays enabled and says what is missing under the box (There is no address here yet. for an empty box, An address starting with http:// or https:// for anything that is nothttporhttps), rather than being disabled until the box parses: a control whose enabled state follows the last keystroke cannot be pressed in the same breath as the typing, which is how both an agent and a person pasting with the keyboard work. A place’s add tile is drawn only where the form accepts input (PageAccess): a read-only source hides it, as do somebody else’s version or a missing handle when those rules make the page read-only, and the page’s guard explains the refusal; the tiles open in every mode. Compare pairs the card by its id and a<adds, group by group, the live entries the left version lacks and removes none (CopyMediainfilmopen_format/copy_left.dart), giving an entry both hold in the same group the right’s set flags and note where the left’s is not deleted, copyingpreviewas any other word — but never one naming an entry the left holds deleted, nor one the right holds in an entry the copy cannot bring — and no bytes at all. The takes tab draws every take of the entity with the same tile (filmopen_browser/takes_tab.dart): aMediaTileas wide as its card, wearing its own batch’s provenance strip (a take is found by its name, so its record is its own batch’s), identifiertake:<take stem>, opening the viewer; beneath it the caption as before — the deliverable and the take’s number, the picked chip, the version it was made from, the batch chip (take:<take stem>.batch) that opens the batch, the renderer and the file’s name. -
Media still in the cloud (§6.14). A media tile whose file the sync left on its target (
MediaFetch.isRemote) paints the kind’s stock picture with a small cloud mark below it, and its tooltip and label add In the cloud; opening it downloads it.; while the file downloads the mark is a ring with the download’s progress. A tile never downloads. The viewer opened on such a file says Downloading… with a progress bar, then shows the file; a failed download says The file could not be downloaded ({code}). It logsmedia.viewer.open {ref, film, path, kind, status and reason (the status box's code and its first change's kind, where the caller gives one), stage: fetch | open, outcome: start | ok | failed | closed, code or type, ms, totalMs}— each stage’s start before its wait, andclosedwhere the dialog was closed before the file opened — digests only, the samefilmandpathas the download’smedia.ensure.*lines,msthe stage’s own time. A tile follows its file: once the file is here it is drawn as any other. The epoch card, the outfit row and a mention’s picture show the stock picture for a file still in the cloud. -
View / Edit (first, for every entity type;
tabs/entity_form.dart) — one presentation of the file’s data in two modes: Edit on a version the viewer may write, View on any other, the same fields in the same order either way. The form renders sections → fields, a field being the JSON keys it owns, a preferred width and a builder taking aFormScope(the file, the draft, whether input is accepted, whether the column is narrow, a text controller by key, a redraw, andlock, which hands a locked boxlockedDecoration— the one locked look, shared by the script editor’s boxes, a character’s relationships and an outfit’s garments); nothing in the layout names an entity type, so a type is added by supplying a sections function. A section’s title row carries its badge, the AI icon where the assistant’s catalog offers an action on the form section (§6.11a, the dock), and the title; the first card’s row carries the file menu and the save status at its right, and a card with an action that too; a row with an action, the status or the AI icon wraps, what stands at its right going, each at its own width, to a line of its own where the title leaves it no room. The project, the character, the location, the outfit, the prop, the folder and the scene have forms (tabs/project_edit_tab.dart,tabs/character_edit_tab.dart,tabs/location_edit_tab.dart,outfit_edit_tab.dart,prop_edit_tab.dart,folder_edit_tab.dart,scene_edit_tab.dart); every other type shows the generic section oftabs/generic_view_tab.dart— one row per dotted leaf path of the file (filmopen_format/flat_json.dart, the rows Compare uses; a scene’s or an episode’s storyepochamong them, since the filename carries none —nonValueKeysFor(type)), label above value with the raw key on hover, values rendered as the attribute view renders them, read-only until a type has a form of its own. (The rows of a scene’s blocks, one per block id and labelled by place, areblockRowsSections, which a comparison draws; on a scene’s page the Dialog and actions card is the editor.) Whatever no field names is shown read-only under Other attributes: a trailing card of its own, absent when nothing is left — or, on a project, a scene, a character, a location, an outfit and a prop, a heading in Other (below). No sentence stands above the cards in either mode (and): the tab’s title says View or Edit. Then the cards about the file rather than in it: Notes when the file has one and File (path,filmopen, the header dates, andby/source/license/xwhen present); a red card listing header/filename disagreements stands above the cards. On a project, a scene, a character, a location, an outfit and a prop what no field names and the file’s facts fold into one last card, Other (EntityDataSpec.foldsOther,filmopen_forms/other_section.dart, drawn byfilmopen_ui’sDisclosureCard): its title row opens and closes it — one button to the semantics with its expanded state (form.other), taking Enter and Space — and it holds the sections the form folds, each under its title as a subsection — on the project Release and the treatment’s Camera, Look, Light and composition and Grading — then Other attributes, where anything is left, and File, as headings. It is closed on every entry to a version — any navigation closes it (otherOpenProvider: the version’s stem and the entry it was opened on, compared by identity where the card is drawn, so that a navigation made while the page is offstage closes it too) — and left as the person left it by an edit and its save, a re-read, a change of theme or of the window’s size. Notes, words a person wrote, stays a card of its own, and nothing that warns is ever in Other: a refused save, a field’s error, what the page may not do and the red card stand where they stood. A folder and a type with no form keep both cards, and Compare folds nothing. A section may carry an action on its own card’s title row (FormSectionSpec.action), at its right at its own width, on a line of its own below the title on a narrow card; the form draws it and Compare’s columns do not. A locked field is read-only rather than disabled, so its text stays at full strength: it wearsAppColors.locked, floats its label, shows an em dash where it is empty, and drops the author’s validation errors, which only the author can answer; a locked choice is drawn as the box a pull-down leaves behind, and a chip inside a locked box carries an outline. On the first card’s title row, at its right — no row of its own, and a 28 × 20 px button that leaves the title row as tall as its title — an overflow menu (detail/entity_file_menu.dart) holds Edit JSON… on the viewer’s own file (tabs/json_editor_dialog.dart: it opens on a tree of the file,voo_json_treewithJsonTreeConfig.full, where a long value keeps to its row, cut short with an ellipsis, and a tapped value is asked for, whole, and written into the document — a number,true,falseornulltyped where one stood stays one, anything else is text, and a path two keys would spell alike changes nothing — and a Tree / Text switch shows the whole file as text for adding or removing keys, back to the tree only when the text parses; both edit the one text an explicit Save writes, a duplicate key refused before anything is written, the identity fields refused by the writer, a changed-on-disk file refused, a confirm before discarding changes — the form’s pending draft is written first, so the editor never opens on text the form has not saved), Show in folder (disabled with an explanation when the file is not on disk; absent on the web) and Copy JSON, which copies exactly what the writer would write. Fork is in the version row above the tabs, not in the tab. Every form autosaves throughtabs/draft_saver.dart: a working copy of the file’s JSON — a deep copy, as is every snapshot written, so nothing done to a nested object reaches the index — written 700 ms after the last change, one write at a time; the file the writer hands back is the base of the next write, taken as it comes with the times it stamped, so the next write is checked against the text this draft wrote; a saver is for one file: when the form moves to another file it gets a saver of its own and the old one is disposed, and a change typed while a write is in flight is written once, when that write returns, against the text it handed back — however many files the form passed through meanwhile, each keeps its own saver and its own typing (a write that fails after the form has gone is logged with what the draft held, having no form to show it or offer Retry, and is not tried again; §14);settlewaits for a write in flight and writes what is pending, and every fork settles every open draft first (DraftSaver.settleOpen, fromWriteActions.fork, which also waits for the drafts forms left behind when they moved on or went away), so a fork carries what was typed a moment ago, and a draft that cannot be written stops it with a message saying so; flushed when the page is left, when the form moves to another file and when the app loses focus; a keystroke during a write keeps the draft dirty (a change counter); a re-read of the same file — a new index, never the page drawn again over the same one, which may not yet hold the draft’s own landed write — is ignored while the draft is dirty or writing, so an outside change is refused as stale rather than overwritten, and, while it is clean, gives the draft its times where only the times differ and re-seeds the boxes otherwise; a draft whose write landed while its publication failed takes the re-read that shows that write, the times it stamped included (§7.5); a status on the first card’s title row that shows only a failed write — the error, its sentence wrapping rather than cut, with Retry, or Discard my changes and re-read when the file changed on disk, beside it where both fit whole, else under it, the sentence keeping its width: belowBreakpoints.singleColumn(560 px) of the status’s own room — and nothing while the draft is saved, unsaved or saving. Closing the window writes every open draft first (lib/app.dart‘sDraftsBeforeExit: anAppLifecycleListenerwhoseonExitRequestedwaits forOpenDrafts.settleOpen, at most 10 seconds, then lets the app exit, loggingapp.exitRequested). A change that leaves the draft as it was — an empty box cleared, the same kind chosen again — is not written: the draft is compared with the file where it would be written — after the 700 ms, on leaving the page, on moving to another file — never per keystroke. A change does not draw the form again: a box shows its own text, so a letter typed — in a field or in a scene’s script — draws the form again only where the draft’s state changes (its first letter) or in a comparison, whose copy controls answer the draft; a control drawn from the draft — a chip, a pull-down, a row, a change of a script’s list of blocks — draws the form again on its own change; the epochs’ editor draws only itself again, a letter of a label or a year included; a folder’s form is drawn again when a letter sets a leaf of its treatment or empties one, since the boxes’ inherited help follows which leaves it sets, and Clear local overrides draws only itself again when an edit — a leaf or the treatment’s prompt — gives the folder a treatment or takes it away; and a box whose error follows its text (a number, the name, the age, a checked box) draws only itself again. Each write logs, at its start, the fields changed since the last write with their values and their count — a change that names no key, such as a script’s keystroke or a copy, by the top-level keys where the draft and the file differ, the keyed ones among them — then, once, how it ended — written, with the values the writer handed back for those keys — the times it stamped included, later typing never — and whether it handed the file back (handedBack, with theupdatedit stamped; without a hand-back, the values the draft gave the writer), or unchanged, no writer called — and its time; the autosave logs nothing per keystroke. The writer’s own checks apply to every form: a name is required (an empty box is an error, not a removal), a version label is at most 32 characters, a project keeps at least one epoch. The character form (tabs/character_edit_tab.dart) owns every key of A.1 that is typed rather than generated, in eight sections and three Prompt cards in its media card, each with a small icon badge on its title row (FormSectionSpec.icon, drawn bySectionCard.icon): General — name, kind (A.1), Birth date (birth_date) acceptsYYYYorYYYY-MM-DD; empty removes it, while malformed text stays in its box with an inline error and changes no draft value. A valid nonempty date fills numericappearance.agefrom its birth year and the era’s year when possible, otherwise keeps age with the helper’s reason. Age is one free box: 1–3 digits write a whole number, exactly 4 move the year to Birth date and fill numeric age, other text stays a phrase as typed ("31 (1999) "stays a phrase), and empty removes age. On a fill, both boxes show their written values and Age is read-only inAppColors.locked, with the tooltip From the birth date and the era’s year. This lock remembers only a fill in this form session and for this stem; opening an existing matching date and age does not lock it. A kept fill or an emptied birth date clears the lock. It is the person’s lock: an agent’s field setter can still set age. A four-digit year with no era year or after it stays in the Age box with its inline error; Birth date is still written and seeded, and the draft’s age is restored from the value held when Age gained focus (the file’s age when it never gained focus), even if partial digits were autosaved. The hint names the era as the tree does.form.birth_datelogs start, set or refusal, text, stem, epoch/year, decision/reason, before/after ages and elapsed time; an unexpected failure records its stage, error and stack and is rethrown. A refused Age year also records the restoration’s values —, Gender (appearance.gender) as three chips, Male, Female and N/A, writingmale,femaleandnonbinary(a tap on the chosen one removes the key: undecided; another word a file holds,woman, is a chosen chip of its own, kept until a choice replaces it), summary, aliases (comma-separated),default_outfit(a file’sarc, which A.1 no longer names, is shown under Other attributes as any unknown key); Features (a face) —appearance.face, the four keys ofhair, the three ofeyes,skin,facial_hair,teeth,glasses; Body —species,ethnicity,height_cmandweight_kg(numbers),build,posture,gait,hands,jewellery,marks(one per line),appearance.notes; Personality —summary,traitsandmannerisms(one per line),wants,needs,fears,speech; Voice —description,pitchandpaceas chips of A.1’s vocabulary,timbre,accent,lang— what its previews say,audition_text, stands on the voice’s Prompt card above Voice samples, under what the voice is designed from, so that the card holds a voice’s prompt as a picture’s card holds its prompt, in View/Edit and in Workflow alike; Relationships — one row per{character, relation}with a remove button and Add relationship (one empty row at a time; a row keeps any other key its object holds; a row emptied while it is retyped stays on screen, with its focus, until its × removes it, and the file leaves it out); then Creator notes,creator_notes, a section of its own on all four forms (sectioncreator_notes). A box of a prompt’s words takes away the prompt’s writer —pluginandwriter— where the words change (pathBoxSetter, throughpromptFields; the rule iswithPromptWordChanged,filmopen_prompt_ui’s, which a block’s frame prompt and the render dialog’s card use too): a person’s words are no writer’s, and the writer stamps the prompt’screated. Each prompt stands above the place it renders, in the media card, under the place’s title and above its tiles (MediaPlace.header): on a character, a location, an outfit and a prop the Reference prompt card —prompts.reference.positive,.negative(sectionprompt) — above Reference images, the Sheet prompt card —prompts.sheet(sectionprompt.sheet) — above its sheets, and on a character the Voice prompt card —prompts.voice(sectionprompt.voice) — above Voice samples, its voice panel under them; inspirations, motion clips and a cover have none (placePromptKind, the places table’s prompt column). The form hands the card its prompt sections by kind (referenceMediaSection’sprompts); each iscompareOnly— drawn as no card of its own, its fields still in the form’s field index and covered keys — and a comparison draws no card in the media card (FormScope.comparing) and pairs each prompt’s boxes as rows where its section stands, with their<. A prompt’s section is its Prompt card (promptSection,filmopen_forms/form_fields.dart; the card,PromptCard, isfilmopen_prompt_ui’s): aFormSectionSpecwhosebodydraws the card in place of the section’s own card, handed the section’s built boxes with their identifiers — prose boxes whose@offers the media a prompt of the card’s kind may name, not the film’s entities (§9.8) —, so the card owns the section’s whole frame — its one header, its border, its collapse — while a comparison reads the section’s fields as any other’s. The card reads the prompt from the form’s draft (in View, the file as read) and paints its state against its holder (Project Specification §9.0,promptStateOf): empty — expanded, the plain outline; current — collapsed to one line: the icon, the card’s title (Reference prompt, Sheet prompt, Voice prompt, Treatment prompt, First-frame prompt, Last-frame prompt), written by the writer’s name wherepluginis set, found by its pair,pluginandwriter(the plug-in’s tag where no writer offered has the pair) else the version’s author, and how long ago in whole minutes, hours or days (just now under a minute), then the arrow that opens it; its border is a current image’s of the stage (the document’srender_stage, a scene’s for a block’s prompt;stageBorder,filmopen_media_ui: final green, draft green and dashed, prototype blue, fromrender_stage, absent being prototype); outdated — expanded, red. Turning outdated or empty opens a card; open, its header shows who wrote it and when whole, wrapping as it needs. Acreatedthat is not a string, or not an instant, is no time: the prompt is outdated. Where the form takes input, the card’s top row is the Writer pull-down — every enabled plug-in’s writers of the card’s target (promptWriters;fo-prompts’s first), starting on the writer Render would ask (startingPromptWriter: the first stockfo-promptswriter of kindllmwith a text model reachable, else Basic) where the card offers it, else on the first offered, until the person chooses another, which stays chosen across a change of model (prompt.writer.default: the place, the writer, its model, rate and quote,why:model,noLlmornotOffered; the card’s own Generate is a paid model call where it started on that model’s writer; a card on screen keeps the writer it is on while that is still offered, chosen or not, so that a key or a model coming or going meanwhile changes its default only when the card is next built; Render itself does not read the card’s choice), each by its name, <plug-in> / where two plug-ins offer writers, chosen by its pair, No writer for this prompt where none — with, under it for an LLM writer, one line: its text model and rate (GPT-5.6 Sol · rate $4.00/1M in, $20.00/1M out), the model with rate unknown where its platform states no plain rate, or Text model unknown where none is reachable; nothing for a template writer — and Generate, with the AI icon; held, with its reason as its tooltip, where there is no writer or while one runs. The card listens to the host’s changes (PluginInvocation.changes: a key, a credit answer, a change of Use the LLM:, a plug-in turned on or off) and reads the writers again, their models and rates, keeping the writer it is on where that is still offered, else the one it would start on. Where the card’s caller gives a catalogue model (model), the writers offered and Generate are that model’s: a change of model reads the writers again, keeping the writer it is on where that is still offered; where its caller gives the blocks a clip covers (covered), Generate hands them to the writer. A page’s cards give neither. The render dialog’s card gives the model chosen — its catalogue entry, never its way, so a change of way within the entry asks the writer nothing again — and, at a clip over several blocks, the blocks after the owning one, in order: one prompt, on the clip’s own block, written over the blocks it covers, its state the ordinary state of that block. Generate asks the writer from the draft as it stands, typing included (anEntityof the file’s identity and the draft’s snapshot), never the file the page opened on, handing it the media tokens the prompt may name ( the card’stokens, asked of its owner once as Generate starts): the section computes them from the draft for the open film and the viewer and the frame card from its scene’s film (promptWriterTokens, overpromptTokenCandidates), each as written with the name its chip shows — inspiration 1, reference 1, Kira Voss · sheet 1 — and the voice’s and the treatment’s hand none; then settles the draft, and where its save was refused drops the answer (Not generated: the page could not be saved.); then, in one change through the draft, setspositive,negative(removed where the writer gave none) and the writer’s pair,pluginandwriter, and removescreated, which the writer stamps with the holder’s time, so the prompt is current and the card collapses. A page left while the writer answers or the draft settles — the card gone, or its draft no longer the one open on its file — drops the answer: nothing is applied and nothing is reported written (prompt.generate.refused {code: pageLeft, stage}); written is logged only once the change went through the draft without a refusal. A writer’sno-promptchanges nothing and says so (Nothing generated: the writer had no prompt.); a failure says its code (Not generated (…).), and a writer refused for want of a key, credit or a server (credentials, ornoProviderforcredentials,noPluginor no reason; a chosen model the catalogue no longer has keeps its code) says so (No text model is available — add a key, credit or a local server in Settings.). One Generate at a time: the button is held while one runs. In View, or where the form takes no input, the card shows the prompt read-only with Read-only: edit your own version to write this prompt. and no top row. The card’s doors areprompt:<stem>:<path>:toggle,:writer,:llm(the line under the pull-down) and:generate(a block’s path being<id>.prompts.<kind>); a collapsed card does not draw its boxes, which an agent’sset_fieldstill types into through the form’s fields. It logs, each line with the kind, the stem, the block’s id and the model as they are — the lines of a Generate the model its request was made for, whatever is chosen meanwhile — and each writer by its pair (writerthe plug-in’s tag,writerIdits id):prompt.state {kind, state, was, reason}when the state it shows changes (held while Generate applies, so no outdated is shown between the words and their stamp),prompt.toggled,prompt.writer.chosen {writer, writerId, writerName, was, wasId, reason: person},prompt.model.changed {writers, writer, writerId, was, wasId, kept},prompt.card.relisted {cause, writers, writer, writerId, was, wasId, kept, reason}(each writer with its model and rate;stillOffered,notOfferedornoneChosen),prompt.card.llm {writer, writerId, kind, llm, llmName, rate, shown, why}(what the line under the pull-down says, and whether it was read, chosen, relisted or read for another model),prompt.generate.start {writer, writerId, writerName},.tokens {count, tokens}(each token handed and its name, once the owner is asked),.settled {writer, writerId, writerName, positive, negative, clean, refused},.doneor.refused {writer, writerId, writerName, outcome, code, reason, line, stage, positive, negative, ms}(the stage where the page was left; the writer’s words wherever it answered some) and.failed {writer, writerId, stage, cause, message, ms}; a box’s hand editprompt.pluginCleared {stem, path, plugin, writer, text}. What is generated or chosen from media —media,picks,preview,voice.provider_bindings— stays in Other attributes. The outfit form (outfit_edit_tab.dart, Project Specification A.2) opens on General — Name, Description (description, the outfit as a whole, multiline), Era, Footwear, Condition, A.2’s seven tokens as chips (a tap on the chosen one removes it), and the chosen sheet —, then the media card, Garments as one group of boxes per garment (item, colour, material, fit, pattern, condition — prose here, not the outfit’s enum — and notes) with Add garment (disabled, saying why, while an empty garment is on screen) and a remove button each, a garment keeping the keys the form does not draw, a value of another shape in it shown locked and left alone, and one with nothing typed not written — an agent fills a garment box by box (garments:<stem>:<n>.<key>),garmentstaking noset_field, Accessories and palette (one per line; a palette takes hex or names), its Reference prompt and Sheet prompt cards in the media card, and Creator notes; an outfit has no voice. The prop form (prop_edit_tab.dart, A.3) opens on General — Name, Kind as A.3’s ten chips in A.3’s order (optional), Hero prop as a switch writingtrueorfalse, Description, Scale, Material, Condition, which is prose, and the chosen sheet —, then the media card, with its Reference prompt and Sheet prompt cards (a prop’s sheets render from itsprompts.sheet), Handling and sound and Creator notes. Their prose boxes are plain text boxes with the microphone; the chosen sheet (Chosen sheet,picks.sheet,sheetPickField) is chips over the version’s own sheets (Outfit sheets, Prop sheets), each named as its tile is — one chosen writes a reference, several a list in the place’s order, none no key; a chosen reference the place no longer holds stays chosen whichever door sets the field — a chip, or an agent typing references, which one rule serves; a typed reference the place does not hold is not chosen, so a sheet is uploaded before it is chosen —, in the locked look, named by its file, its tooltip giving it whole and saying the JSON editor removes it, never dropped; with no sheet yet the field says inside its box Add a picture to … to choose the sheet; a value of another shape gets no field and shows in Other attributes, read-only; neither form has a plug-in card. The location form (tabs/location_edit_tab.dart) does the same for A.3 in eight sections: General — name, kind (a pull-down of interior / exterior / both), Located within (within, Project Specification 1.9 §9.3: a pull-down of the project’s other locations by name, None removing the key; a tag no other location has — its own included — is kept, offered as an entry and said under the box, as every pull-down of a token says it (above); the names are the viewer’s version in use, as in the tree; an agent types a tag, and one that is not another location’s writes nothing; purely informational — the tree, the category page and resolution ignore it), aliases,description,era,architecture,areas(one per line); Geography —city,region,country,coordinates,access; Time and weather —time_of_dayas chips of A.5’s vocabulary,weather; Dressing and palette —dressing,palette(one per line); Lighting —default,practicals(one per line),windows; Sound —room_tone,ambience; Creator notes — and its Reference prompt and Sheet prompt cards in the media card, as the character’s. A box inside an open object is labelled without the object its section names (Hair · Colour, throughflatPathLabel). The boxes on dotted paths aretabs/form_fields.dart‘s: a type’s form names each path once with what it holds (PathBoxes: text, number, lines; a character’s, a location’s, an outfit’s and a prop’s maps are the format’sentity_fields.dart, which the assistant’s context reads too, §6.11a), which gives the seed and the box its kind — a field the sections draw after the seed, a plug-in’s card arriving while the form is open, is seeded from the file when it is first drawn, every other box kept as typed —, and each box’sonChangedand itsFormFieldSpec.setare one function, so an agent’ssetFieldonappearance.hair.coloror on a chip’s key (typing the token) writes as a person does. The rules are pure Dart infilmopen_format/field_paths.dart: a box starts with a string as it is, a number as its digits, a list one item per line; typing writes the trimmed text, a number or nothing (text that is not a number writes nothing), lines without the blank ones, creating the objects on the way and copying every container it descends; an emptied box removes its key and every object the removal leaves empty (and clearing what was not there changes nothing); and a box never writes over a value of another shape (pathFits: an object on the way that is not one, an object or a list where a word goes, a list holding an object) — such a field is drawn locked in Edit too (FormScope.lock(locked:)), with The file holds this value in another shape; change it with Edit JSON — the value itself, where the path reaches one (an object where a word goes), rendered in the field’s place (ShapeLocked), and an object on the way that is not one (appearancewritten as a sentence) shown in Other attributes. The age box follows the same rule: it never puts an age into anappearancethat is not an object. The boxes carry no prefix icon (an icon moves the label off the column the others share and makes the box taller), and their widths are shares of a four-column grid (FormFieldSpec.widthat 1 and below:0.25a column,0.5two,0.75three; two columns where one would be under 150 px), so a section’s boxes line up and end at the row’s right edge. A chip row shows a token the vocabulary does not know as a chosen chip of its own; tapping the chosen chip removes the key; locked, it is the chosen word in aLockedValue. Compare pairs the new sections by id (general,features,body,personality,voice,prompt.voice,relationships,prompt,prompt.sheet,creator_notes;general,geography,time,dressing,lighting,sound,prompt,prompt.sheet,creator_notes), draws them without the icon badges, and copies each box’s path as it copies any row. The project form (tabs/project_edit_tab.dart), in its cards’ order: General — the title on its own row; then Type of project (the keykind, so labelled on this form alone: a pull-down of the four words and None, narrowed to a quarter of the row where the row holds four boxes, and without a helper; a word for people that chooses nothing, sochildrenis untouched — The project’s kind, above), Aspect ratio (format.aspect: 16:9 wide screen, 9:16 cell phone, 1:1 square, 4:5 social media, 3:4 analog TV, each after an outlined rectangle drawn in its ratio, writing the ratio as a string) and Resolution (format.resolution:480p,720p,1080p,1440p,4k, written as shown), both keeping the rest offormatand showing a value the list does not have as itself; Subtitle font (format.subtitles.font: Noto Sans, Noto Sans Bold, Noto Serif, Noto Serif Bold, the faces bundled underassets/fonts/, Default (Noto Sans) removing the key) and Subtitle size (format.subtitles.size, pixels at the output’s height: a whole number above zero written as a number, an empty box removing the key — the default, 48 —, anything else refused under the box with the old value kept), each one setter for a person’s choice or typing and an agent’sset_field. The font, a gesture, logsproject.subtitles.font.start{value as entered, old}, thenproject.subtitles.font{old, new, outcome, ms}or.refused {value, reason: shape | not_listed, old, ms}. The size logs nothing per letter: a size taken is named by the write that carries it (draft.save.start’sfields), and a text refused is logged once,project.subtitles.size.refused {value, reason: shape | not_whole | not_positive | too_large, old, ms}— when the person leaves the box, or at once for an agent’sset_field. A token pull-down’s choice goes through the field’s own setter where it has one (Kind’s, the subtitle font’s), so a person’s choice and an agent’s typing are one operation; then the Style (below, under the visual treatment) under the type of project, and genre (filmopen_ui/genre_picker.dart: a pull-down of check boxes over the recommended list plus whatever the file names, shown as chips) beside it. Where the pane holds two boxes to a row and not four they pair in that order, and below 560 px of pane they stand one to a row; Synopsis — logline and synopsis; Epochs, the line Time period(s) in the film, such as present day, 17th century, 2030, future under its title (FormSectionSpec.subtitle, drawn bySectionCard.subtitle, which Compare’s columns do not draw), over the epochs (tabs/epochs_editor.dart: a reorderable list with a drag handle, label and year boxes — in a card narrower than a form’s single column, the label beside the handle and the remove button and the year under it — no explanation above them and no token chip or default badge, item 12 — a remove button disabled for an epoch that files use — by filename, by a scene’s story epoch orlocation_epoch, or by a cast or props entry (FilmProject.epochUsage) — or the last one; a label box cleared to blank is refused in place, the last label kept and An epoch needs a label under it, and the year is optional. Reordering and editing labels and years are changes of the draft; adding is a write of its own: Add epoch (epochs.add) opens a dialog asking for a label, required, and a year, optional — no token, which the writer allocates (ProjectWriter.declareEpoch) — and declares on the project version in use, throughWriteActions.declareEpochandProjectController.declareEpoch. On the viewer’s own version in use it is saved in that file, which the clean form adopts; on another author’s version in use the dialog says Adding an epoch makes your own version of the project, and the writer forks it and picks the fork, to which the page moves, saying so; a fork whose pick failed still declares the epoch, and the page moves to it and says it is not yet the version in use, whose check picks it. A project pick or another handle meanwhile refuses with the changed-binding sentence. The dialog logsepoch.add.start {label, yearText, year}andepoch.add {label, year, outcome: added | refused | failed, refusal or error, pageOpen, ms}(a missing labelrefused,no-label). Each row’s label box takes every re-read of the file, its own echoes apart, and a reorder writesorderalone. Add epoch is disabled, saying why, on a version that is not the one in use and wherePageAccess.canCreateVersionsdoes not hold. The dialog stays, busy, through the declaration and its re-read, and shows a refusal inside it: the film, the viewer and the version in use are taken at the click and checked again in the project’s commentary queue (ProjectController.declareEpoch), so a project pick, another handle or another film meanwhile refuses rather than declaring on a version the person did not see); the media card; the treatment’s Prompt card; and, folded into Other, Release — year, rating — and the rest of the visual treatment in subsections (below). A change is saved 700 ms after the last keystroke, unless the draft then holds what the file holds; a failed write shows its error on the first card’s title row; a pending change is written when the page is left. An official version shows a banner offering Fork. On somebody else’s file, or a read-only project, the tab says so and offers Fork. -
Script (a scene, a folder, and the Script page’s second tab) — read-only (
filmopen_browser/script_tab.dart). A scene is its blocks as a screenplay page (filmopen_ui/screenplay_view.dart, each block through the format’sScriptBlock), set against a US Letter page’s six inches of text: the action across the full width, the speaker’s cue in capitals at its indent — with Fountain’s(O.S.),(V.O.)or(dual)from the head of the direction on the cue’s line — the rest of the direction in parentheses under it, bracketed once (a direction that carries its own brackets keeps them), the dialogue in its narrower column, left aligned, and a transition (screenplay_element: transition) at the right margin. A mention in the words is read through the reference codec’s tokens (§5): a character the project has shows its name, a sentence’s full stop after it staying the sentence’s; a media reference such as@ch_kira:character_sheet-1is one token and left whole, as is a mention of anyone or anything else. An empty block draws nothing, and an object the format does not read as a block shows as its JSON, muted (an entry that is no object at all is not drawn: the page reads the scene’s objects). No block id is drawn: an id is the file’s address for a block and means nothing to a person. The reading page is a screenplay: it shows dialogue, action and directions, and not a block’s subtitles or beats, which stay in the editor and the timeline; reading writes nothing, so they are kept as they are. A folder and the whole film read every scene beneath them in the script’s order, depth first, through the working list (below; §5.12), and the Script page’s walk, as a Page (each scene’s slug line in capitals with its number at both margins, a rule between scenes; a lazy list, so only the headings, rules and blocks on screen are built) or a Scene grid (filmopen_ui/scene_grid.dart: one card per scene,SceneCard‘s facts read from its file — number, slug line, its first words with their mentions read as on the page,INT/EXTfrom the slug line’s head, the file’stime, the source’s page eighths, up to four of the cast and the count of elements — as many columns asBreakpoints.sceneCardallows, at most six, a card opening its scene,script.card.<stem>; a lazy list of the sections’ headings and rows of cards, so only those on screen are built, an agent reaching a card off screen by scrolling) or a Timeline (filmopen_ui/script_timeline.dart: the reading view’s strip, not a player — each scene a block in order, as wide as its imported page length in eighths,TimelineLayoutworking the widths out once for the strip and every lane; the scale fills the strip’s room (the pane less its padding and the lanes’ names) or keeps the lightest scene 3 px wide, whichever is larger, and a strip wider than its room scrolls sideways; where some lengths are unknown each of those takes the median known length, marked beneath and said length unknown in its tooltip; where none is known every scene takes an equal width, and a line above the strip says which rule holds — Widths by page length, eighths, the same with how many scenes of unknown length take the median width, or Equal widths: no page lengths; nothing is inferred from dialogue or seconds; under the strip a lane for each of the eight characters in most scenes shown, keyed by tag and labelled by name, the tag in its tooltip, so two characters of one name are two lanes; the blocks coloured by the grid’s stripe, a missing fact neutral and said unknown in the tooltip, or by a hue for each location’s tag; under either, the tooltip names each block’s location — resolved as the scene’s own reference, at itslocation_epochor its epoch and official for an official scene, its tag where that resolves to nothing, never another epoch’s name — or says location unknown, so the colour is never the only sign; a block opening its scene,script.timeline.<stem>, the colouringsscript.timeline.colouring.boardand.location), chosen withscript.mode— below 560 px of pane the switch shows its icons, each word a tooltip and the icon’s semantic label, so a segment is chosen by its name (tap {"label": "Timeline"}) at any width. The choice, and the timeline’s colouring, are the reader’s (scriptViewProvider, §7): kept from one folder or tab to another and across a re-read, back to Page and the grid’s stripe when another film opens. Filters (filmopen_browser/script_filters.dart), under the switch, narrow a folder’s and the whole film’s scenes, never a scene opened by itself, which is always shown whole and carries no filters — its folder keeps them for the reader’s return: the chipsINT,EXT,DAYandNIGHT(script.filter.int….night; of a pair, neither asks nothing and both ask for either; a scene whose file gives no night time counts as day), a character of the scene’s cast, a location, and a prop or outfit of the film’s (the pull-down’s), matched against what the scene names in itsprops,outfitsandresources— as a tag or as a map naming its tag,{"prop": "chart", "epoch": "default"}, a list of another shape naming nothing (script.filter.character,.location,.resource; a tag the film no longer has stays the filter, shown by its tag) — and the search (script.filter.search), which looks in the words the page draws for a scene: its slug line and each block’s speaker, direction, dialogue and action, mentions read as the page reads them, so a character’s name finds the scenes that mention them or where they speak. Every filter set must hold. While anything narrows, the bar says how many scenes are shown, with Clear (script.filter.clear), which empties every filter and the box; where nothing is left, the page says so. The filters are kept while the reader moves about the film and are never written; another film starts with none. A scene the filters hide is left out before the sections are reckoned, so a section starts against the scene shown before it. Sections: where the folders a scene is in differ from the scene’s before it — told apart by their stems, so two sibling folders of one name are two sections — a section starts, headed by the folders’ kinds and names from the outermost (Act: The voyage > Sequence: The storm); a scene back at the top after a folder starts an untitled section, ruled, never the folder’s. A cycle, an unresolved entry and a second listing are not read again; the recycle bin is not read (§7): a deleted scene is skipped, and what a deleted folder holds stands in its place, its own sections only. The stripe of a card is one rule (AppColors.sceneStripe): interior or exterior, by day or by night,INT/EXTstriped as an exterior — a presentation default — and a card whose slug line says neither, or whose file gives notime, neutral: a missing fact is shown unknown, never guessed; the card’s words say what the colour says. The log is one line per act, its per-scene values as lists in it: each list an act gathers from its scenes is a key ending in_list, and a count keeps its name; the walk’s, a narrowing’s and the search’s lines each carry the speakers and locations resolved while they ran (speaker_list,location_list), empty where none was. Each open logs, once and never on a redraw,script.projection(the scope, the stem, the rows walked, the scenes read and what was skipped and why), carrying each row’s decision (row_list), every distinct mention with what it was read as and why (mention_list) and the speakers it resolved for them (speaker_list); every speaker of each scene is logged once with the version and name it resolved to, in thespeaker_listof the search, the walk or the narrowing that resolved it, and one resolved while the page is drawn (on the Page, once its block is first drawn) as a line of its own,script.speaker, as a mention decided outside the walk isscript.mention; andscript.page.open,script.grid.openorscript.timeline.open; each change of mode logsscript.mode, and each card or block chosenscript.card.open(viathe grid or the timeline). The timeline logs its geometry so that the strip can be rebuilt from the log —script.timeline.layout.start(the scope, the stem, the scenes, the pane’s and the strip’s pixels), thenscript.timeline.layout(the rule, the median, the scale, the width, whether it scrolls, the time) with each scene inscene_list(its length as imported, its eighths, its width, and whether the width is its own, the median or an equal share) — for each reading, a filter’s included (a keystroke’s search excepted: its geometry and lanes are logged once it settles, where the timeline is drawn) and one that leaves no scene (scenes: 0, the ruleempty, no width), and laid out once per reading and room; a resize logs onescript.timeline.resize(the pane, the room, the rule, the scale, the width, whether it scrolls) once no new room has been laid out for 300 ms; its lanesscript.timeline.lanes(each lane’s tag, name and scenes, the characters left out, the time); each scene’s location once an open,script.timeline.location, in a line of its own or an act’slocation_listas a speaker is (the scene, the tag, the epoch and whether the scene gave it, the version it resolved to, the name shown,resolvedorunresolved, the time); an unexpected failurescript.timeline.failedwith its stage, thrown on. The rows are walked once per open and only narrowed when a filter changes. A narrowing for anything but a keystroke — Clear included — logsscript.filter.start(the scope, the stem, the filters) andscript.filter.applied(shown, hidden, the sections, the time) with each scene inscene_list(shown or not, and the first filter that failed with what it compared:{filter: resource, wanted: chart, had: [...]}), every section’s start insection_list(from, to,enter,leaveorroot) and the speakers it resolved; each pull-down’s entities are named once per film and viewer (script.filter.list), an unexpected failurescript.filter.failed; each change of a chip, a pull-down, the mode, the colouring or Clear logsscript.view(what changed, to what, the mode, the colouring and the filters), and another filmscript.view.reset. A keystroke’s narrowing logs nothing; the settled query logs one line,script.search(the query, where it was typed, the scenes it left and their stems, each scene hidden with the first filter that hid it and that filter’s value, the time and why now), with the speakers — the grid’s cast included — and the timeline’s locations its keystrokes resolved (speaker_list,location_list) and — the first time an open or a re-read is searched — each scene searched inscene_list(shown or not, and the very words it was searched in): once typing stops for the autosave’s 700 ms, and at once before Clear, a navigation or another film — never per keystroke; a search kept into a new reading, a re-read’s or another folder’s, is logged once that reading is drawn (reason: reading). A folder’s list is the working list the tree and Contents draw (§5.12), the viewer’s assistant draft’s where it holds one, a version the script does not draw reading its own outline (folderTabRows;script.projection.startsayssource: working|onScreen,rowsandlist, the file the list was read from); a scene the script read in an earlier folder is read there, and not again here (§8.5). -
Workflow (a scene, a folder, and the Script page’s third tab, after Script;
filmopen_browser/workflow_tab.dart) — the chosen scriptwriting plug-in’s steps from the version on screen down, read from the plug-in’s workflow file (§6.11), each run from its row. A folder’s and a scene’s tab first names the plug-in (Script Aid — change it on the project’s Workflow tab); the project’s has the Create Script card (above). What is shown where: the focus’s own sections first, with no heading of their own (the page names the focus) — on the project those of scopeproject, on a folder those of scopefolderwith nokindor the folder’skindas written, on a scene those of scopescene— then each folder and scene beneath (an entry of another type has no steps), in the script’s order, headed as the Script tab heads a section (Episode: The return) or by the scene’s name, with theirs; a folder with no section of its own still heads what is beneath it. The rows are read as the Script tab reads them, through the project’s script or the folder’s working list (§5.12): a cycle, a second listing and an unresolved row are skipped, a deleted row is not shown and a deleted folder’s children stand in its place. A section’s heading is sized by itslevel(1–3) with its help under it; the headings indent 12 px a depth, up to three; the plug-in’s words — a heading, a help, a label — are its strings’ keys, shown through the plug-in’s strings where it has them. A step’s row (workflow.step:<stem>:<step>): at the left a mark — lit where the step’s done holds, unlit where not, none where the step has no done — read from the project (workflowDoneProvider, §7), never ticked, with its tooltip (Done — read from the project, Not done yet;…:<step>.done); the label with its help under it; at the right a chip with the model decided for the step and its rate (…:<step>.model: Claude Opus 5 · rate $15/1M in, $75/1M out, … · rate unknown, … · set in Settings where Use the LLM: chose it, Text model unknown with the reason in its tooltip), decided once per step id, not per row, before anything runs; a step that declares no model (llms: []) calls none and has no chip. Below 560 px of pane the chip and Run go under the text. Run (…:<step>.run) opens a dialog titled with the step (workflow_run_dialog.dart): On <the focus>, then a row per field of its action (workflow.run.field:<name>) —texta line,notea paragraph box (3–8 lines),numbera box read as a number (anintwhere whole; a comma decimal is not read),choicea pull-down of itschoicesas written,flaga check, off until ticked — empty every time it opens; then the quote (workflow.run.quote): the step’s estimate at the decided model’s rate (About 3 calls on Claude Opus 5 · about $2.25,DecidedModel.quote), … · rate unknown without a rate, the model and its rate alone without an estimate, No text model can be reached for this step. without a model, and No model call for a step that declaresllms: []; then the ceiling (workflow.run.ceiling: Stops at $5.00., or No ceiling — set one on the project’s Workflow tab.), left out where the step calls no model; Cancel and Run (workflow.run.cancel,workflow.run.go, 44 px). A required field left empty (Fill this in.) or a number that is not one (Enter a number, like 5.) is said under it and nothing runs; from then on each field is checked again as it changes. Logged:workflow.dialog.opened(the fields, the model, the estimate, the quote, the ceiling),.refused(what was typed, the missing and unread fields),.submitted(what was typed, and what is sent with the reason for each field: as typed, blank and left out, a whole number, a choice, a flag) and.cancelled(what was typed), each with its time. The run goes to the entity the row stands under — the page’s own for the focus’s sections, the folder or scene whose heading it sits beneath otherwise — as{type, tag}, with the action’sargsbeneath the fields, and the ceiling (§6.11,runOffer); it is held while the app is open (workflowRunsProvider), not by the row, so it survives leaving the page, and a row’s next run replaces its last; nothing is saved. While it runs the row shows a progress bar (….progress), the time so far (m:ss) and Stop (….stop) in place of Run; every other step of that plug-in has Run disabled with Another step of Script Aid is running: Write the scene., since a plug-in runs one call at a time. At its end the row shows what it spent and on which model (….spent: $0.58 on Claude Opus 5, An amount not known on …; not for a step that calls no model), the plug-in’s text as written, selectable (….text; Markdown reads as plain text), a failure in the plug-in’s words in the error colour (….failure; an answer the plug-in refused or failed among them, beside its spend; Stopped when stopped, Stopped: this step reached its $5.00 ceiling. when the ceiling refused a call after some spend, with the spend unknown, or where the call alone was within the ceiling; before any spend, for a call whose worst case alone passes the ceiling, This step could cost up to $1.42 — more than its $1.00 ceiling. Raise the ceiling to run it.; a model with no known price, This model has no known price, so it cannot run under a ceiling. Clear the ceiling or choose another model.; a call with no output limit, The plug-in did not say how long its answer may be, so this step cannot run under a ceiling.), kept until the row runs again, and after a done run Show the script (….script), which opens this page’s own Script tab (EntityTabs.show), where an applied proposal is drawn. A proposal the call holds opens in the import review by itself, as every plug-in’s does (below); the run reads no proposal. Logged:workflow.step.start(the arguments whole, the model, the estimate, the quote, the ceiling),.stop,.done(the spend, whether known, the model, the text),.failed(the code, reason and message; a throw from the runtime with its stage, type and error, at warning, shown as a failure) with the time, andworkflow.step.script(the tab and whether it opened). A long script costs what is on screen: the lines are one lazy list. Each open, and each change of the chosen plug-in or of the host’s plug-ins, logsworkflow.shownonce — the plug-in, the cause, the focus, the state shown (shown,noWriters,noneChosen,missing,refusedorempty— {plugin} has no steps here …), every row whole (where, the step, done, the model, why, its rate and the reason where none was decided), the done rows and the time —; a draw logs nothing. The media sections (filmopen_browser/workflow_media.dart), below the steps — shown whether or not a plug-in is chosen and whether or not its read failed — gather the focus’s scope (workflowScopeProvider, §7) as more lines of the same lazy list: Characters, Locations, Props and Scenes, each with its heading. A character, a location or a prop is a line per tag and occurrence epoch, headed {name} at {epoch} — the name the version in use itself carries, the epoch by its label —, and under it the version’sMediaFormCard(§9.3) where the line is the first to resolve to that version; one that falls back says so (Uses the 1990 version); a later line resolving to the same version is its heading alone, Mia at 2010 — uses Mia at 1990, with no card, so that one version has one form on the screen. A character’s outfits at that epoch follow it, indented, by the same rule. Every character and location of the project is listed, not only those the focus’s scenes use: after the walk’s lines, each other at every epoch with a version, the recycle bin’s left out, the library’s unless a scene uses them, and an epoch that resolves to nothing for the viewer (several owners, none official, none the viewer’s) skipped and logged. One no scene of the project uses says Not in any scene under its title: any scene version counts, a recycled or superseded one too, and a scene naming an epoch with no version counts for no version; one used only outside the focus says nothing. They start unchecked like every place, and Select all takes those the viewer may write. An empty Characters or Locations section says the project has none yet, a recycled one counting as none. A line that would show only its heading, a version an earlier line’s fallback already shows with no outfit at that epoch, is left out. A line that does not own its version but has outfits at the version’s own epoch reads its plain title; one the walk reached with none reads Kai at 1990 — uses Kai at 1990, naming itself. Props stay those of the focus’s scenes. Scenes heads each folder at its depth and draws each scene as its card. A section with nothing says what would fill it (No props appear in the scenes below. …). Each card is writable as its own page’sPageAccesssays. The headings, frozen: an entity’s heading — a folder’s and a scene’s above their steps, a character’s, a location’s, a prop’s or an outfit’s above its card, a folder’s among the Scenes — is ruled and titled as Compare titles an item, a rule, the type’s icon and the name; a step’s row is spaced as Compare’s rows (8 px between its controls). Over the list’s top, opaque, one row (workflow.heading) names the entity the topmost line stands under — a row’s and its card’s, a scene’s card, a folder’s — with its icon, and the next entity’s name replaces it as that entity’s first line reaches the top; it does not push the last one up. The focus’s own steps, a section’s heading and its empty state stand under none, and nothing is drawn there; a step’s own heading scrolls. The row is found among the lines the list has laid out, never by building or measuring another, so the list stays as lazy as it was: on a film of 100 scenes of three blocks each, no card is mounted at the tab’s top and two halfway down, before and after. Every media line is keyed by what it shows — a card its stem, an entity’s heading its section, type, tag and occurrence epoch, a folder its stem, a section itself — and the list finds a line again by its key (findChildIndexCallback), so a dirty or failed card keeps its draft and its way out when the steps above it change. The selection: the tab holds one set of checked places under aPlaceSelection(§9.3); the places offered are computed from the scope, not from what is built — every named place of a card the viewer may write, and every named place of each block an editable scene’s card draws (an id the scene gives twice names none) —, so that a place’s checkbox appears where it is offered. A section with a place to check carries Select all and Select none on its heading (workflow.<section>.all,.none), which check or uncheck that section’s places alone. The selection lives as long as the tab: a page of another focus starts with nothing checked. Logged:workflow.places.shown(every line with owner, fallback and access and, but for an outfit’s,inScene, once per scope and access rules),workflow.offered.startandworkflow.offered(each row’s or block’s decision with its reason — offered, not owner, not editable, id given twice, no id),workflow.select.startandworkflow.select(the section, all or none, the keys and the selection),workflow.selection.reset(what was dropped and why); a throw in any is logged….failedwith its stage and time and thrown on, the tab saying The places could not be shown: … in the sections’ place. Render (workflow_render.dart): a button on a row of its own at the tab’s bottom right, below the list, which does not scroll with it and covers none of it, Render {n} with the number of checked places (workflow.render), disabled with its reason while nothing is checked (Check the places to render first.) and while a press is under way (Rendering…) — one press at a time, claimed before its first wait, until it is cancelled, refused, failed, the pass answers, or the tab has gone. A press settles the open drafts first (one that cannot be written stops it with its own error, nothing prepared); then each checked picture or clip place whose Generate box on its Prompt card reads checked has its writer decided, the boxes read once before the first write and in the sections’ order: the first stockfo-promptswriter of kindllmwith a text model reachable (the app’s own as bundled, never a development copy), else, only where no such writer is offered at this decision, the free Basic writer (fo-promptsbasic, a template), loggedworkflow.generate.choosing(the place, before the writers are read) andworkflow.generate.writer(place, writer, model, rate, quote,why:modelornoLlm). Then each such place is asked whether its render can run, before anything is agreed or paid (QueueRunner.prepareBeforeWriting,prepareRenderBeforeWriting): a place with a picture its prompt may name is let through with nothing prepared, since its words decide the kind, the route and the count; with none, a clip always among them since its writer is handed none and its frames choose its kind, it is prepared on one stand-in word with nothing written. A refusal no words can settle — anything butno-prompt,prompt-too-long,tokens-missing,references-countandno-placeholders— takes the place out of the writing: it is not in the prompts summary, not written and not charged, its box stays as it was, and it reaches the render summary with that reason; a ready answer is only leave to write, and a place that passes is prepared again from its real words. A place whose readiness changes after this check, or whose words then fail to use its pictures, is said by that later preparation. Loggedrender.prepare.beforeWriting(the target, the tokens, the stand-in word,wordsDecideor the preparation’s answer),workflow.render.ready(per place: readiness, reason, whether it is written and why) andworkflow.prompts.dropped(the places left out with their reasons). If any writer left is a model’s, the prompts summary comes first, before any writer’s call and even where the price is unknown, since an unknown price never means free: a line per model, {n} prompts to write with {model}, about {amount} from the writers’ quotes, or price unknown where none has one; Price unknown, not counted above:; Written free by Basic (no text model reachable):; Cancel makes no call and leaves the boxes as they were, Write (workflow.prompts.confirm) answers once (workflow.prompts.summary, then.confirmedor.cancelled;.nonewhere no writer is a model’s). With Basic alone there is no first summary. Then each prompt is written by its writer, the button reading Writing prompts {n} of {m}… beside a spinner meanwhile, a model’s with the decision agreed passed tobuildPrompt, which the host refuses once its model, rate or quote has changed; a writer not offered as decided, stock and of the kind decided, is not asked and the place is skipped (no-basic-writerfor Basic, the reason loggedabsent,notTemplateornotStock;decisionChangedfor a model’s, said as the model’s price or choice changed after you agreed: press Render again) and no other writer is asked — Basic never stands in after a paid failure; with a form open on the file, through its draft, taken before the ask and checked after the ask and after its settle (draftGone,saveRefused,promptChangedwhere the person’s words changed since), else through a save of a copy of the file (sourceChanged); a refused place is skipped, not prepared from its older words; if the tab goes during the pass, the call in flight ends as it ends, its words saved where no form of the tab held them, no further writer is asked and no render is sent, and a message says what was written, what was refused and why, and what was not reached, the places the check left out among the refused (workflow.render.stopped) (workflow.generate.decided,.start,.settled,.done,.refused,.failed,.none,.stopped;workflow.render.summarylistsgeneratedandfree); then, in the sections’ order, each checked place the tab still offers, but those the check left out, is prepared from its saved words (QueueRunner.prepare, §6.15; no prompt is written), with the target its card builds, so a place checked off screen is prepared too; a checked place no longer offered — its row left the scope — stays checked and is listed no longer here, never prepared. A summary follows: {n} renders, about {amount} (the known costs summed), the ready ones of unknown price listed apart and never counted as zero, each skipped place by its name at its epoch and its place with its reason in words (no prompt — write one on its card, at a voice sample keeping no voice no voice prompt — write it on the voice’s Prompt card, at one keeping a voice no audition text — write it on the voice’s Prompt card, the audition text is not the length the voice’s model takes, at a block’s voice line no line to speak — write it in the scene, a route’s own reason, …), the places whose prompt Basic wrote named free, and Clips use the first frame each block holds now — frames and then their clips are two presses; Cancel adds nothing, the prompts already agreed and written staying written, Render {n} (disabled at 0) answers once. On it the ready items alone are added to the viewer’s pending batch, and the pass takes exactly their numbers (submit(only:)), an item already waiting there left for its own Render; the message says the pass’s answer — {n} renders sent to the batch, or the queue’s own reason followed by The batch’s page shows each render and whether it was sent. — with Open to that batch record — opened only while the film the renders were sent in is still the open one, else refused with That batch is in the film that was open when you rendered: open that film to see it. —, and each result lands in its place through the place’s own jobs. A pass that answered ({n} renders sent to the batch) has handed its places to the batch, so each place it took leaves the checked set — one the pass itself refused or left unresolved included, which the batch’s page says — and Render {n} counts what is left: the places skipped in the summary, and any checked meanwhile; a cancelled summary, a refused pass or a failure leaves the set as it was. Logged:workflow.render.start(focus, checked),.busy(a press while one runs),.settle(the refusal),.prepared(per place: stem, group, readiness, reason, route, model, platform, cost),.stale(a place no longer offered),.summary(ready, skipped with reasons, sum, unknown),.cancelled,.queued(the record and each number’s place),.submitted(the numbers and the answer),.cleared(the places cleared and those left checked, or that the tab has gone),.open.startand.open(the film, the record, opened or refused and why; a throw.open.failed, said as The batch could not be opened: …),.failed(the stage and the error, said as Rendering failed: …) and.idle(the outcome), each but.staleand.open.startwith its time. The Generate box. A Prompt card of a picture or clip place the tab offers carries a Generate box, beside its title, or beneath it on a card underBreakpoints.treeHeaderFull: it reads the person’s choice, else checked while the prompt is empty, or outdated with words a writer wrote (it carries the writer’spluginandwriter), and unchecked over any other outdated prompt, the person’s own among them, whose words Render leaves as they are. A choice back at the default is dropped, so that the box follows the state again, and a tick is spent once written; a voice sample, speech, a read-only card and a card outside the tab have none. A card whose prompt Render has written collapses on that write unless its text field had the focus as the write began (prompt.card.written; a press of Render with a pointer takes the focus first, so this holds for a field focused during the pass, or on touch);workflow.generate.selectandworkflow.selection.resetlog the box’s changes. A folder’s rows and media read the same working list as its Script tab (§5.12): the steps throughfolderTabRows(workflow.shownsayssource,listandrows), the media throughworkflowScopeProvider, so that the scene cards and the block places a press offers are the ones the tree draws. A clip whose block’s first frame is checked in the same press waits for the next press, whether or not a frame is already there, since a clip sent beside its frame would start from words alone and be billed: decided at the start of the press, on the layout the prompts are decided from, it asks no writer and neither preparation door, is not queued, stays checked, and the summary lists it under the skipped places as Waiting for its first frame — render again (workflow.generate.decidedandworkflow.render.preparedcarryoutcome: first-frame-pending,reason,block;workflow.render.summarylists it underpending). A clip whose frame is not in the press renders as before, from what its block holds at that moment. -
Compare (
tabs/compare_tab.dart,tabs/compare_form.dart) — exactly two versions: the viewer’s own latest version at this epoch on the left (EntityVersions.highestByunder their exact handle — a workspace does not inherit its owner’s versions for this), and the version they selected on the right, whatever it is — unless the page was asked for an exact pair (BrowserState.compare,openCompare, §7): after a write the app made, the right version’s page opens on Compare with the left column the version written, whatever else the viewer holds at that epoch, until the next navigation (EntityTabsopens a tab it is asked for once for each request,requestedandrequest;CompareTab.requestedLeft). The others are reached by selecting them. Both columns are the View/Edit layout oftabs/entity_form.dartin single-column mode, so a field looks the same here as on its own page; the left is editable when the project can be written and the file is not the library’s; the right is editable only where it is the viewer’s own version too — never another author’s, official or not, and never the library’s (CompareTab.rightAccess, logged ascompare.columns {left, right, viewer, left_editable, right_editable, right_reason}, the reason one ofown,no_viewer,not_yours,library,not_writable, as the tab opens and whenever that answer changes, never for a redraw alone); each column that writes carries its own save status under its heading, which shows only a failed write and its way out, the right’s controls namedright:draft.retryandright:draft.discard. The right column’s controls that name no version — the epochs’ rows and Add an epoch on a project — carry the sameright:prefix (FormScope.idPrefix), so each column’s is reached by its own identifier; a field’s identifier names its version already. It is one scroll view of paired rows, not two forms side by side: sections pair by id, fields pair by id within them (filmopen_format/flat_json.dart’smergeOrderkeeps a key only one version has beside its siblings rather than below everything), and the taller cell sets the row’s height so the next pair starts level on both sides. A key the other version does not have draws nothing, its row keeping its place. Every label stands at the top of its box and an empty box is empty, no hint in it, no dash and no placeholder words — Select Style, a token pull-down’s None (no entry chosen; None stays the menu’s way to clear it, its entry drawn by its own widget so that choosing it leaves the box empty), a scene’s empty blocks list’s (empty), the empty sheet’s sentence, a generic or residue row’s dash, an empty relationship, garment or cast list’s dash or line about what fills it (FormScope.lockin a comparison, andComparingFields, a plain inherited widget, for the boxes a field decorates itself; not a theme, which a dialog would take with it, so that a dialog opened from a column, such as Add epoch, keeps its own hints) —, so that a value that was empty reads as empty; elsewhere a label sits in an empty box and rises as it fills. No column is bordered for being picked: the pick is the version row’s. The path, the format and the dates are no field and are not compared: there is no File card in a comparison; Other attributes keeps the film’s keys no field names (a scene’sweatherandprops, a folder’schildren) and never a header key. A scene’s script is a git-like block diff (filmopen_format/script_diff.dart‘sscriptDiff), wherever the two columns draw block rows: the longest common run of the two versions’ block ids pairs its blocks, same or changed; a block only one version has is a row blank on the other side; and a block both have outside the run is moved — two rows, the block out at its place on the right with the left blank, and in at its place on the left with the right blank, each blank side empty, since the block is in both versions. Each cell is drawn from its own side; every other section keepsmergeOrder. The diff is logged ascompare.diff.start {stem_left, stem_right, left_ids, right_ids}andcompare.diff {stem_left, stem_right, rows: [{display, kind, left_id, right_id, left, right}], counts, ms}, each row with its two blocks as compared. The diff is derived when the comparison opens and again only when either column’s file or blocks change — another version, a copy, the times an autosave hands back to a written block —, so typing outside the script, its save and a redraw (a theme, a resize) neither derive nor log it again; what Show changes only hides is derived again when the project, either column’s file or either draft changes, or when the rows change while the drafts do not — a plug-in turned on or off in Settings moves its data between its card and Other attributes —, never for a redraw alone. Show changes only, a checkbox under the column headings (compare.changes-only), kept in the settings (compareChangesOnly, on until chosen otherwise, a saved choice standing; its write logged assettings.compareChangesOnly.start {from, to}, thensettings.compareChangesOnly {value, written, ms}, a warning with the failure’scodeandcausewhere the store refused it, the choice kept for the session either way), hides every row whose two versions are equal — a block row where the diff calls it the same, any other row where its keys hold equal values (canonicalJson); a section ends with n unchanged for the rows it hid, the invisiblenext_blockrow never counted, and a section with nothing changed is hidden whole; the Notes card stays. While it is on,compare.filter {stem_left, stem_right, changesOnly, hidden: [{section, row, left, right}], hidden_sections, ms}says what it hid, each row with its two values as compared. Down the middle runs the gutter of copy-left: a 28 px box for<, and a second for>where the right column may be written — 56 px then, 28 px everywhere else. The column headings open that version, each on one line: whole where it fits, else its handle cut and the version’s number kept (maximiliana-… v7), the left heading taking the larger share beside the save status; the right one carries the check and the star, the left — the viewer’s own — no pencil and no check, only the star when it is official. The intro line, the file menu and the official-edit banner are the page’s, not a column’s, and are not repeated here. -
The media place (
filmopen_insertion_ui/media_place.dart) is the one element a page shows and adds media with. OneMediaPlacedeclares a place — itsInsertionTarget, the version and aSlotPlacenaming a group of itsmedia(§9.0), or aBlockPlacenaming a group of a block’s (§10.3); the document the page is about to write;onDocument, the page’s door for every edit of the entries, its draft and autosave; whether the version may be written; a title;newSince, the instant a take is new after (the viewer’svisited, §13.4); andonDelete, the page’s delete door — and the element draws one wrap of tiles. The add tile comes first, on an editable place only: + as its heading above one button per action ofInsertionActionsthat makes a kind the place takes (acceptedKindsFor) — Upload, built in with the idupload, then what the application registers — Render (idrender, the render dialog below) — in that order. A button that may not run keeps its place, disabled, with its reason in its tooltip: no handle, no media folder, Adding… for Upload while a job of this place is queued, working or waiting, and for every action while an upload or submission is under way; Render and Voice can queue beside producer jobs — busy is per place, and another place on the page submits independently — or the action’s own reason (availableFor). The tile is one tile wide and grows with its buttons, and a button whose word does not fit keeps its icon, its tooltip and its name and drops the word, measured in the person’s language; a disabled one keeps its name too, its reason what its tooltip adds. The whole media area of an editable place takes a drop where the platform can drop files (file_drop.dart, ondesktop_drop; never on the web) — the add tile, the job tiles, the entries and the space between and after them, the area as wide as the place: the add tile’s + says so in its tooltip; files dragged over the area colour its edge and say, in a label drawn over the tiles, what letting go would meet — Drop files here to add them, or why not yet (wait until this place has finished adding, no handle, no media folder) — painted over the tiles, so that nothing on the page moves and no pointer is taken; and the files let go on it are judged as an action’s are, through the same submission as Upload’s file dialog — those the place takes submitted as one selection in the order dropped, the rest named under the tiles — or, where nothing may be added now, refused whole with the reason under the tiles (wait until this place has finished adding while it is busy). A link dragged from a browser arrives as its address, which no file is: it is named under the tiles, by its last part and never its query, and pointed to Upload’s address box, and never read as a file. A folder dropped is judged by its name as a file is: the wrong kind where its name has no media extension, unreadable where the import tries to read it; a link to a file is followed as the file dialog follows it. A drop outside the area reaches nobody — nor does one under a dialog, a menu or a drawer, or on a page hidden behind another, since the seam asks a hit test at the drop’s point over the area’s whole box — nor one on a version that may not be written, which takes no drop; and the keyboard reaches every button, so a drop is never the only way. Where media goes is fixed when it is dropped or chosen: what the file dialog answers after the page has moved to another place is discarded, never inserted there; a submission keeps the place busy until it ends, and only that place; a failure is said on its own place: a failure that comes back while the page shows another place is kept for its own place and said when the page shows that place again; it is never said on another place; it is lost when the page’s element is disposed before the place returns. The insertion runtime does not yet log its refusals before submission. A refusal line and an action’s failure are cleared when the page shows another place. A refusal of a drop is said for as long as its reason lasts. Then this place’s jobs, newest first — one tile per import: one file’s import or render at its attachment — the stage at which the document comes to name its output, which the place draws first, where the job’s tile stood — is drawn on that entry’s own tile, the tile saying the job’s words with its own and wearing a small spinner at its centre, the picture in full view, until it ends, never as a second tile beside it, its Stop not offered for that moment; a job at any other stage keeps its own tile and its actions — one that waits, needs attention, failed or is being stopped, one run again, and one whose output the place does not draw: deleted, archived behind the fold, another group’s — and one of several files keeps its tile until it is done, a job naming its first output alone — each aJobTiletwo tiles wide — the phase’s mark, its sentence throughjobStateText(a render’s — a producer’s job,producerJobKind— in a render’s words where its code is the render’s: where it is by the render’s phase, Waiting to render…, Rendering… — from the moment it is sent, a render done within one call as much as one its service holds — Fetching what it made… (renderPhaseText), and why it ended where the insertion kept the producer’s own code (producerFailureOf,renderEndedText): the plug-in’s own words where it gave them (§6.15), else the host’s refusals in the sentences its pages use, those that would name what a job does not keep in general words, FilmOpen closed while this was rendering; it is never sent again by itself., the contract’sunknown— a render its service took whose outcome could not be read — as The render was sent, and whether it finished is not known: it may still be running where it was sent. Nothing is sent again by itself; look there before rendering again., a render queued a week that nothing took in given up without being sent, and after any of these, where a paid service took a request of the render — any of its calls, its submission kept with the job through a collect that then fails, or a call on a person’s key in flight when the application closed — (its code markedcharged., the mark kept where the code is too long to keep with it), It may have been charged: Activity → Usage shows the cost. before its own sentence, so that a tile too narrow for the whole says it, a failing tile showing up to eight lines and whole on hover and to a reader, a plug-in’s own code as it gave it, The render did not finish (missing-models); nothing is sent again by itself., and the same without a code where it failed with none the job could keep; a code of the insertion’s own after the render, a take it could not write, keeps the job’s words), a bar where progress is a number, one small button per action it offers (job:<id>:<action>) — keyed by the job’s id, as an entry’s tile is by its reference, so that a rebuild moves nothing while a job runs. A job that reaches done is said once (Added …) and dismissed, its outputs arriving as entries with the re-read its attachment made; every action the place sends for a job — that dismissal, or a tile’s button — is logged as it is sent (media.jobAction: the job’s id, kind, phase and state, the action, the place and the reason,doneShownorperson) and as it ends (media.jobActionDone: the outcome, the acknowledgement’s code and the time, a refusal or a failure a warning, a failure with its error’s type and text); one that endsrecordNotWrittensays so, with Dismiss. Then the entries: the live ones, then, behind Archived (n) (media:<stem>:<group>.archived), the archived ones, dimmed and wearing the archive mark; a deleted entry is never drawn. Each is aMediaTile(filmopen_media_ui) wearing what the film knows about its take — the provenance strip, from the batch item the entry names and from nowhere else (provenanceText: the model on its platform and the cost, as every amount is shown (§9.5), and left out where the item says none; a model the catalogue knows named as the catalogue names it, as the render dialog and Usage name it, else as the item wrote it, and the platform as the item wrote it, since one platform runs on this computer and on a server; Imported for an import batch; nothing where the entry names no batch, even where the take’s own record exists, or where its item names no model) — a star where the version marks it a favourite, and New where the batch the entry names was created afternewSince(arrivedAfter, §13.4), which an entry that names no batch, a nullnewSince, or an entry withkept_atnever is, so Pick clears it at once; its note, provenance and kind are in its tooltip, a note also wears a mark, and a screen reader hears what is drawn — the name, the kind, the marks, that there is a note, and the strip; an archived tile as archived, not new. Its border says what a person needs to know about a render, from three plain values the place hands it — the entry’sstageandunreviewed, and the status of the oneMediaProvenance.ofthe place computes per entry per build (§6.9) — in one order: a status that is unknown, its record gone or saying norequest.asked_at, the plain 1 px outline, whatever stage the entry retains; unreviewed orange and dashed (AppColors.caution,DashedBorderPainteroffilmopen_ui: 6 px dashes, 4 px gaps); outdated red (colorScheme.error); then the stage — final green, draft green and dashed (AppColors.positive), prototype blue (AppColors.prototype); else, an upload, the outline; every coloured border 2 px, painted over the picture where it is dashed. So a new render already outdated is orange, and Pick turns it red. The unreviewed renders of one batch — the entries naming the samebatch, neither archived nor picked — are drawn together inside one rounded orange dashed border (MediaBatchFrame, the borderunreviewedBordergives) where the first of them stands, each tile inside it with no border of its own, and New once above them where any is new; the frame lasts, across visits, until each of them is picked or trashed, while New keeps its visit-time meaning. An archived unreviewed render, or one whose entry names no batch, wears its own border. Each frame is logged when it differs from the one the last build drew, its return to an earlier look included:media.batch.shown {stem, place, batch, entries, excluded: [{entry, why: picked | archived | hidden | noBatch}], ms}. The tooltip’s top line is the status in a box with the border’s colour, width and dash (MediaStateBox): the word of the border that holds — Unknown, Unreviewed, Outdated, Final, Draft, Prototype, or No stage for a current placement with no recorded stage, an import or a render whose entry names none — and one sentence saying what it means, built bymediaStateOfin the border’s own precedence, so the box and the border cannot disagree: outdated names the object its first change was made to, never a field — The character changed after the image was generated, Block 3 changed …, The blocks it covers changed …, The character Aaron changed … (a source the project cannot name: A source it was made from changed …), The character is now at the final stage; this image was generated at draft — in the media’s own word (image, clip, sound); unreviewed Nobody has picked or deleted this image yet, with the outdated sentence as the next line where both hold; a placement’s current stage Generated at the final stage, and nothing has changed since; a stage with no placement status (a take placed twice or nowhere, a frame in the render dialog) Generated at the draft stage, claiming nothing about changes; unknown FilmOpen cannot tell what this image was generated from; no stage Nothing has changed since it was added, claiming no origin; nothing said, no box. The label leads with the same word and sentence, so the colour is never the only sign, and ends with what only the tooltip shows — the note’s words and why the tile cannot open — since the tooltip itself is kept out of the semantics; then the tooltip’s lines —provenanceLines, thenfactLines: its length and its size, the record’s where it has them, else the entry’s, so an entry no record names still says its own. The tile reads nothing of the project: it computes no status of its own, only the box from what it is given. A row narrower than one tile, a Compare column’s, gets tiles as wide as it, so that they stay square. A tap opens the viewer in every mode, with the strip’s words and the entry’s note under the picture. Each tile has a menu (media:<stem>:<group>:<n>.menu) on an editable place, and on any version plug-ins may run on (PageAccess.canRunPlugins: a project the viewer writes, a handle, never a library file) — there, on another author’s version, Open and the injected actions alone, nothing that edits the version. On an editable place each tile carries its own controls (MediaTileButton): at its upper left, on a render, the Pick check (media:<stem>:<group>:<n>.pick) — grey (Pick) on an unreviewed render, where a tap removesunreviewedand writeskept_at, a UTC instant to the second, throughonDocument; green (Picked, the version row’s check and colour) on a render withkept_at, where a tap does nothing, the lit check being no toggle; none on an upload —; at its lower right, above the strip, archive (…:<n>.archive, Archive or Unarchive by the entry’s flag) on every entry, and on an unreviewed render trash (…:<n>.trash, Delete this render), which is Delete without its question: the entry flagged deleted throughonDocument, then the page’s delete door, the record kept. None of them asks, and none moves the document’supdated. Each logsmedia.entry.<pick|archive|unarchive|trash>.start {stem, place, entry, before: {unreviewed, kept_at, archived, deleted}}, then.drafted {after, ms}once the change is handed to the page’s draft (whose save logs the write under the same stem), and for trash the page’s delete.done {outcome: returned | flagged, outcomeIn: write.deleteTake, ms}— its door returning says only that it returned; whether the file went, or why not, iswrite.deleteTake’s own line — or.failed {stage, error, shownHere}, said under the tiles of the place the trash was pressed at and never on another; a tap on a place that may no longer be written is.refused {why}. The tile’s menu: Open; on a take whose record is a render’s where the registered Render applies at the place, Render from this take (media:<stem>:<group>:<n>:render-from, dimmed with Render‘s own reason where it may not run), which runs Render through the same checks as its button — a block’s extent asked first — continuing the take (InsertionAction.continueFrom): the render dialog opens on the record’s model on the route of the plug-in tag the record’s version stem names, whatever its version, and its platform, at the place its record names — the server it was sent to, as its request’sdestinationholds it, else this computer, as a take made here and every record written before the destination was kept say — and only where no candidate is there, the first of that route (render.continued {recordedAt, at, reason: recordedPlace|routeOnly|modelNotOffered|routeNotOffered}), with the prompt and the negative prompt as they were sent, whole, and no treatment added (the record’s already holds it), the take’s own seed — else the one sent where it was applied —, the way, the values, the stage and the first frame where the model takes it, never the take itself; where that model or route is offered no more the dialog opens as a new one does, keeping the prompt, the seed and the stage; the request carries the take asfrom(MediaProvenance.of, Project Specification §12.2); on a take whose record asked at Draft, Render at final resolution (:render-final, dimmed with the same reason), the same continuation at Final (continueFrom(…, stage:)), which the dialog refuses in its body at an outfit’s composed sheets and at a place that names no kind, opens only on the accepted Final default — Settings → Models’ where one is chosen and offered (§9.4), else the picker’s — and gives only the take’s recorded first frame and references (§6.15); Favourite or Unfavourite, Set as preview (a picture only), Move earlier and Move later (among the live or the archived entries, the rest of the array kept in its order), Note…, Delete (not on an unreviewed render, whose door is the trash) — an item that may not run disabled, with why under its name (Already the preview, Already at the start, Already at the end) — every one but Open and Delete one edit of the document throughonDocument, which writes backmedia,previewandpicksand nothing else, and only where the edit changed something. Delete asks once and says what goes: the file too where the take is the viewer’s own — made by them, or under a workspace of theirs as a plug-in’s render is (isViewersTake) — and no other file of the film keeps it in a live entry — the rule the writer applies again when it deletes (takeNamed,documentsNamingTake, §6.1a) — and otherwise the entry alone, and why: a file no upload or render of theirs made, a take whose file is not in this folder, another author’s take, or a take other files keep. Confirmed, it writesdeleted: truethroughonDocument, which clearspreviewand everypicksvalue that names the reference in the same change (§9.0), and then callsonDelete. After the built-in items come the injected actions (MediaEntryActionsinfilmopen_state, registered by the application): each action whoseappliesToholds for the entry and the version, read as the menu opens, behind a divider, identifiermedia:<stem>:<group>.entry.<id>, enabled or disabled with why under its name — a local reason or a provider’s refusal, each in a sentence of its own (mediaActionReasonText,providerReasonTextinlabels.dart, from the reason’s code) — and told the entry and the version’s access: its project and stem, whether the page may write it (the caller’s word andPageAccess.editabletogether) and whether plug-ins may run there. The film it stands on is the one the state holds only while that film is open, not loading while another source is opened, and is the film the place names (projectKeyOf); a film closed, loading or replaced by another source is no identity, and every injected item then says The version or the media changed since the menu opened: open the menu again., disabled, and the choice is refused with it. Chosen, everything is asked again — the action still registered, the place and the version the same, the entry still in the document, the action still applying and available, and writing or plug-ins still allowed — and a refusal is said in a message and runs nothing; an action that fails is said under the tiles, by its name, and reported where the app’s log hears Flutter’s errors (flutter.error, with the package, the error’s own words and its stack, and the action, the entry and the place as itscontext) (Analyze did not work.), until the next choice, drop or submission, or another place. An action that cannot answer whether it applies is not drawn, and one that cannot answer whether it may run is disabled, saying so in general words; where the version allows neither writing nor plug-ins — a version the film no longer holds among them — every injected item is disabled with Plug-ins cannot run on this version. whatever the action answers. Open says why it may not run where the media folder cannot be reached, as the tile’s own tap does. The element imports no form: an action is the application’s to register. On another author’s version where plug-ins may not run — a read-only project, a library file — there is no menu; on any version that may not be written there is no add tile and the tiles open, and a place with nothing in it says that what the version’s author adds will be shown there, in a line of the strip’s size rather than a pane’s empty state, whose words a column as narrow as Compare’s does not break. What an action brings is judged by the rule the service holds it to (acceptsFile, §5.10): a picture, a clip or a sound of a kind the place takes, never a file of no media kind, though a group may hold one placed by hand. Upload opens the upload sheet (filmopen_insertion_ui/media_upload_sheet.dart) for the place’s kinds — the file dialog filtered to their extensions (chooseMediaFiles(context, kinds:)), and an address judged by the name the import would give it, its last part, before anything is fetched: one that names another kind is refused, the sentence saying what the place takes (pictures, clips, sounds, or all three on a place of an author’s own), and one that names no file at all is asked to end in the file’s name — and every file an action brings that the place does not take is named in a line under the tiles with what the place takes, which stays said until the next submission, whatever that brings, while the rest go to the service as one selection, in their order, and arrive at the front of the place’s group, before what was there — the newest beside the add tile and the place’s jobs, as the jobs are newest first; the service’s refusal stays inline too. The element holds no writer: it imports no journal or importer, of the store only the log (AppLogandLogLevel), and of the jobs and the insertion it names only their contract with a page — the service, the target and its places, the sources, a producer’s job’s kind and its failure’s code, the job’s view, phase and action, an action’s acknowledgement and the error’s description its log line carries (JobActionAck,describeJobError), and the two refusalsinsertthrows,MediaImportExceptionandJobUnavailable(test/boundary_test.dart, which reads every package import of the element). Its identifiers carry the version’s stem and the group —media:<stem>:<group>.action.<id>,media:<stem>:<group>:<n>(the n-th tile shown),media:<stem>:<group>:job:<n>(the n-th job, newest first) and the two above — and its dialogs’ aremedia.note.text,media.note.save,media.note.cancel,media.delete.confirmandmedia.delete.cancel. Render and Voice remain available while this place has only queued or active producer jobs, so several outputs can be requested for one character. Submission in progress and an active upload still hold every add action; system drops retain their busy refusal. A producer job needing attention withbindingChangeduses the render recovery sentence only when it offers Retry; a view without Retry, including Dismiss alone or Stop alone, uses the action-neutral insertion sentence about the changed film or settings. A block’s place is drawn and edited as an entity’s is, through the same readers given the block’s id (entriesIn,withEntryAttributes,withEntriesReorderedwithblock:; Pick one edit throughwithEntryAttributes), every edit writing backmedia,blocks,previewandpicksas the result holds them; its identifiers carry the block’s id (media:<stem>:<block id>:<group>, since two blocks’ places of one group share a page), a tile’s status is read for its covered blocks (MediaProvenance.ofwith the group and the block), and a frame offers no Set as preview,previewbeing the entity’s picture. An add to a block’sclips,musicorsound_effectsasks first what it covers (spans_chooser.dart, What does it cover?), once, before any road — an action’s, the render dialog’s included, which copies the target it is given, and a drop’s — and offers only what the group allows (§10.3): a clip This block only, the default, which hands nospans, or Through block n over the blocks after its own, by position with each block’s line, so that a list of blocks always names two or more, consecutive from the owning block; music and sound effects Until the end of the scene, the default, Until block n, or For n seconds, Continue waiting for a number above zero and saying so; a clip on the scene’s last block, and the frames and voice lines, ask nothing. Cancel adds nothing. The answer travels in the target (BlockPlace.spans) and is read-only afterwards: an entry’s extent is one line of its tile’s tooltip, in the chooser’s words, and none where it has no extent its group allows or the block it names is no longer in the scene. -
The render dialog (
filmopen_render_ui/render_dialog.dart,RenderRequestDialog) is what Render on a place’s add tile opens (RenderAction, idrender), the one dialog a render is asked for with wherever it is asked. Top to bottom: the reading line, or that no model is ready, where either is said; the stage (below); the Prompt card or the box; the treatment’s lines and box; the box’s negative and the treatment’s negative where there is no card; Recommend — one line, not editable, Recommend: <model>, <place>, <settings>, the model’s name, the route’s own label as Runs on names it — its plug-in and origin (renderRouteLine): <plug-in> / Your key, FilmOpen credit, this computer, a program as Codex on this computer, or a place’s own name, a server’s never its engine’s platform and the chosen value of each choice among values in the route’s order, whoever chose them (render.recommend), with an arrow, collapsed as the dialog opens and opened by its arrow, or by a refusal among the controls under it (no model ready, no route here, a route or a way not ready, a server gone, a value outside its bounds), once for each refusal, loggedrender.recommend {expanded, reason: tap|refusal, code?, text}, the line as shown, andrender.recommend {reason: shown, why, text, ms}as the line first shows and each time its words change, while the plug-ins are read too, never per rebuild —whythe last landing’s reason (ownServer,recorded,settings,remembered,picked,ready,first),chosenwhere the person chose the model, the route, the way or a value since,serverGonewhere the dialog moved off a server that went, orready/firstwhere it moved off a place on this computer that went; opened, it shows the model, the span, the servers, the route, the way and the other choices below; Images (render.images, a whole number, the route’s default, else 1) — the route’s ownnum_imageschoice where the way chosen offers it as a whole number, within its bounds, a change asking the price again as any priced choice does, else held at 1 for an image model, its tooltip saying the model renders one image at a time there, and not shown for a model that renders no images — loggedrender.images.start {from, offered}andrender.images {typed?, count?, offered, max?, outcome: set|refused|held|hidden, reason: accepted|below|above|notWhole|notOffered|notImageKind, kind?, ms}— an emptied boxsetwith no count, the route’s default — the text typed, the parsed count and a code; the price beside it, once, the route’s quote being for every image; then what a model needs below it — the first frame, the refusals, an outfit’s prompt-only note, a voice’s audition and line, the seed; and Cancel, Render and Add to batch (render.queue), held with Render and saying its reason, absent while a voice is designed (its previews are auditioned at once): it freezes the request Render would send, loggedrender.answer {queued, model, platform, stage, target}as Render‘s is, and adds it to the viewer’s pending render batch, inserting nothing —queue.add.start {target, model, platform, request}, thenqueue.add {stem, n, target, model, platform, ms}beside the pending set’s ownset.written— with the message Added to the pending batch and Open (render.queue.open), which shows the batch’s Pending row; a refused add (the record changed meanwhile, another author’s, a failed write) is loggedqueue.add.failed {stem, cause, error, target, model, platform, ms}and said under the place’s tiles as the action’s failure, nothing inserted; a page left before the add begins logsqueue.add.failed {cause: unmounted, target}and adds nothing. It offers the models of the kinds the place takes — a picture’simageentries, a clip’svideo, a voice sample’stts(renderModelsFor), grouped by kind and by name — that the dialog can give everything they require (the prompt, a negative prompt, a seed and a first frame — one of the version’s pictures; a model that needs a reference picture, a last frame or a sound is not offered until the dialog can give it — an outfit’s sheet gives its references, a take continued the ones it was made from, and a prompt the pictures it names with@(§9.8)) — at a character’s, location’s or prop’s reference pictures or sheets and at a block’s first or last frames only thei2ientries where the prompt names pictures, or a take continued carries them, and only thet2iwhere it names none — the kind following the words as they are typed, here or on the page, and as Generate writes them: where they come to name pictures or stop naming any, the offers are read again for the new kind and the model picker asked for it, Render held with the stage’s asking reason until its answer lands, a model chosen by hand under the old kind replaced by the picker’s (a catalogue entry is of one kind) — one chosen from the old kind’s offers while the new kind is asked is shown, but Render stays held until the new kind lands (render.kind.held {reason: modelOfOldKind, to, model}) —, and a reading that fails held as a stage’s re-pick that fails, where a model that needs a picture, chosen with words naming none, holds Render with its sentence (references-needed); loggedrender.kind.start {from, to, reason: tokens}andrender.kind.done {outcome: landed|superseded|failed|modelChosen|closed, offers, picker: asked, ms}—, and at a character’s motion clips only thei2v(the entry’stag), and of those only the ones some route here can run (RenderBackend.candidates). With none it says so and what would — a plug-in that renders, installed and on — with Settings (render.settings), which closes the dialog and opens Settings on Plugins. While the plug-ins are read again — one allowed, turned on or off, given a key or set a moment ago — what runs where is not known yet: the dialog says so in place of that (render.reading), says no route’s not-ready reason meanwhile, and holds Render, its tooltip saying the same; when the reading ends it reads again what runs where — the models a plug-in allowed meanwhile runs among them — starting where a new dialog would where the person has chosen nothing, and otherwise keeping what they chose — the model, where it runs, its way, its values, the first frame — where it still stands, a server chosen and gone said as such, and saying that no model can render here where none runs any more. The end of a reading never tears down what the person has open: where it leaves the model and the place on screen as they are, they stay — the way, the values and the first frame with them, nothing asked again, a place that became ready asked what it offers over what stands — and any other change — another model, another place, the places’ pull-down giving way to Runs on … where one place is left, a place no longer ready, which then says why in place of its way and values, the choices the person made kept for when it is ready again — waits while a menu stands over the dialog, a pull-down’s open — a menu opened in the very moment counting — and lands once it closes, Render held meanwhile and until the reading that follows the menu’s close has landed, a bound that passed under the menu included; a model or a place chosen in a menu that a reading landed under is the one offered now, each found by what makes it that model or that place — a model by its id, a place by its plug-in, its platform and its server, never by its words, which name a plug-in or a number where two places are alike and change when one of them comes or goes. Every reason to read what runs where again — a server started or stopped, a plug-in turned on, a reading ended — starts a reading of its own and only the last asked for lands, so one asked for while the end of a reading is taken in is read in turn; the line and the hold last until the last has landed, a change waiting for a menu included, and for no longer than 30 seconds, after which a reading that has not ended is not waited for. Otherwise it starts — at a place with a kind, on Settings → Models’ default for it, else the model picker’s: the one model picker (§6.11) is asked as the dialog opens, for the place’s kind at the dialog’s stage (renderPicked), and again at a stage changed in the dialog unless the person chose the model — from its pull-down, or by continuing a take, whose recorded model is its choice; a route, a way, a value, the count or a frame is no model choice —, its answer landing as a new dialog’s would, the way and settings of a row naming the model and route already chosen applied over them and priced, and the dialog opens on the model, the plug-in and the platform its row names where that route is offered here — ready or not, its reason said and Render held where it is not, unless a ready server the person started here (a session of theirs, not one its provider merely listed) runs an offered model, where the dialog opens on that server instead — with the row’s variant as the way and its settings over the route’s defaults where the route takes them, a setting it does not offer dropped and never sent (render.picker,render.routewith itsdecision—accepted,noDefaultornoRouteon the picker’s default; where the picker was not asked,recordedfor a take’s record,gonefor the model chosen offered no more,unreadfor the picker’s answer not come — and the opened route’s label, destination and why it opened there:ownServer,recorded,settings,remembered,picked,readyorfirst,render.picker.params); applied again where a reading starts the dialog again with nothing chosen, never over a choice; Settings → Models’ default for the place’s kind stands before the picker’s, and at a block’s clip or a voice design, which have no kind, the default for their role (§6.15), whose opening logswhy: settings; the last options, below, stand after Settings’ default and before the picker’s, at a place with no kind too — else, at a place with no kind and no default for its role, where the picker names none, or where its route is not offered here, at the first model with a ready route, a ready server the person started first, then their own computer: the model (render.model), where a GPU server is started — in advance, on the rail’s GPU page (§9.7a), never from the dialog, which holds no provisioning open: where a server provider is on and no server is starting, ready or costing, one line says so and Open GPU (render.openGpu) closes the dialog and opens that page; a server starting or ready is a route under Runs on; with every provider off, a server still costing is the GPU page’s and the rail’s badge’s to say, and the dialog says nothing of servers. The provider’s window (ServerOffersDialog), which Start server on the GPU page (gpu.start) opens — it says it is checking, then a search (servers.search), when the prices were read, that they hold five minutes, and Refresh (servers.refresh), and the catalogue’s GPU configurations (servers.offer.<n>, scrolling within a box of their own with its bar showing,servers.offers) with their priority in the person’s language, availability unchecked and the whole hourly price. Selecting a row checks only that GPU class, retaining the catalogue while it asks; the selected result says its count or that availability is unknown. Unknown or zero capacity and malformed or exhausted quota hold Deploy with their own explanation. Discovery, each selection and the explicit Deploy recheck each have a thirty-second budget, including their wait for admission; discovery shares it across capabilities and catalogue. A new gesture or closure cancels its preceding check, and only the current generation may end its spinner; an expired or obsolete recheck starts nothing. A host timeout says the provider did not answer in time, and cancellation says stopped; a provider message is never the only explanation of a host-generated failure. Where quota is absent, the window says quota is checked when the server starts; Refresh, disabled while the provider is asked, saying so; its title names the provider’s platform (SaladCloud); the inactivity minutes (servers.idleMinutes, a whole number, 15 to start at, 5 to 60, or the provider’s declared range), legible and read-only during a selected check, recheck or deployment so the pending explicit action keeps the lease shown, under them that a render keeps the server awake and the window does not; Deploy (servers.deploy, 44 px), enabled on a GPU whose selected check confirms capacity, usable quota, fresh prices and minutes in range, its tooltip saying why not, which rechecks the selection and starts one server and closes the window with a message saying when it stops by itself — or, where the provider did not answer, that FilmOpen is checking whether it started — and a refusal stays in the window (servers.refused), the provider’s words where it gave them, else the refusal’s in the person’s, with where a server was made all the same that it is listed to be stopped; an engine (servers.engine) where the provider runs more than one, its first engine chosen when the window opens, and sent only there, and, once a GPU is chosen, a model stack (servers.stack) among the engine’s stacks that fit its VRAM — each its label, its download in GB and about N minutes to start where the provider gives them, The provider’s choice first, which sends none —, a GPU no stack is known to fit saying so and that Deploy lets the provider choose, one that cannot hold the stack chosen dropping it, so that a stack hidden is never sent, each choice loggedservers.window.choicewith what it left chosen; the agreed price: where Deploy’s recheck answers another hourly price than the one shown, nothing starts, the window shows the new price in its row and in the refusal line (The price is now … Press Deploy again to start at this price., to the tenth of a cent where the price has one), and only a second Deploy at that price deploys, a further change asking again (servers.window.recheck{shownUsd, checkedUsd, decision:deploy|shown-again|refusedwith the code and the provider’s words — quota, capacity, a failed quote, a stopped operation —, ms}; a failure nobody expected loggedservers.window.deployFailedwith its type and words); a provider pull-down where more than one is on (servers.provider); Cancel (servers.cancel), which leaves a deployment asked going on — the server started then listed on the GPU page; where it runs (render.route, a pull-down where there is more than one, else Runs on …) — a ready server the person started here first, since its minutes are paid while it waits, then the person’s own computer, then each other server whose engine the route runs on (SaladCloud · RTX 3090 (24 GB), not ready while it starts, or found at its provider and not started here), then what bills by the call, two named alike each with its plug-in’s name — read again whenever a server starts or stops, the place chosen kept — a server past its lease offered no more, and a server chosen that stops or runs past its lease giving way to the first place that is ready, with a line under Runs on saying so and naming that place (render.serverGone), and Render held until the person chooses where a choice of places is left — and, where the route is not ready, why — never allowed, off, needing a newer FilmOpen, no key saved, a key not granted, no server, no way of running it ready, or the plug-in it runs through never allowed, off, needing a newer FilmOpen or not installed, named (renderNotReadyText); a key’s reason names its platform, never the route’s label, and the plug-in whose own key is missing, where it is a plug-in’s, or the plug-in it runs through that may not use it yet (The plug-in OpenAI may not use your OpenAI API key yet.), no key saved being said before a key not allowed — with Settings, which opens Plugins, where a plug-in’s key, its grants and its switch are; where no model has a ready route, its first line says that no model is ready yet. A clip its entry says makes no sound says so under the model (render.silent: The clip is silent: this model makes no sound, whatever the prompt says.). At a block’s clip, a model left out for want of a kept first frame says how to have it, under the model menu or in the empty state (render.keepFirstFrame: Keep a first frame to use it for a clip). At every place, each model left out for an input the place cannot give is logged with what it lacks, what the place gives, the kind and a command’s reference (render.offer.leftOut). A line’s words are what speech is, not an input: a speech model declares notextinput, and an emptyinputsmap still refuses every media name, where an entry with no map admits them. The first frame (render.frame), on a model whose entry takes one: the version’s pictures — as the open form holds the version, typing included, else as its file does — that the entry’s first frame takes (firstFramesOf): live and not archived, a file the film holds rather than an address, of a format the input lists and, where the import measured it, no larger than the input names (inputRefusal, §5); its reference images first, then its sheets — a character’s or a location’s — then its other places in the order of its file; each a small tile (render.frame.<n>, its picturerender.frame.<n>.picture, its border its record’s stage alone, never a review or a status) chosen by a tap, the chosen one outlined and checked, a screen reader told which, and named under them (The clip opens on …; choose another picture to change it.); the first chosen to start, and a picture chosen for one model kept for another that takes it too; where the version holds none, what to add — a picture of the formats the entry lists (Add a PNG or JPEG picture to this version first: the clip opens on one of its pictures.) — in the error colour, the model offered all the same. On a ready route it asks the route what it offers (options) and shows its ways of running the model (render.stack, where there is more than one, those not ready marked, the chosen one’s issues under it — in the plug-in’s words — and, where the chosen way is not ready on a ready route, Settings (render.settings) under them, opening Plugins, where a plug-in’s key is entered on its page and where the plug-in’s own actions, such as checking or fetching its files, are), its choices (render.param.<name>), each drawn by its kind — a pull-down of the values both the route and the chosen way’s variant allow (§14.7), else the entry’s (renderValuesAllowed), a switch, a box for a number within its bounds — read-only on the locked look, showing the one value, where its bounds allow no other — or a text — in the person’s language as the host gives them, or, where the route names none, in the app’s own for the catalogue’s parameters (labels.dart‘srenderParamLabel: Resolution, Quality, Format, Expand the prompt, Variant, Background), and its values likewise where the route gives no words for them — the qualities, backgrounds and formats the catalogue’s image models share (renderValueLabel: Low to Highest and Automatic; Automatic, Opaque, Transparent; PNG, JPEG, WebP), the id sent as it is, another way keeping only the values it takes and giving the rest its own — and another route of the same model, chosen by the person or found anew when the servers change, keeping the values the person chose that it takes, so that a size or a quality chosen on OpenAI stays chosen on fal where fal takes it, and giving every other choice its own default, never the last route’s, while another model starts afresh — a box the person empties leaving the choice to the route’s own default, never sent, and a box holding text that is no value saying so under it; the prompt (render.prompt) — the place’s own (renderPromptAt: a block’s first or last frames or its clip that block’sprompts.first_frame,.last_frameor.clip, a place whose prompt kind is a sheet (placePromptKind: a character’s, a location’s, an outfit’s or a prop’s*_sheets)prompts.sheet, every other place the document’sprompts.reference; an outfit’s sheets the outfit’sprompts.sheetalone, its character’s pictures sent with it) — starting — as the open form holds the version, typing included, else as its file does — at that prompt’spositivefor a picture or a clip — empty for a character’s motion clip that opens on a first frame, whose picture shows the looks already (a block’s clip shows its card on the block’s own words instead), labelled Motion prompt, with a line saying to write what moves and how the camera moves and an example in the empty box, in italics and never sent — and, labelled Words to say, at what its voice says (voice.audition_text) for a voice sample, and never replacing words the person wrote; labelled Your prompt wherever it holds the person’s words; at a picture’s place of a character, a location, an outfit or a prop — their reference pictures and their sheets, an outfit’s on its ownprompts.sheet, its words alone sent with the character’s pictures first (§10’s outfit composite), a prop’s sheets on its ownprompts.sheet(the writers’ targetprop-sheet) — and at a block’s first and last frames and its clip (the block’sprompts.clip), the prompt is a Prompt card (PromptCard, the writers’ targetpromptCardTargetAt, the prompt’s placerenderPromptPathAt), bound to the page’s own draft of the file (OpenDrafts.of, anEditableDraft): the dialog’s prompt and negative prompt boxes, inside the card, type into that draft as a keystroke on the page does, the page’s boxes behind the dialog following — the prompt’s box aProseEditorwhose@offers the media the card’s kind may name, read and written by its source, the negative a plain box offering nothing —, and a changed word takes the prompt’spluginaway (withPromptWordChanged, the one hand-edit rule the forms’ boxes use too); its Generate is the one way the dialog writes a prompt, into the page’s draft, the writer handed the place’s media tokens of the draft as it stands (renderWriterTokens) — at a block’s clip only where the selected model’s row mapsreference_images, else none, so a frames, voice or text way is never written a prompt naming pictures it would refuse —, Render held while it runs, saying Writing the prompt…, and sending what the draft holds once it ends (the card’sonGenerating); a side whose place holds a value of another shape is locked with the page’s reason (The file holds this value in another shape; change it with Edit JSON.,canSetPathText), never taking words it would drop, nothing coerced; where no page is open on the file in Edit the card is read-only, saying Read-only here: open the page to edit this prompt., its words still sent; Render sends the draft’s words as they stand when it is pressed, a draft closed meanwhile read as it stands. A voice designed shows the card on the character’sprompts.voice(the writers’voice, no@), its audition a box of its own. A take continued, a line and a clip at a place with no prompt of its own (motion) keep the box — a block’s clip shows its card on the block’sprompts.clip—, and the dialog writes no prompt of itself. It logsrender.prompt.bound {target, editable, block, reason: pageOpen|noDraft|notEditable, mentions: <kind>|none}where it shows the card,render.prompt.box {reason: continued|line|motion|place|noSource}where it keeps the box,render.prompt.typed {target, side, block, text}once a side is typed,render.prompt.held {held, reason: generating, ms?}as the hold starts and ends,render.prompt.locked {target, side, block, reason: shape}once a side is found locked, andprompt.pluginCleared {stem, path, block, side, text, reason: person-edit}, the words typed with each; the negative prompt (render.negative) where the model takes one, starting at that prompt’snegative; the film’s treatment on a picture’s or a clip’s model where one applies to the place’s entity (treatmentFor, §5, as the open form holds the entity, else as its file does): itsprompts.treatment.positivein a box of its own under the prompt (render.treatment), and, between the prompt and that box, where any of its primary leaves applies, the words they add as the model is given them (render.treatmentFields: After the prompt, from the project: Camera: 35mm film camera; Lens: 50mm), changed only where they are written, the line naming each document they come from once (from the project, from Harbour) — the treatment’spreset, the style chosen, is stored and never added —, the box’s help then saying its words come after them, and itsprompts.treatment.negativeunder the negative prompt (render.treatmentNegative) where the model takes one, which the person may change or empty for this render alone, each box’s help saying that its words are added after the prompt, or the negative prompt, and the document they come from — the project, a folder, a scene’s Style — or, where none gives words, that what is typed is this render’s alone, and where the place’s entity stands ambiguously in the script, that no folder above it passes on its treatment there, and why; a line spoken and a voice designed take none; the seed (render.seed) where the model takes one, empty for a new one each time; and the price (render.quote): the route’s quote — on a server, that it is paid by the hour while the server runs and the render adds nothing; About … for an estimate, the service’s own, the catalogue’s exact price, Cost unknown until it runs — else the catalogue’s estimate on a route that bills, else that no price is known; on the person’s own computer, nothing to pay. The price shown, and frozen, is the one for exactly the way and the values shown: a choice that changes the price asks again, the last answer winning; where an answer leaves a way or a priced value other than the one asked about — the way it suggests, a default filled in, a value the model does not allow replaced — the dialog asks again, twice at most, and a price that never matches the choices is unknown. A busy inspection keeps the chosen way and values visible, says that it is waiting rather than that the way is unavailable, and schedules another ask from the host’s retryAt: a positive remaining delay is clamped to 2–30 seconds, and an absent or elapsed hint uses 3 seconds. A cold busy answer whose only way is the empty, unlabelled default retains the previously completed labels and controls, even when the person selected a completed way whose id is default. It grants no readiness or invented price. Busy with the chosen way not ready shows the current asking sentence inline as well as on the disabled button. One timer belongs to the latest options generation; choosing another route or closing the dialog cancels it. One person-choice episode permits at most five automatic re-asks; another busy reply does not replenish that budget, while a new choice of model, route, way or priced value starts another episode. For the same unchanged episode, an automatic ask and its cold busy answer or exception retain already-earned Render permission, options and price, including an unrelated publication that recreates equal offers. Equality keeps the same model, complete route and key/way, destination and parameter values, with the candidate still ready; a changed choice or genuine nonbusy refusal holds Render normally. A paid speech line without a route quote keeps its catalogue estimate while a busy answer retains the chosen way as ready; the estimate is held only where the answer is busy and that way is not ready. A changed-parameter answer with no ready way therefore still holds Render without a price. A route that cannot say what it offers is not ready, and says so. The prompt’s pictures (§9.8): the media its@tokens name are sent in order asreference_images, the words written in the chosen route’s own form for them (the priced access row’splaceholders, image 1,@Image1), the file keeping its tokens; Render is held, with a line in the error colour (render.prompt.tokens.refused), where a token names nothing (The prompt names a picture that is not there: @reference-2. …), the prompt names more pictures than the model takes, pictures where the model takes none or none where it needs one, or the route’s row has no form for them; nothing is cut down to the bound. The words as sent — the prompt with its pictures’ words written and the treatment’s after it — are held to the bound the chosen way states (its effectiveinputs.prompt.max_characters, a way’s in place of the entry’s; none stated, none invented), counted in characters: above it Render is held, with a line in the error colour (render.prompt.length.refused: The prompt as sent is {count} characters; this model takes at most {most} here. Shorten it.), never cut; at a frame, a clip and a sheet alike. Each check is logged once per change of the words as sent, the model or the way, asrender.prompt.length {model, variant, chars, bound, outcome: ok|refused|unbounded, prompt, ms}(variant: nonewhere the model has no way,bound: nonewhere none is stated), with the words it measured. Render (render.submit, 44 px) is enabled only on a ready route and a ready way, with the price answered for the choices shown, no box holding text that is no value, a first frame where the model requires one, and for a clip that opens on one the motion’s own words — the treatment’s alone say how it looks, which the picture shows (Say what moves in the clip first.) — a prompt — the one input every model offered is given: the entity’s words and the treatment’s, either alone, the treatment’s primary leaves never being one alone, since they name the camera and the grade and not what is in the picture — and a seed that is one, its tooltip saying why not; a value the dialog itself changes is the one its box or pull-down shows; it closes the dialog answering the request, frozen: the model, the route with its key’s id and the way chosen, the destination, the prompt composed once — the entity’s words first, then the treatment’s primary leaves that apply, each as Camera: … in their order and in English whatever the language, then the treatment’s words, joined by;, the negatives the entity’s then the treatment’s (composePrompt) —, the choices shown with the negative prompt, the treatment that applied with the document each leaf came from, a leaf the person changed naming none and one emptied or not sent left out (RenderTreatment, which the batch item records, §6.15), the seed, the first frame by its reference under the entry’s input name (inputs), the quote the person was shown, the place captured when Render was pressed, the version and the purpose, and when. Cancel (render.cancel) asks for nothing. Where renders do not run — a build with no render manager over a backend, the web — Render stays on the add tile, disabled, saying that renders run in the desktop app, and in a desktop session whose renders could not start, saying that restarting tries again (RenderRunsHere). A line spoken in a character’s voice — a candidate that speaks in the voice the character keeps on its platform — says so under the route (render.voice: Spoken in the voice the character keeps on …) and sends the idvoice.provider_bindingsholds for that platform as the paramvoice; where the character keeps none there it says so (The character keeps no voice on … yet.), in the error colour, and holds Render with the same words. Such a route is offered on a character’s places, and on a block’s Speech: elsewhere there is no voice to speak in. A dialogue block’s line in its speaker’s voice: Render on a block’s Speech (media:<stem>:<block id>:voice_lines.action.render) speaks the block’sdialoguein the voice its speaker keeps — the speaker the scene’s own reference names (speakerOf, §6.1: thecastentry’s epoch, else the scene’s), read from the scene as the person sees it, the open form’s draft else the file (blockLineAt, which the button and the dialog both read). The dialog offers only the routes that speak in a voice the speaker keeps invoice.provider_bindings— a route on a platform it keeps none on, or one that speaks in no character’s voice, is not offered — and shows the line in the prompt box, read-only in the locked look, whatever model is chosen: the line is edited on the block. The render lands in the block’s Speech with its stage andunreviewed, and nospansand novoice_binding, which is a voice sample’s alone; the voice a line was spoken in is its render record’s request, asparams.voice. Render there is disabled, after the build’s own reason, with This block has no speaker., The speaker is not a character this scene can see. (no such character, none at the scene’s epoch, or several), The speaker has no voice: keep one under the character’s Voice samples. or This block has no dialogue. A line’s price follows its words: where its route names no price, the dialog shows the catalogue’s estimate for the characters typed, as they are typed, and freezes that into the request. The same dialog designs a voice: Render (media:<stem>:voice_samples.action.render) on a character’s Voice samples, while the character keeps no voice on any platform, opens it titled Generate voice — once one is kept, Render there speaks the audition text with it, as on any place; the samples have Upload and Render as every place has —, offering the models of voice design alone; the prompt is the card on the character’sprompts.voice(the Voice prompt, its words alone, empty until written or generated; the box keepsvoice.descriptionas its start only where there is no source file), with a line saying what to describe, and under it the Audition text (render.audition), starting atvoice.audition_text, with the model’s bounds in its help and an error that holds Generate voice where the words fall outside them; empty, none is sent and the plug-in’s previews say its own sample text in the person’s language, which the help says — never that the service chooses the words. Its previews land in Voice samples as a render’s takes do, each keeping the preview it is (§9.0). A server render with a missing required key says A key required to render on this server is not saved yet. and opens Plugins. Its readiness metadata does not distinguish a route key from a gateway key, so that sentence names neither platform. The stage a render is asked at starts at the version’s own (render_stage, Project Specification §9.0), which its media card sets, and is chosen at the dialog’s top with the same three-position control (StageSelector,render.stage, its segmentsrender.stage.<token>) for this request alone — the version’srender_stageis never written there — Render held while the picker’s answer at a changed stage is asked (Asking which model is recommended at this stage…), a model the person chooses meanwhile ending the wait and kept when the answer lands, and an answer that cannot be read keeping Render held (No model is recommended at this stage: choose one, or choose the stage again, its tooltip and a line at the top of Recommend,render.stage.noRecommendation), Recommend opened, until the person chooses a model or the stage again — nothing asked again by itself; loggedrender.stage.start {from, to}andrender.stage.done {outcome: changed|same|failed|closed, picker: asked|kept, reason: same|noKind|modelChosen|noModelChosen|modelChosenWhileAsking|readFailed|dialogClosed, ms}, where the picker is asked once its outcome is decided — the model landing, a landing that waited for a menu included, the reading failing, a model chosen meanwhile, or the dialog closing: read from the document as the person sees it — the open form’s draft, typing included — else Prototype, a render continued from a take included, whatever the take was asked at, except at Final where Render at final resolution forces it (source: forced), the selector held there with why, loggedrender.stage.held {stage, reason: forced}; Render freezes it into the request (stage), and the preparation logs it,render.prepare.stage {ref?, stage, source: entity|absent|forced}(refwhere a command prepares). A block render’s span is read, never chosen here: where the block’s place covers several blocks, a line under the model says Through block {n} (render.span), the position in the scene, from 1, of the last block covered, in the version the dialog opened on; none for the owning block alone, an extent, or a block no longer there. The chooser is the place’s, at its add. At a block’s clip the frames and the voice line are the block’s own, never the scene’s: First frame and Last frame tiles (render.frame,render.endFrame) of itsfirst_framesandlast_frames, and Voice line (render.voiceLine), a pull-down of itsvoice_lines, given only where the clip covers that block alone — each the ones the route’s row maps and the way takes (format, size, a sound’s seconds), the person’s choice, else the continued take’s, else the favourite, else the first; an optional input’s row only where the block gives one, and a model that needs one the block lacks is not offered. Same-named clip models say their inputs (from a first frame, from first and last frames, from a frame and a voice line). One line says what the model does with sound (render.sound): The soundtrack is the voice line., The soundtrack is the voice line, lip-synced., The model voices the dialogue itself., or the silent line. A choice that fails holds Render: The clip’s inputs could not be chosen. (render.inputsFailed). The choice logsrender.inputs.start(its trigger:open,model,continued,route,stack,person),render.inputs.excluded(input, bound, reason) for each candidate left out,render.inputs.chosen(input,how, reference,unreviewed,favorite),render.inputs.given(each inputofferedor why not,sound,reference_images— how many pictures the prompt’s tokens send as the inputs are chosen, andreferences, those pictures, none where they are refused — andtokens:offeredwhere the selected row mapsreference_images, elsenone) andrender.inputs.donewith its milliseconds, orrender.inputs.failed(stage, cause, the error’s message, milliseconds); a route left out at a clip for a required input its row does not map,render.offer.unmappedwith the model and the platform as they are. -
The render dialog’s last options. A new render opens, in this order: on a take continued’s record; on a forced render’s Final default (§6.15); on Settings → Models’ default for the place’s kind or role (§9.4, §6.15), where its model and route are offered here; on the last options; on the model picker’s default; on the first model with a ready route. A take continued and a forced render never read the last options, even where their own opening fails. The last options are the model, route with its destination, way and values last sent by Render or Add to batch, a take continued’s included, never a cancel’s, a forced render’s or a sound’s; the stage, the words, the negative prompt, the seed and the inputs belong to the place and are not kept. They are kept per machine in the preference
render.last, one entry per media kind and stage (image/<stage>,video/<stage>), and read whether or not the place asks the picker, so a block’s clip and an outfit’s sheet whose words name no picture open on them too, as the render commands do (§6.15). A place reads the entry of each ofimageandvideoit accepts, and uses the first whose model, route and destination it offers, without changing what it offers; otherwise it opens as it would have. That is what they are: the last options, not options per mode, so someone alternating a words-only model and an edit model at different places gets the default where the last one is not offered. A remembered route that is not ready is treated as the picker’s is: the dialog opens there, with its reason, unless a ready server the person started runs an offered model. The way and values settle like any choice, a value no longer taken giving way to the default, and the price is the settled choice’s or unknown. Recommend’s line summarises what is chosen, so it stands over a remembered model as over any other. A stage changed in the dialog reads that stage’s entry where the person has not chosen the model and the place has a kind — or, at a block’s clip or a voice design, which have none, a role (§9.4): there the dialog lands as a new one would, on Settings → Models’ default for the role at the new stage, else that stage’s entry, else the first model with a ready route, so that the stage can change the model; an outfit’s sheet whose words name no picture keeps what stands. Loggedrender.remembered, what the memory supplied at the opening;render.settled.changed, every way or value the dialog removes or replaces, where it happens and whoever chose it, with why (wayNotListed,notSpecified,valueNotTakenas it settles,routeResetas a place or model is left,pickedas a row is applied,chosenas the person changes it), andrender.remembered.droppedfrom the preparation’s (wayNotListed,valueNotTaken,notOffered);render.remembered.writeand.write.skipped, and in the settingssettings.readat the start andsettings.keep.refusedfor a value JSON cannot hold. -
Analyze (
filmopen_forms/analyze_action.dart,AnalyzeEntryAction; the reviewanalysis_review_dialog.dart; the proposalanalysis_proposal.dart; the lineanalysis_line.dart) is an item of a media entry’s own menu (media:<stem>:<group>.entry.analyze), injected throughMediaEntryActionsby the media card for the three places an analysis interface serves: a character’s Reference images (a picture,reference-analysiswith the kindimage,describeReference), its Voice samples (a recording,reference-analysiswithaudio,describeReference), and a project’s or a folder’s Reference images (a picture’s look,style-analysis,describeStylewith the scope project or folder). One registration stands per place of a version, however many cards show it — a comparison’s column beside the page — passed to a card that stays when one goes, and removed with the last; registering asks nothing of a plug-in. It applies to the place’s live entries of its kind, never to a sheet or another group. Whether it may run is read from what is already known — the plug-ins’ listing, their grants and keys, a check under way — and is said under its name, one sentence per link (MediaEntryActionReason): plug-ins cannot run on this version (a read-only project, the library, which in development mode is writable and still runs none); no plug-in that analyzes media is installed; none for this kind; the one that takes this file is off, cannot run in this build, works through a plug-in that is off or missing, is not allowed the key it needs, or has none saved (each only where no route without a key is open); it is being checked; the file is of a type no analyzer takes; the version’s own changes are not saved, being saved, or failed to save — the analysis is built from the saved document, so they go first; another analysis of the place is under way. Several analyzers that can run are a choice by name (Analyze with), asked again at the choice. Chosen, it freezes the entry, the analyzer (its tag and version), the version (its stem and its file’s text), the film, and the settled draft, and asks the plug-in once, starting no check before it; Analyzing kira-front.png… shows in its review — under Described by …, naming the plug-in, with the host’s progress where it reports one — and on the line under the place; Cancel while it works asks the plug-in to stop and frees the place. A failure is said in its own sentence from its code — a key not granted, none saved, the key store unreadable, the platform plug-in missing, off, unable to run or without what it was asked for, a kind or a function the plug-in lacks, the media missing from the film’s folder, larger than an analyzer may be sent, not the type it is recorded as, changed while it was sent, or of a type the plug-in does not take, the key changed under the call, its code where it gave only one and that is a short lowercase word, else failed — never the plug-in’s or the host’s words, and the line keeps it, naming the entry; the log keeps them:analysis.started {stem, kind, plugin, function, media},analysis.proposedwith the paths,analysis.editedwith the paths, each edited leaf’s value and a refusal,analysis.discarded {proposal, pending_text}when the person discards the review’s edits — the proposal’s leaves as last checked and the words typed in the text and not read yet, as they are —,analysis.droppedwith its sentence andms,analysis.failed {plugin, code, reason, message, path, file, line, answer, problem, fields, sentence, ms}(fieldsthe answer whole where a projection refused it atpath; the code as it came), a call that throwsanalysis.callThrewwith its error, andanalysis.appliedandanalysis.forkRefusedwith the write’s error; with nothing left to cancel the review’s button says Close. An answer is checked before anything is projected: one naming which voice speech uses, the sample in use or a sample’s binding is refused whole (The analysis tried to change which voice or recording is in use…), the version’s own bindings left as they are; then its leaves are read (analysisChanges,styleChanges,withAnalysisApplied, §5) — a value no leaf takes, a leaf the file holds in another shape, an answer that tells nothing and one the file already says are each said, and nothing proposed. The review shows how many fields would change and, for each, its label with its whole new value and what it held before (Was: …), wrapped and selectable (analysis.review.fields) — what is written is read first — and, for a look, what one picture cannot tell, shown and written nowhere; below, the whole file as it would be, editable as the JSON editor edits a file (JsonDocumentEditor, the editor’s own presentation over the proposal: a tree whose values are changed by tapping them,analysis.review.treewith the value boxanalysis.review.value,.value-cancel,.value-apply, or the whole text,analysis.review.text, the switchanalysis.review.mode), its top level open and the changed leaves’ branches opened besides. Every edit is checked again (revalidateProposal): on each value applied, at the switch back to the tree and at either Update — the text never per keystroke —, the text through the duplicate-aware decoder; the keys that name the file as the filename says them (a project’sforked_fromas it was), the bindings exactly as the file held them, by key and value at any depth,validateContent, and something still changed; the changed fields are found again, so the list and Was: stay true. A refusal is said under the editor and holds both buttons with the same sentence (As edited, the proposal changes “author”, which names the file: put it back as it was to update., …changes which voice or recording is in use…, …changes nothing in this file…). Cancel writes nothing and leaves the draft as it was; after an edit, it — or a step back — asks first (analysis.review.discard,.discard-cancel), and nothing closes the review while it writes. On the viewer’s own version, which they may write, Update this version writes the proposal once: everything frozen is asked again first — the analyzer, the file, the draft’s changes, the entry — and anything changed since the click refuses it, the draft left as the person had it; then the draft takes the proposal and the save goes out bound to the version’s file as it was read and the film it was asked in, so a file changed on disk meanwhile is refused as stale, the save status offering Discard my changes and re-read, and a refused write keeps its bound writer for Retry; written, 6 fields filled from kira-front.png. (or, for a look, where its leaves are folded). Update a new version (analysis.review.fork, the version row’s fork icon) is offered on every version — beside Update this version on the viewer’s own, alone on another author’s, where the line under the proposal says the version is not theirs to update: it writes the proposal, as edited, as the viewer’s own new version in one write (ProjectController.forkwith the proposal as its content, in the film of the click): everything frozen asked again, then the writer re-reads the version against the text the click froze — a file changed since is refused as stale —, checks the keys that name it and the content, writes the new version once and picks it; a refusal creates nothing, picks nothing and is said in the review; written, 7 fields filled from kira-front.png into ch_kira_now_ann_v2, a new version of your own., the review closes and the page opens the new version. An edit of a key a new version writes for itself —version_label,label,created,updated,forked_from— holds it, saying so, Update this version left free. An analysis is dropped while it is under way or reviewed when the entry or the version changes, the card that showed it goes — the card that started it, even where another card of the version stays, which keeps the registration and may start its own — or the analyzer itself changes: the catalogue’s notification, which the host sends after every re-read of the film, drops it only where the analyzer’s own description differs from the one at the click — gone, another version, asking for consent, off, unable to run, its interfaces, its keys and grants, or what it may reach (hosts, servers, the plug-ins it calls and what they may spend, the AI, the upload) — each said by its own sentence naming it (Vision was updated after this analysis was asked for: nothing was written. Analyze it again.); a change of the film alone — the form’s autosave just before the click, the look record, an import settling — keeps it, and a change of its words keeps it too. Otherwise The plug-in, the file or this media changed since this analysis was asked for: nothing was written. Analyze it again. A call still working is asked to stop; a failure already said keeps its reason. On another author’s version, where plug-ins may run, the menu holds Open and Analyze (in development mode, where that version may be edited, its editing items too). -
The review of an import (
filmopen_forms/import_review_dialog.dart,showImportReview,ImportReviewDialog; §6.18, §7.1) is what the browser opens when a plug-in proposes an import. The browser’s listener (BrowserPage) subscribes toImportProposals.changeswhile the page lives and, after the frame and never during a build, opens the review of the first proposal held for the film open and its viewer whose origin names no operation and that was not put aside — a proposal made by the AI assistant’s operation is its own dialog’s —, one at a time: the next one held is offered when the review closes; a film still loading reads nothing, and a closed one is offered nothing. It offers one only while the rail shows the Project page, asked again as the frame runs, and offers again as the rail comes back to it: no review opens over Settings or another page. Put aside: a proposal closed with Later is flagged on the seam and moved last (ImportProposals.defer,ImportProposal.deferred) and is never offered again on its own — the proposal the review showed as it closed, so that one replaced or let go meanwhile is not put aside and another document held under the id as it closed is a new proposal, offered as one; the flag lives as long as the proposal, a session’s memory that nothing writes. At the bottom of its detail region — below the page, never above its cards, in the theme’s ordinary colours — a compact control says how many of those held for the film and its viewer are put aside (2 import proposals waiting,import.waiting) with Review (import.waiting.review), which reopens the first and whose tooltip says the imports wait only until FilmOpen closes; Later again puts it last, so the next Review reaches the next one. The control wraps in a narrow pane and is gone when none waits. The flag goes with its proposal on every road a proposal ends — applied, discarded, replaced by another document, dropped at read for another film or viewer, or with an explicit close of the project, even of the same film reopened —: the browser re-reads the proposals held, after the frame, on the seam’s notifications, on a change of the film, the settings or the closed flag, as the rail comes back to the Project page, after every review and as Review reopens one, whether or not a review may open then — the read drops a proposal of another film or username, and every one after an explicit close (with no film), even behind Settings; a film still loading reads nothing. The browser says nothing of those dropped, which the log names (import.dropped); one put aside under the other development mode is not counted and is offered, and its review lets it go saying why. Each review the browser opens logsimport.review.opened(the proposal’s id, plug-in, function, film and viewer, whether Review asked for it and whether it was put aside) andimport.review.closed(the id, how it closed and how long it was open); a Review that opens nothing logsimport.review.ignoredwith why (none_waiting,reviewing).showImportReviewanswers how the review closed — applied, discarded, put aside, or dismissed, every other road out — and a proposal dismissed or gone is never put aside. The review (import.review, under the title Import from …, naming the plug-in, found in the catalogue by the tag its door passes) says first how many changes it proposes, that nothing is left to change, or that nothing in it can be written, then lists the document’s problems as sentences and each change (import.change:<id>, the versionless id): its type and the page’s name — the version in use’s, a creation’s own, a creation without a name its tag with (new) (Scene · 8 (new)), refused or not —, what it does in words (Updates your version sc_1_ann_v1., in development mode another author’s project file Updates bob’s version …, which development mode lets you write., Forks bob’s version into a new version of yours, which you then use., Forks the library’s version into a new version of yours, which you then use., Creates it as a new version of yours, which you then use., Already as proposed: nothing to change., Refused: and why — a new entry of achildrenlist that names neither a scene nor a folder named as written (an entry that is no string in JSON), An entry in this list is not a scene or a folder: ch_kira, while achildrenof another shape as a whole keeps the shape sentence —; a scene or a folder created is filed where the proposal lists it where a project’s or a folder’s change of the same import, not refused, lists it in itschildren, and otherwise lands unlisted, under Not in the script, until you place it), its notes as sentences — the version exported no longer in use, changed since the export, a field changed since the export on both sides, no longer in the export’s scope, another author’s project file written in development mode, an era the project change declares (Declares the era The war (1943)., without a year Declares the era The war.) and one whose label the project already has (The era The war is already declared; it is left as it is.), the era’s token unsaid —, and each changed field by its label — a key with no label as written, a list’s entry by the value it is found by — with its new value and Was: …, a field removed as Removed; a value reads as words (import_review_text.dart,importReviewText): a text keeps its lines without its Markdown marks — a heading’s#,**and__, a*or_paired around a word (never one inside a word, as inold_coast, nor one between spaces, as in2 * 3), backticks, a closed fence’s own lines (a ```json body that parses reads the way an object and a list read here, under no word json; a body that does not parse, or of another language, keeps its lines as any text’s; a fence not closed reads as text), a table’s separator row —, a list’s-or*as•and a table’s cells apart (Kira · the truth); an object is one Label: value line per key, a key with no label as written, nested values indented; a list one entry to a line, objects apart by a blank line, so a scene’s blocks read one after another and none as JSON; every key and value stays represented, and a long value stays inside the list, the buttons kept on screen; where a field’s new value and what it held read the same as words but differ as stored — a change only in Markdown marks,**Before**overBefore— both are shown as stored, marks included, a value that is no text as its JSON, so the change Apply writes is seen; the notes on the document as a whole are folded under one row. Every code reads as a sentence (importProblemText,importNoteText; the three refusals of Apply inwriteProblemText), an unknown one as one generic sentence; no code, exception text or path reaches the screen. Each proposal shown is logged once as it is first drawn — opened, replaced under its id, or replaced by an Apply — asimport.review.shown(the cause, the id, the plan’s state and, per change, its id, action, paths, removals, each path’s values before and after as they are, the words shown and whether they were shown as words or as stored, and ms). Nothing in a proposal is edited here. Apply (import.apply, 44 px) callsapplyImportProposalwith the proposal as shown and nothing else, and is held, saying why, unless the plan’s state is proposed, while it runs, and while another import is applied (importApplyRunning, read as the review rebuilds); Discard (import.discard) lets the proposal go and writes nothing (Import discarded. Nothing more was written.), and is held while Apply runs, since it cannot stop writes already started; Later (import.later, between the two) closes the review with the proposal held as it is — what an earlier Apply completed included — writing nothing and saying nothing, since the control at the bottom of the page then shows it, and is held while Apply runs, with its own reason. The review re-reads the proposal on each notification of the seam and each change of the film or the settings, and after each Apply: a proposal replaced under its id is shown as it is now, and one gone closes the review with its cause — the film it was made in no longer open, the username changed, development mode changed, or taken away elsewhere; a notification during its own Apply waits for the answer. The answers: the plan found moved redraws the review (The film changed since you reviewed this import, and nothing was written.), each update of the viewer’s own version whose file changed since saying You edited it meanwhile; written whole, the review closes and a message says how many changes were written, the page having opened what was written (§7.1) — naming the version it moved to (…; sc_2_ann_v1 is open.) where the Apply moved the page; with any change refused, the review stays with each change’s outcome in place of what it does — Written., Written as …, now in use., or why not, a file edited since the review said as the person’s own edit only for an update of their own version whose file shows one, else The file could not be confirmed as written; Apply again shows what is left. — over what is left to do, and Apply again (import.retry) on the same rule; a refusal before anything was written is said inside the review (import.failure) — a draft’s failure with no sentence of its own as See the page’s save status, never its text, and a draft’s refusal with the way out, since the review covers the page (choose Later, settle the page, then reopen the import with Review at the bottom of the page.) —, except a proposal dropped, which closes it with its cause. There is no other way out but these: Escape and the system’s back are Later, except while Apply runs. -
The visual treatment (
filmopen_forms/style_card.dart,visualStyleSections) is, on a folder, the last card before the plug-ins’ (Project Specification §8.7, A.11); on the project the Style stands in General and every other leaf in subsections of Other, folded — Camera (Camera, Camera mount, Lens, Lens character, Camera character, Depth of field, Colour grading, Look · Stock, Depth of field scale, the grain), Look (Description, Mood, Medium, Palette, the colour temperature, the look’s halation, contrast, saturation, sharpness and vignette), Light and composition (Lighting, Composition, Texture) and Grading (the grade’s shadows, midtones and highlights), each a folded section (visual_style.camera,.look,.light,.grading) that Compare shows as any other, the boxes and chips as below. On a folder the card holds first the Style (PromptBuilderField,filmopen_picker) — the effective style’s thumbnail and title, the folder’s own else what it inherits, or Select Style where none is chosen or the chosen one is not found (nothing repairs it); a tap, or Enter, opens the popup — each enabled style builder’s name and its categories beside the open category’s tiles (a row above them under 600 px), the chosen tile outlined — and a tile’s full-screen icon opens the full view: the full picture, or the thumbnail with The picture could not be shown., the prompt read only, Copy (the button then reads Prompt copied), Use this and Close; on a folder that sets none, Inherited from … under it; locked in View; held, saying why, where no style can be read; written only on a pick, as the folder’s own, never on opening or closing, and never typed (it has no setter); then the five primary leaves, each a box with suggested words (EditableComboBox,filmopen_ui) — Camera (High-end digital cinema camera (ARRI ALEXA 35), Consumer SLR (Canon), Cell phone camera (iPhone), 35mm film camera, VHS camera, then Other), Camera mount (Tripod, Dolly, Handheld), Lens (28mm, 35mm, 50mm, 85mm), Depth of field (f/1.4 very shallow, f/2.8 shallow, f/4.5 medium, f/8 deep, f/11 very deep) and Colour grading (Natural, Cinematic warm, Cool blue, Teal and orange, Muted/desaturated, High contrast, Black and white, Vintage film), the words in the person’s language where it has them. The editable combo box takes any words typed, as typed, and never makes them one of its words; its arrow, after the box’s microphone (<field>.suggestions), named for its box (Suggestions for Camera) and saying to a screen reader whether its list is open, opens a list as wide as the box — the keyboard reaching the arrow by Tab after the box, the list opening on the words the box holds where they are offered, else on the first, Escape closing it with nothing written; Down or Alt+Down in the box opens it too, and a click back in the box closes it — and a word chosen (<field>.suggestion.<n>, from 1) fills the box as typing would, through the field’s own change; Other (<field>.other) puts the caret in the box after what it holds — on a desktop and the web too, where a one-line box selects all of its words as it takes the focus — and writes nothing; a box read only has no arrow. Each leaf is written on its own, never as one prompt, and a box emptied removes its leaf; Camera, whose words run longest, has a row of its own, the other four two to a row where the form is. Under the card’s title a line says what the five are for — each goes into the prompt of every picture and clip rendered, after the entity’s own words — and where the rest went (under Advanced treatment, below), and that an empty box takes what is above it; the project’s five have no such card or line. A box whose file holds a number, written by hand, shows it and says the treatment takes none of it (Written as a number…) — on a folder, that what is above it applies; where the box may be typed in, that words replace it — the reader taking words alone. A folder’s detailed leaves are folded under Advanced treatment, a card of its own (form.visual_style.advanced), closed on every entry to a version — each a box or a row of chips on its dotted path: Description, Mood, Medium, Look · Stock, the look’s grain, halation, contrast, saturation, sharpness and vignette as chips, Palette one#rrggbba line (at most eight; a folder’s list replaces the one it would inherit; a list that is not one is said under the box — Each line a colour as #rrggbb, eight at most… — and not written, as a number box’s text that is no number is not), the colour temperature as chips, Lens character, the depth of field’s scale as chips (Depth of field scale, told from the words of the primary Depth of field), Camera character, Lighting, Composition, Texture, the grade’s shadows, midtones and highlights. The treatment’s prompt is a Prompt card of its own (above, under View / Edit), after the primary leaves’ card and before Advanced treatment on a folder, after the media on the project, never folded —visual_style.prompts.treatment.positive,.negative, the writers oftreatment(sectionprompt.treatment); a folder’s card is the folder’s own prompt, and shows none it would inherit. On a folder, a leaf it leaves out shows what it inherits (treatmentFor(…, withOwn: false), the script as the viewer reads it, so a fork of a folder they picked is the one that passes it on): the value as the box’s hint, in italics under a floated label, and where it comes from as its help — Inherited from the project., Inherited from Season 1. — and a row of chips the value too (Inherited: Fine, from the project.); in View a locked box’s hint is the dash, so its help alone says where; a folder whose place in the script is ambiguous says, under the card’s title, that no folder above it passes on its treatment; a row whose file holds a word it does not know shows it chosen, as written, and says that the treatment takes none from it. A leaf emptied is inherited again. Clear local overrides (style.clear), on the title row of a folder’s card — its words, on a line of their own where the title leaves them no room beside it, and its icon, with its tooltip, only on a card too narrow for them — removes its profile through the draft, its pictures kept, so that it inherits every leaf (The folder’s own treatment is removed: it inherits the treatment again.); it is held, saying why, where the folder sets none or the form may not write. -
The voice panel (
filmopen_forms/voice_panel.dart,VoicePanel,voice:<stem>), under a character’s Voice samples, shows the character’s voice (Project Specification §9.1): for each platformvoice.provider_bindingsnames, the sample the voice was kept from, playable, and Speech on … uses the voice kept from this sample. — or, where no sample names it, that speech uses a voice no sample here names — and, where the samplevoice.active_referencemarks as in use was kept as another voice, that speech keeps its own, in the tertiary colour; then the previews to audition, each a voice sample whose binding names a preview and no voice, live and not archived: its tile, played where it is tapped, its border and its lines as a place’s tile’s, and Use this voice (voice:<stem>:preview:<n>.keep). Use this voice asks first — Keep this voice?, saying that the platform keeps it as a voice of the account, which may take one of its voice slots — true of a platform that takes a slot and of one whose design already made the voice —, with the voice’s name there (voice.keep.name, the character’s name to start, at most 100 characters) and Keep (voice.keep.confirm, 44 px) — then keeps it through the plug-in whose render made the preview, the description it was designed from sent with it, and writes the choice into the file in one change (withVoiceKept, through the draft, the Voice section’s boxes re-seeded): the sample’s binding given the voice,voice.provider_bindings,voice.active_reference, andvoice.audition_textas the preview’s record holds it — and no prompt; a save whose record names no words names the character’sprompts.voice.positive. Recordings to keep, where a design model keeps a recording — a catalogue entry of kindvoicewithcapabilities.keeps_recording, with an access row on the platform of an enabled, runnable, consented plug-in’svoiceinterface that serves it (an interface naming no models serves any there): each voice sample with no binding, live and not archived — an upload — its tile, and Use this recording (voice:<stem>:recording:<n>.keep), the offer logged once per display asvoice.record.offeredwith the models, plug-ins, platforms and prices. It asks the same question, adding the price — Keeping this recording costs … on …. from the chosen model’s singlecloneprice on that platform, else …’s price for this is not known here., since a row without one is unknown, never free —, a pull-down of the models where several keep it (voice.keep.model), and What it says (optional) (voice.keep.words), which nobody transcribes; each wording the question shows is logged asvoice.keep.wording, and a question closed without keeping — Cancel, Escape or a tap outside, for a preview’s or a recording’s — asvoice.keep.cancelledwith the name, words and model it held and how long it was open, nothing sent or written. It then keeps the recording through that model’s plug-in —saveVoicewith the model, the sample’s reference, the name and the voice prompt or else the name — and writes it in the same one change, the sample bound on the model’s platform, its words, where given, on the sample and onvoice.audition_text; a failed write keeps it with Retry write, as a preview’s. A save the plug-in could not make says the plug-in’s own words under the previews and writes nothing; one whose plug-in is not on says so; one the application refused before anything was sent says the voice was not kept; and one whose answer was lost, cut short or of another shape once the request may have gone says that nobody knows whether the platform kept it, to check its voices before keeping it again — never that it failed. A save acknowledged whose write fails keeps its voice on the panel — Kept on …, but not yet written to this file., in the error colour, with Retry write (voice:<stem>:kept:<n>.retry), which writes the same change again and sends nothing — until the file holds it, however it came to. A save acknowledged after its page was left is written into the file it was kept for all the same. While a voice is being kept every button of the panel waits, saying so; on a version the viewer may not write, Use this voice and Use this recording are disabled, saying to keep a voice on one’s own version. -
Copy-left (
tabs/compare_copy.dart,filmopen_format/copy_left.dart) — both ways: everything below about<into the left holds of>into the right, the format’s rules being asked of the receiving column and the column copied from (CopyDirection). Each direction describes its rows from the column it copies from, and the other’s field only where that column has no such row (the one copy that deletes): so{custom: {x: 1}}on the left against{custom: {}}on the right offers>and not<. A moved block’s row at the source’s place copies nothing — the row out for<, the row in for>— and a block’s insert lands after the nearest block of the receiving column above its row, a row Show changes only hides included, found in one pass over the section’s rows. A>is drawn only where the right column may be written; elsewhere there is no>and no box for it, and an agent’s tap on one finds nothing (not_found). The controls arecompare.copy-row:<row>,compare.copy-section:<card>andcompare.copy-allfor<,compare.copy-row-right:<row>,compare.copy-section-right:<card>andcompare.copy-all-rightfor>, the chevrons pointing the way they copy. The answers are cached per direction by both drafts and both savers’ revisions, so an edit in either column is seen by both. In detail, for<: a<in the gutter of every row that has something to copy, a<<on each section card’s title row over the same gutter (SectionCard.titleRowlays the title out on the rows’ grid, so the<<sits on the split and not at the card’s right edge) and a<<<over the split in the header, drawn as one, two or three overlapping chevrons so the three read as one gesture at three scales. A row offers a<when the right version has a value at one of the row’s paths that the left does not match, compared by content rather than by key order. A copy never removes a key from the left, and four things would: a key only the left has; an empty value of any kind —null, an object or a list with nothing in it, a string with nothing but space — which everywhere else in the app is how a key is deleted; a path that runs through a value the two versions disagree about the shape of, where writing the leaf would replace the left’s list or sentence with a fresh object; and an object whose visible remainder is empty, which is on screen only so that nothing of the file disappears. None of them offers a<at all — the JSON editor is the way through a disagreement that deep. The keys it will not move depend on the file (uncopyableKeys(entity)): the identity the filename owns,created,updated,forked_from,version_label(withlabel, its 1.5 spelling),byandsource, this version’s own provenance,deleted, its recycle-bin mark, anddiscarded, a version set aside (Project Specification §7), and a prompt’screatedandplugin(§9) — words copied are a person’s: a left prompt whose words a copy changes loses itsplugin, keeps its owncreatedfor the writer to stamp, and a block copied whole carries its prompts’ words alone — in every file;epochonly where the filename carries one, since on a scene or an episode it is the story’s epoch (§10.2), a row that copies like any other;contributorson a project file, the roster and its director roles, which change through the JSON editor and never through a copy; andnext_blockon a scene, its own count of the numbers it has given out; everything else a row shows may travel, and<<<is exactly the rows’ own copies applied one after another, so nothing the screen did not offer is ever written — the Notes card is about the file, not a field of it, and carries no control. Where the source’s value is empty and the receiving column’s is not, the row’s arrow is still drawn, disabled, saying Nothing to copy: this is empty there, since an empty value never replaces one (a block’s own delete stays enabled); a disabled arrow is no operation and logs nothing. A nested object copies path by path, which is a subtree without ever dropping the sibling a field of the form owns (appearance.age), and a list copies whole. A scene whoseblocksholds the ids a cue writes (§10.3) has no script to compare: both columns then showblocksas an ordinary value row and no block moves either way, because the two columns must answer that question together — one drawing a Dialog and actions section of block rows while the other draws a value row would leave both rows without a twin, and the empty-handed side would say the key is not in the other version while it plainly has it. A scene’s script is copied by block id (§13.3’s table):<replaces, inserts — where the block diff drew the row, after the nearest block above it that the left version has (CopyBlock.placed), or first — or deletes that block, the one row whose right side is absent that still carries a control; a moved block’s in row replaces the viewer’s block where it stands and its out row carries nothing, since copy-left has no move;<<and<<<replace and insert — an insert wheremergeOrderdraws it — but never delete, so one press cannot take away every line only the viewer has written, and a block with no id, or an id that names two blocks in one file, offers nothing. A block’s copy never lowers the number the scene’s next block takes (applyCopy; Project Specification §10.3): where an insert, a replace or a delete would let it fall — the highest block deleted, or a block inserted and deleted again — the copy writesnext_block, so a number given out once never comes back under a cue or a shot that still names it. A scene’snext_blockitself is never copied (uncopyableKeys): no row offers it, and copy-all leaves the left’s where it was. Copies are not confirmed (§13.3): they are written through the left column’s own autosave, so the identity lock, the content check, the stale refusal and the 700 ms debounce apply unchanged, a whole card or a whole file is one write, and copying an equal value writes nothing. A copy that would break the file is refused before the draft is touched, because a draft cannot be unwritten: a project copy that would drop an epoch some file is at is refused naming the epoch and the way round it — copy the other rows one at a time — (filmopen_format/epochs.dart’sepochsDroppedInUseoverFilmProject.epochUsage, the same question the epochs editor asks of one row; a file is at an epoch its filename carries, the story epoch its content names, itslocation_epoch, and every epoch a cast or props entry of it names, each counted once per file), and so is anythingvalidateContentwould reject, in the writer’s own words. Each row’s answer is kept until the left draft (its identity and revision) or the right one changes, so a rebuild that changes neither — a theme, a resize — asks no row again;FormPaneindexes its sections and fields andmergeOrderis linear, since nothing bounds the blocks of a scene. One message names what moved. A copy is logged ascompare.copy.start {stem, from, direction, how, row, kind, after, block_case, values: {into_before, from}}— its inputs, taken before anything can refuse it,stemthe column written anddirectionleftorright— thencompare.copiedwith the same, the keys andms, orcompare.copy.refused {stem, direction, row, reason, ms}(nothing_to_copy;epoch_in_usewith the first epoch and every onedropped;invalid_contentwith the refusal’scode, its type, and the writer’smessage). Where the project cannot be written, or the file is the library’s, every control is still drawn over what differs and every one of them is disabled with the reason. Where either column that writes is the version official names, the §13.2 banner sits above the rows as it does on the page — a copy is an edit, and it changes what everyone sees — and its Fork forks the left version, after writing what a copy left pending; the version row above the tabs forks the page’s version, which is the right column, so a banner over an official right column has no Fork of its own. A column that takes no typing shows its save status once a copy has given it something to write, so a failed write of a copy into a scene or a location is shown with its way out. -
Other attributes in Compare — the trailing card is a section like any other here, one paired row per key, because the keys no field names — on a project the contributors, the tracks and the script’s
children— are shown here and a<needs a row to sit on (View/Edit hideschildrenon a project,entityDataSpec’shiddenKeys, since the tree’s Script branch is where the script is read; Compare keeps its row, which is how one project version takes another’s script); the tracks andchildrencopy, and the contributors never do (the roster is the project file’s own, and its row offers no control). The rows carry the paths the card actually shows, so copying a character’sappearancemoves what is in that card and not the age the form holds. -
When Compare cannot open — (enforcing; in development mode the tab opens on the same reason as a red line, §9.5) the tab is greyed with a tooltip saying why: the selected version is the viewer’s latest, so there is nothing to compare it with; there is no username in Settings; or the project is read-only (or the file is the library’s) and they have no version of their own. A greyed tab stays hoverable so its tooltip shows, and a tap on it puts the page back where it was. When the only thing missing is a version of their own, the tab looks open and asks instead (
tabs/compare_fork_dialog.dart): You don’t have a working version to compare against. Would you like to fork one? Yes pins the selection to the version on screen — without that the new fork becomes the version in use and the page would re-resolve onto it, disabling the tab it was asked to open — then forks that version without navigating, and opens Compare once the re-read makes it openable. No goes back to the file’s own data, wherever Compare was tapped from. A refusal is shown inside the dialog, and the buttons are dead while the write is in flight, because a fork is not idempotent. The tabs have an explicit controller (detail/entity_tabs.dart) rather than aDefaultTabController: it also carries the selected tab across a change of the tab set, falls back to the first tab in the same frame when the current one stops being openable, and refuses a swipe, which would change tabs without ever asking. -
Takes — every take discovered for any version of the entity, as cards: the one tile media is drawn with (
MediaTile,take:<take stem>), square at the card’s width, wearing its own batch’s provenance strip and opening the viewer, above the caption — deliverable, take number, source version, batch chip (take:<take stem>.batch, opens the batch), renderer, path; “picked” marks takes the chosen version’spicksname. Its border is drawn as a place’s tile’s is: always its record’s stage; the review and the status only where the version shown places the take exactly once — the live entries naming it, in either spelling, across the version’s groups — and otherwise none, Placement status unavailable in its tooltip. -
Commentary — all authors’ commentary consolidated: a Picks card, then entries newest first with rating icon, author, date,
onchip, block/field chips, text. -
Batch detail — header chips (stem, author, kind, tier, status, created, item count, total cost) and a table of items; the source column links to the entity.
BatchDetailalso dispatches a pending set’s record, andPendingSelection, a session’s record aside (its review, below), to the one set page (filmopen_browser/sets/set_list_page.dart,SetListPage), the same for a comp, an assistant session and a render queue: a header with the type’s icon and the title (the comp’s title, Pending comp, Pending render batch), chips for the stem, status, author, created and item count, the primary action the type supplies (none until the type’s own page gives one), and the items, each drawn by the type’s row builder. The × removes an item only on the viewer’s current pending record of its type (project.pendingOf(type, viewer)) — never on an older pending record, another author’s, a posted or a closed one — throughPendingSets.remove, the outcome a message. Every empty page says what fills it: Add media to a comp from any medium’s menu., Start the assistant to collect a session., Add to batch from the render dialog. It logsset.page.open {type, stem, status, items, removable, reason}andset.page.remove.requested {type, stem, n}. -
An assistant session’s review — a session’s record (its row in the tree’s Chats, the open one’s included) opens on its own page,
SessionPage(filmopen_compare), not the set page: a header — over it, what the chat was asked (session.origin), the record’sactionby its label in the record’s own assistant’s catalog, else its id, and for a field’s or a form section’s action:and the entity’s name, else its id (Edit character: Julius), none on a record that names no action (the origin and each fallback’s reason onsession.review.open‘sorigin); then The assistant’s changes and Your current picks side by side as the two columns’ headings, the summary under them — drawn from the items, not the record’s ownsummary: one mention per stem, Created where any of its items is acreate, else Edited, the project as Wrote Project Summary, a patch sharing a create’soperation(its listing) left out, grouped by verb and type in order of first appearance, named as the tree names them (a stem with no version left by its stem), the type’s word plural where several, the groups apart by semicolons: Wrote Project Summary; Created characters Julius, Gabriela, Adrian; Edited character Kira (a later edit of a stem whose first item was a listing patch is not mentioned, its row still on the page) — then Pick all (session.pick-all) and Discard all (session.discard-all) beside Show changes only — that stays above the rows while the pane is tall enough for both and scrolls with them below that height (the same height as Compare’s headings), and under it, per item in order ofn— but the script’s folders and scenes in the script’s order: where the chat’s first such item stood, as the working script (scriptOutlineProvider, the chat’s own drafts of the lists included) walks them depth-first, each title stood in 28 px a level, so that a scene created at the top and moved into its episode stands inside it; a folder that holds the chat’s items but is none of its own a heading of its icon and name (session.heading.<short stem>) with no Pick, no Discard and nothing under it; what the script does not list after what it does; for display only, every item keeping itsn, its identifiers, its actions and the summary, loggedsession.items.placed {session, source: working | none, root, items: [{n, stem, parent, depth}], headings: [{stem, depth}], unplaced: [n], ms}when the placement changes, not per paint —, a rule, a title row (the type’s icon, the name in use —groupName, the last-updated version’s where every version is discarded —, the item’ssummary, a Pick over each column that holds a file —compare[<n>].pick-left, andcompare[<n>].pick-rightfor a fork’s base — and Discardcompare[<n>].discard), then the left version’s media card in View (the type’sreferenceMediaSection, undercompare[<n>].media, its prompt cards left out as a comparison’s column leaves them; none on a type without one), read from the version as the project holds it, so a picture written while the page is open — the assistant’s render landing — shows there at once, the item still closed; then the fold: the first row the item hides — the first field after the name with words in it (a string or a number), under its own label — fading out to blank (aShaderMask, words alone: no field, no focus, out of the semantics), and at its right the down arrowcompare[<n>].fold(Show the other fields), alone where nothing else has words. Nothing of the comparison is built while the item is closed. The arrow opens it: the Compare form embedded under the arrow, now pointing up (Hide the other fields), with the media section left out (CompareForm.withoutMedia, after Other attributes has counted its keys as shown), its height its content’s, every identifier undercompare[<n>]— each column’s cells built under anIdentifierScope(filmopen_ui) ofcompare[<n>].leftor.right, which every control in them reads throughscopedId(fields, relationships, cast, garments, media tiles with their actions and their jobs’ buttons, voice, analysis, a scene’s clear buttons, the prompt card, a style’s clear button, the Other card), so a patch’s two columns of one stem and two items of one stem are each their own; outside a scope an identifier is unchanged. Pressing the arrow on an open item writes both columns’ drafts first (DraftSaver.settle, the left then the right) and folds it only once both are written; while they are written the arrow is disabled with Working…; a refused write keeps the item open, its failure and its way out on the column’s save status as anywhere (Retry, Discard my changes and re-read, Re-read files), nothing retried by the fold; and where a side holds work again when the item would fold — an edit typed into one column while the other was written — the item stays open with it, its autosave writing it as ever, and the next press folds it. No item is remembered open across visits. The fold logssession.fold.open {session, stem, item, before: closed, ms},session.fold.close.start {session, stem, item, dirty: {left, right}}(each side’s save state,nonewhere it has no autosave),session.fold.close {…, settled: {left, right}: written|clean|none, ms}and a refusalsession.fold.refused {session, stem, item, why: vanished|saveRefused|editedMeanwhile, stage: open|left|right|check, error, ms}(editedMeanwhilewith each side’sstatesat the check; an unexpected failure with itstypetoo, at error level),vanishedwhere the version left the folder between a re-read and the page’s redraw, said on the page. An item whose version the folder no longer holds shows This version is not in the project. with its buttons and no media or fold; an epoch the chat declared (kind: epoch, §7.4) is drawn inside the Project group, folded with the rest of the project’s data: in the record’s lowest-numbered project item, at the foot of its comparison’s Epochs card, after the Synopsis as the project’s page has them, below the item’s media and fold and so shown only while the item is open (CompareForm.sectionFooters; Show changes only never hides that card whole), or, where the chat changed no project file, under a heading Project of its own with no Pick and no Discard and a fold of its own,session.project.fold, closed until pressed (session.fold.open/session.fold.close {session, stem: null, item: null, epochs: [n], before | settled, ms}); each epoch keeps its number and its row — the epoch’s clock icon, Epoch: The war · 1943 (its label, its year after it, else its token), the line Declared by this chat. Discard removes it, or gives back its earlier name, where nothing is set at it., its Discard and no Pick and no comparison —, its answer Removed from the project’s epochs., Given back its earlier name. or Kept: something in the film is set at this epoch., and the summary leaves it out; Discard all publishes the film once, at its end, so the page takes the pass in one snapshot; a record with no items says Start the assistant to collect a session. The conversation is not on the page: the dock beside it holds the chat;session.review.opencarriesmessages, each of the record’s entries that is an object, andmessagesUnread: not a listwhere the member is no list. The left column is the version the session wrote, editable as Compare’s left is — Discard sets it aside as it then is, an edit written there included; the right is, for afork, its recorded base, whatever is in use now, so the pair stays put while the person picks back and forth; for apatch, the version as it stood before the session, read from the item’sbefore, locked, never copied into and with no Pick, since it is no file (Before this session); for acreate, nothing — the left’s rows beside a blank column with no heading, under Show changes only only its rows that hold a value, a block’s included, a card with none hidden, logged at each build (compare.filter {stem_left, stem_right: null, changesOnly, created: true, hidden, hidden_sections, ms}). Each item’s rule and title row stay under the header while its comparison scrolls under them, until the next item’s push them off (PinnedSection,filmopen_ui: Flutter’sSliverMainAxisGroupandPinnedHeaderSliverin oneCustomScrollView, the items keyed, so that a form keeps its controllers, text, selection and focus across a scroll and across the height line; belowBreakpoints.headingsStayPutthe titles scroll with the rows, as the header does). The item’s name is a link to the object’s page (compare[<n>].open), throughbrowserProvider.navigateas the tree opens it, loggedsession.item.open.start {stem, item, from, to}thensession.item.open {stem, item, from, to, now, ok, ms}, a failure with its type and error, said on the page; a version no longer in the folder has no link. A Pick is the version row’s check with its word, the check left of Pick: grey, and green with white text (black on the dark theme’s lighter green) wherepickAtresolves to its version — plain official included, which carries no check on the version row —, so that one of an item’s two is green where either is in use, neither where a third is; Pick all is green where every item’s left is. Every press goes throughSessionReview(§7.4), which decides: Pick makes that column’s version the one in use and decides nothing, so it stays pressable; Discard sets the version aside (never deletes), its tooltip saying it ends the chat; while one of an item’s operations runs its Picks and its Discard are disabled with Working…, and the all buttons while any is; on the open session’s page with its chat still live, a Discard, once any left draft is written, moves the page onto the record (BatchSelectionof its stem: the same page goes on, where the pending page would fall to its empty state once the record is closed), then closes the session (closeSession(id, 'reviewed'), the record taking the items’ words; the dock, told the close began, ends its chat and joins it), then reviews, Working… through it all; a discarded item’s are disabled with Already decided. (a legacyaccepteditem reads as open); a patch’s Discard is offered as every item’s is, the review deciding what it writes (§7.4). Before an item’s Discard the left column’s draft is written (DraftSaver.settle), and a write that fails stops it there, said on the row. Each answer is said on its item’s row (compare[<n>].answer) in words (sessionReviewAnswerText) — an all pass’s skip of a decided item keeping what an earlier press said, its skip of a version gone or discarded meanwhile saying so —, and astaleone offers Re-read files (compare[<n>].reread). It logssession.review.open {stem, viewer, items: [{n, kind, left, right, right_reason: base|before|none, outcome}], kinds, project: {item, epochs}, ms}(project: the Project group’s item, null where it is a heading, and how many epochs it holds), per presssession.review.press {stem, item, verb, how, side?}, the movesession.review.move {stem, from, to, moved, reason: record closes|page left}— a page the person left while a draft was written is not brought back, and the session closes all the same — and the closesession.review.close {stem, session, live, reason, outcome: closed|still-live|not-live, ms}, before a Discardsession.review.settle {stem, item, flushed, outcome: proceed|refused, reason?, ms}, each answersession.review.answer {stem, item, verb, how, side?, outcome, code?, kept_earlier, ms},session.review.reread.startandsession.review.reread {stem, item, read, ms}, and an unexpected failuresession.review.failed {stem, item, verb, how?, stage, type, error, ms}(verb: rereadfor a re-read), said in a message, beside the review’s own lines. A scene item shows its blocks’ media under its title (sceneBlockMediawithprompts: false,filmopen_forms/embedded_form.dart): each block’s first frame and clip that hold an entry not deleted, a block id given twice naming no place, read-only — no Render, no delete door, no prompt box, so that the list keeps no text controllers for them — and nothing of a block that holds neither; a take landing re-reads into its place without the page being reopened (session.item.media {session, n, stem, blocks: [{id, groups, entries, references, reason}], shown, reason, ms}, once per change of what is shown). -
The text boxes on a session’s review are dormant.
SessionPagemarks its list withDormantFields(filmopen_forms), which setsFormScope.dormant; every other page, an ordinary comparison included, draws its fields live. A dormant field (DormantField) is its text in a box that looks like the field — the field’s own decoration and lines, its label, hint, locked fill, microphone and (for a box with suggestions) arrow, its chips by the one helper the editor uses (proseChip, §9.8), clipped beyond the editor’s maximum lines — and holds noEditableText. A click, a tap or a right-click turns that one box into its editor in place, the caret at the point clicked; Tab or keyboard focus reaching it opens it with the caret at the end; the locked column’s box opens its read-only editor. Leaving the field, Tab or Escape (after any menu of the field’s own) turns it back; the window going to the background does not, so the focus returns as it was. The form’s controller and draft stay as they are, so the autosave is untouched: nothing is written on leaving. Pull-downs, chips and checkboxes are unchanged, and the boxes with editors of their own that this does not wrap (an epoch’s label, which keeps its own focus rule; the script’s rows, a frame’s prompt, an outfit’s prose, a voice’s and a plug-in’s fields) stay live. The box’s text is laid out by the render object the editor uses, so it wraps and places its chips as the editor does. A box’s Semantics carries its field’s identifier and its value, each mention by its name; an agent’stapopens it andset_fieldwrites through the scope. Logged:form.box.activate {stem, key, value, by: tap|focus|agent, column, localOffset?},form.box.open {stem, key, displayOffset, by, column, ms}orform.box.refused {stem, key, value, by, column, reason: cancelled|gone|not_focused, ms},form.box.close {stem, key, value, reason: blur|tab|escape|dispose, openMs},form.box.edit {stem, key}; andfields, the text boxes the items seed, onsession.review.open. -
The pending render batch (
filmopen_browser/sets/queue_page.dart, §6.15) — the queue record’s page onSetListPage: Render (primary, 44 px,queue.render) with Render this batch from one app at a time under it, Finish the interrupted pass (queue.finish) on an own record a pass leftsubmitting, and Your other batches waiting: with Open (queue.other:<stem>); one row per item — the place’s name in use with its group or block, the model, platform, stage, frozen price and when it was added — and its state onqueue.item:<stem>:<n>, with that state’s actions (.mark,.readd,.job.<action>,.record,.place); the × (set:<stem>:<n>.remove) on never-submitted rows of the viewer’s current pending batch. Rows and header wrap; nothing overflows at 375 px. -
Comps — Add to comp is an injected item of every placed medium’s menu (
MediaEntryActions;filmopen_browser/sets/comp_action.dart), held by the browser page while it is mounted. It appends{n, reference, batch?, item?, source, deliverable?, added}to the viewer’s pending comp throughPendingSets.add, creating the record where none is pending — the source and the purpose the take’s, else its record’s, else the version it is placed on and the purpose an upload of its kind takes there — and says Added to the pending comp with Open (comp.pending.open) to its page. It refuses, writing nothing, a reference already in the pending comp as decisions read it (PendingSets.pending, §7.5: an add a moment ago included, while its index is still being published) (Already in the pending comp), a medium without a file on this computer — an address, bytes held only in memory or in the bundle, as the web’s sample — (This medium has no file to upload; the sample extracted on a desktop or Android build has its media on disk, which a comp takes), and the formats the comps service does not take, GIF, BMP, MOV and FLAC, read from the take’s file (A comp cannot take .gif files). The comp is the viewer’s own record, not the medium’s version, so the item is not refused by the version’s access (gatedByEntityAccessfalse) but disabled, its reason under its name, where the project may not be written (a read-only project shows no media menu at all) or there is no username (noHandle), and while this app posts a comp (compPosting, Wait for the comp being posted), since the comp being posted is no longer pending and an add would make a second pending comp beside it. The comp’s page (sets/comp_page.dart,CompPage) is the set page with the comp’s fields in itsheaderslot, scrolling with the items: on the viewer’s pending comp Title (comp.title, one line, 160 characters), Description (comp.description, 2,000) and Publish this comp for the public / Keep it a private URL (comp.visibility.public,comp.visibility.private) with Unlisted · anyone with the link can view under it — absent, the visibility reads private — each written throughPendingSets.commit700 ms after the last change, on leaving the page and on turning to another comp, a refused write said; an edit not yet written is an open draft (OpenDrafts), so closing the film and changing the username write it first, as the one who typed it, and a write whose record is no longer the viewer’s pending comp is refused with nothing written, and said; the rows areMediaTiles, a tap opening the viewer, each with ×; POST (comp.post, a primary button, 44 px) is disabled, its tooltip the first reason that holds: Add media to the comp first, Give the comp a title first, Sign in to post (the account’s click is the sign-in gesture), This project is read-only here., Posting… while a post of this app runs, and it stays drawn, disabled, with Posting… on the record this app is posting. Pressed, it writes the fields first — where they cannot be written it posts nothing and says Not posted: the comp’s fields could not be written. Try again., in a message and inline (comp.post.error) on that comp until its next press, never on another comp the page turns to — then the record’s statusposting, the page following the record to its own page (BatchSelection), its fields locked, with Posting… and the publish’s progress (comp.posting); then it prepares and publishes throughCompPoster(§6.17). A receipt makes the recordpostedwithposted: {at, url, revision}; a refusal or a failure makes itpendingagain witherror: {code, at, detail}, said above the fields until the next post (comp.post.error: The last post did not go through (code). Posting again starts a new post.,account-mismatchin its own words). One post runs at a time:compPostingProviderholds the running post’s stem for the app’s run, from beforepostingis written until the post ends, so a post outlives its page, and a second press does nothing. A record of the viewer’s foundpostingin a film that may be written, whose stem is not held there — a crash or a quit during the upload — ispendingagain when its page opens, witherror.codeinterrupted, and says The last post did not finish in this app. Posting again starts a new post.; the service publishes a revision only once its upload is complete, and a new POST is a new revision. Another author’s record foundposting, or one in a film that may not be written, is left as it is. Another author’s or an older pending comp, and one in a film that may not be written, is read in the locked look. A posted comp’s page is read-only: its title, description, visibility and URL, with Open in browser (comp.open_url, a web address only, since the record is a project file another author may write) and Share (comp.share, the URL on the clipboard, then Link copied), a refusal said; no POST is offered on it. It logscomp.add.start,comp.add.resolved {reference, path?, extension?, location, source, deliverable?}, thencomp.add {stem, reference, batch?, item?, deliverable?, n, created, ms}orcomp.add.refused {reference, reason, extension?, pending?, ms}, an unexpected failurecomp.add.failed {reference, stage, code, error, ms}, rethrown for the menu to say;comp.add.unavailable {reference, reason: compPosting, posting}as the menu asks while a post runs;comp.page.open {stem, items, status, mine, editable, title, visibility, error?, stoppedCleared?},stoppedClearedthe stem whose stopped POST the page stops saying; the page’scomp.post.start {stem, items, title, visibility, description, film, poster}andcomp.post.step {stem, step, ms}(the poster’s lines of the same names carry astage, §6.17) aspostingis written (or a warning with the poster’s unexpected error),comp.post.refused {stem, reason: already-posting, running}for a second press,comp.post.failed {stem, code, error, ms}where a record write is refused, andcomp.post.done {stem, outcome: posted|refused|failed, code?, url?, ms};comp.post.posting {stem, running, own, canWrite, decision}as a record foundpostingopens, thencomp.post.interrupted {stem, outcome: returned|unchanged, status?, ms}, or a warning{stem, outcome: failed, code, error, ms}where the write is refused; per autosavecomp.field.save {stem, why, changed, pending?}andcomp.field {stem, field, value}, the values as typed, orcomp.field.refused {stem, field, reason: record-changed, pending?, values, ms},valuesthe three fields as the refused save held them;comp.field.written {stem, written}should another record be written;comp.draft.settle {stem, clean, dirty, failed, ms}as a close or a change of username settles the page’s edit, a refused write answered unclean once;comp.page.access {stem, editable, reason: viewer|film-access, mine, canWrite};comp.open_url.start {stem, url, web},comp.share.start {stem, url}, thencomp.open_urlandcomp.share {stem, url, outcome, reason?, code?, error?, ms}. -
A discarded version’s page (Project Specification §7) opens with a notice, This version is discarded — nobody uses it, and it is still its author’s to edit — with no action: nothing brings a discarded version back (§7.4’s one door only sets aside). The star is drawn unlit there, and the official-edit banner is not shown, since a pointer naming a discarded version counts as none. The version pull-down lists the author’s discarded versions greyed, after a divider where retained versions of theirs are above, so it is never blank on a discarded page, and its check is unlit where the viewer’s pick names a discarded version. A refused discard of an official version or of the project file of record says the director moves or clears the star first; a reference nothing resolves — no version, or every one discarded — says No version of “…” is in use here.
-
Other files — one list of files the project loader does not understand, including misplaced documents, nested files and non-JSON companions. The page reports their paths and never changes them.
A pick the project overtook (§13.2, §13.4): when Pick.staleSince is set for the viewer at the epoch on screen, a StalePickBanner sits above the header on every tab of that entity — where PageAccess.canPick holds, so never on a plug-in, whose old pick chooses nothing, nor on a page whose pick could not be written — the error-container card with the sentence Yes, abandon this (the pick and its date are removed, the viewer follows official, and — as the question says, go to the official — the page moves to the official version and the message names it; where there was no pick of the viewer’s own to abandon, nothing is written and the message says so) and No, keep this as my pick (the pick, in use before and after, is re-dated, so it is no longer stale and the banner goes). Both go through WriteActions, so a refusal is a message and changes nothing, and both are followed by the re-read; the buttons wrap at a narrow width.
A version in the recycle bin (Project Specification §7; entity_detail.dart’s _DeletedBanner): when the version on screen carries deleted: true, a card in the error container’s colours sits above the header on every tab — after the guard lines and the stale-pick banner — saying the version is deleted, that whoever uses it has no such entity, and that the file is kept; with Restore (entity.restore) and, under it, its last answer (entity.restore.said). Restore is restoreEntity (filmopen_state/entity_restore.dart), one rule with two doors, ownership read from the filename’s author and never from development mode’s PageAccess.editable: the person’s own file is saved through the controller’s own write with the deleted key removed — never set to null, which the format does not allow —, the open drafts settled first and the writer’s stale check kept, the person’s draft and a v1 the assistant created included, which are theirs and stay unpicked; any other file — another author’s, an older <owner>.ai version included, the library’s — is forked as the person’s own with the key removed (ProjectController.fork(…, content:)), which picks it, and nothing of another author’s is written. After the fork’s re-read the fork is checked to be the person’s pick; where it is not, that is said as a long message, which outlives the card the re-read may take away (Restored as your new version …, but your pick of it could not be written: pick … to make it the version you use., which says nothing of the version the page shows — a page opened on the deleted version’s own stem still shows it), and under the card where it is still there; nothing is retried, forked again or repaired. Every answer is said on the messenger and the container retained before the write, since the re-read can rebuild the page. Its tooltip says which door it is; without a username, or on a project that cannot be written, it is disabled with the reason as its tooltip. A restore in place says Restored.; a fork says Restored as your new version …, now in use. and the page moves to it; a refusal (RestoreRefused: a draft that could not be written, the file gone, the version no longer deleted) or the writer’s refusal stays under the card, as a sentence (restoreRefusalText, writeProblemText). Use the version before is not offered: no history of earlier picks is kept. Its log: entity.restore.start {stem, path: own | fork}, entity.restore.stage {stem, stage: settle | write | fork | refresh | pick, outcome, ms} — the write’s or the fork’s outcome its own result, then the controller’s re-read after it as refresh with its own outcome and time, a refused write logged failed once, a pick not found after the re-read not-picked with reason: no-own-pick | other-pick — and entity.restore {stem, fork?, path, outcome: restored | forked-picked | forked-not-picked | refused | failed, reason, stage?, type?, ms} — a refusal with its stage, a failure with its stage and its type alone, a restore whose film was not re-read after the write reason: not-reread —, the stem as a digest.
The media card in a list (filmopen_forms’ MediaFormCard, the Workflow tab’s row): a version’s media as one card that sits in a scrolling list rather than on a page of its own — the version’s form state (FormSide) with one card and no page around it. An entity’s card is its media card limited to the type’s named places (workflowMediaGroups: a character’s Reference images, Character sheets and Voice samples; an outfit’s, a location’s and a prop’s Reference images and their sheets), each under its Prompt card, with no other place and no loose preview; referenceMediaSection and MediaCard take those groups, every page passing none and drawing every place as before. A scene’s card lists its blocks in script order — those with an id the scene gives once —, each headed by its name and first words, with that block’s places (BlockMediaPlaces — the Script page’s own block places, lifted whole — given workflowBlockGroups: First frames, Last frames, Clip, and Speech where the block has a speaker), and no Add a place…. A card’s controls name its version first (<stem>.draft.retry, <stem>.draft.discard), so several cards share a page; a card whose draft is dirty, saving or failed stays alive when its list scrolls it away, its failure and its way out still there when it comes back, and a clean one goes with nothing to write. The place’s checkbox: under a PlaceSelection (filmopen_insertion_ui) — one notifier of checked places and the set offered — a MediaPlace that is offered and editable leads its title with a checkbox (<place>.check, its label the place’s title); every other place, and every place with no selection above it, draws none.
9.4 Settings
Section titled “9.4 Settings”Appearance (System / Light / Dark), Language (a pull-down drawn as the microphone’s beside it is, settings.language: while FilmOpen is in development English is the one language shown (§10), and the control reads English, held, its tooltip saying Other languages return after development., a press opening nothing; with more than one language shown it lists System first, chosen while no language is and keeping none — never the language it resolves to — then each language by its name in its own language, the chosen one checked), Dictation (filmopen_dictation’s DictationSettingsCard, §9.6: the one place the microphone every box records from is chosen — Microphone, a pull-down naming the one chosen, which asks the system for its microphones when it opens and never before; none on the web), Identity (signed in, the username chosen from the account’s names, never typed — AccountUsernameChoice as the new-project director draws it (§9.2), settings.handle its identifier, the pull-down setting the owner alone, a workspace dropped — with the same refusal and Try again, and the claim of another; signed out, the username in a ShortNameField labelled Username that takes a workspace and allows an empty box — default its hint, shown at rest too, an emptied box staying empty while the username is default —, with helper text naming the username in effect and, for a text that is not a handle, why — official is reserved, one dot at most, a name on both sides of a dot —; under it, while the box holds a text that is not a handle, Your username is still “{handle}”.; a change refused by an open page’s draft, with Try again (settings.handle.ask-again), which asks the box’s text again; and a username not saved, with Try again (settings.handle.retry). The box follows a username changed elsewhere — an agent, a dialog — and never one it asked for itself, so an answer that comes late never writes over what is being typed), Shared library (what it is; the folder in use with a show-in-file-browser button; Choose folder…; Use default when a folder was chosen), Export encoder (on Linux, Windows and macOS, where the dock exports; not on the web, Android or iOS: Automatic / Software only, settings.export_encoder, with the helper Automatic uses a graphics-card encoder when one is available and works, otherwise the software encoder. Software only uses the software encoder.; Settings.exportEncoder, auto or software, kept in the shared preferences as exportEncoder, an unknown stored value read as auto and logged settings.export_encoder.unknown {stored, read_as}; a choice logged settings.export_encoder.start {old, new}, then settings.export_encoder {old, new, outcome, code, cause, ms}, a value other than the two refused, after its .start, and logged settings.export_encoder.refused {old, new, outcome, code: unknown_value, reason, ms}; a choice the preferences could not keep holds for the session, marked in Settings.unsaved until a write of it succeeds, and the card says under its helper This choice could not be saved: it holds until FilmOpen closes. — no Try again, the next choice writing it again; the dock’s MP4 export asks for it, §19), Logs (what is logged; the Verbose logs checkbox; the log file’s path and a View log button that shows it in the file browser; on the web, a line saying entries go to the console), Development (only in a build that has development mode: what it is, and the Warn instead of refuse checkbox with a line naming the three rules it governs), About (the build’s stamp, §6.6a, selectable; specification version, URL, the preferences folder with a show button, Open-source notices → Flutter’s licence page, which includes the vendored API Dash licence).
Remote control (filmopen_relay’s RemoteControlCard, §6.6c) follows Identity on General where the root made a relay client — not on the web, nor in a test: the switch Let my chat app use this device (settings.remote), disabled with the tooltip Sign in to use Remote control. while nobody is signed in, or on a build that registers no device — a switch left on stays turnable off, and a signed-in device that a refusal left unregistered can turn it on, the name’s claim registering it again; under it the state — Off (the switch off, whatever socket lending holds), Connecting…, Connected, Stopped () or Another copy of this device took over — and the device’s name, This device’s name: <slug>, or This device has no name yet: Remote control asks for one. — with the switch on on a registered device without a name, as after it was forgotten on the website, Turn the switch off and on to name this device.; a stop with 403 or 4003 reads The website no longer admits this device: check its Devices tab, then sign out and in again.; while connected with a name, Connector address and https://<kFilmOpenDomain>/v1/mcp/<username>/<slug>, selectable, with a copy button (settings.remote.copy): Address copied once the clipboard took it, The address could not be copied. otherwise. A switch whose setting could not be saved says This setting could not be kept: it lasts until FilmOpen closes. Turning the switch on opens the device-name dialog first where the device has no name (§6.6b), and a cancel leaves it off. The card adds no device card to Settings.
Sharing (filmopen_sharing’s FilmOpenSharingCard, §6.10, §6.14) follows Remote control (or Identity) on General, at most 620 px wide. Under the title, the open film’s name — or Open a film to share it where none is open; on a session copy Copy the film to a folder first, with no account line, and beside it only what belongs to no film — the step under way, a notice about no film in particular, and a sign-in under way, whose wait and Cancel stay whichever film is open; on the web Sharing needs the desktop app and nothing else. The state in one line (sharing.state): Not shared; Shared on Google Drive, in FilmOpen/<name>; Shared to <path>; or Shared to a folder that this computer has not chosen. Under it, while the film is shared, two independent sentences count its jobs from syncJobsProvider. The first appears only when it has send-kind jobs and counts files waiting to send, sent and needing a person; the second appears only when it has fetch-kind jobs and counts files waiting to receive, received and needing a person. Queued, working and waiting jobs are pending, done jobs are complete, failed and attention jobs need a person, and cancelled jobs add to none of those counts. A done fetch whose explicit outcome is keptLocal or alreadyAgreed adds nothing to the received total; missing or unknown outcomes retain the normal done count. The receive sentence still appears when these are the only fetch jobs. Counts describe final job outcomes, not an audit of physical transfers across interrupted attempts: recovery may finish with agreement after bytes landed in an earlier attempt. Each sentence has its own singular and plural forms. A film no longer shared shows neither sentence; its remaining jobs belong to Share, and the card draws no job. Where a folder dialog exists (folderDialogsAvailableProvider), a row holds the box for a folder of this computer (sharing.folder-path), typed or filled by Browse… (sharing.folder-browse, the folder picker); under it the buttons: Share on Google Drive (sharing.drive), Share to a folder… (sharing.folder; Choose folder… where the film is shared to a folder this computer has not chosen) and, where the film is shared, Stop sharing (sharing.stop). A share stays offered on a film already shared: pressing it again is how a person restarts the sends they stopped, and sharing elsewhere moves the film to that target. Each disabled button says why in its tooltip: no Google client in this build, no sync in this session — the application could not start it (Sharing could not start in this session. Restart FilmOpen to share.) — no username (the manifest is written as the viewer), no folder typed yet, or a step under way (Wait for the step under way to finish.), which Browse… and the box wait for too, the box saying so in a tooltip of its own. While a gesture is under way or queued the card says so inline (sharing.working, One moment: FilmOpen is on it.), whichever film is on screen, or none.
Share on Google Drive asks for the sign-in first (below), then finds or makes FilmOpen/<tag> in My Drive — the folder the manifest names where it is still there, else found by name, else made, over one client asked for within 30 seconds — clears the stopped sends of that folder, writes the manifest’s sync with the folder’s id and name and forgets this machine’s folder for the film. Share to a folder… takes the path inside quotes where it is given in them, as a file browser copies one, and forward slashes for backward ones on Windows; it refuses, before anything is written, a folder not given in full on this computer’s file system (on Windows a leading \ alone is the current drive’s root, not a full path), one that does not exist or cannot be listed, and the film’s own folder, one inside it or one that holds it — the two folders compared as the file system resolves them, their links and . and .. segments followed, without case and without Windows’ extended-length \\?\ — logged sharing.folder.check {folder, resolved, film, filmResolved, outcome: taken | refused | unreadable, reason: notFull | missing | filmFolder | holdsFilm | noFilmFolder, code?, error?} — saying That folder cannot take the film: give the whole path of a folder that exists, neither the film’s own folder, one inside it, nor one that holds it.; then clears the stopped sends of that folder, keeps the path in this machine’s preferences by the film’s key — before the manifest is written, so that the reconcile the write starts finds it, and put back as it was where the write does not happen — and writes sync with the target folder alone. Stop sharing removes sync and this machine’s folder, and the reconcile that follows retires every sync job of the film the engine holds — a film whose restore failed at opening keeps its journaled jobs until an opening whose restore succeeds (§6.14); the shared folder keeps what it holds. The manifest is written through the state’s writer, as the viewer and with the film’s guards, and a film without a manifest or without a username is said before anything is asked of Drive or a folder. The manifest’s re-read reconciles the film: the state’s binding does (§6.14), and a gesture adds no pass of its own — one behind the binding’s would list the target while its first sends are under way and queue again a send done meanwhile, a second Sent. in Share; so a reconcile that fails is the binding’s to log (sync.reconcileFailed), after the card has said Sharing started. A reconcile retires the jobs of a target the film named before and queues what the new one lacks. An explicit Share restarts stopped sends: before it writes the target it waits up to thirty seconds for that target’s sends already stopping, then dismisses its cancelled sends — a person’s earlier Stop among them — reading every acknowledgement, each dismissal logged as it is sent (sharing.jobAction: the job’s id, kind, phase and state, the action, the target and the reason, restart) and as it ends (sharing.jobActionDone: the outcome, the code and the time, a refusal or a failure a warning, a failure with its error’s type and text); a send that offers no dismissal, a refused dismissal or a wait that runs out is said as such (The stopped sends could not be cleared. Try again.) and nothing is written. Background reconciliation respects a person’s Stop. Gestures run one after another, in the order they came — a stop pressed while a share waits runs after it — and each checks, before it writes, that the film it was pressed on is still the open one: another film opened meanwhile writes nothing to either. The state’s binding, not the card, delivers after Settings is left.
FilmOpen Cloud and the record on the card. Beside Share on Google Drive, Share on FilmOpen Cloud (sharing.filmopen), and after the others Record on {domain} (sharing.record), both on every film with a folder. Each disabled says why: signed out of the account (Sign in to {domain} to use FilmOpen Cloud., Sign in to {domain} to record this project.); for the share, a balance known to be zero (You have no balance. Top up at {domain}.) or not known yet (Your balance is not known yet.); for the record, a project already recorded from this device (This project is recorded on {domain}.); then the reasons above — for the record only no username and a step under way, since it writes no manifest. The card follows the account as it changes. A film on FilmOpen Cloud reads Shared on FilmOpen Cloud as <username>/<tag>, and Share on Google Drive is disabled on it (This project is shared on FilmOpen Cloud.). What became of a record is said under the buttons (sharing.record-outcome), on its own film only: Recorded on {domain}., Recorded again. (the account had it from a press whose answer was lost), the confirmation this device could not save, a record under another username or tag, or Its record could not be written ({code}). — never claimed when it was not. On Share, a row of a film on FilmOpen Cloud asks Sign in to {domain} to continue syncing., its sign-in opens the account’s popup on Settings with that sentence, and its wait reads Signing in to {domain}… until the account is signed in, Cancel is pressed or 30 s pass.
Under a divider, the Google account, drawn from the driveSignedIn preference the last sign-in or sign-out left, never from the credential store, since Settings is built at the application’s start: Signed in to Google with Sign out (sharing.sign-out), or Not signed in with Sign in (sharing.sign-in, disabled where the build has no client). A sign-out forgets the Drive folders’ lookups too (§6.10), and so does a sign-in — Google’s account chooser can land a consent on another account with no sign-out between — so a film is looked up afresh under the account signed in next. One sign-in for the card and Share (§9.7), the model’s: a share, or Share’s Sign in, asks whether the machine holds a sign-in Google takes (ensureSignedIn). Nobody signed in opens the consent page. A sign-in held is tried with a forced refresh within 30 seconds, its client closed as it comes: taken, it opens no consent page; unreachable, too slow or refused, the credential store is asked again — empty, a sign-out came in meanwhile, and the answer is no, with no consent page; tokens still held, Google unreachable or too slow lets the action go on, the send saying what it meets, and Google refusing them is a sign-in Google refused, which the consent page renews. A second sign-in asked for while one is under way joins it. While it waits the card says Signing in to Google: finish in your browser if it opens. with Cancel (sharing.sign-in-cancel), and Share says the same under the rows that asked, with a Cancel of its own: either stops the one wait at once, at either stage — the consent flow is abandoned, so an answer that comes after stores nothing, and a refresh still out has its client closed when it answers. Back cancels nothing; the wait ends by a Cancel, by the flow’s own end or with the application. The model is one for the application’s life (sharingModelProvider), not one per card, which Settings takes away with its tab: coming back finds the wait, its Cancel and the last notice where they were.
Notices stand inline in the card (sharing.notice), above the account’s line, until the next gesture or sign-in, on the film they were raised on — a refusal of one film never stands under another’s card — while the account’s, the session’s and another film was opened stand whichever film is open; a cancellation is drawn in the neutral colour, the person’s own doing, every other notice in the error colour. There are eighteen: sharing could not start in this session; no Google client; a sign-in that failed; a sign-in Drive refused after it was refreshed (Sign out, then sign in again); a sign-in cancelled; a folder that cannot take the film, or cannot be listed; no manifest; the manifest changed meanwhile; Drive unreachable; another film opened before the share finished (nothing was written); no username; a manifest that could not be written; stopped sends that could not be cleared; for FilmOpen Cloud, signed out, a balance known to be zero, a username that is not one of the account’s (Your username in Settings is not one of your usernames on {domain}.) and the account’s usernames not readable (Your account on {domain} could not be reached. Try again later.); and a failure nothing foresaw (Sharing did not work. Try again.), logged by its type and said, never thrown at the card. The two confirmations, Sharing started and Sharing stopped, pass through showMessage. Every gesture is an operation of the log: sharing.start {op: shareOnDrive | shareToFolder | shareOnFilmOpenCloud | stopSharing, film} at the press, each stage reached as sharing.step {op, film, stage, outcome, …, ms} — Drive’s signIn (ok, or refused with the notice’s code), its folder found with its id and name, a folder’s folder listed with the path and the target’s identity, and manifest written — and one sharing.done {op, film, outcome: done | refused | failed | dropped, code?, cause?, ms}, ms counting from the press, the wait in the queue included, code the notice said and cause the decision behind it where one code has several: the restart’s timedOut, notDismissable or dismissRefused behind restartRefused, the folder check’s cannotTake or unreadable behind folderUnavailable, Drive’s own code behind signInRefused, driveUnconfigured or driveUnavailable; a sign-in that answered no with no notice (a sign-out came in while the held sign-in was tried) ends refused as notSignedIn, the cloud’s record not made as the record’s code, a failure nothing foresaw failed, and a gesture the model’s disposal kept from running dropped with the cause disposed. The restart of stopped sends logs sharing.restart.start {film, target, stopping} with the ids still stopping, sharing.restart.step {…, stage: wait | dismiss, outcome, job?, ms} as they settle and per dismissal, then sharing.restart.done {…, dismissed, ms} or sharing.restart.refused {…, stage: wait | dismiss, reason: timedOut | notDismissable | dismissRefused, job?, ms}, a dismissal that never answered within the deadline refused at its own stage with its job. A sign-in logs sharing.signIn.start {mode: consent | ensure}, its decisions sharing.signIn.step {mode, stage: held | refresh | heldStill | decision, outcome, code?, ms} and one sharing.signIn.done {mode, outcome: signedIn | refused | cancelled | failed, code?, ms}, the askers that joined it adding no line; a sign-out sharing.signOut.start and sharing.signOut.done {ms} ({outcome: dropped, cause: disposed, ms} where the disposal kept it from running). Beside them sharing.started {target, ms}, sharing.stopped {target, ms}, sharing.signInCancelled, sharing.notice with a notice’s code, and warnings — sharing.failed {op, film, type, error, ms}, sharing.signInFailed, sharing.signOutFailed, sharing.writeFailed — with an error’s type and words; the folder ids and paths they worked on are logged, never a token; building the application and the card reads no credential.
Pages (Plugins): General, these cards and how keys are kept — the app owns no key (§6.7) —, Plugins, each plug-in’s own keys on its page, the speech services’ providers among them, and Models, the render defaults, text model and route, and speech service (below); this device’s usage is Activity’s (§9.7). At Breakpoints.treeInDrawer (700 px) of window and more, a list beside the page in the browser’s sidebar and its divider — the title, then one row per page with its icon, the page shown drawn as the tree draws its selected row; a list, not a tree, with nothing to expand — starting at the sidebar’s narrowest, 220 px, so that a 700 px window leaves the page some 410 px; a width dragged to lasts while Settings is shown. Below it, the title and a labelled pull-down above the page (Settings page) with the same three. Settings opens on General each time the rail shows it, since leaving Settings puts the page back (Dashboard); settingsTabProvider holds the page, so a link elsewhere can open Settings on the page it needs, and a resize across the breakpoint keeps it — the page keeps its element too, so what is typed on it survives the resize. The list is a tab bar to the semantics — each page a tab with its selected state — and the pull-down an ordinary labelled one (settings.page), whose value is the page shown: the state’s tab and the recorder read the selected tab, or the pull-down’s value (currentTab, settingsPickerPage), and the agent’s set_tab general|plugins|models taps the tab, or opens the pull-down and chooses the page from its menu by its name, as a person does. General’s list is the scrollable that holds its cards; the list beside it scrolls on its own where a window is too short for it. A page’s name too long for its row wraps inside it.
Models (lib/screens/settings_models.dart, SettingsModelsTab). At the top a two-segment control, Prototype | Final (settings.models.stage.prototype, .final) — a control, not tabs, so that set_tab models and the state’s tab know one level; then a sentence saying that each row is the model a kind of render opens on at that stage, before the render dialog’s last choice, and that Draft uses Final’s model at its lowest resolution; then one card per kind of render a place asks by — Text to image (t2i), Image to image (i2i), Text to video (t2v), Image to video (i2v), First and last frame to video (flf2v), Image and sound to video (ia2v), First and last frame and sound to video (flfa2v), Voice design (ttv) — shown only where a catalogue model of that tag has a route here (RenderBackend.candidates, a route not ready counting; none shown, an empty state says that turning on a plug-in that renders fills it); tts and ttm are not offered, since no place asks a model by those kinds. Each card: a model pull-down (settings.models.<kind>.model), The picker’s default first, which clears the choice, then the models by name, one none of whose routes is ready reading <name> — not ready here in the menu — the field holds the name alone, a long one wrapping, and a chosen model with no route ready here says <name> — not ready here under it, wrapping; where the chosen model has more than one route, Runs on (settings.models.<kind>.route), one row per plug-in, platform and key, named by its plug-in and origin (renderRouteLine) — <plug-in> / Your key, FilmOpen credit, this computer, Codex on this computer, a shared offer by where it is shared (Shared on filmopen.ai, Your other computer), never as the person’s key — never by a place: a route a server the person started or a shared offer comes first on reads as the plug-in’s own candidate does, a local engine’s this computer, else as the shared offer, its readiness any candidate’s and every funding folded into it logged (settings.models.read’s folded) — the place within a route is not chosen here: the render takes the place it last ran that route on, else its first ready one (§6.15). On Final, where Draft would render at Final’s own resolution — the model lists only one (for its variant, where the default names one), or Final’s resolution as asked (the model picker’s for that model, renderSettingsPicked, else none) and Draft’s (the lowest listed) settle to one value on the chosen route, each the asked value where the route’s options take it, else their default (renderSettledValue, as a render settles them) — the card says Draft has no lower resolution than Final on this model., logged settings.models.draft {role, model, variant?, route, listed, finalAsked?, finalFrom: picker | routeDefault, final?, draftAsked, draft?, same, ms}. A default kept for a model or a route not offered on this computer — a model offered on another route included — reads as The picker’s default, the preference left as it is. A choice the store refuses holds for the session, and the card says under it, at that stage only, This choice could not be saved: it holds until FilmOpen closes. (settings.models.<kind>.failed) until a later choice at that stage is kept; RenderDefaultsController.choose answers whether the store kept it. After the render kinds, Text holds the assistant’s text model (AssistantTextModelPicker, assistant.textModel), which has no stage, and the chosen model’s decided plug-in / origin (§6.11a: the plug-in admission takes for that model). Its Runs on menu (settings.models.text.route) offers Automatic and that plug-in’s explicit candidates, retaining an unavailable saved choice so it can be cleared. With no model chosen — the assistant deciding — the row claims no route: one sentence says the assistant decides each action’s model and route and that the assistant panel shows where each will run before it starts, and the menu is shown only for a refused saved route, to be cleared. Choosing a route also prefers its text provider; Automatic clears the route while keeping that preference. The Speech service card follows Text, with its own decided route. A render choice is kept at once in the preference render.defaults (renderDefaultsProvider, filmopen_state): keyed <kind>/<stage>, each entry the model, the route as RenderRoute’s codec writes it and, where set, the variant; read at the start (settings.read {key, value, outcome, code?, cause?, entries, ms}) and written with settings.keep.start {key, role, entry, value, store} then settings.kept {key, role, value, outcome, code?, cause?, ms}. The page logs settings.models.read.start, settings.models.read {kinds: {kind: [{model, routes}]}, ms} or settings.models.failed {type, error, ms} (and says the models could not be read), settings.models.stage {from, to}, and settings.models.shown {stage, kinds, hidden, unavailable, text: {model, plugin, route, candidates}, speech} once per stage, text or speech decision, set of kinds shown and defaults kept whose model or route is not offered here (unavailable, each <kind>/<stage> with its entry).
The keys (§6.7) are a plug-in’s, each on its page (KeyCards(owner:), one owner’s cards from PluginCatalogueView.current.keyOwners); the app owns none. General’s keys section (GeneralKeysSection, below the logs and above Development and About) holds no key card: a card on how keys are kept; its leading slot is empty; then a line saying the other platforms are pending and the website lists what stands in their way, with See the platforms. A plug-in’s page under Plugins offers Use Automatic beside a stale text route when that plug-in is not the preferred text provider; the action clears its saved route and retains the preference. The page draws its own cards while it is on, through the builder the app hands PluginsTab — the speech services’ keys on fo-openai’s and fo-xai’s. A door elsewhere opens a plug-in’s page through settingsTabProvider’s openPlugin(tag), which settings_page.dart hands the tab as openPlugin and takes back once the page has opened (settingsPluginToOpenProvider); a tag not installed leaves the list. A page has no owner’s header. There is a card per key slot whose platform the catalogue lists, or else the owner’s own platform file defines, titled with the slot’s label or else the platform’s name, the slot’s help under the title — its http and https addresses drawn as links that open in the browser, a sentence’s closing punctuation left outside them, a tap logged keys.help.link {plugin, url, ok, refused?, ms} beside the opener’s keys.link, an address that is no URI refused (refused: notUri) and said as a page that could not be opened — and under it the card’s state, or using FilmOpen credit where the card runs on credit: where the owner’s text route (PluginSummary.route) names this card’s key, the sentence follows that route — credit only where it is credit, so that a key saved shows its state —, elsewhere whether the platform has credit, logged keys.card.credit {key, onCredit, why: route | platformCredit, route, platformCredit} where the decision changes; while the sentences around a card — Save’s question and Remove’s, what a save or a removal says — name the platform, since each says key itself and a slot’s label is a whole phrase (OpenRouter key); a plug-in turned off takes its cards away and leaves its keys in the store, its page saying to turn it on to enter them. A catalogue that yields no platform says so, and that installing FilmOpen again restores its list. Each slot’s card holds:
The keyring line (§6.7). Where the credential store returned none of the keys FilmOpen saved, one line stands above either page of Settings, and in the assistant dock under the assistant’s name, saying that the store, named as the platform’s (the system keyring, FilmOpen’s protected file, the Keychain, the Android Keystore), returned none of FilmOpen’s keys and that nothing was changed, with These keys are gone: start again, which asks before it acts; it goes once a key reads again. A card whose key its record names but the store no longer gives back says Missing from this computer’s keyring and Enter it again in the box below. In the assistant’s chat a request refused before it left the computer reads Not sent: … — for a missing key with Open Settings, the dock’s way to the provider’s key card — and never as an outcome nobody knows.
- its state: No key, Stored, Verified on <date>, Rejected by <platform> or Could not check, with ends in <last four> where known — and since a save stores only a key the platform accepted, the last two reach a card only from a state an earlier build wrote, which the next save replaces; under such a state, a line saying why and what to do (a 403 has its own); while a check runs, a line saying the key is being checked with the platform;
- a box for each credential the platform file lists, labelled with FilmOpen’s word for the value where it knows the file’s (API key, Secret key, Key ID, Key secret), else the file’s own, obscured — with an eye at its end that shows what was pasted until the eye or a save hides it, since a phone’s clipboard is not always what it seems; a refused key stays in its box as it was, shown or not; the box’s identifier stays outside
valueIdentifiers(§4.3), so what the eye shows never travels in the data an agent or the tape reads; a picture shows what the screen shows, so while the issue recorder runs (recordingProvider, §7.1) every box is hidden and the eye says so — with no suggestions or autocorrection, and replaced by an empty one as Save reads it. A helper line in its own colour warns when the value looks like another listed platform’s key or, once what was typed can no longer become the platform’sprefix, when it does not start with it; Save still works (warn, don’t enforce); - the actions:
- Save, the only door a key goes through, checking with the platform file’s
verify, else with the owning plug-in’sverifyKey(§6.11), else storing unchecked: disabled while a box is empty, saying Paste the key first, and pressed by Enter in a filled box. One press checks the key with the platform and stores it where the platform accepts it; a key it refuses, and one whose check has no answer, are not stored, and the box keeps the paste so that it can be put right. Its answer is a line under the boxes — accepted in the positive colour, invalid in the error colour, a check that could not be made in the warning colour, each saying what became of the key — and the line goes as soon as a box changes. Save asks first, withkeys.replace.cancelandkeys.replace.confirm(a 44 px target), when it would replace a stored key, naming its last four, or when the value looks like another listed platform’s key or does not start as this platform’s keys do; cancelled, the boxes keep what was pasted; - Remove, disabled while no key is stored, saying so. Remove asks first, in a dialog whose Remove is a 44 px target;
- while an action runs, the boxes and these three are disabled, each saying why;
- How to get a key (the platform’s guide on the documentation site,
kFilmOpenDocsUrl, §3) and Open the keys page (the platform’s own), each saying so when the page cannot be opened; - messages: a transient message after a save that stored the key, and after a removal; the answer to its check is the line under the boxes, not a message that goes by. Inline until the next action: a store that cannot be used, a value no key could hold, or an action refused because another was running.
There is no way to show a saved key. On the web General’s keys section is one sentence saying that keys are kept by the desktop app, and that FilmOpen on a computer adds them. Identifiers: keys.<owner>.<slot>, keys.<owner>.<slot>.<credential> (keys.fo-openai.openai.key), .key.show (the eye), .save, .remove, .guide, .keys-page and .answer, the line a save’s check leaves; keys.remove.cancel and keys.remove.confirm in Remove’s dialog, keys.replace.cancel and keys.replace.confirm in Save’s; and keys.platforms.
Plugins (filmopen_plugins_ui, §3.11). The list: a line on what plug-ins are and that one not from FilmOpen stays off until allowed; Install from file… (plugins.install: a zip unpacked into the library, then the plug-in’s page and its consent; a zip refused says the file is not a plug-in FilmOpen can install, with its code, and an install that failed in FilmOpen — a write or another step — says Installing this plug-in did not work, with writeFailed or failed, and one whose files are written but whose plug-ins could not be read again says the plug-in is in the library, that the plug-ins could not be read again, with reloadFailed, and that it shows once FilmOpen starts again); Preferred providers, Text and Images and video (plugins.prefer.text, plugins.prefer.media), each a pull-down of the enabled plug-ins providing complete or render, by name and platform, or a line saying none does, redrawn as the plug-ins change while the list stays on screen — showing None: ask me where the person chose none, and offering it to take a choice back. The general provider chooser asks only where more than one plug-in remains after named-model and local ranking. For LLM calls, Text supplies the provider preference to the shared decision (§6.11): among the decided model’s reachable providers the preference wins, else stock, else first by tag; leaving Text unselected neither asks nor advances to another model. The list then has a card per plug-in (plugin:<tag>), the cards a tree (below) — its name, author, version and where it came from (Comes with FilmOpen, In your library, In your development folder), its description, why it is off or cannot run, how many problems its files have, its switch (plugin:<tag>.enabled, held with the reason where it cannot run, and on from the moment an On is accepted, as its line says Checking…, the plug-ins read again only confirming it or turning it back) and Configure (plugin:<tag>.open), and under them its status line (plugin:<tag>.readiness, plugin_status_line.dart, a live region that a screen reader hears change): what is known of whether it can work, read from the runtime’s record (§6.11) and always in words, with a colour and a mark, so that the colour is never the only sign — Checking… in the primary colour while it is asked (turning it on, or Check), Turning off… while it tidies up; its state in the app’s words — Ready in green, It works, less well than it could in amber, It needs something from you first and What it needs is not there in red — then the plug-in’s own words and, where it gave them, what it measured with the app’s labels and units (GPU, VRAM and RAM in gigabytes of 1024³ to one decimal in the reader’s notation, Version), each one it did not give said unknown, never zero, and a label never parted from its value at a wrap; Enabled — open to check. in the muted colour where nothing is known, which is every plug-in that is on at a start; a failure of turning it on or of its check in red, in the plug-in’s words where it gave some, with a small Try again (plugin:<tag>.retry); and for a plug-in that is off, nothing beyond its switch, but an amber caution where its clean-up was not confirmed, it did not stop when asked, it did not come on, or its switch — an Off or an On — could not be saved, that last beside whatever its clean-up left, neither hiding the other; nothing either for a plug-in on that cannot run, whose notice says why. It wraps, and the card holds no address, box or setting: those are on the plug-in’s page. A switch or an Allow whose write fails is said there, not thrown. Turning on a plug-in that needs consent opens the consent screen instead — what it asks for: its own keys, which spend money, and the keys it wants to use — each named in words, its slot and whose it is, never by its id — each key it wants followed, indented, by what it may spend in the app’s own words — It can use your fal.ai key, the one fo-fal holds. FilmOpen adds the key to each request it sends; what it asks for is charged to your fal.ai account., said whether or not that key’s owner is on — the text model when it declares capabilities.ai, the hosts it reaches without a key, the servers whose address the person types on its page — which the plug-in can also read — each in its own words with the default it starts with, the plug-ins it uses, grouped by the plug-in called and each function said in that plug-in’s own words — a function of the contract, which no plug-in labels, in the app’s: a platform’s run (run jobs on its service) and request (send its service requests this plug-in writes), and any plug-in’s status (ask whether it is ready) — or a line saying that plug-in is not installed, and under a call, the app’s line for each platform whose key the called plug-in may spend — It can make fo-fal use your fal.ai key. FilmOpen adds the key to each request it sends; what it asks for is charged to your fal.ai account. — since a function reached through ctx.call runs as its owner with everything its owner may send: its own keys, the keys it asks to use, and, through the plug-ins it calls in turn, theirs, the chain walked once and the caller’s own keys, said in its first line, not said again; derived from the manifests, never from an author’s words, and nothing under a call on a plug-in that may spend none. Each line’s mark stands in a column of its own, so a line that wraps goes on under its own words, the types it reads and writes, and the film it came with — with Cancel and Allow (plugins.consent.cancel, plugins.consent.allow, a 44 px target); only Allow turns it on. A plug-in’s page: All plug-ins (plugins.back), which returns to the list where it was (the tab keeps the list’s offset as Configure opens the page, and the list comes back at it), its name and switch, Docs (plugin.<tag>.docs) where its manifest supplies a valid guide path — opened in the browser on kFilmOpenDocsUrl, the development documentation site in a build with development mode and the release documentation site otherwise; no link for an omitted or invalid path, and a message in the app’s language where the browser cannot open it, the address it opened logged (plugins.docsOpened, a public page’s) —, Settings on this computer — each machine-scoped setting drawn by the app’s own widget for its type (plugin:<tag>.<key>: a box saved 700 ms after the last change and when the page closes, and showing a value stored while the page is open, as the plug-ins are read again, where the person has not changed it, a number box saying inline when its text is not a number, or is below the setting’s min or above its max (At least {min} / At most {max}: nothing is saved until it is.), saving nothing and logged plugins.settingBox.refused {plugin, key, value, reason: notNumber | belowMin | aboveMax, min, max, ms} while the stored value stays in effect and the refused text stays in its box until corrected or the page is opened again, a switch, a pull-down; a model’s choices are the catalogue’s entries of its category on its platform, each of a text, picture, clip or music model saying where no plug-in that is on can use it — a plug-in naming no models counting for a picture, clip or music only on a row marked render — Nano Banana Pro (Gemini 3 Pro Image) — no plug-in that is on can use it, as a card on the project’s page says it too (§6.11); a server box — one line, a URL keyboard, the address saved normalised, text that is no address said inline and saving nothing as a number box does, and a plain http address at a certainly public IP address warned in the tertiary colour and saved all the same, since FilmOpen warns and does not enforce), registered in FieldRegistry under plugin:<tag> so that set_field plugin:<tag> <key> <text> types as a person does — Actions (plugin:<tag>.action.<id>, one run at a time, with progress and Stop, plugin:<tag>.action.cancel, and the answer as a line, plugin:<tag>.answer: the plug-in’s message, or why it did not finish — a key not entered, a key not allowed, a limit, stopped, a code — an action that answers state: 'failed' said as a failure in the error colour, in its own message or its problem’s words, never Done., its code logged as plugins.actionFailed and never its words (§6.11)), its own key cards while it is on (the cards above), and under them one line on FilmOpen credit (plugin:<tag>.credit) from the app’s funding resolver (§6.6) for its slots’ platforms, asked once per resolver, generation and set of platforms and never as the page draws: Sign in to <the website> to use FilmOpen credit., Ready on FilmOpen credit. or FilmOpen credit: top up on <the website>. (kFilmOpenDomain), and no line where the person’s own key is stored or credit is not configured or not known; then Keys, which, while it is off, says to turn it on to enter them, and holds a checkbox per key it asks to use, plugin:<tag>.grant.<owner>:<slot>, with consent’s line for what that key may spend under it and, in the tertiary colour, which of the two it still lacks — no such key saved, No OpenAI API key is saved yet: save one in Settings → General or on its plug-in’s page under Settings → Plugins, then allow it here., or, the key saved and the plug-in allowed to run, the box, Your OpenAI API key is saved: tick the box to let this plug-in use it. — the key named by its platform, else its slot, and nothing once both are there), models it speaks for (the models of what it implements, with a line naming any the catalogue does not define), what it offers other plug-ins, and uses of theirs (each function it offers, in its own words with its help, and a line per plug-in it uses saying what it uses it for, in that plug-in’s words, or that the plug-in is not installed, with consent’s line for what the call may spend under it), Check (plugin:<tag>.check, §6.11), held while a check or turning it on or off is under way, its answer (plugin:<tag>.status) the same record in the same words, colour and mark as the card’s, Try again beside it where the card has it, nothing while nothing is known, then every route it runs on (plugin:<tag>.route.<n>, PluginSummary.routes, §6.11) in the owner’s order — this computer, program, sign-in, key, credit — each with a green ✓ (check_circle_outline) or a red ✗ (cancel_outlined), its origin (Codex on this computer, Sign in with <plan>, Your key, FilmOpen credit, This computer), what it makes ((text and image), (text only), (image only)) and in red why it cannot run now (Not signed in, No credit, Not set, else not available), drawn always and never probed, and for a plug-in whose code has no status the sentence that api 1 asks for one and its other functions still run — a check left by going back to the list goes on, and its answer is on the card and on the page when it comes — its actions and Check held, with the reason, where it is off or cannot run — and About (its stem, its files’ problems, its README) — a problem said in the app’s words where labels.dart has a sentence for its code, and never by the code: badCodeFile in each of its four cases (a path that is no .js file of the folder or is named twice, an empty entry, more files than a plug-in may have, code larger than it may be) and archiveTooLarge, naming the folder’s two limits from the issue’s args; a code with no sentence yet is shown as it was, path: code (field). The tab redraws on the runtime’s changes. On the web it is one sentence: plug-ins run in the desktop app. A machine setting on a plug-in’s page is drawn by the same plug-in field widgets as a plug-in’s card on an entity’s page (filmopen_forms/plugin_field_widgets.dart): a switch, a pull-down of the field’s choices and a box for text or a number, its help up to eight lines, and an agent’s typing parsed the same way in both places — true or false for a switch, one of the choices or nothing for a pull-down, and for a server an address ServerAddress reads, normalised, anything else refused (pluginServerValue). A server never reaches a card on an entity’s page: it is always this computer’s, so the manifest reader keeps it machine-scoped and the card leaves it out.
The list as a tree (plugins_tab.dart, plugin_card.dart). The cards stand as the manifests’ x.dependencies declare (Project Specification §14.4; PluginManifest.dependencies, carried to PluginSummary.dependencies): a plug-in that names none is a root; one that names others stands once, indented under the first of them by displayed name, its card saying Also needs … with the others’ names; one that names a plug-in that is not installed stands as a root, saying Needs fo-cui (not installed) — the tag, since an absent package has no name to show; one whose needs lead back to it stands as a root with the problem line Needs …, which in turn needs this plug-in, on each plug-in of the loop, which is walked with a set of those reached and so ends. Each level is alphabetical by displayed name, case aside, the tag the tie-break. A plug-in standing under others is drawn grey while one of them is off, saying Off while ComfyUI is off. The tree is presentation alone: each card keeps its own switch, Configure and identifiers; nothing turns a parent on with its child or grants a child the parent’s keys; and nothing reads x.dependencies when a plug-in is called, which keeps its own guards. Where none names a dependency the list is one level, alphabetical; the empty state is unchanged.
A plug-in’s rewrite (filmopen_forms/plugin_hook_dialog.dart). The file menu (file.menu) offers Plugins… (page.plugins) where PageAccess.canRunPlugins holds — a handle, a project that can be written, a file not from the library; from anybody’s version, since the run writes beside it, never over it — and an enabled plug-in’s transform applies to the page’s type. A Stop pressed after the plug-in answered still stops: nothing is written. It writes the form’s draft first, then opens a dialog titled with the version’s name: a line saying the result is a new version under the viewer’s and the plug-in’s names and that this version is not changed, a button per such plug-in (page.plugins.<tag>.transform: <plug-in>: rewrite), and while one runs a progress bar the plug-in’s ctx.progress fills, Running… and Stop (page.plugins.cancel). The answer is a line (page.plugins.answer): Written: <stem>. with Open it (page.plugins.open, a 44 px target, which shows that version and closes the dialog), or why nothing was written — stopped, the file changed meanwhile, no handle, a limit, a key not entered or not allowed, the plug-in’s own words, a code. Closing the dialog while a run goes on stops it. This progress and cancellation are the ones Generate will use.
A plug-in’s card on a page (filmopen_forms/plugin_sections.dart). The characters’, locations’, folders’, scenes’ and project’s forms end with a card per enabled plug-in whose entitySettings name the type — for the project file, its project-scoped settings — drawn by the form’s own fields and stored in the version being edited under plugins.<tag>.<key> (field:<stem>:plugins.<tag>.<key>): a text box, a number box, a switch or a pull-down, autosaved by DraftSaver, locked on a version the viewer may not edit, and paired row by row in Compare, where copy-left moves it. An emptied box removes its key and an object left empty; a pull-down takes only its own tokens and a switch true or false. A plug-in turned off, or missing, leaves its values under Other attributes. Every plug-in’s model setting, fo-assist’s textModel included, keeps its selector on its card, its row in Other attributes where the plug-in is off, and its row in Compare. The types without a form show their plugins object read-only (a scene did until it was given a form). The cards are the ones the runtime answers when the page is built.
9.4a The plug-in developer page — filmopen_plugin_dev
Section titled “9.4a The plug-in developer page — filmopen_plugin_dev”A large dialog, opened by Plug-in developer page… (settings.plugin-dev) on Settings → General’s About card and by Open in the developer page (plugin:<tag>.dev) on a plug-in’s own page, which opens it on that plug-in. In every desktop build, release builds included, with no switch before it and no consent question: the page is for people who write code, and they will not normally have a development build. Neither entry appears where plug-ins do not run — a phone, the browser — and the app hands the opener to filmopen_plugins_ui, which imports neither this package nor the host.
Under its title, a plug-in of the development folder that could not be read is named with its path (plugin-dev.folder-problems, in the error colour), and one standing sentence says what the page does: This page runs a plug-in’s code as it is on this computer, without the question FilmOpen asks before it runs a plug-in you installed, and with every key its manifest asks for. Run only code you wrote or trust. Then: Plug-in (plugin-dev.plugin), a pull-down of the plug-ins installed — the development folder’s or the shared library’s — each by name, tag and where it is, with every problem the reader found listed by file beneath it and, for a stock plug-in’s installed copy, the line that FilmOpen replaces those files when it starts; Function (plugin-dev.function), a pull-down of what that plug-in exposes — status first, then the functions of what it implements, its actions and what it offers other plug-ins, one entry per name, each with its kind in words — every plug-in’s for status, hook, action, offer — the first road kept and its example taken from a later road where the first carries none — with Other… and a box for any name (plugin-dev.function-name); How it is called (plugin-dev.mode), three chips that wrap at a phone’s width — As FilmOpen calls it (plugin-dev.mode.app, the default), Exactly what I typed (plugin-dev.mode.typed) and Console (plugin-dev.mode.console) — each saying beneath what it does, and, for status or an action as FilmOpen calls it, the line FilmOpen calls this with ctx alone, so the box is not sent; Arguments (plugin-dev.args), one JSON box filled with the chosen function’s shape — everything that crosses into a plug-in is JSON, so one box serves every function — text that is not JSON said inline with Call waiting; in the console the box is JavaScript, labelled JavaScript and held to nothing, the Function pull-down gone and Call waiting, with the reason in its tooltip, until a plug-in is chosen and the box holds a script: the host loads the plug-in’s own files and then one more, console.js, whose value is a plug-in of one function, $console, whose body is the box on that file’s first line — so a failure’s line is the box’s and the plug-in’s top-level names are there — and calls it with ctx alone, bound to the open project as every run is, the entity the page was opened on as ctx.selection; its result, its lines, last-run.json (function: $console, mode: console, args the script) and the developer log are a call’s, and the app’s log has plugins.devConsole (the script, the entity, the files) and plugins.devCalled (the script and the value whole); Save what it makes to (plugin-dev.source), the entity the caller passed or nothing, only as FilmOpen calls it, since as typed nothing is saved — three modes: a render as FilmOpen calls it with an entity is a place’s render, the request the app would make handed to the insertion to takes alone (its takes and its record, no entry, the item with no group) and run by the render manager through the page’s working copy while the page waits for it, its calls in the log and Stop stopping its job, the result naming the job and its take, or the job’s code with the app’s sentence for it; a capability (describeReference, describeStyle) with an entity takes the app’s own door — its media granted, its answer’s leaves in the document’s spelling and held as the app holds it — and is refused without one, the app never calling one for no entity; with none, the outputs are answered as the plug-in gave them and nothing is saved; as typed, nothing is saved either; Call and Stop (plugin-dev.call, plugin-dev.cancel), each Call reading the plug-in’s files from disk again, so a developer edits, calls and reads with no restart and no Allow for every save; Result (plugin-dev.result), the outcome as one plain line — a failure’s code, reason, for an answer of the wrong shape the path in it (the whole answer where it is the answer itself), the author’s message and its file, line and column — the time taken, and the value as formatted JSON; Log (plugin-dev.log, a row plugin-dev.log:<n>), the run’s lines as they happen — the seam’s devRun is told of each one, so a call waiting on a door shows what it has done so far, and a plug-in the code called is in the log too, its lines and its doors carrying that plug-in’s tag (PluginDevLine.by), since the frame carries the page’s log down the chain — with Clear (plugin-dev.log-clear), the box following its newest line only while the reader is at the foot, and the run’s own list as the record once it ends; and the two files (plugin-dev.result-file, plugin-dev.log-file), the paths of last-run.json and of the developer log, to open or to tail.
A server provider’s deploy and wake are refused here, as on every other road to a call — an action, another plug-in’s ctx.call — in the app’s words (pluginsServerSessionOnly) and before anything is sent: each would start or keep up a server billed by the hour, and only a server’s own screen, which shows it and stops it — the GPU page’s Start server and its servers’ window (§9.7a, §6.15) — may ask for one. A server provider is a plug-in that declares server-provider, its declaration faulty or not; its other functions — capabilities, lists, resolving, a stop — run as any other (PluginManifest.startsAServer).
Not built: Watch, a history of calls, the page on a phone, and the plug-in’s machine settings drawn on the page itself.
The page follows the host: it listens to PluginDevBench.current.changes, so a development folder named on the card behind it — the box applies on losing the focus, which is the gesture that opens the page — re-reads the plug-in on screen and the list beside it; a plug-in that is gone stops being the page’s choice, and a function it no longer offers becomes Other… with its name in the box.
For an agent the page registers the stem plugin-dev with FieldRegistry, so set_field plugin-dev plugin <tag>, … function <name>, … args <json> and … mode app|typed|console type as a person does, and tap plugin-dev.call, find plugin-dev.result and the two files do the rest with no new op; the agent channel’s valueIdentifiers carry plugin-dev.plugin, plugin-dev.function (with Other…’s plugin-dev.function-name), plugin-dev.args and the page’s two paths, plugin-dev.result-file and plugin-dev.log-file, so that an agent finds the files; a recording replays the choices and the arguments and not the paths, which the tape refuses as it refuses dictation (§6.8), since an app-support path names the person on Windows.
Settings → General’s About card also carries Plug-ins in development (settings.plugin-dev-folder, with settings.plugin-dev-folder-choose and settings.plugin-dev-folder-clear): the folder of §6.11 in a box that is typed — entered, or left behind — or filled by the folder picker, an empty box meaning none chosen, as the × writes, and saying what would fill it.
9.5 Behavioural rules
Section titled “9.5 Behavioural rules”- The username is
default(kDefaultViewerHandle) until the person chooses one. A fresh installation keepsdefaultin the preferences, and a stored username that is missing, blank, not a text or not a handle (§5.6,ShortName.validateHandle:owner[.workspace], the owner a username of 3 to 32 characters, neverofficial) is read asdefault, its reason logged by its code (settings.viewerFallback:missing,blank,invalid,unreadable) anddefaultwritten back.Settings.resolveHandleis the one rule the username is read by, andSettings.vieweris therefore never null: the app always browses and writes as a handle, and the refusals for no handle that a page’s access and the writer keep (PageAccess,NoViewerException) are reached only by a caller that passes none.defaultis a literal author, not an account: two installations that write asdefaultboth own whatdefaultwrote, and on the sample a first run still shows the format’s rule that official wins,defaulthaving no version of its own there. Choosing a username renames, moves and hands over nothing: what was written asdefaultstaysdefault‘s. A change of username is one transition,SettingsController.setViewerHandle, the box’s and the agent’s (set_viewer) alike: an empty box isdefault; a text that is not a handle is said under the box and changes nothing, the last valid username staying in effect; every open draft is written first, as the username it was typed under, pass after pass while a pass leaves one to write — a form opened while the last pass wrote, or a draft typed into during or after its own write — up to five (OpenDrafts.settleUntilQuiet,maxSettlePasses), each pass waiting for every write in flight, a form’s own timed write included, and the username is taken only then: a draft that cannot be written refuses the change — Your username is still {handle}: and the save’s own sentence under the box — and typing that outruns the fifth pass refuses it too, in a sentence of its own, since no save failed (…a page was still being saved. Try again in a moment.,DraftsNotSettledException); a change asked for while another waits on the drafts decides instead of it, while a text that is not a handle is no change at all and leaves a waiting one to land. No dialog that creates a file asks for a username — New project, a new character or location, Add folder…, Add scene…: each writes as the username in effect,defaultwhere none is chosen, read once a change in flight has landed (SettingsController.viewerSettled); one changed while a character’s, a location’s, a folder’s or a scene’s dialog is open is said in it, what was typed kept and nothing written, and the next Create writes as the new one. Nothing dismisses such a dialog while it creates, and it closes by its own answer — its own route removed where another stands over it. The agent’squitwrites the open drafts as a change of username does, puts the session’s username back before anything else ends — refusing at once, with nothing written, where it cannot — and, where the username changed during the session through any door (the agent’sset_viewer, Settings’ box, its own restore, remembered for aquitasked again), waits for the preferences to keep it (SettingsController.viewerStored), writing it again first where the store refused it before; it refuses to quit where either fails, as it refuses for a draft that cannot be written. A username the preferences could not take stays for the session, said under the box with Try again (Settings.unsaved,retryWrite;settings.notSavedlogs the preference, the error’s type and its message).settings.viewernames the username taken, andsettings.viewerFallbackthe value it replaced. A choice written in the background — the theme, the language, the sidebar’s width, a sharing folder, the microphone and the others — is loggedsettings.keep.start {key, value}as its write starts andsettings.kept {key, value, outcome, code?, cause?, ms}once the store answers. The username box validates the owner as a username — 3 to 32 characters, never starting with a dash — and says the username’s rule when it is not one, validates the workspace part at 20 characters and names that bound in its length refusal, and counts up to 53 characters for the two parts and their dot; a segment box validates and names 32 characters and counts up to 32. The username is visible in the tree header. - Development mode: warnings, not locks. While the application is being built, a rule about who may do what warns and lets the person continue: another author’s version, official without being a director, the shared library. One switch governs it — Settings → Development → Warn instead of refuse, on by default in debug and profile builds, absent from a release build, which reads no switch an earlier build stored — and one place applies it,
Guardsin the writer. The rules that protect files are not governed and refuse in every build: a file changed on disk, duplicate keys, the identity fields, the content the format requires, the epoch guard of copy-left, a missing handle, a folder the system will not write. The bundled sample opens as a session copy on every platform, in either mode, editable, with Copy to a folder… in the project menu to keep it. The pages follow the same rule as the writer:PageAccess(filmopen_state/page_access.dart) answers what a page may do — whether the form writes, whether the star, the check and the label can be pressed (canPickandcanSetOfficialare false on a plug-in, wherechoosesNothingholds) — and which red lines it shows, and the page, the version row, the file menu and Compare read nothing else.PageAccess.canCreateVersionsanswers, without an entity, whether the viewer may make new versions in the project — declare an epoch, or make a version at a missing one: the project’s own folder can be written and a viewer is set. It is the destination’s permission alone; the library is never the destination, and where the copied version comes from, another author or the library, is no part of it. A red line (filmopen_forms/guard_line.dart,GuardLine) sits above the page’s header in the scheme’s error colour, one per notice, with a tooltip naming Settings → Development: Editing maria’s version as john123 on another author’s version, the project’s directors beside a star someone who is not one may press, Writing to the shared library on a library file the library lets be written, No handle with a link to Settings, A session copy with Copy to a folder… where folders can be made, a library file the library cannot take (it stays View), and a source that says beforehand it cannot be written (a folder on disk says so only when a write fails, in that write’s message). A star pressed, a label written or the JSON editor’s save past a governed rule says the same sentence as a message; the autosave does not repeat it, since the line is on the page. Compare’s left column followsPageAccesstoo. Compare’s tab is never greyed in development mode: where there is nothing to compare it opens on its reason as a red line, and the question to fork a version of your own stays. - Refresh never blanks the screen.
- Paste as plain text — Control-Shift-V, Command-Shift-V — pastes into the focused field as Control-V does, through the field’s own paste (a prose field’s included, §9.8):
PastePlainTextwraps the navigator, so a dialog’s box pastes too, since a dictation tool sends those keys after transcribing. It logsapp.paste.shortcut {keys, target, handled, reason?, ms},targetthe nearest semantics identifier andhandledwhether an enabled paste ran, never the clipboard’s text, since a person may be pasting a key; where no paste ran,reasonsays why:no-focus, orno-paste(a focus that is no text field). A read-only box’s paste runs and changes nothing, so it logshandled: true; the prose editor’s ownprose.clipboardsaysrefused,read_only. - An amount is shown one way (
labels.dart’samountText, which takes an exact decimal): to the cent at a cent or more ($1.23, half away from zero); under a cent and above zero, rounded up to the next tenth of a cent (ExactDecimal.ceilTo) and written$0.00followed by that tenth as one raised digit, a Unicode superscript —$0.0004and$0.001read$0.00¹,$0.0037reads$0.00⁴, and$0.0099, rounding up to a cent, reads$0.01—, the digit there only to show that the amount is not zero, smaller and higher so that it is never read as a cent; zero as$0.00; below zero a true minus before the currency mark. Dollars are written as the language writes them, another currency by its code. Every price, cost and total on screen goes through it — the render dialog’s price, a server’s and the GPU page’s cost, a lent offer’s price per image, the speech service’s price of a minute, Activity’s Usage, the recording microphone’s running cost, a batch’s total, a media tile’s cost and the account’s balance in dollars. Not through it: the cloud’s published tariff, fixed text already to the cent. - Every empty state says what would fill it (no takes yet, only one version, no dialog or actions).
- Every write goes through
ProjectWriterand is followed by a re-read; the UI never edits the index. Writes happen only under the viewer’s handle; the star is for directors; the library is never written. - A preference that could not be saved stays for the session and is said where the person can try again: the username under its box (above), and the folder New project proposes next (§9.2) at the top of General — FilmOpen could not save the folder your last project was made in… — with Try again (
settings.new-project-parent.retry), which writes it again (retryWrite). The export’s encoder and development mode’s switch are said under their own cards in the error colour — This choice could not be saved… and This switch could not be saved: it holds until FilmOpen closes. — with no Try again: the next change writes it, and a write that reaches the store clears the line (Settings.unsaved). - On the web nothing reaches a disk: the sample and new projects are session copies, and the tree header says so.
- Every transient message goes through
filmopen_ui/messages.dart(showMessage), so they all look alike and can carry one action (for instance Settings when a username is missing). A message goes by itself: one with an action stays at leastkLongMessage, ten seconds, or longer where its caller asks, then goes like any other if it is not tapped; one without keeps its caller’s duration. An optionalactionIdentifiernames that action for an agent; the action retains Material’s label, theme, once-only press and dismissal with the action reason. Without an identifier the ordinary snackbar action is unchanged. - A copy-left gesture (§9.3) is checked before it is applied and never after: what it would make of the draft is built to one side, and a refusal is a message with nothing written. What it does write goes through the same autosave typing does, and is logged as
compare.copiedwith the two stems, whether it was a row, a section or the file, the keys — a block asblocks.<id>, its own id whatever it says — and the values the copy read on each side, as §9.3 lists them: nothing is masked. epochis a header field only where the filename carries one (§7): on a scene it is content, the epoch the scene is set in (§10.2), and the writer lets it be written — as it does on a folder, where the format gives the key no meaning and a reader keeps it like any key it does not know (§10.1; an episode is a folder since the format’s 1.12).Entity.headerMismatchesandProjectWriterapply the same rule.
The project’s next_epoch is the writer’s: the owner’s epoch allocation floor (Project Specification §8.2) is among the project’s keptOut paths, so no Other attributes draws it on Edit or View and no comparison gives it a row; a save of the form keeps it in the file as it was. The JSON editor still shows it.
A dialog’s write acts in the film it was asked in. Move to scene… and the file menu’s Edit JSON… and Plug-ins take their film as they are asked — before the open drafts are written — so a film left meanwhile, or one holding the same stem, is never read or acted on: the file menu opens nothing.
The copy stands a dialog over the app. Copy to a folder…, from the project menu (in its drawer below 700 px) or from a page’s line, shows Copying the project to the folder… once the folder is chosen and before the open drafts are written, and keeps it until the copy is open or has failed: neither its barrier nor Escape nor Back closes it, and it holds the focus, so a field losing its focus to it is written with the drafts and nothing a person types or clicks lands in the session after its files were read. It is taken away by its own route — never the drawer or the page beneath — and the outcome is said as before. One copy runs at a time: a second ask, from the other door or a double click before the picker answers, does nothing; and closing the window waits for a copy under way — from the folder’s choice, through the drafts’ settle, the copy and its open — within the exit’s bound, as for the other exit tasks. A write not made by a person (an agent’s field, a plug-in’s run finishing) can still land in the session while the copy runs.
9.6 Dictation — filmopen_dictation
Section titled “9.6 Dictation — filmopen_dictation”EditableTextTarget in filmopen_ui is the shared editable-selection contract for the editor, dictation and the assistant. It contains no controller, widget or Riverpod dependency. It captures the current selection as UTF-16 base and extent offsets and an opaque target identity plus revision. One ProvisionalTextEdit replaces its own provisional span as partial text changes and seals before committing or cancelling, so reentrancy and a thrown callback cannot commit twice. Each mutation rechecks generation and read-only state; a stale handle never edits or cleans up a replacement target. Target implementations preserve ordinary field validation, caret and undo semantics, and invalidate and remove their provisional spans after external deletion, target switch or a read-only transition. Dictation reaches editable fields through DictationTarget, whose optional editable target uses this capture for inline insertion; plain controllers use ControllerDictationTarget.
- Where the microphone is. A small microphone at the right of every box a person types into — a form’s prose, names and numbers alike (
FormScope.dictate, which places the AI icon after it where the assistant’s catalog offers an action on the field, §9.3), everyShortNameField(through its suffix builder), the tree’s filter, the new-project and new-entity dialogs, the version label, the epochs editor, a relationship’s two boxes, the boxes of a scene’s open block (§9.3), the JSON editor’s value dialog, and the description of a recording (Describe the problem, §6.8). It is one widget, and every microphone looks and behaves the same — the AI assistant’s placeholder microphone aside (§9.3), which is no box’s, dictates nothing and is disabled on every build: a box only places it —DictateButton(target: …), or a form’sFormScope.dictate— and how it looks and behaves is decided in one place,DictationLookinfilmopen_dictation: one control in every box, with no microphone menu beside it — which microphone records is chosen in one place, Settings → General → Dictation (DictationSettingsCard), which keeps the chosen microphone’s name beside its id so as to name it without asking the system, and logs both — and the microphone not a Tab stop, so Tab goes from box to box and the microphone is a click away. A microphone chosen there records from then on, a dictation under way included; one no longer offered records from the system’s, the choice kept for when it returns. The height of the box beside it is the theme’s (InputDecorationTheme.suffixIconConstraints: an icon at a box’s right never makes a box taller than its neighbours).scripts/tests/test_microphone_one_place.pyfails where a box decides any of it for itself. It is left off secrets (the key cards’ boxes and the e-mailed sign-in code), boxes speech cannot fill (the sign-in e-mail, folder paths and the JSON editor’s text mode), a box that is read-only or disabled, and the web build. The microphone is 32 px in the theme’s compact suffix slot, so it never makes a box taller. While it records it is a square stop (Icons.stop_rounded), in every box alike, with a ring around it whose width follows the voice’s level, so the person sees when they are too loud or too soft:DictationController.level, the current frame’s loudness from 0 to 1 against the room’s level as the speech gate judges it — a quarter at what the gate hears as speech, 1 at four times that —, set every frame and 0 once capture stops, never logged frame by frame; whether a build shows one at all isDictationLook.available(not the web), which a box that keeps room around its microphone — the tree’s filter, beside its clear button — asks rather than decides. - Settings’ Microphone lists the microphones the system offers, asked when it opens, anchored at the pull-down. The choice is a preference (
dictationDevice), the chosen microphone’s name kept with its id (dictationDeviceLabel, read back only beside that id — every build shares the preferences, and a build that chose another without keeping a name leaves a name that is no one’s), both put back by the agent’squit; with no name kept the pull-down says The microphone you chose, and opening it brings the name back where that microphone is offered;settings.dictationDevicelogs its id, its name and whether it is the system’s default. A microphone no longer offered falls back to the default, which the menu then checks; a second press while the menu is opening opens no second one. A microphone chosen while a dictation listens takes over at once, in the same session and chain (through Stop and Resume, whose rules then hold); one chosen while it is stopped, or while its session opens, records from the next start. - The speech service (
dictation_engine.dart,DictationEngineCard). Dictation transcribes with one of two services, each a row ofdictationEngines— its id, its catalogue entry, its platform and the host its sessions open on — a catalogue entry with its platform’s file, and aTranscriberthat speaks its messages: OpenAI’sgpt-live-transcribeand xAI’sgrok-voice-transcribe-2.0, the Grok default at about a fifth of the price by the minute. Nothing else knows there is more than one: the controller, the speech gate, the microphone and the usage log are the same for both. The choice is Settings → Models’ Speech service card (settings.speech-service), a pull-down that names each service as the catalogue does — its platform, the platform’s name for the model and the price of a minute — drawn where the catalogue serves two or the selected service is unavailable. An unknown saved identifier stays selected under an unavailable-service label; a known unavailable service keeps its compact brand. Its disabled row and repair sentence remain even with no served alternative. Only choosing a served row changes the preference; dismissing a highlighted alternative restores the selected row’s label. Catalogue changes refresh its availability and words. An unknown choice or a previously unavailable row retains its disabled repair during loading and catalogue failure, without stale model, price or usable alternatives. It is a preference,dictationEngine(Settings, by the engine’s id; null alone selects Grok; every nonempty saved identifier remains exact and an unknown or unservable one refuses dictation), which the agent’squitputs back with its device choice and label; it takes effect with the next attempt, and a dictation under way keeps the service it opened with. Each service’s key is its provider plug-in’s (fo-openai:openai,fo-xai:xai, §6.7), entered on that plug-in’s page, and only the key of the service in use is read; the card’s Configure (settings.speech-service.configure) opens the selected service’s provider page under Plugins, or the list where the provider is not installed, and changes no choice. A provider that is not ready refuses the press before its key is read (provider). The recording’s commentary inherits the same default. A press captures its selected identifier before awaiting catalogue setup; changing Settings during that wait affects the next press, even when the selection returns to its original identifier before setup answers. An attempt captures one validated setup for its label, endpoint, key slot and Usage identity; a missing or pending platform listing, including an unknown status, is unavailable before any credential, audio or Usage activity. Its plug-in / origin line (settings.speech-service.route) uses the selected native provider’s copied API-key metadata, otherwise only the existing OpenAI transcription credit decision; it says not available where neither can run. Reading that metadata opens no credential store or speech session, and keeps the selected engine and funding rule unchanged. - A press starts inline dictation immediately on down and focuses its box. A focused box supplies its current selection; an unfocused box captures a caret at its end. An unfocused plain box restores that captured caret after desktop focus lands, while the same unchanged attempt remains active. Another tap stops audio, waits for final turns and commits their ordered aggregate once at the captured range. Prose targets replace one provisional span as words are revised, so the words show in the box as they are spoken and Stop settles them there. The chat’s reply box does the same with one difference: the person’s own edit during dictation ends the attempt and keeps the box as they left it, the dictated words included, while a read-only transition — the chat stale, completed or finished — removes the live words as in a prose field. Other plain controllers keep their committed text and show those words in a small wrapping caption beside the microphone. Neither kind publishes provisional words to a file, callback or issue recording. Empty final text cancels without a change. Selection-only movement preserves the captured range.
- Click, hold and keyboard share the captured attempt. A short press toggles: its release keeps recording until the next press. A starting press held for at least 300 milliseconds stops and commits on release; the release of a stopping press never starts again. A cancelled pointer discards its starting capture, and another pointer cannot operate it. The microphone is the theme’s primary blue throughout returned audio capture, including pending Usage or service acceptance and until physical stop completes, and stays out of the Tab order; a stop that fails or passes its limit is said as a failure (below). Inside the focused editable box, Ctrl+Shift+Space follows the same short or held rule, named in the tooltip. One early focus handler finds the mounted microphone by the focused controller and consumes Space down, repeats and matching up; hardware observers still receive them. Repeats never toggle, and an ordinary Space cannot become a shortcut by adding modifiers later. A held chord retains its original target even when focus or modifiers change. Cancellation invalidates the captured press, so an old release cannot stop a replacement attempt. Hiding a retained field cancels capture even when its animations continue. Pending physical or session cleanup names its stopping state instead of inviting another dictation; completion restores the ordinary state and shortcut hint. The handler is installed with the first microphone and removed with the last. Click and hold are always available; no preference changes the gesture, and an old stored hold-switch value is ignored.
- A failure remains in the microphone’s error colour and tooltip until the next attempt. A missing key or a provider that is not ready (
provider, §6.7) on a page also offers Open <provider> — Open OpenAI, Open Grok / xAI — on its transient message, withdictation.open-keys; a refused client handshake offers the same page action without claiming the key is wrong. Inside a dialog, a missing key or a provider not ready shows that identified text action beside the microphone instead of posting a message behind the modal barrier. Other dialog failures remain in the tooltip. Open keys follows each dialog’s guarded pop before Settings → Plugins opens on the provider’s page, where its key is (openPlugin, taken once by the tab); a refused pop leaves its page and settings untouched. The dialog action stays keyboard reachable while the microphone remains out of the Tab order, and clears at the next attempt. Costs are shown in Usage and Activity, with no counter in the field. - Text the person edits is theirs. An external text edit, a switched target, a read-only transition or a removed microphone synchronously seals the capture. Leaving its rail page or tab, hiding its page or covering its route also cancels, even when the field stays mounted. A tab transition refused by its guard keeps the capture. Late session events cannot overwrite that text. Cancellation ends audio and removes the provisional span or caption; restoration requested during a widget build waits until after the frame, with recorder protection retained through the restoration frame. If the same controller receives a new field identifier during cancellation, that identifier also stays protected until restoration has reached semantics. A pending start is joined before its controller is disposed or another attempt can begin. A failed setup releases only its still-owned capture and keeps exception text out of the failure tooltip.
- Applying goes through the box’s own formatters, length limit and
onChanged, as typing does (applyDictation). At the captured range the words are spaced from the surrounding text and shortened to the room a length limit leaves, so the limit never cuts what follows the caret. Words the box’s rules empty leave it unchanged and give the microphone a refusal tooltip; a box already at its length limit says that no room remains (DictationResult). The plain adapter retains that result for the microphone’s sentence. A rejected provisional edit or a throwing acceptance callback cancels with the same inline refusal; the publication guard clears on every path, and changes made by target listeners are rechecked immediately after a partial. A finalization is sealed before waiting for final turns, so duplicate stops and reentrant callbacks cannot publish twice. - A box bound to what it belongs to (
DictationTarget.onStart,DictationTarget.changed):onStartruns before the selection is captured or anything is heard, andchangedis asked while the attempt is live and before final words go in. Where it answers, the words are dropped, its sentence becomes the microphone’s error tooltip and nothing is written. A scene’s block binds its boxes so (§9.3); a form’s field, whose box is its key’s, has no need to. - The session (
DictationController). The key is read on the gesture, and a stored value under twelve characters counts as no key. The microphone starts before anything is written or sent, so one the system refuses costs nothing. Its observable capture state is independent of transcription phase and remains active until recording stop completes. Its capture-cleanup future joins pending audio starts and returned recordings, so editing retirement does not detach recording indication before physical cleanup. Each audio start first acquires a fresh microphone-owner token, whose kind is dictation or recording commentary, independently of the Usage field. A busy microphone refuses before audio, transcription or Usage starts, naming a known owner kind and otherwise speaking generically. Stop seals frame delivery and releases only after recording stop completes; Resume joins that cleanup and reacquires with a fresh token, remaining stopped if another owner acquired first. Concurrent shutdown callers join the same stop, which waits at most three seconds for the recording to stop. A stop that fails or does not answer in that time is logged with its error (dictation.captureStopFailed) and answered with the microphone-failure code —close()completes false, and the field’s microphone and the recording’s commentary say so — and the owner is released all the same, so the next press acquires the microphone afresh; nothing retries the stop. Text and Usage finalization have bounded waits and happen once. A field that departs closes its controller and then disposes it; the app’s exit still waits for its last record. The chain’sstartedrecord, with the first minute as its estimate, is written before the session opens, and a log that cannot take it stops the dictation.runningfollows each full minute of audio sent, andsucceeded, with the minutes sent to six places, whenever an accepted session closes — by finalization, cancellation, the silence limit, the platform’s limit or a dropped connection —canceled, with no cost, when the inline attempt ended before the platform accepted the session, since nothing was sent; orfailed, with its code and no cost, when the session was never accepted otherwise (connectionwhere the end gave no reason). Closing stops whatever is still starting where it is: before the key or the microphone, without a word or a log line; once the first record is written, with that chain endedcanceledand logged. A microphone started for a session not yet opened goes off at once, even while the first record waits for another copy’s lock. Nothing starts after a close. While a session is ending — its end come, its last record on its way — the controller refuses another resume until that end is recorded, so a resumed microphone always has a session that can stop it. Stop takes effect at once, so no frame after the press is sent, and a session that ends meanwhile keeps its end; a second Resume while the first still starts the microphone starts no second one. Stop turns the microphone off and keeps the session; Resume starts it again, or, after an end, opens a new session with a chain of its own while the text stays. Five minutes without speech end a session, and so do 60 minutes — OpenAI’s limit for a session, which the controller keeps for both speech services, xAI publishing none — with the controller raising its near-limit state five minutes before. The inline field shows no session status, near-limit warning or cost counter. A microphone that fails while recording ends the session with its code (mic_ratewhere the system changed the format asked for). Closing waits at most three seconds for the chain’s last record, which is retried until it is written (§16). Closing the window, or the agent’squit, ends every open dictation too (ExitTasks, infilmopen_state), within the drafts’ ten seconds and waiting for each chain’s last record, so a normal exit leaves no chain pending; a crash still does (§16). - On FilmOpen credit (§6.6,
DictationController). Where the speech service is OpenAI’s and no key is stored, the gesture asks the account for credit fortranscriptiononopenai(ProviderFunding.funding). With credit the session opens OpenAI’s own protocol at the website’s transcription pass-through,wss://<kFilmOpenDomain>/v1/portal/transcription(portalTranscriptionEndpoint), its upgrade carrying the portal’s headers for a fresh operation (portalHeaders) and that operation, a v4 UUID, inX-FilmOpen-Operation(transcriptionOperationHeader); the provider plug-in is checked again after those waits, as after a key’s read, before the microphone; everything after the socket opens is the session as on a key — the same messages, the same 24 kHz audio, the same ends. Without credit, or without the operation’s credential, the gesture isno_keyas before. Credit pays OpenAI alone: where the chosen service is Grok and its key is not stored, the same credit is asked for OpenAI’stranscriptionand, with credit, the session runs on OpenAI’s validated setup from the catalogue (dictationCreditSetupProvider: its model, entry, platform and estimate in the records) exactly as above, the active field naming OpenAI’s service and the recording commentary’s provider refusal naming OpenAI’s plug-in, anddictation.fundingsayingreason: fallbackwithselected: xai; the saved choice stays Grok, and a stored Grok key is used as before, credit never asked. The chain’s records sayroutefilmopen.aiand carry no key; itsrunningrecords carry no cost, their basisunknown, and the live cost (DictationController.cost, the recording commentary’s tooltip) is unknown — Cost unknown — never the catalogue’s price. After the session closes, its operation is ended once (end) and its receipt read once (receipt): the last record’s cost is the receipt’s charge in US cents, on the provider’s basis, or unknown where no receipt came — a session refused, failed or canceled before it was accepted included, never free once anything was sent. The End, the receipt and the last record are one write, counted before anything is awaited, so that the app’s exit waits for it while the field’s close stays bounded at three seconds and the microphone goes off at once; a second close repeats neither. A refused upgrade on credit iscredit_ws_<status>— the session refused with its status, with no door to a key, whatever the status (402,503and the rest), never the refused-key sentence; a refused credential (credit_ws_401) is let go (portalRefused) and nothing is sent again, neither End nor receipt, while any other refused status still ends the operation and reads its receipt once. A credential that expires during a dictation is the server’s to close, said as a dropped session; a receipt that never comes leaves the cost unknown. - Turns (
SpeechGate).gpt-live-transcribetakes no voice activity detection, only commits, so the device decides where a turn of speech ends. Each 100 ms frame sent is measured by its level, the root of its samples’ mean square, against the room’s, which the gate follows as it goes: a quieter frame brings the room down to it at once, and a louder one lifts it by at most 3 % a frame, so that a steady noise becomes the room within seconds. While the last two seconds are as uneven as a voice — their loudest frame at least twice their quietest — the room rises no higher than their loudest frame’s level divided by four and a half, so that a voice which talks on without a pause keeps being heard, even one whose syllables vary by less than three times; a steady noise that has sounded two seconds on its own is learned. The room starts at 50 on the 16-bit scale and never goes below it, so that digital silence does not make every sound speech and a voice already under way when the audio starts — a held microphone’s, as a rule — is heard as speech. It is kept across Stop, Resume, a new session and a change of microphone, whose own room it learns as it learns any other. A frame three times louder than the room is speech, and speech keeps the session from its five-minute limit, as the platform’sspeech_starteddid. A turn ends after 0.8 s of quiet that follows its speech, or after 30 s without such a pause. It is committed (input_audio_buffer.commit) where it held at least 0.2 s of speech — a short name — and cleared (input_audio_buffer.clear) where it held less, as a click does, or where it held a second or more of speech and was still even when it ended, its loudest frame less than twice its quietest: a noise that went on sounding while the room learned it, or an even sound cut by Stop. The quiet that ends a turn counts, so an even sound that stops — a beep — is committed, as a cough is, and so is a flat voice followed by a pause. Quiet with no speech is cleared every ten seconds, so the platform never holds long silence. Stop ends the turn in progress the same way, or leaves it alone where it held no speech, and nothing is committed or cleared before a session is accepted or after it ends. Stopping inline insertion while listening or while a committed turn’s text is on its way first waits for the final text of every turn committed (settle: at most three seconds, then the text as it stands; what got no answer in that time is not waited for again, though a late answer is taken). A commit refused for want of audio (input_audio_buffer_commit_empty) and a turn the platform could not transcribe wait for nothing more, the latter keeping what arrived of it, and a session’s end ends the wait. The defaults are untried on real microphones (§16). - For a commentary (
cut,commits,stretches; the recording’s, §6.8 — field dictation uses none of them).cut()ends the turn in progress where it holds speech, sending one commit while the microphone stays on, and in silence commits nothing and clears the quiet; while connecting, stopped, ended or failed, onceclose()has begun, and from a listener ofcommitswhile it is raised, it does nothing.commitscounts the commits sent, raised as each is sent, before any answer, and a turn the platform ended by itself while no commit waited; it is never reset, across sessions.stretchesgives every commit once, in the order sent, with the words of its own turn (after those of any turn the platform ended by itself while it waited); a refused commit, a turn that could not be transcribed, the commits a session leaves unanswered at its end and, atsettle()’s limit, those still unanswered are given with nothing, and a late answer adds none. It is synchronous, sosettle()andclose()return after what they gave is delivered. - The connection (
dictationSetupFromout ofcatalogueProvider, for the engine in use;EngineTranscriber). The URL, the model and the price per minute come from the engine’s bundled entry —stt-gpt-live-transcribeonopenai,stt-grok-transcribe-20onxai— and the setup refuses an endpoint that is notwsson one of its platform’s hosts with nothing before the host (PlatformFile.mayReceiveCredentials), since the key goes wherever the endpoint points, and a platform file that takes its key other than as a bearer token (Authorization: Bearer <key>), which is how both services’ sessions send it.maininstalls one transcriber,createDeviceTranscriber’sEngineTranscriber, which opens each session with the engine whose host the endpoint is on (dictationEngineAt; OpenAI’s for the website’s transcription pass-through, by name; the first engine’s elsewhere): it chooses messages only, and where a key may go stays the setup’s rule. A session’s upgrade carries exactly the headers the controller gives it (Transcriber.open’sheaders) and none of its own: the key’s bearer, or credit’s headers. What follows is OpenAI’s session (OpenAiTranscriber). The handshake follows no redirect: dart:io would drop the key’s header on the way to another host, but the audio would follow. It is logged as it begins (dictation.handshake), with the URL and the names of its headers, never a header’s value, and a failure with its error and code. A close under way is what ends a session, even when its connection then fails or its time for accepting runs out. The session message asks for a transcription session of 24 kHz PCM for the engine’s model and the language, with turn detection off ("turn_detection": null), and the audio frames are that PCM. A session turned down before it was accepted endsrefused, and the log keeps the platform’s error type, code and message and the field it named (dictation.refused), as they came. - xAI’s session (
XaiTranscriber). The session is asked for in the URL’s query — the model, the microphone’s own 24 kHz PCM, interim results,endpointing=2000and the app’s language where xAI formats it — and no message sets it up:transcript.createdopens it. Audio goes as binary frames. An utterance is a turn (turn_<n>): words that can still change and chunks the platform has locked are sent as the turn’s whole text so far (TurnText.whole, which replaces where OpenAI’s text only grows), and the utterance’s end completes it; whether that end carries the whole utterance or only its last chunk, which xAI’s pages leave open, is told by where each result says it starts, so that neither reading repeats or loses words. The device still decides the turns: a commit sendsfinalize, past whose 0.8 s the platform’s own 2 s ending is only the fallback, so both services cut the speech the device commits into the same turns; a clear sends nothing, since xAI keeps no buffer. xAI names no turn for a commit, as OpenAI’sitem_iddoes, so each commit waits on a name of its own (commit_<n>), released with no words right after the words of the end that answers it: an end releases every commit made before its utterance began, which no later end can answer, and then the oldest left — so two commits at once are two waits, and afinalizeleft unanswered holds up none after it. A commit with no audio after the last end, nothing shown and nothing waiting is refused here, as OpenAI refuses an empty one; the frames behind an end come from where xAI says it ended. Closing sendsaudio.done. A refusal carries no code, since xAI’s error is a sentence: its message is the refusal’s, for the log (dictation.refused); an error once the session is open changes nothing and is logged asdictation.platformError {operation?, event, message, ms}. Nothing here has spoken to xAI: what only a real key can show is the results note’s §5. - The microphone (
RecordAudioSource,filmopen_platform): 24 kHz mono 16-bit PCM throughrecord, cut into 100 ms frames byPcmFramer; its codes aremic_unavailableandmic_rate. Native shutdown shares one published future among concurrent callers and reports success only after native Stop and disposal both succeed. Failed Stop or disposal is answeredmic_unavailableand keeps the same recorder; cleanup never retries itself. The first consumer observes the original automatic-stop result, and only a later deliberate Stop retries once. Closing frames does not confirm physical retirement. Native errors are caught and logged by code, without their text. - Seams.
AudioSource,TranscriberandUsageLogare unavailable until an entrypoint installs one:maininstalls the device’s (forTranscriber,createDeviceTranscriber), and the agent build and the test harnesses installFakeAudioSource,ScriptedTranscriberandMemoryUsageLog— the agent build the first two unless live speech is asked for (FILMOPEN_AGENT_LIVE_SPEECH1, §4.3); the usage log stays in memory. The scripted session answers every ten frames with a turn, in two halves and then its final text, and a commit with the turn in progress at once — or, with nothing since the last turn, with a refusal, as OpenAI answers an empty commit; a clear is only counted.FakeAudioSourcerecords silence unless it is given asound, each frame’s loudness by its place in the recording; the agent build’s talks in phrases, 2.5 s of syllables and then 1.5 s of quiet. - Words. A code’s sentence is
labels.dart’sdictationCodeText. The sentences are short, and speak of the speech service, whichever it is: none names a platform, the 60 minutes or the 24 kHz (labels_dictation_test.dartpins it in both languages). The microphone tooltip also names its validated service by compact brand and model before Start, alongside the shortcut. Pending or unavailable setup shows no stale service or model; an active attempt retains its captured identity. Other platform names appear where a person chooses or pays — its key’s card, the Speech service pull-down and the totals of Activity’s Usage. The log carriesdictation.ended {field, minutes, code, turns, cleared, quietClears, speechFrames, room, loudest, text}— six of them how the device heard that session: its turns committed and cleared, its clears of quiet, its frames of speech, and the room’s and the loudest frame’s levels;textthe words dictated so far, which the issue recorder’s tape leaves out (§6.8) — and for what went wrongdictation.refused {type, code, param, message},dictation.notStarted,dictation.recordLost,dictation.setupUnreadable,dictation.targetRejected {field, words},dictation.setupUnusable {code, engine}anddictation.devicesUnreadable, a failure with its type and message: never the key. Each session’s start and end are logged too, the controller’s lines with its operation on credit — afterdictation.funding, which decides it — andmssince the gesture:dictation.funding {engine, outcome, reason}(own,creditorunavailable, and why),dictation.socket.opening {operation?, route, endpoint, platform, entry, model, language, device, credential: key|portal, ms}— the call’s values, logged just before its credential enters the upgrade’s headers —,dictation.closed {outcome: stopped|stopFailed, ms},dictation.session.accepted, every phase asdictation.phase {phase},dictation.socket.refused {code}ordictation.socket.closed {code?, seconds}, and on creditdictation.end(status: unsent-refusedfor a refused credential, which sends nothing) anddictation.receipt {outcome, chargedCents?, receiptId?}(the receipt’s outcome as the website wrote it,nonewhere none came). OpenAI’s session adds, with the operation its upgrade names andmssince the session opened,dictation.session.update {message}(the configuration as sent),dictation.socket.open,dictation.first_event {type}(the platform’s type as it came) and, for a turn it could not transcribe or an error once the session is open,dictation.platformError {operation?, event, item?, type, code, param, message, ms}, the platform’s words as they came. An unexpected failure isdictation.failed {operation?, stage, type?, error, ms}— at the handshake (with itscode,mssince the connection began), the open, either platform’s connection or stream, the end or the receipt —, and a Close that throws or never answers isdictation.closeFailed {operation?, code, type, error}with its error’s text, and its type beside it but at the handshake; never a credential or a header’s value.credit_ws_<status>reads as the session refused with its status. - Identifiers:
<box>.dictatenames each microphone, and<box>.dictate.captionnames a plain box’s provisional caption, never a value for the recorder.dictation.open-keysnames the page message action or the inline repair inside a dialog. In Settings → General, the microphone choice issettings.microphone, its menu usesdictation.device:defaultanddictation.device:<id>; on Models the service choice issettings.speech-service, and its Configuresettings.speech-service.configure. The version label’s box islabel.text, and the account popup’s usernameaccount.username, withaccount.username.retryandaccount.username.ask-again.
9.7 Activity — filmopen_jobs_ui
Section titled “9.7 Activity — filmopen_jobs_ui”The rail’s Activity (nav.activity; NavIndex.activity, which NavIndex.nameOf names activity for the agent’s state, the issue report’s tape and its replay), ActivityPage, in two tabs (TabBar; the agent’s set_tab names them now and files), Now on arriving and again after leaving the page, each change logged at its start activity.tab.start {tab, from, reason, shownMs} and, once its first frame is drawn, activity.tab {tab, from, reason: chosen | leftActivity, shownMs, ms} — the time the tab left was shown, counted from arriving on the page, and the switch’s own. Now: this device’s usage, the media being added to the open film and, while any is, what of the film is syncing, in one scroll view. Its title, Activity, then Usage, then Adding media with Dismiss finished (activity.dismiss-done) beside its heading, which dismisses every addition that is done or stopped and offers a dismissal, and is disabled with the tooltip Nothing is done or stopped yet. A failure is dismissed on its own row. where none is: a failed job stays until it is dismissed on its own row. Under Adding media, while the open film has any, Syncing lists its sync jobs queued, working or just done (syncJobsProvider): one line each with the file’s name, a bar where its progress is measured and a check once done; a send waiting to retry, held, failed or stopped is Share’s, which draws the same jobs in full. The lines carry no sentence and no action, and are logged activity.sync.rows {film, rows: [{name, kind, phase, progress}]} each time their files or phases change.
Files: every file of the open film against its cloud copy (syncStatusProvider, §6.14). Under Compared with the cloud at , the groups in this order, each a heading with its count and drawn only where it has a file: Changed on both sides, Problems, Waiting — each with Share says what each one it still lists needs; a conflict dismissed there stays here until the copies agree. under it, since Share draws those jobs in full with their actions —, Sending and receiving, To send, To fetch, In the cloud only, In sync. A row is the file’s project-relative path, wrapped rather than cut, with a bar where its progress is measured; rows carry no action. With no film: Open a film to see where its files stand against the cloud; with a film no pass has compared: Nothing has been compared with the cloud yet. A film is shared from Settings → General → Sharing. The rows are logged activity.files.rows {film, at, counts, rows: [{path, state, code, progress}]} once each time a row’s path, state or code changes, and each step of a bar as it is drawn activity.files.progress {film, path, progress}, for the files whose progress moved alone.
The rail’s Share (nav.share; NavIndex.share, named share), SharePage: the open film’s sends and receipts — the sync’s jobs for the open film (syncJobsProvider), newest first, under Sharing, Nothing is being shared where there are none — with its title, Share, and its own Dismiss finished (share.dismiss-done) beside the Sharing heading, as Activity’s stands beside its list. With a film and nothing listed, the empty state says where a film is shared — A film is shared from Settings → General → Sharing; what it sends and receives is listed here. (labels.dart‘s sharingPlace) — and Set up sharing (share.set-up) opens Settings on General. With no film it says Open a film to see what it is sending and receiving, a sign-in under way keeping its wait and its Cancel above that line. How the film is shared — the target, the account, Stop sharing — is Settings’ Sharing card (§9.4). Both pages draw their rows through the one row logic of the package (job_lists.dart): what follows of a row, an action, a refusal and a sign-in holds on both.
Usage (filmopen_keys/usage_tab.dart‘s UsageSection, §6.7, plan), which the application hands to Activity as a builder — read once as Activity is first built at start, and again each time a section is built anew — since the jobs’ pages do not import the keys’ package: a section with no scroll view of its own, drawn first, with or without a film — the totals of this device’s usage log by month and platform (a free call on the person’s own key — a status check, a key check, any server-provider function such as a server’s offers, deployment, keep-alive or stop, and a plug-in’s switch on or off — is in the log of events only, never here, §6.7) — spent, pending or possibly charged, and the uses whose cost is unknown (1 use, cost unknown), where anything is, Spent left out where nothing was spent and a cost is unknown — and each use: what it was and the box it was for, when, the minutes, its state (a failed dictation with its code’s sentence; a failed render as The render did not finish (…), by its plug-in’s code or the application’s, and any other plug-in call as It did not finish (…) — usageErrorText, never a code bare, and said once: in place of the status word, as a clause of the line without its own period; the code as the tile says it, bad-result and not bad_result), what it counts as, where that cost comes from — estimated, at the catalogue’s price, as the platform reported or charged to your FilmOpen credit (the portal’s settled receipt, its id after it as receipt …), or not yet settled by filmopen.ai for a use on credit whose receipt had not settled (labels.dart’s usageBasisLabel, the counted money’s basis) — and the platform’s own id of its request, job or session where any record of the use names one (request …), by which the platform’s usage page finds it; under a use on FilmOpen credit, one button per operation, operation …, which copies the operation’s id — the one the filmopen.ai portal’s ledger shows — and says Operation id copied., or The operation id could not be copied. where the clipboard refused, logging usage.operationCopy with the id before the clipboard is asked, then usage.operationCopied with the id, its outcome and the milliseconds; the card above says that each cost comes from one of the four. Each use is named by its model as the render dialog names it — the catalogue entry’s name (ProviderCatalogue.models), a dictation and a server’s time keeping their own words — and by the box a person saw (labels.dart’s usageBoxLabel: a form field by its label, a project’s name as its Title, a render’s group by its place’s title, another box by its own) and the file it belonged to — a render’s the version it was of, never the project file — a voice kept adding · a voice kept, since it holds a slot at its platform; and a failed dictation’s sentence there offers no Resume. A use that asked no model — a plug-in’s Check that reads its key, an action or a request of its own, whose record’s entry is unknown (unknownUsageEntry) — is named by the plug-in that made it and what it did, from the record’s purpose.plugin (labels.dart’s usagePluginUse: OpenRouter · Check for its status, an action by its label, a server provider’s seven functions in words — SaladCloud · Prices of servers, Start a server, Keep a server awake, Stop a server and the rest — and an entity hook’s likewise, Template · Rewrite, any other function by its name, a developer’s own as the developer page shows it, from the plug-in’s page or the developer page alike), the plug-in found by the tag its stem holds, so that a use of an earlier version bears the name the plug-in has now, and one no longer installed is named by that tag; its cost is what the platform said, which for a check is nothing, so it reads Cost unknown. Each use’s cost is in one of two columns headed Your Key and FilmOpen Credits (each on two lines), by how the record says it was paid (its route: direct is the person’s own key, the cell shaded with the theme’s primary colour at low alpha, legible in light and dark; filmopen.ai is FilmOpen credit), the other cell empty, and a use whose record names neither in neither column, saying paid how: unknown. The totals are split the same way: under the same two headings, each month and platform’s line puts, in each column, that route’s own Spent, Pending, Possibly charged and N uses, cost unknown, one under the other where any is (usageTotals of the uses of each route; the own key’s cell shaded), nothing combined beneath, and a route whose only calls of the month cost what nobody knows, in a currency the month has no line in or with no estimate, counted on the month and platform’s first line; a use whose record names neither route counts in neither, and a line under the totals says how many (1 use is in neither column: how it was paid is unknown.). Under the totals, a line says what pending and possibly charged mean wherever either is shown, and another what cost unknown means wherever it is; such a use’s cost reads Cost unknown. The log is read again each time Activity is shown again (the dashboard calls UsageSection.refresh as the rail enters it; never on the web), since another copy of the app may have written and nothing of this app’s says so, and after each append of this app’s own, keeping the lines on screen meanwhile; a month filter applies on the device, and a call of a version this app does not know is left out and said. The web build says it keeps no log. Identifiers: usage.month, usage.totals, usage.uses, and usage.operation.<id> per operation. Activity → Usage is where a sentence elsewhere says a cost is read (labels.dart’s usagePlace). Money and decimals are written from their exact decimal, with each language’s mark, money as every amount is (§9.5; labels.dart’s amountText, decimalText). Under this device’s cards, Shared plug-ins on your account (usage.shared), read from the account’s ledger (§6.6, §6.7) for the month shown: one row per lending settlement or earning — Earned from a shared plug-in with a plus sign in AppColors.positive (green in both themes), Paid for a shared plug-in with a true minus sign in the text’s own colour, since spending is not an error; the words and the signs carry the meaning, never the colour alone (labels.dart’s signedMoneyText: +$0.40, −$0.10, $0.00, written as every amount is, §9.5) — under it the date and time and request … with the lending job’s id, and no offer, quantity or other party, which the wire does not carry; then the totals as three texts in a wrap, Earned +$0.40 (green above zero), Paid −$0.10 (plain) and Net +$0.30 (green above zero, plain otherwise), the net covering the shared plug-ins alone, since this device’s spending goes to a platform and not through the account. One line otherwise: Sign in to see what you earned and paid for shared plug-ins. signed out or refused; Your account’s earnings and payments could not be read just now. where the ledger could not be read; Nothing earned or paid for shared plug-ins. where it holds none for the month; and under the rows Only the newest records are counted here. where older pages were left unread. A re-read keeps the rows on screen. The month filter lists this device’s months and the account’s together — the website lists every month it has a record in, whatever month was asked — and shows whenever any month exists; a month with no use of this device’s shows this device’s empty state in place of its cards, the account’s card beneath. This device’s part and the card never hide each other: a log that cannot be read still shows the card. The web build keeps its sentence and no card.
Adding media: insertion’s jobs whose place key’s first element is the open film’s key, together with generic unplaced application rows; Nothing is being added. With no film and no unplaced application jobs it says Open a film to see the media being added to it, the usage above it staying. Supplied jobs with no place stay visible with or without a film; malformed non-null places grant no placement. The list is newest first — the service answers in the order the jobs were submitted, and the page reverses it. An addition a place has said done is dismissed by that place (§9.3), so the list holds the additions that run, wait, fail or stop, and one done while its own place is not drawn (§6.13). The page listens to insertion’s views from its first build, which is at the application’s start, since the rail builds its pages together, and sends an addition’s actions to that service. The installed service seeds each subscriber with its current cached snapshot, then publishes changes. A cached job predating the listener is shown immediately; placed jobs are filtered to the open film, while supplied unplaced jobs remain visible without one.
Leaving either page stops neither service, and each keeps its state while another page is shown. A render for a film with no folder that another film has since replaced (§6.13) is that film’s and shows in neither list; the application says so as each such job ends — Something you asked for finished after you left its film, which had no folder, so nothing it made stays in a film. What it cost is in Activity → Usage. — once for each (filmReplacedRendersProvider, heard by the dashboard), and the job’s own sentence says the same (jobFilmReplaced).
A row is a JobRow, a semantics node of its own, so that a screen reader reads its name and its sentence together, one row at a time, and an agent’s find answers one node per row. It shows the job’s display name, else its reference’s last segment, else its kind’s word (A document, A media file, An addition). isSyncFetchKind recognizes exactly sync.document.fetch and sync.media.fetch; both use the same document/media fallback names as sends. jobStateText(code, phase:, name:, sending:, receiving:, syncOutcome:) selects the direction’s state and phase words. Receiving phases say Waiting to receive…, Receiving…, Received., This file could not be received. or This file needs a person before it can be received. Sending phases retain Waiting to send…, Sending…, Sent., This file could not be sent. and This file needs attention. Additions retain their own words, including Adding… and Added. Unknown waiting phases say the common waiting sentence; cancelled jobs say Stopped. For a done fetch only, the row supplies its checkpoint outcome: keptLocal says Local copy kept. (Se conservó la copia local.), and alreadyAgreed says Already in step. (Ya está sincronizado.). Unknown or absent outcomes preserve Received.; these markers change neither a send nor an unfinished fetch. A resolved conflict which sent the local copy therefore never claims that a file was received. Render additions use their supplied safe phase and failure sentence as their insertion tile does: a queued render says Waiting to render…. A before-send binding change says that nothing was sent and asks the person to open Render to choose again; it grants no retained-receipt action. The labels also map pending local acknowledgement to an accepted result whose saving or cleanup is waiting, and an unread carried journal to a saved render needing attention. The application’s insertion adapter associates readable render attention by the insertion-owned validated recovery id, preserving insertion placement and presentation. Tile and Activity route only offered manager actions, without resubmitting accepted work. A render journal entry this build cannot read has no row: the manager logs it and drops it (§6.15). The row draws only the actions its supplied view offers. A refused Dismiss-only insertion continues to say its refusal after collection. A producer binding refusal needing attention uses the existing action-neutral insertion sentence about the changed film or settings whenever Retry is absent, including Dismiss alone and Stop alone. With Retry offered, the row retains the render recovery sentence and exactly its offered Retry/Dismiss actions. While its exact render still offers Stop, the row and tile offer that Stop through the manager instead of removing the insertion; ordinary dismissal returns after terminal cleanup. Activity’s Syncing lines are not JobRows: a name, a measured bar and a check, a semantics node each, with no sentence and no action.
Shared sync codes keep their common sentences for sign-in, film closure, unavailable or busy targets, refusal, missing folders, missing configuration, metered holds, full Drive and unsafe checkpoints. A full Drive or changed upload position distinguishes an automatic retry from a job requiring Retry. Invalid paths, transfers, lengths and content use The file could not be received as it is. for fetches and the existing placement sentence for sends. A received mismatch says the target changed while receiving and retries; after retries stop it asks the person to press Retry. A target file gone, a local name already taken and an unreadable received document each have their own sentence. Additions continue to interpret their own codes, so their unavailable and busy use their phases rather than a target sentence. Paused. and Stopping… apply to every direction. An accepted Stop whose cleanup binding fails continues to say Stopping… without another Stop button while the saved binding retry is pending.
A conflict row is named by its file reference, with Changed here and at the target; nothing is sent or received until you choose. and the offered Take theirs, Keep mine and Dismiss actions. Its hashes in stateArgs are never displayed. Every row uses the error colour when failed or needing a person, a progress bar while working or waiting with a measure, and one text button per offered action under job:<id>:<action>. The actions wrap below the sentence on narrow pages; receiving, received and conflict rows fit at 375, 700 and 1200 px in English and Spanish. Share routes these actions through the existing sync service door.
An action goes to the service that owns the job: the sync’s for a sync. kind, insertion’s for the rest. An action under way is drawn disabled, with Wait for the step under way to finish., until it answers, so a second press sends nothing, while another action of the same row goes (Stop while the row’s sign-in waits). Every action, on Activity and on Share alike, is logged as it is sent (activity.action: the job’s id and kind, the action’s code, the job’s phase and state) and as it ends (activity.actionDone: the id, the action, its outcome — accepted, refused, failed, or unsent where the page closed first — its code and its ms). A refusal, or an action that threw (logged as activity.actionFailed with the job’s id, the action’s code and the error’s type and text), is said under its row — That could not be done now: the job may have moved on. Try again. — and kept beside the job as it was drawn then: its phase, its state and the actions it offers, not its progress, which a send moves every few percent. It is erased at the next action pressed on the row, once the job moves on, once the job is no longer listed and when a row is no longer listed; a job that moves on and comes back as it was shows no old refusal, and a film opened again shows none of before. A row that no longer offers the action when the answer comes — stopped meanwhile — shows no refusal: its state says what happened.
A signIn action runs the application’s sign-in first: beforeSignIn, chosen by the open film’s target when the row asks. For a film on FilmOpen Cloud it is the account’s — its popup on Settings and a wait of at most 30 s for the account, its own wait line and Cancel (§9.4) —; for any other it is the Sharing model’s ensureSignedIn (§9.4), and what follows in this paragraph is that Google sign-in: which tries the held sign-in with a refresh and opens the consent page only where there is none or Google refuses it — one wait for the card and Share, which a row’s Sign in joins where the card started it. While it waits, every row that asked says Signing in to Google: finish in your browser if it opens. beneath it, and the first such row drawn carries the one Cancel (activity.sign-in-cancel) — where every row that asked has left the list meanwhile (stopped, dismissed, another film opened), the wait and its Cancel stand under the Sharing heading instead — which calls the application’s cancelSignIn: the wait ends at once, beforeSignIn answers false, no action is sent, and an answer that comes after stores nothing. The card’s Cancel ends the same wait. Only a sign-in that answers true sends the action to the sync, which lifts the sign-in hold of the job’s lane — every send of its kind that waits on it; one that does not finish leaves the hold, and the row says The sign-in did not finish, so nothing was sent. beneath its own call for one. Back cancels nothing; there is no dialog. Drawing the page, which the rail builds at the application’s start, performs no sign-in and reads no credential. The page draws public JobViews and sends public JobActions; it never reads a journal or starts a runner.
9.7a GPU — filmopen_render_ui
Section titled “9.7a GPU — filmopen_render_ui”The rail’s GPU (nav.gpu; NavIndex.gpu, named gpu), GpuPage, has the owner’s three tabs in their order — Servers, Share my plugins, Using remote plugins — in a scrolling tab bar, so that the third is reached at 375 px, over a body that is a switch on the tab, as Settings builds its own; the tabs carry no identifier, and an agent opens them by name, servers, share and remote (set_tab; state.tab answers the one open). The page opens on Servers and goes back to it when GPU is left, as Settings goes back to General (§9.4): the dashboard keeps the page alive in its stack, so the page listens to navIndexProvider itself, and a link to the page — the render dialog’s Open GPU — lands on Servers. Arriving on the page is its refresh, since it has no Refresh: each arrival asks every enabled server provider for its servers again (ServerSessions.check, reason gpu-page, §6.15) and this computer’s render engines for their status (refreshLocalStatus, §6.11), and reads the lent offers for GPU’s two lines (below), once, in the background; staying on the page asks nothing, and nothing polls. The page has no heading: the rail and the tab name it.
GPU’s two lines. Under the rail’s GPU label two labelSmall lines, one line each, ellipsised and centred — a lone line has the room of two, as the account’s lone status line has —, name the one option a render will use: the first that holds, in the owner’s order (gpu_status.dart: gpuStatusOf, read through gpuStatusProvider). A server the sessions keep that is not over — started here or found at its provider — line 1 its provider’s own name (ServerSessions.labelOf); line 2 unconfirmed where its end is unconfirmed past its deadline (§6.15), whatever its phase, else active where a render holds it, ready where it runs, unsure where it was made and its provider no longer lists it, starting (waiting for a GPU while its provider allocates one), stopping — chosen in that order, the oldest first within each; the badge and Servers keep the others. This computer’s render plug-in, among the enabled platform plug-ins localPlatformsOf names — its name; active where a render under way runs on its local route (the plug-in itself or one that calls it, no destination or a local one), ready where its status is ready as Servers reads it, else offline. This computer’s graphics card, where one was read — its name without the vendor’s words, available. Another device of the account — Your device / ready — then someone else’s shared plug-in — Shared / ready —, each only where a lent offer is eligible as the render dialog decides: published, available, a render of a model an installed, enabled sharing plug-in renders, and a price that can be computed — judged again against the plug-ins whenever they change, so that an offer whose plug-in is turned off stops counting without a new read; a read that was refused or late says unknown, never an earlier ready, and a read that lands after a newer one was taken is dropped. Else none where the card was read and there is none, and not detected where it could not be read. The card is read once a session through nvidia-smi (filmopen_machine, Linux and Windows): NVIDIA only, so a card that could not be read claims nothing and a remote offer still shows. The button’s tooltip says its name, the badge’s sentence, the two lines and, where they speak of the card, that cards are detected for NVIDIA only; the lines’ own text is left out of the semantics, so that they are said once, in the tooltip. The answer is made again whenever a server, a render job, a plug-in or its readiness changes; the lent offers are read, bounded to three seconds, at start once a plug-in that could render one is there, at each sign-in — a change of the account’s authentication into signed in, its read’s reason signIn, each change logged gpu.lending.account with where it came from, where it went and whether it read; a session restored at start reads so too —, once more when the device’s registration lands after a read refused device-not-admitted — heard through the funding resolver’s notice, which the registration raises, its read’s reason deviceAdmitted, each notice logged gpu.lending.funding {awaitingAdmission, registered, device, read}; any other funding change reads nothing, and a refusal is not retried otherwise — and on each arrival at the page; nothing polls. A sign-out lets the former account’s offers go at once — the two lines and Using remote plugins stop counting them, a read still under way is dropped — logged gpu.lending.cleared with how many it let go and the read numbers dropped, even where no read had landed yet. Each change of answer is logged gpu.status with the facts it weighed; each lending read, numbered, gpu.lending.read.start, then .done with every offer as returned and its eligibility and why, or .failed with its code and message, or .skipped where no plug-in could render one; an older read dropped is logged gpu.lending.read.superseded, and an offer’s decision changed by a plug-in change gpu.lending.rejudged. The app names no provider: the names are the platform file’s and the plug-in’s.
Servers, the first tab (servers): every server the application’s sessions keep (ServerSessions, §6.15), the newest first, ended and found ones included — nothing is filtered. Page and badge read one projection, gpuSessionsProvider (gpu_sessions.dart), seeded with the sessions kept, so that a restored one shows at once, and made again on the sessions’ changes and on the plug-in settings’ changes, so that Start server follows a provider turned on or off though no session changed. Each row says of its server: its GPU, its phase (serverPhaseText, an unsure server as such; a starting one whose provider is still assigning it a GPU — its providerState allocating — Waiting for … to assign a GPU, serverWaitingForGpu), its model stack where it names one (Model stack: …), Found at …; not started from this FilmOpen for one its provider listed that this app did not ask for, its lease, a stop its provider took and did not carry out, a stop still owed past its lease (and, its provider’s plug-in off, that it cannot be asked), past its deadline with its end unconfirmed Its time ran out at …. … has not confirmed it stopped. in place of its lease (and, its provider’s plug-in off, how to stop it), though a server starting or running past its lease with its provider’s plug-in off says instead Its time without work ran out at …, and … was still running it when last seen: stop it at …, or turn its plug-in on so that FilmOpen can. (serverLeaseOverrunOff); what it ran for — while unconfirmed, no later than its deadline or than when it was last seen running past that — and its estimate, its last problem. Stop (gpu.<name>.stop) follows the sessions’ own refusals in their order (gpuStopOf): an ended server owing no stop has none; a deployment its provider has not answered with a server is disabled with Not started yet: there is no server to stop; a server whose provider’s plug-in is off with Turn …‘s plug-in on so that FilmOpen can ask for its stop, or stop it at …; one already asked to stop with Stop was asked… until its deadline, past which, unconfirmed, Stop asks its provider again (Press Stop to ask … to stop it again.); a refusal is said under the row, and a press is never left held. Under a running server — the lending loop serves an offer from the first running server of its provider and GPU offer that holds its stack — Stop makes its N shared offers unavailable, N the shared offers on that provider and GPU offer the website acknowledged as published that this server can serve and no other running one can (Lending.published); nothing where none is. Start server (gpu.start) is absent where no sessions are kept (the web), disabled with Turn on a server provider’s plug-in in Settings → Plugins to start a server. where none is on, and opens the provider’s offers (ServerOffersDialog); a server started is said once the window closes: that it starts and stops by itself after its minutes without work, or that FilmOpen is checking whether it started. A refusal said under a row goes once the session moves on or its provider is turned on or off. Under Start server, while any counts, how many servers may still be costing — the badge’s count, said on the page. With no server, No GPU server has been started and where one comes from — or, with no provider on, why none can be started, on the page and not on a hover alone. The rail’s badge (nav.gpu.badge, its own semantics node, its value the count, a press on it the button’s) counts, from the same projection (GpuSessionsView.counts), the servers this app started that may still cost — deploying, uncertain, starting, running or stopping, or ended with a stop owed — never a found one; the button’s tooltip and label keep GPU above N GPU servers may still be costing you, said once. The sessions’ rules are theirs: a server its provider cannot be asked about stays listed and counted until its provider says it stopped. This computer’s ComfyUI stands above the servers (gpu.comfyui), where its platform plug-in is installed: the plug-in that implements platform and that some plug-in rendering on comfyui calls, else one that implements platform and renders on comfyui itself, as the stock fo-cui does — never found by its tag, and a called one always first, since nothing stronger names the engine; of two copies, the development folder’s and the library’s, the first enabled one; no row where there is none, and none on the web. Each finding is logged gpu.comfyui.found {by: servers | share, platforms, called, reason: called | self | none, stem, enabled, ms}. The row names it (This computer’s ComfyUI) and says its plug-in’s state from the readiness record (§6.11), with the Plugins page’s words: Off where the plug-in is off; Checking… while a phase is under way; a problem — read before the fact, as the plug-in’s page reads it, since a failed Check keeps the earlier fact — or any fact that is not ready, in the plug-in’s own words, else What it needs is not there; Ready; and, with nothing known yet, Enabled — open to check., since nothing is asked of a plug-in until a person turns it on or presses Check — the row follows the record’s changes. Its link (gpu.comfyui.open, Open the plug-in’s page) opens Settings → Plugins on the plug-in’s page, where Check is. FilmOpen does not run ComfyUI: the row has no Start and no Stop.
Share my plugins, the second tab (share), opens on the audience block (share_audience.dart, ShareAudience): who may use the offers this person publishes, read from and written to the account (Lending.audience / setAudience, GET / PUT /v1/lending/settings, §6.6). Three controls in the owner’s order: Share my plugins with my other devices, checked and disabled, its tooltip saying that the account always shares them with the person’s own devices (the website fixes ownDevices to true); Share for free with — a box (gpu.share.free.box, Username or group), Add (gpu.share.free.add) and a row of chips, one per id of the account’s freeWith, each with its × (gpu.share.free.<id>.remove, the chip’s one control); Share with the public (gpu.share.public), unchecked as the account answers it. Under them one line says to whom and in what order: Your devices and the names above use your published offers free; with the public on, anyone else can buy them at their price. What is shown is the account’s answer, never the change being sent: a change — the public box, an id added or removed — is one PUT with the revision last read, every control disabled meanwhile with Wait for the step under way to finish., and the answer drawn after it. Add trims and lower-cases the text and holds it to the username rule (ShortName.validateUsername, §5.6; the refusal logged share.audience.refused with the name and the rule’s answer) — a name that fails reads A name is 3 to 32 lowercase letters, digits and dashes. under the box, official its reserved sentence and a name led by a dash the username rule’s, nothing sent —, then resolves it through the public name lookup (AccountProjects.name, sent with no token): a record adds its id; null reads No user or group is called {name}.; an id already listed changes nothing on the account. A chip reads the username this device resolved — kept in the names store (PluginSettingsControl.freeWithNames, §6.11) and in the block’s own memory for the session, the store written back trimmed to the answer’s freeWith whenever an answer leaves it different —; a group the person’s account holds reads its name from the Groups card’s last read (ShareGroups.onNames, logged share.audience.groupNames with the names and the chips they name); an id neither resolved here nor held, added on another device, reads Added on another device and is removed the same way. The block reads in its initState, each time the tab opens and never at the app’s start, and follows the account’s state (AccountSnapshots.current.snapshot.authentication, its installations and changes): authenticated reads; loading shows a wheel and sends nothing; signed out, or the inert source of a build with no account and of the web, shows Sign in to {domain} in Settings to choose who may use your plugins. and sends nothing. A read refused, or nothing answering, hides the controls — never shown unchecked — and shows Your sharing settings could not be read or saved just now ({code}). with Retry (gpu.share.retry); device-not-admitted is such a refusal, the device not registered yet. A write refused keeps the last answer and shows the same line; on terms-conflict — another device changed the setting — the block reads again first and shows the fresh answer beside the line, the change not replayed; a re-read that fails replaces the line’s code with its own. Under the block, signed in, the Groups card (share_groups.dart, ShareGroups, over the CommunityGroups seam, §6.6), shown and read whatever the sharing setting’s read answered — a refused read or a device not admitted yet hides only the audience’s controls, never the groups: read after its first frame — the card is made again each time the tab is shown, so this is the tab’s shown — and again after every gesture and when the app is resumed while it is on screen (AppLifecycleListener, cause resumed; a resume while a read is under way is logged skipped), never on a timer, a change elsewhere while the person looks at the card shown at the next of these, every list through all its pages, with the account’s usernames (AccountProjects.identities); New group (gpu.groups.new) and, while any wait, Invitations to me, each with Accept (gpu.groups.invitation.<id>.accept) and Decline (….decline); then each of the person’s memberships — one row per identity, a group two of their usernames belong to having two — as {name} · {role}, as {username} with Share for free (gpu.groups.<group>.<identity>.share, which adds the group’s id to Share for free with with one write and its name to the chips; disabled, saying Already shared for free with this group once listed, the busy sentence while the audience writes, and Your sharing settings have not been read, so a group cannot be added to them yet without its answer), Invite (gpu.groups.<group>.<identity>.invite) for an owner or an admin, and Leave (gpu.groups.<group>.<identity>.leave) unless that identity is its only owner; under a membership that manages, its members with their roles and its pending invitations, {name} — invited. With no group, You belong to no group yet: New group makes one, and an invitation accepted joins one. New group and Invite ask a username in a dialog (gpu.groups.name.box, gpu.groups.name.ok): the username rule (§5.6) refuses under the box with nothing sent, and the website’s refusal is said in the dialog by its code. A refusal of a read or of a gesture on the card shows the block’s failure sentence in the card with Retry (gpu.groups.retry), which reads again. Every gesture logs share.groups.<load|accept|decline|create|invite|leave|reload> at its start and its end — done (with the counts read, after a gesture that reads), refused with the code, the status and its ms, or, in a dialog, refused with the username rule’s problem and its ms —, the load’s start and done carrying its cause (shown or resumed) and the done naming the memberships read; a group shared logs share.audience.group {group, name}. Below the block, the offer tree (share_tree.dart, ShareTree; sharePlaces builds its rows), shown signed in or not: this computer’s ComfyUI where a platform plug-in reaches one (its state as the Servers row says it, ready only where its plug-in is on, no phase is under way, no problem stands and its fact is ready — comfyuiReady, which the Servers row reads too); one row per provider and GPU offer of a starting or running server, ready where one runs, greyed with its phase while it starts; and one greyed row, Stopped, per server a saved offer names with no such session — a saved offer is never hidden. Under each place, the enabled, runnable plug-ins whose render runs on its engine (comfyui here, the session’s engine on a server) and that share a render of an image — a plug-in that renders on a cloud key is not offered — and any plug-in with offers saved there, else No plug-in can be shared here. Under each plug-in, its offers: Enable (gpu.share.offer.<id>.enabled), the name, {price} per image, and Published only where the website acknowledged it, else Not published, and on a ready server place No running server holds its stack where none of its running servers holds the offer’s; on a ready place where renders run, Edit (gpu.share.offer.<id>.edit) and Add an offer (gpu.share.<tag>@<place>.add, the place local or <provider>:<offer>). The editor (ShareOfferEditor, Shared offer) asks the plug-in’s options on the route the lending loop would use (lendingRoute): Way of running it (gpu.share.editor.stack), a way not ready listed and disabled; a pull-down for each choice among more than one value of the chosen way, its own over the route’s (gpu.share.editor.param.<name>), every other option left to its default and not kept; Name (gpu.share.editor.name), made from the way’s label and the chosen values until the person types one; Cents per image (gpu.share.editor.price), a whole number up to nine digits, refused inline otherwise; Enable (gpu.share.editor.enabled); Save (gpu.share.editor.save), which writes the offer under its id — a new one a fresh UUID — and says a refused write inside the dialog, and Cancel (gpu.share.editor.cancel). A busy answer is asked once more when the plug-in says. There is no Remove: a disabled offer stays listed, withdrawn by the loop. The tab is the list’s one writer, its writes one after another. Under the places, Published, no longer on this computer (_Stale): read once when the tab is shown through Lending.supplied (§6.6, every version of the account’s offers), the account’s published offers whose plug-in id is not among the plug-ins installed here, each as {name} · {plugin} with Withdraw (gpu.share.stale.<offer>.withdraw), which sends the publication with published: false at the version read and drops the row on its answer; a refusal keeps the row and says Your published offers could not be read or withdrawn just now ({code})., disabled meanwhile with the busy sentence. Nothing is shown where none is stale, nor signed out (no lending client installed, or no-token); the website’s own lending-unavailable is said. A sign-out while the tab is shown clears the list (accountSnapshotProvider), logged share.stale.cleared with the offers it held. Logged share.stale.read (start with the installed tags; done with the stale offers’ ids, versions and plug-ins, the published ones installed here, the number of versions read and ms; refused with the code) and share.stale.withdraw (start, done or refused with the offer, version, plug-in, code and ms). An offer published from another device for a plug-in this computer has is not told apart. Using remote plugins, the third tab (remote), lists the offers this person may use, read-only (gpuUsableOffersProvider): the last read of GPU’s two lines, judged as they count it against the plug-ins of the moment, one row per offer — its name (configuration.name, else its model), its lender’s username (No username where the lender has claimed none) and its place: Your device for the account’s own, Free, or each rate as declared, an image’s … per image and another unit’s … per {unit} —, the list logged gpu.remote.listed with its count and ids each time it is made again, at each answer of the two lines. With none it is one empty state: Plug-ins your other devices and other people share with you are offered among the choices when you render. — the render dialog lists them as routes (§6.11). Turning an offer to published asks for the device’s name first — its checkbox, or Enable in the editor of an offer not enabled before — through the root’s shareClaimDeviceName, the same dialog as Remote control’s (§6.6b), logged share.offer.named {named}; a cancel or a refusal leaves the offer unpublished. Offers the supplier publishes at start from saved settings are not asked about: a device may lend without a name, and has no connector address then.
9.8 Prose editing
Section titled “9.8 Prose editing”The shared form name, aliases and fitting prose path boxes, a project’s logline, synopsis and rating, a character’s summary, a script block’s dialogue, direction and action, and every prompt’s boxes, a block’s frame prompts included, draw ProseEditor; so do the boxes a row or a dialog owns in its own state — a relationship’s relation, every garment box, an epoch’s label and the new epoch’s and a kept voice’s name — through LocalProse and localProseBox, with the microphone after them and their committed source sampled as a form field’s is. The synopsis carries the microphone in the editor’s suffix and no assistant control of its own: the dock is the assistant’s one place (§9.3). A treatment’s five primary leaves are prose inside their EditableComboBox, through its fieldBuilder: the microphone and then the arrow after the words, a suggestion filling the box as typing does, and Down going into the editor’s chooser while one is open — a typed mention’s or a chip’s replacement — else opening the suggestions (fieldTakesDown, fed by the editor’s onChooserOpen). Beside a consumer’s identifier and constraints, ProseEditor takes autofocus, onSubmitted (reporting canonical source) and onChooserOpen (whether one of its choosers is open, as it opens and closes). Plug-in text (the entity’s plugins. keys and a plug-in’s own page) and the version label stay plain until the plug-ins can declare which strings are prose. Only drawing that editor upgrades its seeded ordinary controller; subsequent form lookups retain the upgraded controller. Programmatic seeds, agent assignments, external patches and copies use canonical source without reporting typing. Agent setters publish the resulting seeded canonical source through existing validation and draft setters, including removal of foreign shown placeholders. The form’s input mode controls read-only state and cancels pending edits before a mode change is rendered. Literal paths (default_outfit, voice.lang, areas, geography.coordinates, visual_style.palette and plugins.), numbers, the door’s suggestion boxes and token controls stay plain. A path of another shape retains its locked presentation and original value.
The project’s mention source offers character, location and prop groups and only declared epochs, using their canonical tags. It offers no entity in the viewer’s recycle bin — deleted at every epoch it has (isGroupDeleted, §5.4) —, and neither do a scene’s Location pull-down and its Add character list (sceneLocationChoices, sceneCharacterChoices), while a location the scene already names that is deleted is shown as written and not called unknown; nor does a location’s Located within (withinChoices), where a deleted location the file already names is said as one in the recycle bin (withinDeleted), never as a tag no location has. Names use the contextual version selected by the project’s existing resolution rules; a picture comes only from that version’s declared preview through the existing media and thumbnail helpers. An unresolved version keeps its chooser membership with the kind icon. Viewer changes and project re-reads refresh candidates without changing source, controller identity or a captured range: a form holds one candidate source, updated in place; when it takes another film or viewer the form compares each candidate’s target, name and picture with the list its boxes show, and its revision changes only when the list does (form.candidates.changed or form.candidates.unchanged {stem, revision, viewer, candidates, differences, reason, boxes, ms}, each difference a target added, removed or changed with its old and new name and picture); a scene’s script, sharing it, re-projects every box it has made at that revision and at no other build (script.candidates.refreshed {stem, was, revision, boxes, ms}). A script placeholder carries only committed field snapshots when its first words or Add block make a block; a provisional span never allocates a block or enters that change.
filmopen_text_ui separates the field’s shown text from its saved source. ProseController.value.text is the shown text, where each collapsed mention is one U+FFFC code unit. source retains the original reference spelling and all other text. The controller uses ReferenceCodec to recognise references, and one injected MentionCandidates source supplies both the chooser candidates and the answer to whether a target is known. Candidate pictures are MediaLocation values, independent of project state. Each rendered prose consumer registers its committed canonical source for recorder sampling, replacing its registration when its identifier or controller changes and releasing only its own registration on removal. Native and agent display semantics retain shown text. The live-dictation value registry retains a field’s last safe recorder sample, or its canonical committed baseline before the first sample, excluding any captured provisional span. Provisional semantics cannot replace that sample; live field identifiers and caption identifiers are refused as tape values. Caption-derived labels and tooltips from pointer or wheel hits are also omitted, including hits named only as within the caption. A hit within a live field or its dotted derived controls omits source-bearing labels and tooltips when captured, so delayed gesture publication stays private after cancellation; the tape also refuses live derived values and metadata while the guard remains. Their gesture coordinates and deltas remain, as do ordinary control labels. A guard is released only after the committed or restored value reaches semantics.
The controller owns the stable EditableTextTarget adapter exposed as controller.target. Its selection and captured selections are UTF-16 offsets in the shown text. The controller maps those boundaries, the caret and the composing region into source coordinates before comparing them with reference tokens or inserting text. Each chip has two editable boundaries and no editable interior. Captures retain their original source range when the caret moves. Partial updates replace the same opaque provisional span; a final commit or cancellation happens once. External edits, a switch of source and a read-only transition invalidate outstanding captures. Cancellation restores the original source and its collapsed or deferred mention projection. Rollback preserves selection direction and keeps a collapsed caret collapsed; a caret inside the replaced span maps to its restored end boundary. Disposal locks the target and invalidates captures without publishing another value. The consumer sets the controller’s readOnly property as well as the field’s read-only state. ControllerDictationTarget adapts a plain controller without writing provisional words into its text. It captures the range, advances its generation on external text changes rather than selection movement, and commits through the box’s formatters, length limit and callback. Disposing the adapter invalidates its captures and detaches its listener.
The optional pure acceptsEdit(source, shown) check validates complete proposed source and shown values before an edit changes state or notifies listeners. It can combine a form’s source validation with its shown-text constraints. A refused partial retains the last accepted span; a refused final restores its original selected source because the final handle is already sealed. A throwing final validator releases its owned provisional span and restores the original before rethrowing; a throwing partial leaves the accepted span usable. Loading existing source bypasses edit validation so an invalid saved value is not silently truncated. Rich source drops unbacked U+FFFC on construction and source switch, preserving every other byte. Literal fields retain reference and bracket syntax as plain text, including U+FFFC, and apply no reference projection. Their existing single-line, shown-text length and source-validation rules still govern edits; loaded source remains untouched.
Every incoming value passes through the controller’s value setter. The controller records the source and offset map for each emitted shown text, keyed by text alone, retaining the most recent 256 records. Re-emitting a text makes its record newest; the oldest record is evicted when the bound is exceeded. A matching incoming text reuses its record only when every placeholder that survives outside the edited range has the same chip source spelling in that record as it does in the current value. The incoming selection and composing region are retained. Otherwise the unmatched path keeps current prefix and suffix chips and removes foreign placeholders in the inserted rich text. A restoration beyond the retained records uses that same path and cannot reconstruct a removed chip whose record was evicted. These records support incoming shown-text values and foreign-placeholder handling; rich-field Undo restores independent canonical source snapshots.
Rich-field Undo and Redo restore complete canonical source transactions, including a replacement whose shown chip placeholder stays the same. Consecutive insertions and deletions at a moving caret undo as a typing burst within a fixed 500 ms window from its first edit, bounded to the latest 256 transactions. Replacements, the chooser, paste, cut and dictation’s final are steps of their own. A caret jump, a field command or an adjustment of a reference separator ends the burst. Selection-only changes and captured partials add none; a final captured insertion adds one original-to-final transaction and cancellation adds none. A new accepted source change clears Redo, while a source reset clears both stacks. Replay is checked against current field validation before either stack moves; refusal leaves source, selection and history unchanged. Keyboard Undo and Redo, the optional UndoHistoryController and the focused iOS native Undo client dispatch through the same source history. An outgoing editor removes only its own listeners and releases native ownership only if it still owns that client. Literal fields keep the SDK’s ordinary history and batching. A foreign placeholder input that exactly matches a retained shown text restores that record when surviving chips agree, including a paste into an empty field; other foreign placeholders are stripped. Shown-text records remain bounded to the latest 256 texts, and re-emission makes a record newest. This lookup does not decide rich source Undo.
Unknown shorthand references remain plain text. Canonical unknown references collapse into unresolved mentions. Known shorthand references retain their exact spelling through unrelated edits; their own replacement writes a canonical reference. A reference being typed remains text while its source range contains the mapped caret or overlaps the mapped composing region, and collapses when the caret leaves it or a following character ends the token. Already collapsed chips remain atomic as the caret moves across their boundaries while their complete source bounds and spelling remain unchanged. Every source edit preserves each surviving collapsed chip’s target and exact codec range. The source-proposal door inserts one space before new characters that would continue its id, including letters, digits, hyphens and a dot followed by those characters. A terminal dot already in the prose stays before the inserted space. A deletion whose only effect would join a chip to following id characters keeps one space. Collapsed Backspace on that space crosses left; Delete before it crosses right, without removing text. Selected separator replacement keeps the space and ends after it. Deletion that also removes the following characters or the chip proceeds. Selection and composing offsets follow the inserted separator. Each captured partial or final rebuilds from its original source snapshot and captured bounds, then records separator insertions for rollback mapping. Revisions never consume original text shifted by an earlier separator. Cancellation restores the exact loaded snapshot and maps selection through the added separators and replaced span, without committing normalization. The rule applies before projection and validation to typing, paste, cut, selection replacement, composition, captured insertion and chooser actions. A field formatter cannot remove a required separator. Undo and redo restore valid earlier source states. Plain composing text keeps its underline when another mention is drawn as a chip.
ProseEditor takes the consumer’s field identifier, decoration, focus node, text style and line counts. It preserves the consumer’s tap-region group and focus-request rule. A consumer that already wraps its field identifier disables the editor’s own semantics wrapper. Its editable suffix follows any existing decoration suffix, supplies one microphone and disappears in read-only mode. It reports canonical source changes, including a chip’s source-only replacement. While the controller has a provisional span, the editor neither reports a change nor advances its last committed source. A final commit reports the changed source once, including when it equals the last partial; cancellation reports no change. A source-epoch reset synchronizes the editor without reporting a user edit. It resets the actual field undo subtree when the controller or sourceEpoch changes. A same-target source reset retains the suffix control’s state, including a binding refusal, while replacing that undo subtree. Read-only mode or editor removal disposes the suffix normally. A replacement editor retains ownership of its chip renderer and inherits only the original consumer validation rule; an outgoing editor neither clears the new renderer nor leaves its obsolete field constraints attached. The field’s length limit counts shown-text graphemes, with a chip counting once; the consumer’s source acceptance check counts saved source as its validation requires. The editor preserves the field’s single-line filter and other input formatters. Typing and paste prepare the source and its shown projection, run the field formatters and length limit on that shown proposal, then publish only the final accepted source and map; a programmatic edit or chooser action that they would transform is refused before publication. Loading source is never silently truncated by these edit constraints. If a formatter changes text on both sides of a chip, the prepared map preserves the chip’s exact source spelling. Unchanged prefix and suffix chips retain their positions; placeholders in the transformed middle reuse the remaining prepared chips in order, with surplus placeholders removed. The latest retained record takes precedence only when its surviving chips agree, and the editor does not infer which identical chip an ambiguous formatter deletion intended.
A query starts at @ only at the start of text or after a character that is no letter, combining mark or digit of any script and no underscore — the whole character, a supplementary one read from its surrogate pair — so an at sign inside an e-mail address, a handle or a word such as café@b starts none. The menu opens only when candidates match the query or a chip replacement is open, anchored to the field and constrained to its width. Its controls share the consumer’s editing tap group, so using a field’s own chooser keeps an open script block and returns focus to that field; a genuine outside click retains the block’s normal departure rule. Its groups are Media, Characters, Locations, Epochs and Props, Media first in their source’s order and the others with folded alphabetical display names and stable-id ties within each group. A repeated target uses its first candidate, matching membership lookup before filtering and keeping control identifiers unique. A case-insensitive substring of the display name filters the entries, with no kind-prefix filter. The query extends over letters, including uppercase letters, digits and hyphens, and dots between nonempty segments. Uppercase letters are folded for matching. Whitespace, _ and any other character end the query; @ch_ closes the chooser so a canonical reference can be typed unaided. Query detection uses its own grammar and maps the shown caret into source coordinates. Down or Alt+Down enters the first eligible menu control; Tab also enters an open chooser. Menu navigation leaves the field selection unchanged, Enter applies the choice and Escape closes the menu and returns to typing.
A prompt’s @ offers media (PromptMentionCandidates, filmopen_prompt_ui/prompt_mentions.dart): a prompt’s boxes — on the forms, and the render dialog’s prompt at its card, which reads the media again from the page’s draft as it changes, so that its chooser, the writer’s tokens, the kind and the request name the same pictures (render.prompt.media {from, to, media, reason: draftChanged} (media each token and the picture it names)) — take a source of their own, the media promptTokenCandidates gives for the prompt’s writer kind (§5), where every other prose box offers the film’s entities. On an entity’s own prompts the entries are named inspiration 1, reference 1; in a block’s frame and clip prompts every character’s, location’s, outfit’s and prop’s reference images and sheets — Kira Voss · reference 1, Kira Voss · sheet 1, Oilskin · sheet 1 —, each with its thumbnail, or the picture itself where it is a local image, else the stock icon. The voice’s and the treatment’s boxes offer none, so their @ opens nothing; nor does any negative box — an entity’s, a block’s frame’s, the render dialog’s —, a plain box taking its words literally, since media tokens are the positive prompt’s only (Project Specification §9.0) and a render resolves the positive alone. Where a query matches nothing, a prompt that names pictures — a *-reference, a *-sheet, a frame’s, a clip’s — keeps its chooser open on one disabled line (MentionCandidates.emptyHint, <field>.chooser.empty; not a candidate, it inserts nothing): No picture to name when nothing matches, while retained unreviewed pictures are offered just like kept ones; the voice’s and the treatment’s say nothing, and every other prose box’s chooser stays shut as before. The line is asked once per opening and logged mention.empty {kind, query, reason: none, ms}. A form’s prompt sources read the draft as the saver holds it — its snapshot, at a box’s creation and at each build of the form — not the file as last read, so a picture kept a moment ago is offered before it is written. The form holds one source per prompt kind (FormScope.promptCandidates, logged form.prompt.candidates {stem, kind, sources} when made): its own boxes and a scene’s frame and clip prompts in the script (ScriptEditor.promptCandidates) take it there, and a change of a source’s list is logged form.prompt.candidates.changed {stem, kinds, revision, ms} and counted in the revision the script’s boxes re-project on; moving to another file drops the sources no box of the form holds (form.prompt.candidates.dropped {from, stem, kinds, kept}). A prompt’s source still knows the film’s characters, locations, props and epochs, so a reference already in its words keeps its chip, but offers only media. A chosen entry writes its token (@reference-1, @ch_kira:character_sheet-1), never the name, and shows as the chip of its name and picture; a media token that names nothing — a number past the group’s entries, a stem not in the film — is the unresolved chip with Repair and Remove; promptInputsOf (§5) reports it among the tokens that name nothing. A frame’s token typed by hand stays text while the caret is anywhere in what continues its entity after a : (lowercase letters, digits, _, :, -, .), so @ch_kira:character_sheet-1 typed a character at a time is saved exactly so and becomes one chip as the caret leaves; an entity typed alone, or followed by a colon and a space, collapses as before. Building a prompt’s source is logged as prompt.mentions (its kind, bound or changed, the targets offered, their count, each entry’s reference, unreviewed and favorite flags, and the milliseconds); where its film, viewer, language or file is new — the file being the draft read again, on every change of a comparison — a list a box has read is built again and compared as the form compares the film’s (shownMentions: each target, name and picture), an equal one kept, unlogged, and no box re-projected, and a list nothing has read yet is not built for it; a new file whose media — its own and its blocks’ — are those of the last builds nothing for an entity’s own prompts, which read only them. A prompt’s chips look a picture up in its list by target. The film’s candidates (ProjectMentionCandidates) are listed once per project and viewer, pictures resolved, and logged as mention.candidates (the prompt kind whose source holds it, where one does, the viewer, the targets, their count and the milliseconds); every chip and projection looks a target up in that list by its target; the editor reports its chooser’s actions through the source’s report, which the forms write to the log, each naming its box (box, the field’s identifier): prose.mention.opened (the field kind, prompt or prose, how it opened, what was typed after @ or the chip being replaced, the targets offered with their count, and the milliseconds the build took to list them), prose.mention.chosen (the reference’s kind, insert or replace, what was typed after @ or the chip replaced, the target chosen and its name, inserted or refused with the reason — read_only, no_query, stale, unrepresentable, field_refused —, its place in the source, the source as it left it, and the milliseconds), prose.mention.repaired (the kind, the token replaced and the target) and prose.mention.removed (the kind, the token removed, removed or refused with the reason — read_only, stale, field_refused —, the source as it left it and the milliseconds). The chooser lists its entries once a build, and that list is what it draws, opens on and reports; a chip is built with its place and its bold from the span’s one pass, and an edit’s chip protection parses the proposed text once, again only after a separator it adds.
A chosen candidate becomes an atomic @Name chip. A choice at the end of source includes one trailing space in its accepted edit. The very next typed period, comma, semicolon, colon, exclamation mark, question mark, closing parenthesis, closing bracket or quotation mark replaces that chooser-created space as a separate transaction when the caret has not moved. A letter, digit, space, another edit or an intervening command ends this opportunity; Undo does not revive it. A terminal dot remains plain, and continuing id characters typed after it still receive the required separator. In exact-source fields a trailing space left by stopping after the chip remains as entered. A choice before a character that would continue the id includes one separating space; existing whitespace or punctuation that already separates the token is kept. Both the menu and chip draw its resolved picture through the shared media provider, or the kind’s icon when there is no usable picture. A display-name change changes no saved reference id; refreshing candidates updates the projection and name while retaining captured source ranges. Clicking a known chip opens replacement. An unresolved canonical chip displays its source and an unresolved tooltip, and offers Repair and Remove. Repair opens explicit candidates without guessing a replacement. Its chosen entry replaces the whole unresolved token range; choosing another entry on a resolved chip does the same. Each accepted action resolves to the chosen target, preserves all other prose except its required right-edge separator and is one source Undo transaction. Selection-only reprojection rebinds a rendered token by its owner, source epoch, exact range and spelling; a departed token cannot act on another source. A candidate whose canonical encoding does not round-trip to its complete target is refused without changing source, selection or history. Remove deletes the whole token. Backspace and Delete also remove the atomic token.
The field preserves deliberate caret and range selections on focus, including a return from the mention menu. A refused chooser action leaves the menu and selection unchanged. The field keeps its consumer-owned semantics identifier. Its chooser entries derive <field>.chooser.<kind>_<id>, its shown-order chips derive <field>.chip.<n>, and an unresolved chip’s actions derive <field>.chip.<n>.repair and <field>.chip.<n>.remove. Each chip supplies its name or unresolved canonical source as the spoken label. Identifiers remain unique when two fields occur on the same page.
Keyboard and context-menu Copy and Cut carry canonical reference source, never the shown placeholders. Fully selected known shorthand references are encoded canonically without changing the stored spelling; partial raw-token selections retain their selected text. Cut copies only the explicitly selected source and applies paired-delimiter deletion to its complete accepted removal. It removes the selected source only after the clipboard write completes and the same controller, generation and shown value remain current. Paste similarly checks the target after its asynchronous clipboard read, applies existing field constraints and enters through the controller’s ordinary value door, placing the caret after the inserted text. A departed source, replaced chip, switched controller or disposed editor cannot receive a late clipboard edit. Clipboard platform failures leave source untouched. Each copy, cut and paste is reported through the source’s report as prose.clipboard {box, op: copy | cut | paste, start, end, text, outcome: copied | cut | pasted | stale | empty | refused | failed, reason, error, source, ms} — start and end the selection in the shown text, a refusal’s reason read_only or no_selection, error the platform’s message, source the field’s source as the action left it, which shows a field’s limit refusing or trimming it; a box made without candidates — a negative prompt’s — reports nothing and says nothing of a failure. Literal-mode clipboard text remains verbatim. The consumer configures source acceptance before attaching the editor; detachment restores it only if the editor still owns the installed wrapper.
The field keeps ordinary text navigation inside and across bracket pairs. Left and Right move one visible grapheme, crossing a chip atomically and stepping before an opening delimiter or after a closing delimiter. Shift extends selection with its anchor retained. Home and End keep the platform’s ordinary field behavior. Typing either [ or ] wraps a nonempty selection in a pair, or opens an empty pair with the caret inside. The caret after wrapping is immediately before the new closing delimiter. Typing ] at a closing delimiter matched to a balanced unescaped pair moves the caret past it without changing text, running text constraints, invalidating captures or adding an undo entry. Nested pairs are balanced by a stack. Deleting either delimiter of a balanced pair removes its matching delimiter in the same edit, preserving all contents except those explicitly selected for deletion. Exact surviving shown positions retain each chip’s original source spelling and identity, including when a selected range deletes another chip. A bracket action over a provisional span uses its restored source and mapped selection, invalidating the old capture without publishing an intermediate rollback.
Bracket actions prepare their whole source and shown proposal before the consumer’s formatter and length checks. A constraint that changes or truncates that proposal refuses the complete action; refused edits publish no source, map or history record. Rich-field Undo and Redo restore the complete accepted source transaction without reinterpreting restored brackets. Owned typed brackets and paired deletion apply their rules even when the raw proposal matches an earlier shown-text record. A fresh typed delimiter does not inherit an old unmatched spelling, and deleting a paired delimiter still removes its mate. Generic incoming values and clipboard paste retain agreeing-record precedence; rich source replay restores its exact snapshot without bracket repair. An odd run of preceding backslashes makes \[ and \] literal. Clipboard paste, loaded source and active or finishing input-method composition never repair unmatched text. Balanced unescaped pairs alone determine bold styling; nested interiors include inner delimiters, while outer delimiters stay plain. Bold text and chip labels remain editable, retain the base text style and combine with composing underline. Literal fields have neither pairing nor bracket styling.