M8: branch history: entries for a branch's creation and updates from main

A branch's version history starts with "Branch created" (the branch as it
began) and gets "Updated from main" each time it takes main's changes (the
branch right after, on its tab or closed). They are versions like any
other: viewed, restored, saved with the file, never pruned as autosaves,
and they stay out of the main file's history.
This commit is contained in:
bflatastic 2026-10-13 10:02:45 +07:00
parent 47d37167d8
commit aeebf7260d
8 changed files with 141 additions and 15 deletions

View File

@ -0,0 +1,98 @@
//! A branch's history says where it came from: it starts with an entry for its creation and gets
//! one each time it is updated from main, on its tab or closed; the entries are versions like any
//! other (viewed, restored, saved with the file) and stay out of the main file's history.
use figcraft_doc::{NodeId, NodeKind};
use serde_json::{Value, json};
use crate::Session;
use crate::branches::{CREATED, UPDATED};
use crate::collab_tests::base;
fn ok(s: &mut Session, cmd: &str, p: Value) -> Value {
s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd} {p}: {e}"))
}
fn names(s: &mut Session) -> Vec<String> {
ok(s, "version.list", json!({})).as_array().unwrap().iter().map(|v| v["name"].as_str().unwrap().to_string()).collect()
}
fn layer_name(s: &Session, id: NodeId) -> String {
s.doc().unwrap().node(id).unwrap().name.clone()
}
/// The base file with a branch "Explore" shown: (session, branch id, a rectangle).
fn branched() -> (Session, String, NodeId) {
let mut s = Session::with_document(base());
let r = s.doc().unwrap().nodes().find(|n| matches!(n.kind, NodeKind::Rectangle)).unwrap().id;
let id = ok(&mut s, "branch.create", json!({"name": "Explore"}))["id"].as_str().unwrap().to_string();
(s, id, r)
}
#[test]
fn a_branch_history_starts_with_its_creation_and_notes_each_update_from_main() {
let (mut s, id, r) = branched();
let original = layer_name(&s, r);
assert_eq!(names(&mut s), [CREATED]);
let created = ok(&mut s, "version.list", json!({}))[0].clone();
assert!(
created.get("auto").is_none() && created["time"].as_u64().is_some_and(|t| t > 0),
"kept like a named version, never pruned as an autosave"
);
// Main moves on; the branch takes it: an entry of its own, the branch as it was right after.
ok(&mut s, "node.set", json!({"id": r, "props": {"name": "On the branch"}}));
ok(&mut s, "doc.switch", json!({"index": 0}));
assert!(names(&mut s).is_empty(), "the main file's history has none of it");
ok(&mut s, "shape.rectangle", json!({"x": 900, "y": 900, "width": 10, "height": 10, "props": {"name": "From main"}}));
ok(&mut s, "doc.switch", json!({"index": 1}));
assert_eq!(ok(&mut s, "branch.updateFromMain", json!({}))["updated"], true);
assert_eq!(names(&mut s), [CREATED, UPDATED]);
// Again, after a version saved by hand: in order.
ok(&mut s, "file.saveVersion", json!({"name": "Mine"}));
ok(&mut s, "doc.switch", json!({"index": 0}));
ok(&mut s, "shape.ellipse", json!({"x": 950, "y": 900, "width": 10, "height": 10}));
ok(&mut s, "doc.switch", json!({"index": 1}));
ok(&mut s, "branch.updateFromMain", json!({}));
assert_eq!(names(&mut s), [CREATED, UPDATED, "Mine", UPDATED]);
// They are versions like any other: the creation shows the branch as it began, and restoring
// it is one undo step.
let first = created["id"].clone();
ok(&mut s, "version.restore", json!({"id": first}));
assert_eq!(layer_name(&s, r), original);
assert!(!s.doc().unwrap().nodes().any(|n| n.name == "From main"));
ok(&mut s, "edit.undo", json!({}));
assert_eq!(layer_name(&s, r), "On the branch");
assert!(s.doc().unwrap().nodes().any(|n| n.name == "From main"));
// The update's entry is the branch with main's change in it.
let update = ok(&mut s, "version.list", json!({}))[1]["id"].clone();
ok(&mut s, "version.preview", json!({"id": update}));
assert!(s.doc().unwrap().nodes().any(|n| n.name == "From main") && layer_name(&s, r) == "On the branch");
ok(&mut s, "version.exitPreview", json!({}));
let _ = id;
}
#[test]
fn a_closed_branch_gets_its_entry_too_and_the_history_saves_with_the_file() {
let dir = std::env::temp_dir().join(format!("figcraft-branch-history-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("file.figcraft").to_string_lossy().to_string();
let (mut s, id, _) = branched();
// Its tab closes; main changes; the branch is updated from the main file's tab.
ok(&mut s, "file.close", json!({"index": 1, "force": true}));
ok(&mut s, "shape.rectangle", json!({"x": 900, "y": 900, "width": 10, "height": 10, "props": {"name": "From main"}}));
assert_eq!(ok(&mut s, "branch.updateFromMain", json!({"id": id}))["updated"], true);
assert!(names(&mut s).is_empty(), "nothing in the main file's history");
ok(&mut s, "file.saveAs", json!({"path": path}));
// Opened again from the file: the branch's history has both entries.
let mut t = Session::empty();
ok(&mut t, "file.open", json!({"path": path}));
ok(&mut t, "branch.open", json!({"id": id}));
assert_eq!(names(&mut t), [CREATED, UPDATED]);
assert!(t.doc().unwrap().nodes().any(|n| n.name == "From main"));
// Merging leaves them with the branch; main gets its own entry for the merge.
ok(&mut t, "branch.merge", json!({}));
assert_eq!(names(&mut t).last().map(String::as_str), Some("Merged “Explore”"));
ok(&mut t, "branch.open", json!({"id": id}));
assert_eq!(names(&mut t), [CREATED, UPDATED]);
std::fs::remove_dir_all(&dir).ok();
}

View File

@ -35,12 +35,12 @@ fn branched() -> (Session, String, NodeId) {
fn versions_saved_on_a_branch_are_the_branchs_own() {
let (mut s, _, r) = branched();
let original = layer_name(&s, r);
// A new branch's history starts empty: the main file's versions stay with it.
assert!(names(&mut s).is_empty());
// A new branch's history starts with its creation: the main file's versions stay with it.
assert_eq!(names(&mut s), ["Branch created"]);
ok(&mut s, "node.set", json!({"id": r, "props": {"name": "Before"}}));
let v = ok(&mut s, "file.saveVersion", json!({"name": "Branch v1"}))["id"].clone();
ok(&mut s, "node.set", json!({"id": r, "props": {"name": "After"}}));
assert_eq!(names(&mut s), ["Branch v1"]);
assert_eq!(names(&mut s), ["Branch created", "Branch v1"]);
// Viewing a branch version shows it read-only; going back shows the branch as it is.
ok(&mut s, "version.preview", json!({"id": v}));
assert_eq!(layer_name(&s, r), "Before");
@ -77,13 +77,13 @@ fn branch_versions_save_with_the_file_and_survive_merging() {
ok(&mut t, "file.open", json!({"path": path}));
assert_eq!(names(&mut t), ["Main v1"]);
ok(&mut t, "branch.open", json!({"id": id}));
assert_eq!(names(&mut t), ["Branch v1", "Autosave"]);
assert_eq!(names(&mut t), ["Branch created", "Branch v1", "Autosave"]);
// Merging adds a version to the main file's history; the branch keeps its own.
ok(&mut t, "branch.merge", json!({}));
assert_eq!(names(&mut t), ["Main v1", "Autosave", "Merged “Explore”"]);
assert_eq!(layer_name(&t, r), "On the branch, later");
ok(&mut t, "branch.open", json!({"id": id}));
assert_eq!(names(&mut t), ["Branch v1", "Autosave"]);
assert_eq!(names(&mut t), ["Branch created", "Branch v1", "Autosave"]);
std::fs::remove_dir_all(&dir).ok();
}
@ -94,7 +94,7 @@ fn archiving_keeps_the_branch_history() {
ok(&mut s, "branch.archive", json!({}));
assert_eq!(names(&mut s), ["Main v1"]);
ok(&mut s, "branch.open", json!({"id": id}));
assert_eq!(names(&mut s), ["Branch v1"]);
assert_eq!(names(&mut s), ["Branch created", "Branch v1"]);
}
#[test]

View File

@ -244,9 +244,18 @@ fn create(s: &mut Session, p: &Value) -> Result<Value> {
versions: Vec::new(),
});
let index = open_tab(s, uid, &id)?;
// The branch's own history starts with how it was made.
if let Some(tab) = s.docs.get_mut(index) {
crate::cmd::file::add_version(tab, CREATED, "", false);
}
Ok(json!({"id": id, "name": name, "index": index}))
}
/// The entries a branch's version history gets by itself: when it was made, and each time it took
/// main's changes (the branch as it was right after).
pub const CREATED: &str = "Branch created";
pub const UPDATED: &str = "Updated from main";
/// Open branch `id` of the file in the tab with uid `main` in a tab, or switch to its tab.
fn open_tab(s: &mut Session, main: u64, id: &str) -> Result<usize> {
if let Some(i) = tab_of(s, main, id) {
@ -357,15 +366,23 @@ fn combine(s: &mut Session, p: &Value, from: Side) -> Result<Value> {
out["merged"] = json!(true);
out["index"] = json!(s.active);
} else {
if let Some(t) = tab_of(s, main_uid, &id).and_then(|t| s.docs.get_mut(t)) {
let tab = tab_of(s, main_uid, &id).and_then(|t| s.docs.get_mut(t));
let open = tab.is_some();
if let Some(t) = tab {
show(t, doc.clone());
// Undo can't step back past the new base: that would undo main's changes on the branch.
t.history.undo.clear();
t.history.redo.clear();
// In the branch's history (its tab's, while it is open).
crate::cmd::file::add_version(t, UPDATED, "", false);
}
if let Some(b) = s.docs.get_mut(mi).and_then(|m| m.branches_mut().get_mut(bi)) {
b.base = main;
b.head = doc;
b.head = doc.clone();
if !open && b.versions.len() < figcraft_format::MAX_VERSIONS {
let id = crate::cmd::file::unique_id(|id| b.versions.iter().any(|v| v.id == id));
b.versions.push(figcraft_format::Version::new(id, UPDATED, "", crate::cmd::comment::now_secs(), doc));
}
}
out["updated"] = json!(true);
}

View File

@ -547,7 +547,11 @@ fn branches_and_the_version_history() {
// On the branch tab.
// A branch keeps its own version history: a version saved on its tab is listed there only.
ex(&mut s, "file.saveVersion", json!({"name": "On the branch"}));
assert_eq!(versions(&mut s).iter().map(|v| v["name"].clone()).collect::<Vec<_>>(), [json!("On the branch")], "a branch's own history");
assert_eq!(
versions(&mut s).iter().map(|v| v["name"].clone()).collect::<Vec<_>>(),
[json!("Branch created"), json!("On the branch")],
"a branch's own history"
);
rect(&mut s, 900.0, 900.0, None);
ex(&mut s, "doc.switch", json!({"index": 0}));
let made_on_main = rect(&mut s, -500.0, 0.0, None);

View File

@ -1357,6 +1357,8 @@ mod binding_lists_tests;
#[cfg(test)]
mod booleans_tests;
#[cfg(test)]
mod branch_history_tests;
#[cfg(test)]
mod branch_tests;
#[cfg(test)]
mod branch_versions_tests;

View File

@ -1,6 +1,6 @@
# Collaboration parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (live sessions per document; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; cursor chat and spotlight in live sessions; branch version history and finer list merges; comments and version history behaviour suite and fixes; hosting the server: `wss://` in the desktop app, server releases, deployment guide; team libraries on the server and swap library; branches: create, update from main, review and merge; wording: the standalone server with share links to edit or view; numbers unchanged) · **Target:** Figma Design (desktop app 126.10.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (branch history entries for a branch's creation and updates from main; live sessions per document; a public figcraft-server: open hosting, owner tokens, per-address limits, idle expiry, moderation, install.sh; cursor chat and spotlight in live sessions; branch version history and finer list merges; comments and version history behaviour suite and fixes; hosting the server: `wss://` in the desktop app, server releases, deployment guide; team libraries on the server and swap library; branches: create, update from main, review and merge; wording: the standalone server with share links to edit or view; numbers unchanged) · **Target:** Figma Design (desktop app 126.10.7)
Figma is multiplayer first; FigCraft is local first. **Ready ~13% (estimated):** comments, version
history and branches (review and merge) work inside one file; the real-time collaboration core landed in #103 (sync model,
@ -18,7 +18,7 @@ cursors, selections, text carets, cursor chat and spotlights shown live ([`colla
| Share links, permissions (view / edit), teams and projects | yes | **partial**: sharing with a join code from the Share popover (online: the file goes to the server in Preferences and the server names its edit and view links with short codes; local network: the app hosts and the code carries its address; one field joins with a code or a link, in the popover and on the home screen); `figcraft-server` share links per file with edit or view access (a viewer sees changes and presence, a "View only" badge and only the moving tools; the server refuses its edits), revocable at once, tokens stored as hashes; managed from the app's Share popover (first links), the server's command line or a managing connection. On a server open to the public, whoever published a file manages it with its owner token (links, name, removal: `collab.manageFile`), which the app remembers: Home ▸ Online lists your online files (open again as owner, start and stop sharing, copy the code, rename, remove, forget), with one request per server for what became of them; anyone in a file can report it, and the operator blocks addresses. The list is per computer: no accounts, teams or projects | 8–17 |
| Comments: pins on frames and layers, threads, reactions, @mentions, resolve, unread, search and filters, links, in the presenter | yes (shared) | **done, local**: stored in the file, your name from Preferences; a behaviour suite from the documentation (pinning and following layers, threads, own edits and deletes, mentions, reactions, C and Shift+C, stepping, filters, saving, copy and paste); restoring a version keeps them. In a live session threads sync and anyone in it can reply, react and resolve (no accounts: a name is who you are) | (shared once multiplayer lands) |
| Version history: named versions, autosave checkpoints, view before restore, restore, duplicate | yes (cloud) | **done**, saved in the `.figcraft` file: restoring keeps the replaced design as an autosave first, a titled autosave becomes a named version, merging a branch records the file before and after. Versions are per person in a live session (a shared history needs accounts); no comparing two versions (Dev Mode compares a ready-for-dev layer with its state when marked) | — |
| Branching and merging | yes (paid plans) | **done, local**: branches saved in the `.figcraft` file, each opened in its own tab; Review and merge shows both sides' changes since the branch was made and the conflicts (the same property on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merge as one undo step (the branch is archived, the main file's history gets a version for the merge), update from main, archive. Each branch keeps its own version history (saved, viewed and restored on the branch tab, kept when it is merged or archived); fills, strokes and effects merge item by item. Left: branches in live sessions; other lists (text runs, export settings) conflict as a whole | 1–2 |
| Branching and merging | yes (paid plans) | **done, local**: branches saved in the `.figcraft` file, each opened in its own tab; Review and merge shows both sides' changes since the branch was made and the conflicts (the same property on both sides, or a layer deleted on one side and edited on the other), each resolved for the branch or main; merge as one undo step (the branch is archived, the main file's history gets a version for the merge), update from main, archive. Each branch keeps its own version history (saved, viewed and restored on the branch tab, kept when it is merged or archived), which starts with an entry for its creation and gets one each time it is updated from main; fills, strokes and effects merge item by item. Left: branches in live sessions; other lists (text runs, export settings) conflict as a whole | 1–2 |
| Team / organization libraries | yes | **partial**: a file's library is published to `figcraft-server` under the file and enabled in other files by a share link (per-asset versions, background update checks, overrides kept), usage counts per component and file, swapping a library by names ([components-parity](components-parity.md), [server.md](server.md#team-libraries)). No team-wide library list, accounts or analytics over time | 4–8 |
| Notifications, activity | yes | no | with sharing |
@ -26,6 +26,7 @@ cursors, selections, text carets, cursor chat and spotlights shown live ([`colla
| Date | Change | Summary |
|---|---|---|
| 2026-10-13 | minor | Branch history entries (gap 17): a branch's version history starts with Branch created (the branch as it began) and gets Updated from main each time it takes main's changes (the branch right after, also when its tab is closed); they are versions like any other (viewed, restored, saved with the file, never pruned as autosaves) and stay out of the main file's history. Numbers unchanged |
| 2026-10-12 | minor | Your online files (M9): a file put on a server without its key stays its owner's after its tab closes or the app quits. The app records each one with the preferences (server, id, name, owner token, the owner's links, the codes others join with; at most 200; the token never in a command result, `ui.inspect`, a log or debug output) and Home ▸ **Online** lists them: open one as its owner (a new tab, or its tab comes to the front), Start / Stop sharing, Copy code / Copy link, Rename on the server, Remove from server (confirmed), Forget (the file stays on the server). Nothing is asked of a server until the section opens; then one request per server (`Admin owned`, a compatible addition within protocol version 2) says which files are still there, and a server that is offline or older leaves the list as recorded. Tabs of files in a live session carry a mark (shared, or open only to you), toasts about a file that isn't in front name it, and after a crash a recorded file reopens from its server as its owner, its recovery copy kept until its unsent changes are back on the server (or it is opened beside the file). `collab.files`, `collab.openFile`, `collab.forgetFile`; `collab.manageFile` takes its server and token from the record and gains `share` / `unshare`. Files are known per computer, not per person (no accounts): percentages and hours unchanged |
| 2026-10-12 | minor | Sharing with a join code (M9, from the owner's use: sharing wasn't intuitive): the Share popover is one flow, **Online** (the file is published to the server in Preferences, built in `wss://collab.craftapps.dev/`, and the server names its edit and view links with nine-symbol codes like `K7M-2QX-9TD`) or **Local network** (the app hosts; a 20-symbol code carries its IPv4 address, port and a 44-bit secret, with check bits), one Start sharing button, the code large with Copy code and Copy link, who is here, can edit / can view online, and Stop sharing (online: the others' links and codes are revoked and they are disconnected, while the file stays on the server with its owner in it and can be shared again with new codes; removing it is a separate, confirmed step); **Join with a code** is one field (popover, home screen, Join dialog) for a code of either kind or a link; addresses, keys and ports moved under Advanced and Preferences ▸ Server for online sharing (reset, or empty to turn online sharing off). Protocol additions within version 2 (`Resolve` / `Resolved`, `Admin code`): the server keeps a stretched hash and the wrapped token, never the code, and limits wrong codes (10 a minute per address, a 15-minute lockout after 10 wrong, 1,000 wrong an hour for everyone); codes go with their link and file. Older servers leave the links in place of a code; older apps join by link. `collab.share`, `collab.join {code}`, `code` and `share` in `collab.peers`; the browser joins by code. Usability and hosting rather than a new capability: percentages and hours unchanged |
| 2026-10-12 | minor | Live sessions are per document (a fix from real use: after hosting or joining in one tab, a new or other file showed that session, and starting one there silently replaced it). Every open file has its own session or none; several run at once and keep syncing behind the tab in front; the Share popover, the people, the cursors, cursor chat, spotlight, follow and the read-only reason are the file in front's (each window its own); the `collab.*` commands act on the active file's session and `collab.peers` lists all of them (`sessions`); joining opens the session's file in its own tab without touching other sessions; closing a file ends its session only. One link carries the sessions, each under its own id (desktop, browser and tests). Percentages and hours unchanged |

View File

@ -1,6 +1,6 @@
# Where FigCraft falls short of Figma
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (import lines inside generated layer trees; session restore; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; `.fig` import: editable vector networks, more layer kinds, variable prototype actions; `.fig` import, best effort: gap 1 down to its tail; Windows QA pass: everyday workflows on Windows 11, eight fixes incl. unsaved work kept after a crash or a forced quit; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings: gap 14 down to its tail) · **Target:** Figma Design (desktop app 126.10.7)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (branch history entries; import lines inside generated layer trees; session restore; `.fig` export: instance sizes, variables with modes, shared styles, variable-width strokes; sharing with a join code; text box sizes measured against documents re-saved by other software; live sessions per document; .fig export checked by opening written files in other software; experimental .fig export; keyboard shortcut parity suite and bindings; Simplified Chinese draft catalog; Latin American Spanish overlay; prototyping tail: gamepad triggers, widgets in prototypes, mode lookups; plugin directory on figcraft-server; Japanese and Korean draft catalogs and CJK UI fonts; plugin widgets and the plugin panel tail; French, German and Brazilian Portuguese draft catalogs; cursor chat and spotlight in live sessions; layers and assets panels behaviour suite and fixes; vector multi-edit and edit tail; dev resources links; extended collections tail; branch version history and finer list merges; comments and version history behaviour suite and fixes; canvas navigation, guides and grids behaviour suite and fixes; `.fig` import: editable vector networks, more layer kinds, variable prototype actions; `.fig` import, best effort: gap 1 down to its tail; Windows QA pass: everyday workflows on Windows 11, eight fixes incl. unsaved work kept after a crash or a forced quit; Display P3: gamut warning and soft-proof (true P3 blocked upstream); several windows; localization layer with a draft Spanish catalog; hosting figcraft-server: wss in the desktop app, server releases, deployment guide; plugin panels and settings; canvas fixes: boolean hit testing on the result, group drags, frame outlines and slices; code connect; stretch and scatter brushes, pen pressure; text on a path and pattern fills; team libraries on the server and swap library; branches: create, update from main, review and merge; animated GIF fills and TIFF images; Open recent, outlines for the selection and mask outlines: menu breadth 100%; extended collections, variables import/export, gradient stop and variant bindings: gap 14 down to its tail) · **Target:** Figma Design (desktop app 126.10.7)
Every known shortfall against Figma, one entry each, ranked by how much it stops a Figma user from
doing real work in FigCraft. Agents: pick work from the top unless the owner says otherwise. The
@ -349,8 +349,10 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
conflict; only the same item changed both ways, an item removed on one side, or different items
added on both sides conflict (as the whole list).
- **Missing:** branches of a file in a live session are refused; other lists (text runs, export
settings, guides…) still conflict as a whole; the branch history has no entries of its own for
its creation or updates from main (only saved versions, autosaves and the merge on main).
settings, guides…) still conflict as a whole.
- **Landed (2026-10-13):** a branch's history has entries of its own: Branch created when it is
made and Updated from main each time it takes main's changes, shown in the branch's version
list with the versions saved there.
- **Estimate:** 1–2 h.
### 18. Display P3 on screen (several windows landed)
@ -400,6 +402,7 @@ No alpha blockers: the alpha gate in [`roadmap.md`](roadmap.md#alpha-gate) passe
| Date | Change | Summary |
|---|---|---|
| 2026-10-13 | minor | Gap 17: a branch's version history gets entries for its creation and each update from main; left: branches in live sessions and lists other than fills, strokes and effects merging whole; estimate and ranking unchanged |
| 2026-10-13 | minor | Gap 16: import lines for connected instances inside generated layer trees landed (deduplicated and ordered per language); what is left is publishing mappings from the code repository with a CLI; estimate and ranking unchanged |
| 2026-10-13 | minor | Gap 18: window places are restored at the next start (session restore: windows, tabs, pages, views and panels come back as they were left); what is left is wide-gamut colour on screen and the screen-reader tree in other windows, both blocked upstream; estimate and ranking unchanged |
| 2026-10-13 | minor | Gap 1: the experimental `.fig` export writes instance sizes (resized, filling an auto layout, scaled), variables with modes and bindings, shared styles and variable-width strokes, and the import reads them back. Checked by opening written files in other software: resized and scaled instances keep their size and lay their layers out for it, variables with their modes and colour bindings resolve there (a frame set to another mode shows that mode's colours, also inside instances), colour, text, effect and grid styles are listed, and variable-width strokes that are the same on both sides of the path draw with their width; number, string and boolean bindings and bindings on effects, layout guides and gradient stops come across as the values they give but did not show as bound there (their entries now also name the layer field they set, not yet checked there), and strokes wider on one side of the path are written as filled outlines, no longer editable strokes there (the stroke itself is kept as plugin data and our own import puts it back; not yet checked there). Hours and the ranking unchanged |

View File

@ -898,7 +898,8 @@ branches). Driven with `run_command`:
- `branch.archive {id?}` sets a branch aside without merging (it stays in the file and can still be
opened, but not merged).
Each branch keeps its own version history: on a branch tab, `file.saveVersion`, `version.autosave`
Each branch keeps its own version history, which starts with `Branch created` and gets `Updated
from main` after each `branch.updateFromMain`: on a branch tab, `file.saveVersion`, `version.autosave`
and the `version.*` commands (list, preview, restore, rename, delete, duplicate) act on the
branch's versions, which are saved in the file with the branch and stay with it when it is merged
or archived. Branches aren't available in a live session.