Update dependencies and implement git-based sync direction defaults

- Updated `clap` and `unicode-ident` versions in `Cargo.lock` and `Cargo.toml`.
- Added `sha1` crate to `Cargo.toml` for SHA1 hashing functionality.
- Introduced a new `git.rs` module for handling git operations, including changed files and SHA1 computation.
- Enhanced the sync confirmation dialog to auto-select defaults based on git status, improving user experience during two-way sync.
- Updated `PatchTree` to utilize git metadata for determining default selections during patch application.

These changes enhance the integration with git, providing smarter defaults and improving the overall sync process.
This commit is contained in:
Joe 2026-02-17 20:36:28 -06:00
parent df22500102
commit 7ba04f7f6a
29 changed files with 1285 additions and 176 deletions

View file

@ -1,5 +1,5 @@
---
globs: src/web/**/*.rs,src/serve_session.rs,plugin/src/ServeSession.lua,plugin/src/ChangeBatcher/**/*.lua,plugin/src/ApiContext.lua
globs: src/web/**,src/serve_session.rs,src/git.rs,,plugin/src/**
alwaysApply: false
---
@ -63,10 +63,22 @@ Studio → ChangeBatcher → Plugin → WebSocket → Server → Filesystem
"visibleServices": ["Workspace", "ServerScriptService"],
"placeId": 123456,
"gameId": 789012,
"ignoreHiddenServices": true
"ignoreHiddenServices": true,
"gitMetadata": {
"changedIds": ["ref-1", "ref-2"],
"scriptCommittedHashes": {
"ref-1": ["a1b2c3...40-char-sha1-hex", "d4e5f6...staged-sha1"]
}
}
}
```
**`gitMetadata`** is `null` when the project is not in a git repo. Contains:
- `changedIds`: Instance Refs whose backing files have changes relative to HEAD (staged + unstaged + untracked)
- `scriptCommittedHashes`: For changed script instances, 1-2 SHA1 hashes (HEAD version and staged version if different) computed using the git blob format (`"blob <size>\0<content>"`). Plugin checks if Studio `script.Source` matches ANY hash to default "Atlas".
Recomputed fresh on each `GET /api/rojo` connect. Uses a two-phase tree lock: snapshot path mappings briefly, then release before running git subprocesses.
### Write Request (`POST /api/write`)
```json
@ -74,10 +86,13 @@ Studio → ChangeBatcher → Plugin → WebSocket → Server → Filesystem
"sessionId": "uuid",
"removed": ["id-1"],
"added": { "id-2": { "parent": "parent-id", "className": "Folder", "name": "New", "properties": {}, "children": [] } },
"updated": [{ "id": "id-3", "changedName": "Renamed", "changedClassName": "Script", "changedProperties": { "Source": "print('hi')" }, "changedMetadata": {} }]
"updated": [{ "id": "id-3", "changedName": "Renamed", "changedClassName": "Script", "changedProperties": { "Source": "print('hi')" }, "changedMetadata": {} }],
"stageIds": ["ref-1", "ref-2"]
}
```
**`stageIds`**: Instance IDs whose backing files should be staged via `git add` after writing. Includes all push-accepted IDs + auto-selected pull-accepted IDs. The server resolves these to filesystem paths and runs `git add`. Staging is split: `api.rs` stages additions/removals/push files directly; `change_processor` stages Source writes after they complete (via `stage_ids` on PatchSet).
**Format detection:** `Name/init.luau` → directory | `Name.luau` → file | neither → use `has_children` (`.lua` also supported)
## WebSocket Protocol (v6)
@ -603,6 +618,25 @@ end
- Safe for production environments
- Team viewing same project simultaneously
### Git-Based Sync Direction Defaults
When the plugin shows the sync confirmation dialog, items get smart default selections based on git status:
- **File has no git changes**: default to "Studio" (pull) -- any diff is because Studio was changed
- **File has git changes AND is a script AND Studio Source matches committed/staged hash**: default to "Atlas" (push) -- developer changed the file, Studio hasn't diverged
- **Otherwise**: `nil` -- user must decide (both sides changed, or non-script with changes)
- **No git repo**: all defaults remain `nil` (feature is no-op)
Only **Updated** items get defaults. Added/Removed items remain `nil`.
**Hash computation**: Both sides use `SHA1("blob <byte_len>\0<content>")` (git blob format). Server sends up to 2 hashes per changed script (HEAD + staged if different). Plugin computes hash of `script.Source` (non-draft version) and checks if ANY hash matches.
**Auto-staging**: After confirmation, accepted files are staged via `git add`:
- Push-accepted items: always staged (all types, including non-scripts)
- Pull-accepted items: only staged if auto-selected (`defaultSelection ~= nil`); manually-chosen pulls are left unstaged for manual diff
**Implementation files**: `src/git.rs` (server git helpers), `plugin/src/PatchTree.lua` (default selection logic), `plugin/src/SHA1.luau` (hash computation), `src/web/interface.rs` (`GitMetadata` struct, `stage_ids` on WriteRequest/PatchSet)
### Known Limitations (Detailed)
| Issue | Cause | Impact |
@ -671,3 +705,4 @@ Browser: `http://localhost:34872` → Network tab → WebSocket messages
| `src/serve_session.rs` - Session | `plugin/src/ChangeBatcher/` - Two-way sync |
| `src/change_processor.rs` - FS writer | `plugin/src/Reconciler/` - Patch application |
| `src/rojo_ref.rs` - Ref paths | `plugin/src/Reconciler/matching.lua` - Matching |
| `src/git.rs` - Git integration | `plugin/src/PatchTree.lua` - Default selections |

View file

@ -0,0 +1,338 @@
---
globs: plugin/**
alwaysApply: false
---
# Rojo Studio Plugin
Built with Roact (React-like), handles WebSocket sync with server, patch application to Studio, and UI.
**Entry:** `plugin/src/init.server.lua` | **Protocol:** WebSocket v6 | **Must match server MAJOR.MINOR**
## Architecture
### Key Components
**1. UI Layer** (`App/`)
- **init.lua** - Root component, manages connection state, auto-reconnect, sync reminders, playtest server auto-connect
- **Theme.lua** - Studio-matched color palette (auto light/dark)
- **Page.lua** - Page wrapper component
- **Components/** - Reusable UI components
- `PatchVisualizer/` - Tree view of changes with property diffs (`init.lua`, `DomLabel.lua`, `DisplayValue.lua`, `ChangeList.lua`)
- `StringDiffVisualizer/` - Side-by-side script diffs (`init.lua`, `StringDiff.lua`)
- `TableDiffVisualizer/` - Attributes and Tags diffs (`init.lua`, `Dictionary.lua`, `Array.lua`)
- `Notifications/` - Toast notifications (`init.lua`, `Notification.lua`, `FullscreenNotification.lua`)
- `Studio/` - Studio API wrappers (`StudioPluginAction`, `StudioPluginContext`, `StudioPluginGui`, `StudioToggleButton`, `StudioToolbarContext`, `StudioToolbar`)
- General: `Header`, `TextInput`, `TextButton`, `Checkbox`, `Dropdown`, `Tag`, `IconButton`, `Tooltip`, `TouchRipple`, `VirtualScroller`, `ScrollingFrame`, `Spinner`, `BorderedContainer`, `EditableImage`, `SlicedImage`, `ClassIcon`
- **StatusPages/** - Main UI pages
- `NotConnected.lua` - Connection form (host/port input)
- `Connecting.lua` - Loading screen
- `Confirming.lua` - Patch confirmation dialog with push/pull/ignore per-node selections
- `Connected.lua` - Status display with patch history
- `Error.lua` - Error display
- `Settings/` - Settings UI (`init.lua`, `Setting.lua`)
- Uses Roact for declarative UI
**2. Communication** (`ServeSession.lua`, `ApiContext.lua`)
- WebSocket connection to server via `/api/socket/:cursor`
- Handles patches, errors, reconnection
- Protocol version checking
- Confirmation flow with push/pull/ignore
- One-shot mode support
**3. Reconciler** (`Reconciler/`)
- **applyPatch.lua** - Applies server patches to Studio tree
- **reify.lua** - Creates instances from snapshots (parent last)
- **hydrate.lua** - Updates InstanceMap with existing Studio instances
- **diff.lua** - Computes patch between virtual and Studio state (for two-way sync)
- **matching.lua** - Recursive change-count scoring algorithm for pairing virtual instances with Studio instances
- **trueEquals.lua** - Shared value equality (fuzzy floats, Color3, CFrame, Vector3; NaN handling; nil/null-ref equivalence)
- **decodeValue.lua** - Converts encoded values to Roblox types
- **setProperty.lua** - Sets properties safely (handles unscriptable)
- **getProperty.lua** - Gets properties with fallbacks
- **Error.lua** - Reconciler error handling
**4. ChangeBatcher** (`ChangeBatcher/`)
- **init.lua** - Main batcher: accumulates changes every 200ms, pause/resume, deferred Ref resolution
- **createPatchSet.lua** - Converts pending changes to PatchSet
- **encodePatchUpdate.lua** - Encodes property updates including Ref properties via InstanceMap
- **encodeInstance.lua** - Full instance encoding (for additions/syncback)
- **encodeProperty.lua** - Single property value encoding via RbxDom
- **propertyFilter.lua** - Property type filtering (only `UniqueId` blocked; `Ref` is allowed)
**5. State Management**
- **Settings.lua** - Persistent plugin settings (prefixed `Atlas_` in plugin store)
- **InstanceMap.lua** - Bidirectional Server ID <-> Studio Instance mapping with change detection
- **PatchSet.lua** / **PatchTree.lua** - Patch representation and visualization tree (PatchTree.build accepts optional `gitMetadata` for smart default selections)
- **Types.lua** - Type definitions and runtime validation
- **ChangeMetadata.lua** - Change metadata tracking
**6. Utilities**
- **Timer.lua** - Performance timing
- **DiffUtil.lua** - Diff utilities
- **Version.lua** - Version parsing and comparison
- **Dictionary.lua** - Dictionary utilities
- **Assets.lua** - Asset references
- **SHA1.luau** - SHA1 hashing (used for git blob format hash comparison in PatchTree default selections)
- **XXH32.luau** - XXH32 hash function
- **soundPlayer.lua** - Sound effects
- **preloadAssets.lua** - Asset preloading
- **ignorePlaceIds.lua** - Place ID ignore list
- **timeUtil.lua** - Time formatting
- **strict.lua** - Strict module wrapper
- **invariant.lua** - Assertion utilities
**7. Testing**
- Unit tests: `.spec.lua` files (TestEZ) throughout all modules
- Integration tests: `integration/` directory (`confirmBehavior`, `oneShotSync`, `syncflow`, `twoWaySync`)
- Stress tests: `.stress.spec.lua` files (applyPatch, diff, reify, encodeInstance, PatchSet)
- Chaos tests: `chaos/` directory (`stressPatterns.spec.lua`)
- Test utilities: `testUtils/` (`MockApiContext`, `PatchGenerator`, `LargeTreeGenerator`)
- Test runner: `runTests.lua`
## Build Structure (`plugin.project.json`)
The plugin is built from `plugin.project.json` at the repo root:
```json
{
"name": "Rojo",
"tree": {
"$className": "Folder",
"Plugin": { "$path": "plugin/src" },
"Packages": {
"$path": "plugin/Packages",
"Log": { "$path": "plugin/log" },
"Http": { "$path": "plugin/http" },
"Fmt": { "$path": "plugin/fmt" },
"RbxDom": { "$path": "plugin/rbx_dom_lua" }
},
"Version": { "$path": "plugin/Version.txt" }
}
}
```
Vendored packages live under `plugin/Packages/`. Custom modules (`Log`, `Http`, `Fmt`, `RbxDom`) are injected into the Packages tree from their own directories at build time.
## Key Configuration (`Config.lua`)
```lua
{
isDevBuild = false, -- Detected from ROJO_DEV_BUILD instance
codename = "Epiphany",
version = { major, minor, patch, metadata }, -- Parsed from Version.txt
expectedServerVersionString = "7.7 or newer",
protocolVersion = 6, -- MUST match server
defaultHost = "localhost",
defaultPort = "34873",
ignoredClassNames = { -- Auto-created instances to skip
TouchTransmitter = true,
Camera = true,
},
}
```
**When changing protocol:** Bump `protocolVersion` in both `plugin/src/Config.lua` AND `src/web/interface.rs`.
## Settings
Persistent settings stored with `Atlas_` prefix in Studio plugin store:
```lua
{
openScriptsExternally = false,
twoWaySync = true,
oneShotSync = true,
autoReconnect = false,
showNotifications = false,
enableSyncFallback = false,
syncReminderMode = "Notify", -- "None" | "Notify" | "Fullscreen"
syncReminderPolling = true,
checkForUpdates = false,
checkForPrereleases = false,
autoConnectPlaytestServer = false,
confirmationBehavior = "Always", -- "Never" | "Initial" | "Always" | "Large Changes" | "Unlisted PlaceId"
largeChangesConfirmationThreshold = 5,
playSounds = true,
typecheckingEnabled = false,
logLevel = "Info",
timingLogsEnabled = false,
priorEndpoints = {},
}
```
## WebSocket Protocol (v6)
1. Connect to `ws://host:port/api/socket/:cursor`
2. Receive `ServerInfo` handshake (protocol version, project name, session ID)
3. Plugin sends: `{type = "Subscribe"}`
4. Server sends patches on filesystem changes (MessagePack encoded)
**Patch structure:**
```lua
{
type = "ApplyPatch",
sessionId = "uuid",
patch = {
removed = {"id-1", "id-2"},
added = {
["id-3"] = {
id = "id-3", className = "Folder", name = "New",
properties = {}, metadata = { ignoreUnknownInstances = false },
children = {"child-1"},
},
},
updated = {
{ id = "id-4", changedName = "Renamed", changedProperties = { Source = "code" } },
},
}
}
```
## ServeSession (`ServeSession.lua`)
Manages the lifecycle of a connection to the server.
**Status flow:** `NotStarted` -> `Connecting` -> `Connected` -> `Disconnected`
**Key methods:**
- `new(options)` - Creates session with `ApiContext` and `twoWaySync` flag
- `start()` - Connects, computes initial patch, shows confirmation
- `stop()` - Disconnects and cleans up
- `__onWebSocketMessage()` - Handles incoming patches
- `__applyPatch()` - Applies patches via Reconciler
- `__confirmAndApplyInitialPatch()` - Handles user confirmation with push/pull/ignore
- `__computeInitialPatch()` - Computes catch-up patch
- `hookPrecommit()` / `hookPostcommit()` - Lifecycle hooks
**Confirmation flow:**
1. Receive patch (initial or streaming)
2. Check `confirmationBehavior` setting
3. If confirmation needed: pause ChangeBatcher, build PatchTree with git-based default selections (from `serverInfo.gitMetadata`), show Confirming page with PatchVisualizer
4. User reviews defaults and selects push/pull/ignore per node
5. Build `stageIds` list (all push-accepted + auto-selected pull-accepted), send with write request
6. Apply via Reconciler, resume ChangeBatcher
**One-shot mode (`oneShotSync: true`):**
- ChangeBatcher still collects changes (for diff visualization)
- Automatic outgoing writes are blocked
- Only explicit "pull" selections in confirmation dialog can write
- Disconnects after initial sync completes
## Reconciler (`Reconciler/`)
**applyPatch.lua** - Main patch application:
1. Remove instances in `removed`
2. Create instances in `added` via `reify`
3. Update instances in `updated` via `hydrate`
4. Handle deferred Ref properties
5. Collect errors, return unapplied patch
**reify.lua** - Create instances (parent last - Roblox best practice)
**hydrate.lua** - Update InstanceMap with existing instances (uses matching algorithm)
**diff.lua** - Compute patch between virtual instances and Studio state
**setProperty.lua** / **getProperty.lua** - Handle unscriptable properties
### Matching Algorithm (`matching.lua`)
Pairs incoming virtual instances with existing Studio instances during hydration.
**Entry point:** `Matching.matchChildren(virtualChildren, studioChildren, virtualInstances)`
**Algorithm:**
1. **Fast path:** Group by (Name, ClassName). 1:1 groups get instant-matched with zero scoring.
2. **Ambiguous groups:** For groups with multiple candidates, compute pairwise `_computeChangeCount()` (recursive, scores entire subtree). Greedy assignment sorted by cost ascending, child-order tiebreaker for stability.
3. **Leftovers:** Unmatched virtual = new instances, unmatched studio = deleted.
**Sort stability note:** `table.sort` is NOT stable in Luau. Comparator breaks ties by insertion index to ensure deterministic ordering.
**Value equality:** Shared `trueEquals.lua` module used by both `matching.lua` and `diff.lua` -- fuzzy equality for floats (epsilon 0.0001), Color3 via RGB ints, CFrame/Vector3 component-wise, NaN handling, nil/null-ref equivalence.
## InstanceMap (`InstanceMap.lua`)
Bidirectional Server ID <-> Studio Instance mapping with integrated change detection:
```lua
InstanceMap = {
fromIds = {}, -- Server ID -> Studio Instance
fromInstances = {}, -- Studio Instance -> Server ID
pausedUpdateInstances = {}, -- Set of instances to ignore changes from
instancesToSignal = {}, -- Instance -> RBXScriptConnection(s)
onInstanceChanged = fn, -- Callback: (instance, propertyName) -> void
}
```
**Key methods:**
- `insert(id, instance)` - Add mapping and connect change signals
- `removeId(id)` / `removeInstance(instance)` - Remove mapping and disconnect signals
- `destroyInstance(instance)` - Parent to nil (preserves undo history)
- `fromId(id)` / `fromInstance(instance)` - Look up mapping
- `pauseInstance(instance)` / `unpauseAllInstances()` - Pause/resume change detection
**Signal connection (`__connectSignals`):**
- Regular instances: `instance.Changed` event (fires for any property)
- ValueBase instances: Explicit `GetPropertyChangedSignal` for Name, Value, Parent (ValueBase.Changed only fires for Value)
**Change guards (`__maybeFireInstanceChanged`):**
- Skipped if instance is in `pausedUpdateInstances`
- Skipped if `onInstanceChanged` callback is nil
- Skipped if `RunService:IsRunning()` (Play mode)
**Pause mechanism:** During incoming patch application, instances are paused to prevent their changes from being sent back to the server (feedback loop prevention). `unpauseAllInstances()` is called each frame in `ChangeBatcher:__cycle()`.
## Two-Way Sync
Watches Studio changes and sends them to the server in real-time. Enabled by default (`twoWaySync: true`).
**Data flow:** Studio property change -> InstanceMap signal -> ServeSession callback -> ChangeBatcher -> 200ms batch flush -> `POST /api/write` (MessagePack) -> Server writes to filesystem
**ChangeBatcher state:**
```lua
{
__accumulator = 0, -- Time since last flush
__pendingPropertyChanges = {}, -- Instance -> { PropertyName -> true }
__pendingUnresolvedRefs = {}, -- targetInstance -> [{sourceInstance, propertyName}]
__syncSourceOnly = false, -- Only sync Source property
__paused = false, -- Pause during confirmation
__renderSteppedConnection = conn, -- Frame loop connection
__instanceMap = instanceMap,
__onChangesFlushed = callback,
}
```
**Deferred Ref resolution:** When a Ref property targets an instance not yet in the InstanceMap, the property is deferred in `__pendingUnresolvedRefs`. When the target appears via `onInstanceInserted`, the property change is re-queued for the next batch.
**Guards:**
- `oneShotSync` setting blocks automatic outgoing writes
- `__paused` flag blocks flushes during confirmation dialog
- `RunService:IsRunning()` blocks during Play mode
## Dependencies
### Vendored Packages (`plugin/Packages/`)
Roact, Flipper, Promise, TestEZ, t, Highlighter, msgpack-luau, json5. **Don't edit vendored packages.**
### Custom Modules (injected via `plugin.project.json`)
| Module | Path | Purpose |
|--------|------|---------|
| Log | `plugin/log` | Structured logging with configurable levels |
| Http | `plugin/http` | HTTP client utilities |
| Fmt | `plugin/fmt` | String formatting with debug output |
| RbxDom | `plugin/rbx_dom_lua` | Reflection database, property descriptors, encoded value encode/decode |
## Common Issues
**Plugin not reloading:** Enable Studio setting "Reload plugins on file change"
**Connection fails:** Check server running, host/port match, protocol versions match
**Properties not syncing:** Verify scriptable in rbx_reflection_database
**Duplicates:** InstanceMap corruption - disconnect, restart Studio
**Two-way sync feedback loops:** Check pause mechanism is working; instances must be paused during reconciliation
## Debugging
Settings -> Log Level -> "Trace" for verbose output. `ROJO_DEV_BUILD` instance enables auto-tests via TestEZ.
**Timing:** Enable `timingLogsEnabled` setting to see `Timer.start`/`Timer.stop` measurements for Reconciler and other hot paths.

View file

@ -1,5 +1,5 @@
---
globs: rbx-dom/**/*.rs,rbx-dom/**/*.lua,rbx-dom/**/*.toml
globs: rbx-dom/**
alwaysApply: false
---

View file

@ -1,5 +1,5 @@
---
globs: src/**/*.rs,crates/**/*.rs
globs: src/**,crates/**
alwaysApply: false
---
@ -380,6 +380,7 @@ src/
meta.rs - Meta file helpers
web/ - HTTP/WebSocket
change_processor.rs - Two-way sync filesystem writer
git.rs - Git integration (changed files, SHA1 hashing, auto-staging)
rojo_ref.rs - Ref path system (Rojo_Ref_*, RefPathIndex)
variant_eq.rs - Property value comparison
lib.rs - Public API

View file

@ -1,5 +1,5 @@
---
globs: src/syncback/**/*.rs,src/cli/syncback.rs
globs: src/syncback/**,src/cli/syncback.rs
alwaysApply: false
---

View file

@ -323,6 +323,7 @@ src/ - Rust server/CLI
meta.rs - Meta file read/write helpers
web/ - HTTP/WebSocket server
change_processor.rs - Two-way sync filesystem writer
git.rs - Git integration (changed files, SHA1 hashing, auto-staging)
rojo_ref.rs - Ref path system (Rojo_Ref_*, RefPathIndex)
variant_eq.rs - Property value comparison
@ -333,6 +334,8 @@ plugin/src/ - Lua Studio plugin
trueEquals.lua - Shared value equality module
ChangeBatcher/ - Two-way sync patch creation
Config.lua - Version and protocol config
PatchTree.lua - Patch visualization tree (git-based default selections)
SHA1.luau - SHA1 hash for git blob format comparison
XXH32.luau - Hash function for plugin
rbx-dom/ - Git submodule (separate repo)

View file

@ -1,136 +0,0 @@
---
globs: plugin/**/*.lua,plugin/**/*.luau,plugin/**/*.json
alwaysApply: false
---
# Rojo Studio Plugin
Built with Roact (React-like), handles WebSocket sync with server, patch application to Studio, and UI.
**Entry:** `plugin/src/init.server.lua` | **Protocol:** WebSocket v5 | **Must match server MAJOR.MINOR**
## Architecture
### Key Components
**1. UI Layer** (`App/`)
- **App.lua** - Root component, manages connection state
- **Components/** - Reusable UI components
- `StatusPages/` - Connected, Connecting, Error, Settings screens
- `PatchVisualizer/` - Shows changes before applying
- `StringDiffVisualizer/` - Side-by-side script diffs
- `TableDiffVisualizer/` - Attributes and Tags diffs
- `Notifications/` - Toast notifications
- **Theme.lua** - Studio-matched color palette
- Uses Roact for declarative UI
**2. Communication** (`ServeSession.lua`)
- WebSocket connection to server (previously long-polling)
- Handles patches, errors, reconnection
- Protocol version checking
**3. Reconciler** (`Reconciler/`)
- **applyPatch.lua** - Applies server patches to Studio tree
- **reify.lua** - Creates instances from snapshots
- **hydrate.lua** - Updates existing instances
- **diff.lua** - Generates patches (for two-way sync)
- **decodeValue.lua** - Converts encoded values to Roblox types
- **setProperty.lua** - Sets properties safely
- **getProperty.lua** - Gets properties with fallbacks
**4. State Management**
- **Settings.lua** - Persistent plugin settings
- **InstanceMap.lua** - Tracks server ID → Studio instance mapping
- **PatchSet.lua** / **PatchTree.lua** - Patch representation
**5. Testing**
- Unit tests: `.spec.lua` files (TestEZ)
- Integration tests: `integration/` directory
- Stress tests: `.stress.spec.lua` files
- Test runner: `runTests.lua`
## Key Configuration (`Config.lua`)
```lua
{
version = {7, 7, 0, "-rc.1"},
protocolVersion = 5, -- MUST match server
ignoredClassNames = { TouchTransmitter = true, Camera = true }
}
```
**When changing protocol:** Bump `protocolVersion` in both plugin AND server.
## WebSocket Protocol (v5)
1. Connect to `ws://host:port/api/rojo`
2. Server sends: protocol version, project name
3. Plugin sends: `{"type": "Subscribe"}`
4. Server sends patches on filesystem changes
**Patch structure:**
```lua
{
type = "ApplyPatch",
sessionId = "uuid",
patch = {
removed = {"id1", "id2"}, -- Array of IDs (Vec<Ref>)
added = { ["id3"] = {...} }, -- Map: ID → Instance data
updated = { -- Array of updates (Vec<InstanceUpdate>)
{ id = "id4", changedName = "...", changedProperties = {...} }
}
}
}
```
## Reconciler (`Reconciler/`)
**applyPatch.lua** - Main patch application:
1. Remove instances in `removed`
2. Create instances in `added` via `reify`
3. Update instances in `updated` via `hydrate`
4. Collect errors, show in UI
**reify.lua** - Create instances (parent last - Roblox best practice)
**hydrate.lua** - Update existing (only changed properties)
**setProperty.lua** / **getProperty.lua** - Handle unscriptable properties
## UI Components (`App/`)
**StatusPages:** NotConnected, Connecting, Connected, Error, Settings
**PatchVisualizer:** Tree view of changes with property diffs
**StringDiffVisualizer:** Side-by-side script diffs
**TableDiffVisualizer:** Attributes and Tags diffs
**Notifications:** Toast messages
**Theme.lua:** Studio-matched colors (auto light/dark)
## InstanceMap
Bidirectional Server ID ↔ Studio Instance mapping:
```lua
instanceMap:insert(serverId, studioInstance)
local instance = instanceMap:fromId(serverId)
local serverId = instanceMap:fromInstance(instance)
```
Critical for patch application and avoiding duplicates.
## Two-Way Sync (Experimental)
⚠️ Watches Studio changes, sends to server. Has known issues - use with caution.
## Dependencies (`plugin/Packages/`)
Roact, Flipper, Promise, TestEZ, t, Highlighter, msgpack-luau. **Don't edit vendored packages.**
## Common Issues
**Plugin not reloading:** Enable Studio setting "Reload plugins on file change"
**Connection fails:** Check server running, host/port match, protocol versions match
**Properties not syncing:** Verify scriptable in rbx_reflection_database
**Duplicates:** InstanceMap corruption - disconnect, restart Studio
## Debugging
Settings → Log Level → "Trace" for verbose output. `ROJO_DEV_BUILD` instance enables auto-tests.