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
44 changes: 44 additions & 0 deletions UnityCtl.Cli/BridgeClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text;
using System.Text.Json;
Expand Down Expand Up @@ -159,6 +160,12 @@ public BridgeClient(string baseUrl, string? agentId = null, string? projectRoot
var error = await response.Content.ReadAsStringAsync();
Console.Error.WriteLine($"Error: Bridge returned {response.StatusCode}");
Console.Error.WriteLine(error);

if (response.StatusCode == System.Net.HttpStatusCode.GatewayTimeout && _projectRoot != null)
{
DisplayDialogHint(_projectRoot);
}

return null;
}

Expand Down Expand Up @@ -331,6 +338,7 @@ private void DisplayUnityNotConnectedError()
else
{
Console.Error.WriteLine("Error: Unity Editor is running but not connected to the bridge.");
DisplayDialogHint(_projectRoot);
Console.Error.WriteLine("Ensure the UnityCtl package is installed and enabled in Unity.");
}
}
Expand All @@ -341,6 +349,42 @@ private void DisplayUnityNotConnectedError()
}
}

/// <summary>
/// Check for popup dialogs blocking the Unity Editor and print a hint if any are found.
/// Called on 504 (command timeout) and 503 (Unity not connected) to help diagnose the cause.
/// </summary>
internal static void DisplayDialogHint(string projectRoot)
{
try
{
var unityProcess = EditorCommands.FindUnityProcessForProject(
Path.GetFullPath(projectRoot));
if (unityProcess == null) return;

var dialogs = DialogDetector.DetectDialogs(unityProcess.Id);
if (dialogs.Count == 0) return;

Console.Error.WriteLine();
Console.Error.WriteLine($" Popup dialog detected — this is likely blocking Unity:");
foreach (var d in dialogs)
{
Console.Error.Write($" \"{d.Title}\"");
if (d.Buttons.Count > 0)
Console.Error.Write($" [{string.Join("] [", d.Buttons.Select(b => b.Text))}]");
if (d.Progress.HasValue)
Console.Error.Write($" ({(int)(d.Progress.Value * 100)}%)");
Console.Error.WriteLine();
}
if (dialogs.Exists(d => d.Buttons.Count > 0))
Console.Error.WriteLine(" Use 'unityctl dialog dismiss' to dismiss");
Console.Error.WriteLine();
}
catch
{
// Best-effort — never fail the parent error path
}
}

private void DisplayBridgeConnectionError()
{
Console.Error.WriteLine("Error: Failed to communicate with bridge.");
Expand Down
185 changes: 185 additions & 0 deletions UnityCtl.Cli/DialogCommands.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
using System;
using System.Collections.Generic;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.Linq;
using UnityCtl.Protocol;

namespace UnityCtl.Cli;

public static class DialogCommands
{
public static Command CreateCommand()
{
var dialogCommand = new Command("dialog", "Detect and dismiss Unity Editor popup dialogs");

// dialog list
var listCommand = new Command("list", "List detected popup dialogs");
listCommand.SetHandler((InvocationContext context) =>
{
var projectPath = ContextHelper.GetProjectPath(context);
var json = ContextHelper.GetJson(context);

var dialogs = FindDialogs(context, projectPath);
if (dialogs == null) return; // error already printed

if (json)
{
var infos = dialogs.Select(d => new DialogInfo
{
Title = d.Title,
Buttons = d.Buttons.Select(b => b.Text).ToArray(),
Description = d.Description,
Progress = d.Progress
}).ToArray();

Console.WriteLine(JsonHelper.Serialize(infos));
}
else
{
if (dialogs.Count == 0)
{
Console.WriteLine("No popup dialogs detected.");
}
else
{
Console.WriteLine($"Detected {dialogs.Count} dialog(s):");
Console.WriteLine();
foreach (var dialog in dialogs)
{
Console.Write($" \"{dialog.Title}\"");
if (dialog.Buttons.Count > 0)
{
var buttonLabels = dialog.Buttons.Select(b => $"[{b.Text}]");
Console.Write($" {string.Join(" ", buttonLabels)}");
}
if (dialog.Progress.HasValue)
{
var pct = (int)(dialog.Progress.Value * 100);
Console.Write($" ({pct}%)");
}
if (dialog.Description != null)
Console.Write($" - {dialog.Description}");
Console.WriteLine();
}
}
}
});

// dialog dismiss
var dismissCommand = new Command("dismiss", "Dismiss a popup dialog by clicking a button");

var buttonOption = new Option<string?>(
"--button",
"Button text to click (case-insensitive). If not specified, clicks the first button.");

dismissCommand.AddOption(buttonOption);
dismissCommand.SetHandler((InvocationContext context) =>
{
var projectPath = ContextHelper.GetProjectPath(context);
var json = ContextHelper.GetJson(context);
var buttonText = context.ParseResult.GetValueForOption(buttonOption);

var dialogs = FindDialogs(context, projectPath);
if (dialogs == null) return;

if (dialogs.Count == 0)
{
if (json)
{
Console.WriteLine(JsonHelper.Serialize(new { success = false, error = "No popup dialogs detected" }));
}
else
{
Console.Error.WriteLine("No popup dialogs detected.");
}
context.ExitCode = 1;
return;
}

var dialog = dialogs[0];
if (dialog.Buttons.Count == 0)
{
if (json)
{
Console.WriteLine(JsonHelper.Serialize(new { success = false, error = "Dialog has no buttons" }));
}
else
{
Console.Error.WriteLine($"Dialog \"{dialog.Title}\" has no detectable buttons.");
}
context.ExitCode = 1;
return;
}

// Resolve which button to click
var targetButton = buttonText ?? dialog.Buttons[0].Text;

var clicked = DialogDetector.ClickButton(dialog, targetButton);
if (clicked)
{
if (json)
{
Console.WriteLine(JsonHelper.Serialize(new
{
success = true,
dialog = dialog.Title,
button = targetButton
}));
}
else
{
Console.WriteLine($"Clicked \"{targetButton}\" on \"{dialog.Title}\"");
}
}
else
{
if (json)
{
Console.WriteLine(JsonHelper.Serialize(new
{
success = false,
error = $"Button \"{targetButton}\" not found",
availableButtons = dialog.Buttons.Select(b => b.Text).ToArray()
}));
}
else
{
Console.Error.WriteLine($"Button \"{targetButton}\" not found on \"{dialog.Title}\".");
Console.Error.Write(" Available: ");
Console.Error.WriteLine(string.Join(", ", dialog.Buttons.Select(b => b.Text)));
}
context.ExitCode = 1;
}
});

dialogCommand.AddCommand(listCommand);
dialogCommand.AddCommand(dismissCommand);
return dialogCommand;
}

private static List<DetectedDialog>? FindDialogs(InvocationContext context, string? projectPath)
{
var projectRoot = projectPath != null
? System.IO.Path.GetFullPath(projectPath)
: ProjectLocator.FindProjectRoot();

if (projectRoot == null)
{
Console.Error.WriteLine("Error: Not in a Unity project.");
Console.Error.WriteLine(" Use --project to specify project root");
context.ExitCode = 1;
return null;
}

var unityProcess = EditorCommands.FindUnityProcessForProject(projectRoot);
if (unityProcess == null)
{
Console.Error.WriteLine("Error: No Unity Editor found running for this project.");
context.ExitCode = 1;
return null;
}

return DialogDetector.DetectDialogs(unityProcess.Id);
}
}
Loading