Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 45 additions & 42 deletions .agents/skills/uloop-wait-for-pause-point/SKILL.md

Large diffs are not rendered by default.

87 changes: 45 additions & 42 deletions .claude/skills/uloop-wait-for-pause-point/SKILL.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,23 @@ public void CreateHeartbeatResponse_WhenSerialized_ProducesFrozenJson()
"{\"jsonrpc\":\"2.0\",\"id\":7,\"result\":{\"alive\":true},\"uloop\":{\"phase\":\"heartbeat\",\"mainThreadStallSeconds\":12.5}}"));
}

[Test]
public void CreateErrorResponse_ForBusyException_IncludesSecondsSinceLastMainThreadTick()
{
// Verifies busy error responses carry secondsSinceLastMainThreadTick so clients can
// distinguish a live main thread from a frozen Editor while BUSY.
UnityCliLoopToolBusyException busyException = new(
"running-tool", "requested-tool", isPlaying: true, isPaused: true);

string response = JsonRpcResponseFactory.CreateErrorResponse(1, busyException);

JObject json = JObject.Parse(response);
JToken dataToken = json["error"]!["data"]!;
Assert.That(dataToken["type"]!.Value<string>(), Is.EqualTo(JsonRpcErrorTypes.ServerBusy));
Assert.That(dataToken["secondsSinceLastMainThreadTick"], Is.Not.Null);
Assert.That(dataToken["secondsSinceLastMainThreadTick"]!.Value<double>(), Is.GreaterThanOrEqualTo(0));
}

[Test]
public async Task ProcessRequest_WhenProtocolVersionIsTooOld_ProducesFrozenMismatchJson()
{
Expand Down
2 changes: 1 addition & 1 deletion Assets/Tests/Editor/SkillInstallLayoutTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ public void GetToolDescriptionsByToolName_WhenSkillHasDescription_MapsDescriptio
IReadOnlyDictionary<string, string> descriptions = SkillInstallLayout.GetToolDescriptionsByToolName(_projectRoot);

Assert.That(descriptions["compile"], Is.EqualTo("Compile the Unity project and report errors/warnings. Use after C# edits."));
Assert.That(descriptions[UnityCliLoopConstants.COMMAND_NAME_WAIT_FOR_PAUSE_POINT], Does.StartWith("Pauses Unity's playback"));
Assert.That(descriptions[UnityCliLoopConstants.COMMAND_NAME_WAIT_FOR_PAUSE_POINT], Does.StartWith("Pauses Unity playback"));
}

// Tests that duplicate skill names use the earlier source root across each precedence boundary.
Expand Down
87 changes: 45 additions & 42 deletions Packages/src/Editor/CliOnlyTools~/PausePoint/Skill/SKILL.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Packages/src/Editor/FirstPartyTools/Compile/CompileTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ protected override async Task<CompileResponse> ExecuteAsync(CompileSchema parame
UnityCliLoopCompileSessionLifecycleFacade.Service,
UnityCliLoopCompileResultSessionRepositoryFacade.Repository,
UnityCliLoopPendingCompileSessionRepositoryFacade.Repository);
return await useCase.CompileAsync(parameters, ct);
return await useCase.CompileAsync(parameters, ct).ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class ControlPlayModeTool : UnityCliLoopTool<ControlPlayModeSchema, Contr
protected override async Task<ControlPlayModeResponse> ExecuteAsync(ControlPlayModeSchema parameters, CancellationToken ct)
{
ControlPlayModeUseCase useCase = new();
return await useCase.ExecuteAsync(parameters, ct);
return await useCase.ExecuteAsync(parameters, ct).ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class FindGameObjectsTool : UnityCliLoopTool<FindGameObjectsSchema, FindG
protected override async Task<FindGameObjectsResponse> ExecuteAsync(FindGameObjectsSchema parameters, CancellationToken ct)
{
FindGameObjectsUseCase useCase = new(new GameObjectFinderService(), new ComponentSerializer());
return await useCase.ExecuteAsync(parameters, ct);
return await useCase.ExecuteAsync(parameters, ct).ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class GetHierarchyTool : UnityCliLoopTool<GetHierarchySchema, GetHierarch
protected override async Task<GetHierarchyResponse> ExecuteAsync(GetHierarchySchema parameters, CancellationToken ct)
{
GetHierarchyUseCase useCase = new(new HierarchyService(), new HierarchySerializer());
return await useCase.ExecuteAsync(parameters, ct);
return await useCase.ExecuteAsync(parameters, ct).ConfigureAwait(false);
}
}
}
2 changes: 1 addition & 1 deletion Packages/src/Editor/FirstPartyTools/GetLogs/GetLogsTool.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class GetLogsTool : UnityCliLoopTool<GetLogsSchema, GetLogsResponse>
protected override async Task<GetLogsResponse> ExecuteAsync(GetLogsSchema parameters, CancellationToken ct)
{
GetLogsUseCase useCase = new(new LogRetrievalService(), new LogFilteringService());
return await useCase.ExecuteAsync(parameters, ct);
return await useCase.ExecuteAsync(parameters, ct).ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class ReplayInputTool : UnityCliLoopTool<ReplayInputSchema, ReplayInputRe
protected override async Task<ReplayInputResponse> ExecuteAsync(ReplayInputSchema parameters, CancellationToken ct)
{
ReplayInputUseCase useCase = new();
return await useCase.ReplayInputAsync(parameters, ct);
return await useCase.ReplayInputAsync(parameters, ct).ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class RunTestsTool : UnityCliLoopTool<RunTestsSchema, RunTestsResponse>
protected override async Task<RunTestsResponse> ExecuteAsync(RunTestsSchema parameters, CancellationToken ct)
{
RunTestsUseCase useCase = new();
return await useCase.ExecuteAsync(parameters, ct);
return await useCase.ExecuteAsync(parameters, ct).ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class SimulateKeyboardTool : UnityCliLoopTool<SimulateKeyboardSchema, Sim
protected override async Task<SimulateKeyboardResponse> ExecuteAsync(SimulateKeyboardSchema parameters, CancellationToken ct)
{
SimulateKeyboardUseCase useCase = new();
return await useCase.ExecuteAsync(parameters, ct);
return await useCase.ExecuteAsync(parameters, ct).ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ public class SimulateMouseInputTool : UnityCliLoopTool<SimulateMouseInputSchema,
protected override async Task<SimulateMouseInputResponse> ExecuteAsync(SimulateMouseInputSchema parameters, CancellationToken ct)
{
SimulateMouseInputUseCase useCase = new();
return await useCase.ExecuteAsync(parameters, ct);
return await useCase.ExecuteAsync(parameters, ct).ConfigureAwait(false);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ private static void UpdatePointerRaycast(PointerEventData pointerData)
pointerData.pointerCurrentRaycast = hit ?? new RaycastResult();
}

internal static async Task<bool> InterpolateDragPosition(
internal static async Task<MouseUiFrameWaitOutcome> InterpolateDragPosition(
PointerEventData pointerData,
GameObject target,
Vector2 endPos,
Expand All @@ -96,10 +96,10 @@ internal static async Task<bool> InterpolateDragPosition(

if (duration <= 0f)
{
bool frameReady = await MouseUiEditorFrameWaiter.WaitForEditorFrameAndSwitchToMainThreadAsync(ct).ConfigureAwait(false);
if (!frameReady)
MouseUiFrameWaitOutcome frameOutcome = await MouseUiEditorFrameWaiter.WaitForEditorFrameAndSwitchToMainThreadAsync(ct).ConfigureAwait(false);
if (frameOutcome != MouseUiFrameWaitOutcome.Completed)
{
return false;
return frameOutcome;
}
await MainThreadSwitcher.SwitchToMainThread(ct);

Expand All @@ -110,18 +110,18 @@ internal static async Task<bool> InterpolateDragPosition(
ExecuteEvents.Execute(target, pointerData, ExecuteEvents.dragHandler);

SimulateMouseUiOverlayState.UpdatePosition(MouseUiCoordinateConverter.ScreenToInput(endPos));
return true;
return MouseUiFrameWaitOutcome.Completed;
}

float startTime = Time.realtimeSinceStartup;
float t;

do
{
bool frameReady = await MouseUiEditorFrameWaiter.WaitForEditorFrameAndSwitchToMainThreadAsync(ct).ConfigureAwait(false);
if (!frameReady)
MouseUiFrameWaitOutcome frameOutcome = await MouseUiEditorFrameWaiter.WaitForEditorFrameAndSwitchToMainThreadAsync(ct).ConfigureAwait(false);
if (frameOutcome != MouseUiFrameWaitOutcome.Completed)
{
return false;
return frameOutcome;
}
await MainThreadSwitcher.SwitchToMainThread(ct);

Expand All @@ -140,7 +140,7 @@ internal static async Task<bool> InterpolateDragPosition(
}
while (t < 1.0f);

return true;
return MouseUiFrameWaitOutcome.Completed;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,30 +1,58 @@
#nullable enable
using System.Threading;
using System.Threading.Tasks;
using UnityEditor;

using io.github.hatayama.UnityCliLoop.Application;
using io.github.hatayama.UnityCliLoop.ToolContracts;

namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
{
/// <summary>
/// Reports whether a mouse UI frame wait finished normally, was cut short by a
/// Pause Point, or exceeded the wall-clock guard.
/// </summary>
internal enum MouseUiFrameWaitOutcome
{
Completed = 0,
Paused = 1,
TimedOut = 2
}

/// <summary>
/// Waits for an Editor frame and restores execution to the Unity main thread.
/// </summary>
internal static class MouseUiEditorFrameWaiter
{
internal static async Task<bool> WaitForEditorFrameAndSwitchToMainThreadAsync(CancellationToken ct)
// EditorApplication.update keeps ticking while Play Mode is paused, so a frame wait
// keyed only to update ticks would never report the pause. Duration-based callers
// (MouseUiOverlayAnimator, the LongPress hold loop) measure elapsed time with
// Time.realtimeSinceStartup, which freezes once EditorApplication.isPaused is true,
// so without this check they would spin forever: every individual frame wait keeps
// succeeding while the duration they are accumulating toward never advances.
internal static async Task<MouseUiFrameWaitOutcome> WaitForEditorFrameAndSwitchToMainThreadAsync(CancellationToken ct)
{
if (EditorApplication.isPaused)
{
return MouseUiFrameWaitOutcome.Paused;
}

bool frameReady = await EditorFrameWaiter.WaitFramesOrTimeoutAsync(
1,
UnityCliLoopConstants.EDITOR_FRAME_WAIT_TIMEOUT_MS,
ct).ConfigureAwait(false);
if (!frameReady)
{
return false;
return MouseUiFrameWaitOutcome.TimedOut;
}

await MainThreadSwitcher.SwitchToMainThread(ct);
return true;
if (EditorApplication.isPaused)
{
return MouseUiFrameWaitOutcome.Paused;
}

return MouseUiFrameWaitOutcome.Completed;
}
}
}
Loading
Loading