Implement one-shot mode for explicit confirmation in ServeSession

- Introduced one-shot mode to block automatic outgoing writes and require explicit user confirmation for all changes.
- Updated confirmation logic in App:startSession to ensure no auto-acceptance occurs during one-shot mode.
- Enhanced logging to provide clarity on actions taken during one-shot mode, improving user awareness and control over sync operations.
This commit is contained in:
Joe 2026-01-29 13:47:56 -06:00
parent d002c4e821
commit 78aab0eca4
2 changed files with 80 additions and 68 deletions

View file

@ -709,28 +709,19 @@ function App:startSession()
end)
serveSession:setConfirmCallback(function(instanceMap, patch, serverInfo)
-- Helper to schedule one-shot disconnect after auto-accept
local function scheduleOneShotDisconnect()
if Settings:get("oneShotSync") then
task.delay(0.5, function()
if self.serveSession then
Log.info("One-shot sync: disconnecting after sync")
self:endSession()
end
end)
end
end
local isOneShotMode = Settings:get("oneShotSync")
-- ONE-SHOT MODE: Never auto-accept, always require explicit confirmation
-- This ensures nothing can "sneak through" without user review
if not isOneShotMode then
if PatchSet.isEmpty(patch) then
Log.trace("Accepting patch without confirmation because it is empty")
scheduleOneShotDisconnect()
return "Accept"
end
-- Play solo auto-connect does not require confirmation
if self:isAutoConnectPlaytestServerAvailable() then
Log.trace("Accepting patch without confirmation because play solo auto-connect is enabled")
scheduleOneShotDisconnect()
return "Accept"
end
@ -742,7 +733,6 @@ function App:startSession()
Log.trace(
"Accepting patch without confirmation because project has already been connected and behavior is set to Initial"
)
scheduleOneShotDisconnect()
return "Accept"
end
elseif confirmationBehavior == "Large Changes" then
@ -751,7 +741,6 @@ function App:startSession()
Log.trace(
"Accepting patch without confirmation because patch is small and behavior is set to Large Changes"
)
scheduleOneShotDisconnect()
return "Accept"
end
elseif confirmationBehavior == "Unlisted PlaceId" then
@ -762,13 +751,11 @@ function App:startSession()
Log.trace(
"Accepting patch without confirmation because placeId is listed and behavior is set to Unlisted PlaceId"
)
scheduleOneShotDisconnect()
return "Accept"
end
end
elseif confirmationBehavior == "Never" then
Log.trace("Accepting patch without confirmation because behavior is set to Never")
scheduleOneShotDisconnect()
return "Accept"
end
end
@ -787,10 +774,12 @@ function App:startSession()
and datamodelUpdates.changedClassName == nil
then
Log.trace("Accepting patch without confirmation because it only contains a datamodel name change")
scheduleOneShotDisconnect()
return "Accept"
end
end
else
Log.info("One-shot mode: skipping all auto-accept paths, requiring explicit confirmation")
end
self:setState({
connectingText = "Computing diff view...",
@ -845,6 +834,13 @@ function App:startSession()
end)
serveSession:setPatchUpdateCallback(function(instanceMap, patch)
-- ONE-SHOT MODE: Ignore all further changes during confirmation
-- They will be picked up on the next connection attempt
if Settings:get("oneShotSync") then
Log.info("One-shot mode: ignoring incoming changes during confirmation")
return
end
-- If all changes have been reverted, auto-accept the empty patch
if PatchSet.isEmpty(patch) then
Log.trace("Patch became empty after merging, auto-accepting")

View file

@ -78,6 +78,15 @@ function ServeSession.new(options)
end
local function onChangesFlushed(patch)
-- ONE-SHOT MODE: Block ALL automatic outgoing writes from the ChangeBatcher.
-- The only writes allowed in One-shot mode are explicit "pull" selections
-- from the confirmation dialog (handled directly in __confirmAndApplyInitialPatch).
-- This ensures nothing can be pushed to the server without explicit user action.
if Settings:get("oneShotSync") then
Log.info("One-shot mode: blocking automatic outgoing write from ChangeBatcher")
return
end
self.__apiContext:write(patch)
end
@ -211,7 +220,14 @@ function ServeSession:__onWebSocketMessage(messagesPacket)
end
-- If we're already waiting for confirmation, merge into the patch being confirmed
-- UNLESS we're in One-shot mode, where we ignore all further changes
if self.__confirmingPatch ~= nil then
if Settings:get("oneShotSync") then
Log.info("One-shot mode: ignoring incoming changes while confirming (will be picked up on next connection)")
self.__apiContext:setMessageCursor(messagesPacket.messageCursor)
return
end
Log.trace("Already confirming, merging into current patch")
PatchSet.merge(self.__confirmingPatch, combinedPatch, self.__instanceMap)
-- Notify the UI to update the displayed patch