455 Commits

Author SHA1 Message Date
Andriy Mykhaylyshyn d8d06cbd5a fix: make stdio bridge command timeout configurable (default 5m)
Long-running tool calls (asset imports, test runs, batched edits) were
cut off ~30-90s into execution, so the task could never finish. On the
stdio transport this was governed by hardcoded values on both hops:

- Unity side: StdioBridgeHost.FrameIOTimeoutMs (30s const) capped every
  command's execution and frame I/O; on timeout the client reconnected
  and re-sent, which force-closed the prior client and made the bridge
  restart on a new port (the repeated "StdioBridgeHost started on port
  6400/6402" churn).
- Server side: ServerConfig.connection_timeout (30s socket recv) and
  command_total_timeout (90s cross-retry ceiling) cut the command off
  first.

Unlike the WebSocket transport (WebSocketTransportClient reads a per-call
timeout off the wire), the stdio bridge had no way to raise these.

Make all three configurable with a 5-minute default:
- FrameIOTimeoutMs: 30s -> 300s, env UNITY_MCP_STDIO_COMMAND_TIMEOUT_MS.
  ReceiveTimeout now scales with it (max(60s, timeout)).
- connection_timeout: 30s -> 300s, env UNITY_MCP_CONNECTION_TIMEOUT.
- command_total_timeout: 90s -> 600s, env UNITY_MCP_COMMAND_TOTAL_TIMEOUT.

Invalid/non-positive env values fall back to the default so a bad
override can't disable the timeout. Updates the config characterization
test to the new defaults.
2026-08-07 21:00:55 +03:00
Shutong Wu 5595c7be99 Merge pull request #1300 from kpkhxlgy0/codex/codedom-assembly-dedup
fix: deduplicate CodeDom assembly references
2026-08-03 11:23:34 -04:00
XiaoLongHan d2247e942e perf: cache CodeDom assembly paths 2026-08-03 14:42:32 +08:00
Shutong Wu 8bf889cf50 Merge pull request #1250 from beast-ofcourse/fix/execute-code-bom-phantom-error
fix(execute_code): skip phantom CodeDom BOM error from mcs compiler
2026-08-02 17:23:27 -04:00
Shutong Wu 07e8680df9 Merge pull request #1298 from asavs/fix/gameobject-create-component-properties
fix: make component properties reachable on manage_gameobject create
2026-08-02 16:25:25 -04:00
Shutong Wu 32428e820e Merge pull request #1302 from KamilDev/fix/resource-uri-prose
fix(server): address resources by URI in agent-facing prose
2026-08-02 12:21:12 -04:00
KamilDev 69267c536c fix(server): address resources by URI in agent-facing prose
A resource's name and its URI are deliberately different (`editor_state` vs
`mcpforunity://editor/state`), and the URI scheme is not derivable from the
name -- most resources are `category/thing` but several are flat
(`mcpforunity://instances`, `mcpforunity://menu-items`, `mcpforunity://tests`).

Several agent-facing strings still named resources without their URI, so an
agent following them built `mcpforunity://editor_state` and got a 404:

- server instructions listed resources by bare name and told the reader to
  "poll the `editor_state` resource's `isCompiling` field" (that field path is
  also wrong -- payloads are wrapped, so it is `data.compilation.is_compiling`)
- `refresh_unity`'s `wait_for_ready` parameter description referred to
  `editor_state.advice.ready_for_tools`
- the hint Unity returns in the `refresh_unity` result said "poll editor_state
  until ready_for_tools is true"

#1244 added a warning that names and URIs are not interchangeable, but left the
strings that trigger the mistake unchanged. Spell every resource reference as a
full URI instead, and correct the field paths while here.

Adds a regression test asserting that no agent-facing prose -- server
instructions, resource descriptions, tool and parameter descriptions, and
multi-word string literals under MCPForUnity/Editor -- mentions a resource by
its snake_case name without also giving that resource's URI.
2026-08-01 13:23:23 +10:00
XiaoLongHan 6f4c81224a fix: deduplicate CodeDom assembly references 2026-07-30 03:37:54 +08:00
Shutong Wu 267b465109 fix: restore HTTP transport for Codex (#1193)
#1292 declared Codex stdio-only. Tested against Codex CLI 0.47.0 with an
isolated CODEX_HOME, that is wrong: a bare

    [mcp_servers.unityMCP]
    url = "http://127.0.0.1:8123/mcp"

reports `transport: streamable_http` from `codex mcp get`, and Codex completes a
full MCP handshake against a live mcp-for-unity HTTP server - initialize 200,
notifications/initialized 202, SSE GET 200, tools/list 200 - with no feature flag
set at all. Adding [features] rmcp_client, the deprecated root-level
experimental_use_rmcp_client, both, or a deliberately bogus feature key all give
byte-identical results; unknown feature keys are silently ignored.

So #1292 removed a capability Codex has, for every Codex user.

Drop SupportsHttpTransport = false (the McpClient default is already true) and
delete the SupportedTransports override, since the base default is already
{ Stdio, Http }.

Delete the GetManualSnippet stdio coercion too. It was added by #1292 to stop a
stdio-only client rendering a url block, and CodexConfigurator is the only
subclass of CodexMcpConfigurator, so once Codex is HTTP-capable that branch is
unreachable.

Leave [features] rmcp_client = true alone: it is the current key name (the root
experimental_use_rmcp_client form is deprecated per openai/codex#6995), it is
harmless, and it enables the RMCP client that OAuth needs. Deliberately not
adding the deprecated key - it does nothing on current Codex and would just
linger in users' configs.

Tests now assert both transports and cover the snippet in both directions.

Caveat for review: this was verified against the Codex CLI. #1193 was reported
against Codex Desktop on Windows 11, which is untested here. #1292's remedy was
too broad, which does not mean the reporter was wrong - ask for their version and
CLI-vs-Desktop before closing #1193. If Desktop genuinely cannot do HTTP, that
belongs in Desktop-specific handling, not a blanket capability removal.
2026-07-29 00:18:25 -04:00
asavschaeffer 503d938b4a fix: make manage_gameobject component properties reachable on create
Fixes #1297. At action:"create", component_properties was accepted and
coerced by the C# dispatcher (ManageGameObject.cs) but only ever consumed
by the "modify" handler, so it silently did nothing. Meanwhile the shape
"create" already reads directly out of each componentsToAdd entry
({typeName, properties}) was rejected before it reached Unity, because the
Python schema typed components_to_add as list[str].

- GameObjectComponentHelpers.cs: factor the componentProperties loop +
  error aggregation out of GameObjectModify.cs into a shared
  ApplyComponentProperties helper, so both actions apply it identically.
- GameObjectCreate.cs: call the new helper after components are added,
  destroying the partially-created object and returning the error if any
  property fails to set (matching how component-add failures are handled).
- GameObjectModify.cs: switch to the shared helper (behavior-preserving
  refactor, no functional change on the modify path).
- manage_gameobject.py: widen components_to_add to accept
  {"typeName": ..., "properties": {...}} objects alongside plain strings,
  matching what GameObjectCreate.cs already reads.
- Regenerated website/docs/reference/tools/core/manage_gameobject.md via
  tools/generate_docs_reference.py for the updated parameter docs.

Tested: Server/tests/test_manage_gameobject.py exercises the Python
contract end-to-end, including a real fastmcp/pydantic schema validation
run of the issue's exact repro payloads (confirmed the pre-fix
ValidationError reproduces on the unmodified file, and is gone after).
Added TestProjects/UnityMCPTests/Assets/Tests/EditMode/Tools/
ManageGameObjectCreateTests.cs coverage for the C# side, but this was not
run against a live Editor.
2026-07-28 15:59:38 -07:00
Shutong Wu 94f192bfa3 Merge pull request #1291 from Scriptwonder/fix/1276-deferred-reload-compiling
fix: trust the pipeline flag when a domain reload is deferred (#1276)
2026-07-28 14:44:04 -04:00
Shutong Wu 70a96ec362 Merge pull request #1290 from Scriptwonder/fix/1279-windows-headless-stdin
fix: redirect stdin from NUL when launching the server on Windows (#1279)
2026-07-28 14:42:01 -04:00
Shutong Wu afc51b53e9 fix: force stdio in the Codex manual snippet, assert the exact transport set
Two review findings on #1292.

Copilot: advertising Codex as stdio-only was not enough. GetManualSnippet()
calls BuildCodexServerBlock directly, which reads the global UseHttpTransport
pref itself. Configure() gets that pref coerced for it by
ClientConfigurationService.ConfigureWithTransportCoercion; the snippet path
does not. With the HTTP pref on, the copyable snippet still rendered

    [features]
    rmcp_client = true
    [mcp_servers.unityMCP]
    url = "http://127.0.0.1:8080/mcp"

reintroducing the exact silent-failure path via manual setup. Coerce to stdio
around the call for any client that does not support HTTP, restoring the pref
afterwards, mirroring ConfigureWithTransportCoercion.

Deliberately not removing the HTTP branch from CodexConfigHelper: it is covered
by BuildCodexServerBlock_HttpMode_GeneratesUrlField and is a general-purpose
helper, so narrowing the caller is the smaller and more honest change.

CodeRabbit: assert SupportedTransports equals exactly { Stdio } rather than
contains-stdio plus not-contains-http, which would also pass if a third
transport were added.

Adds Codex_ManualSnippet_IsStdio_EvenWhenHttpPreferred, which fails with the
url block above before this change.
2026-07-28 13:12:00 -04:00
Shutong Wu 4ff8ff85b1 fix: advertise Codex as stdio-only (#1193)
Codex does not expose MCP tools that are configured through the HTTP block,
so writing an HTTP config produces a client that connects but surfaces no
tools.

Declare SupportsHttpTransport = false and restrict SupportedTransports to
stdio, so CoerceTransportFor settles on stdio before Configure() runs.
2026-07-28 12:05:43 -04:00
Shutong Wu 777e8a9c7a fix: trust the pipeline flag when a domain reload is deferred (#1276)
EditorApplication.isCompiling conflates three states: actually compiling,
compilation queued, and finished-but-reload-deferred. A project holding
EditorApplication.LockReloadAssemblies sits in the third state for as long
as the lock is held, with no compilation running, and isCompiling stays
true the whole time.

Eight call sites gated on that raw flag, so they refused work indefinitely:
the stdio bridge would not start, unity_reflect and manage_scriptable_object
returned "Unity is compiling", refresh_unity reported the wrong resulting
state and never completed its wait, the stdio reload handler deferred its
resume, and TestJobManager both mis-attributed its init timeout and reported
a bogus "compiling" block reason.

Route all eight through EditorStateCache.GetActualIsCompiling(), which falls
back to the event-tracked CompilationPipeline flag, and drop the isPlaying
gate that previously limited the workaround to play mode.

Verified live: with LockReloadAssemblies held after RequestScriptCompilation,
EditorApplication.isCompiling is true while the pipeline flag is false, so
CompilationPipeline.compilationFinished does fire while the reload is held.
2026-07-28 12:05:30 -04:00
Shutong Wu b954897efd fix: redirect stdin from NUL when launching the server on Windows (#1279)
The Editor is a console-less GUI process. TerminalLauncher spawned cmd.exe
with UseShellExecute=false and CreateNoWindow=true and never redirected
stdin, so uvx.exe inherited an invalid stdin handle and died with
"The handle is invalid. (os error 6)" before the server could start.

Redirect stdin from NUL inside the cmd.exe payload so the child gets a
valid handle regardless of whether the Editor has a console.

Regression from #1201, shipped in v10.1.0.
2026-07-28 12:05:19 -04:00
Shutong Wu 36cbe62fe1 refactor(asset-gen): dedupe generate_* tool shell (CodeRabbit review)
status / cancel / list_providers were near-identical across GenerateAudio,
GenerateImage and GenerateModel (differing only by a kind label + poll
interval), and NormalizeOutputFolder was a verbatim triple copy.

- New AssetGenToolHelpers.{Status,Cancel,ListProviders} — the three tools now
  delegate, passing their kind label / poll interval.
- NormalizeOutputFolder moved to AssetGenPaths (it already lived on
  TryGetAssetsFolder).
- Behaviour preserved (image `remove_background` arm kept; provider-list
  filtering unchanged). Net -120 lines of duplication.

Verified: EditMode 1167 tests / 0 failures + in-editor smoke (audio/image/model
list_providers, status/cancel error paths, image remove_background all intact).

Claude-Session: https://claude.ai/code/session_015KYy51gwBuhDuLZXXoqc98
2026-07-13 10:26:31 -07:00
Shutong Wu 26f8a1002e fix(asset-gen): address Copilot review — floor duration, fail-closed allowlist
- FalAudioAdapter.BuildBody: floor (not Math.Round) the clamped duration so
  it never exceeds the requested value, then enforce >= 1 (banker's rounding
  could round 2.5 -> 2 and could exceed the request).
- AssetGenJobManager.AllowedExtensionsFor: fail closed — an unexpected/unknown
  job kind now allows nothing instead of falling through to the model
  allowlist, so the RCE boundary never opens by default. Explicit cases for
  model + marketplace.
- Tests: pin floor semantics (10.9 -> 10) and the fail-closed behavior
  (unknown/null/empty kind -> not allowed).

Verified: EditMode 1167 tests / 0 failures + in-editor smoke (BuildBody(10.9)
-> duration=10; IsAllowed("bogus","glb")=false).

Claude-Session: https://claude.ai/code/session_015KYy51gwBuhDuLZXXoqc98
2026-07-13 00:44:28 -07:00
Shutong Wu ea3ce5a7e0 chore(asset-gen): "Asset Gen" tool-group label + regenerate reference docs
- McpToolsSection: add the asset_gen -> "Asset Gen" group display name so
  the tool-group tab no longer falls back to the raw "Asset_gen".
- Update the asset_gen group blurb (registry + CLI) to include audio gen.
- Regenerate the tool reference docs from the Python registry: add the
  missing generate_audio.md and refresh the asset_gen landscape/index.

Claude-Session: https://claude.ai/code/session_015KYy51gwBuhDuLZXXoqc98
2026-07-13 00:29:58 -07:00
Shutong Wu 4faf7a527b fix(asset-gen): security hardening + correctness fixes (+19 tests)
Security (from a dynamic security audit of the branch):
- H1: UnityWebRequestTransport disables auto-redirect on auth-bearing
  requests (redirectLimit=0) so a provider 3xx can't re-send the API key
  to a redirect host.
- H2/P8: per-kind result-extension allowlist in AssetGenJobManager.WriteFile
  (+ defense-in-depth in the audio/image import pipelines) — a provider
  can no longer land a .cs/.asmdef/.meta/.asset under Assets/ (Editor RCE).
- H3: ProviderHttp.RequireHost pins the submit URL and the provider-supplied
  response_url to https://queue.fal.run before the fal key is attached
  (both fal image + audio adapters).

Correctness (from code review):
- C1: Tripo image->3D now sends model_version.
- C2/C3/C6/C10: FalAudioAdapter.BuildBody is catalog-driven — duration-
  required models (CassetteAI SFX/Music, Stable Audio) send a default
  duration when the caller passes 0 (fixes the default-input 422),
  fractional durations floor to >=1, Lyria stays prompt-only and its GUI
  no longer advertises a duration it ignores, and the clamp ceilings come
  from the catalog (no more duplicated 190/30/180).
- C4: an unmapped fal poll status now fails fast instead of polling to the
  600s timeout (both fal adapters).
- C5: a stale/invalid selected-model pref is cleared on dropdown fallback.
- C7: the audio fal-key status refreshes when the shared 2D fal key changes.

Cleanup: extract AssetGenModelCatalog.ResolveModel (dedupes the model-
resolution chain across the three generate tools) + DefaultModelId no-alloc.

Verified: full EditMode suite 1166 tests, 0 failures (+19 new regression
tests); 34 Python asset-gen tests pass.

Claude-Session: https://claude.ai/code/session_015KYy51gwBuhDuLZXXoqc98
2026-07-13 00:29:48 -07:00
Shutong Wu 5e3107e470 feat(asset-gen): group 3D/2D/sound into darker panels; inline key status
Wraps each category (3D Models / 2D Images / Sound) in its own darker rounded panel
so they read as distinct blocks, and moves the per-provider key status (saved/not set)
up into the header row to the right of the provider name — reclaiming a line per
provider. Verified live: 3 panels (bg alpha 0.20), status inline in each header.

Claude-Session: https://claude.ai/code/session_01GCxmdd4qo7MG6J4M6WcT9Y
2026-07-12 14:32:25 -07:00
Shutong Wu 94122fb10e feat(asset-gen): rename the 'Asset Gen' tab to 'Generative'
Visible tab label only; the internal assetgen-tab id (and all C# wiring) is unchanged.

Claude-Session: https://claude.ai/code/session_01GCxmdd4qo7MG6J4M6WcT9Y
2026-07-12 14:24:57 -07:00
Shutong Wu a2890bf81a fix(asset-gen): model dropdown ballooned to a huge box
The provider row is a vertical (column) container, so the setting-dropdown-inline
class's flex-grow:1 stretched the DropdownField vertically. Wrap the Model dropdown
in a horizontal .setting-row with a .setting-label (matching the Format row) and use
a label-less DropdownField. Verified live: all 5 model dropdowns now render at 18px
inside setting-row containers instead of ~400px boxes.

Claude-Session: https://claude.ai/code/session_01GCxmdd4qo7MG6J4M6WcT9Y
2026-07-12 14:23:16 -07:00
Shutong Wu 236e718bab feat(asset-gen): GUI model dropdowns + fal audio row + refresh
Phases 4-5. Extends the existing Asset Gen tab (no new window): per-provider
'Model' dropdowns on the image + 3D rows with price/duration/use-case metadata, a
fal audio row (no key field — reuses the shared fal key) with a model dropdown and
the Stable Audio license caveat, and a Refresh button that re-validates key
presence + the curated catalog. Selecting a model writes the per-(kind,provider)
pref that generate_* reads as its default. fal has no public list-models API, so
Refresh is a curated re-validate (the plan's gated fallback), never a network fetch.
Characterization tests assert the new builders + three-phase lifecycle.

Claude-Session: https://claude.ai/code/session_01GCxmdd4qo7MG6J4M6WcT9Y
2026-07-12 11:21:25 -07:00
Shutong Wu 8c0a5dfe0d feat(asset-gen): model catalog + generate_audio tool + default-model wiring
Phases 2-3. Adds a curated AssetGenModelCatalog (image/3D/audio models with
price/duration/use-case metadata; defaults reference the adapter constants so the
panel default equals what an omitted model resolves to). Adds the generate_audio
MCP tool + CLI, threads a per-(kind,provider) selected-model pref so a GUI choice
becomes the default generate_* uses when no model is passed, and makes
Tripo/Meshy consume req.Model. Per-provider prefs (not per-type) so disjoint model
lists never clobber. Compiles on 2021.3 floor; Python + adapter/catalog tests green.

Claude-Session: https://claude.ai/code/session_01GCxmdd4qo7MG6J4M6WcT9Y
2026-07-12 11:20:16 -07:00
Shutong Wu 66b96d2626 feat(asset-gen): audio backend — fal adapter, import pipeline, job manager entry
Phase 1 of fal.ai audio generation. Adds AudioGenRequest + IAudioProviderAdapter,
a FalAudioAdapter fronting the v1 fal audio models (stable-audio-25, cassetteai/*,
lyria2), an AudioImportPipeline that branches AudioImporter load type on clip
length, AssetGenProviders.Audio + an audio List row, and
AssetGenJobManager.StartAudioGeneration. Also threads ModelGenRequest.Model
(consumed in Phase 3). Compiles on the 2021.3 floor; adapter/import/e2e tests green.

Claude-Session: https://claude.ai/code/session_01GCxmdd4qo7MG6J4M6WcT9Y
2026-07-12 10:59:53 -07:00
Shutong Wu 283e393f90 docs: address PR #1262 review feedback and regenerate tool reference
Review responses (CodeRabbit + Copilot):
- Document the 'legacy' animation_type value in the C# XML summary, MCP tool
  description, param help, and CLI help (Copilot x4).
- SKILL.md: drop multi-material zones as a GLB-only trigger (both formats keep
  slots), define out_glb in the glTF branch example, document the full
  animation_type contract, probe both Principled emission socket names
  ('Emission Color' 4.x / 'Emission' 3.x), keep use_active_scene=True in the
  Notes GLB example, and distinguish FBX vertex-color data transfer from URP
  Lit shader display.
- bridge-fidelity.md: make export_apply conditional (static-only; skinned/
  shape-key meshes need False) and document export_animations' active/NLA-
  stashed semantics.

Regenerate website/docs/reference for the updated tool description — fixes the
"Check docs reference is fresh" CI failure ("differs: asset_gen/import_model_file.md").

Claude-Session: https://claude.ai/code/session_01Cya1SZmg7CJgJS61nhjLH4
2026-07-12 00:15:47 -07:00
Shutong Wu 0eee1568d8 feat(asset-gen): add animation_type rig mode to import_model_file
A rigged/animated FBX imported through import_model_file always arrived
with zero AnimationClips because the import pipeline hard-coded
ModelImporterAnimationType.None. Thread an optional animation_type param
(none|generic|humanoid|legacy) through the MCP tool, the CLI command, and
the C# pipeline so callers can surface a model's clips; the default stays
None, so existing imports are unchanged. glTF/GLB ignores the knob
(glTFast imports animation itself).

Also fold the tested Blender-handoff findings into the blender-to-unity
skill: GLB-vs-FBX fidelity guidance (with the new
references/bridge-fidelity.md matrix, force-added since .claude is
gitignored), FBX emission restoration steps, and the animation_type knob.
Sync uv.lock with the pyproject 10.0.0 version already on beta.

Claude-Session: https://claude.ai/code/session_01Cya1SZmg7CJgJS61nhjLH4
2026-07-11 22:59:48 -07:00
Shutong Wu e67d653502 Merge pull request #1234 from Scriptwonder/fix/issue-1229-reload-resume-race
fix(editor): survive domain reloads in HTTP auto-start and reload-resume (#1229)
2026-07-11 10:05:56 -07:00
Shutong Wu 9eaadd8dcd Merge beta (b7213383) into #1207 orphan-detector branch
Semantic coexistence resolution in McpConnectionSection.cs: keep both
sibling debounce mechanisms intact — beta's health-check verification
debounce (UnhealthyVerificationThreshold / ShouldReportUnhealthy /
consecutiveVerifyFailures, c344625e) and this branch's orphaned-session
down-poll debounce (OrphanedSessionDownPollThreshold /
ShouldEndOrphanedSession / consecutiveServerDownPolls + reset on
session start). Both EditMode test files
(McpConnectionSectionHealthDebounceTests, McpConnectionSectionOrphanDetectionTests)
are preserved.

Claude-Session: https://claude.ai/code/session_01C8TU8ibk8gv3h4LqBxPbQi
2026-07-10 22:53:48 -07:00
Beast-ofcourse 65983021aa fix(manage_build): stop forcing PVRTC texture compression on Android via subtarget
When manage_build builds for Android, BuildPlayerOptions.subtarget was
always set to StandaloneBuildSubtarget.Player (0). On Android, subtarget
maps to MobileTextureSubtarget (texture compression format). Building
with subtarget=0 on Unity 6000+ triggers a confirmed Unity bug (IN-102413)
that forces PVRTC texture compression — ignoring the project's Player
Settings (ASTC/ETC2).

Worse: setting subtarget persists the value in EditorUserBuildSettings,
overwriting whatever the user configured in Build Settings.

Fix: only set subtarget for Standalone platforms (Windows/OSX/Linux),
where it distinguishes Server vs Player builds. For all other platforms
(Android, iOS, tvOS, WebGL, etc.), leave subtarget at its default so
Unity respects the project's Player Settings.

Fixes #1212
2026-07-07 13:06:05 +05:30
Beast-ofcourse 127315b7d9 fix(execute_code): skip phantom CodeDom BOM error from mcs compiler
Mono's mcs compiler emits a stray U+FEFF BOM character on stdout during
compilation. CodeDom's CSharpCodeProvider misinterprets this as a compiler
error (no ErrorNumber, ErrorText is just the BOM), causing HasErrors to be
true and results.CompiledAssembly to be null — even though mcs exits 0 and
wrote the DLL successfully.

Fix:
- Compile to a temp DLL path (GenerateInMemory=false + OutputAssembly)
  instead of relying on results.CompiledAssembly.
- Skip phantom errors where ErrorNumber is empty and ErrorText is only
  BOM/whitespace.
- Load the produced DLL via Assembly.Load(File.ReadAllBytes(...)) when
  no real errors exist.
- Clean up the temp DLL in a finally block.

Fixes #1186
2026-07-07 11:57:56 +05:30
Shutong Wu afb1a4d56e Merge pull request #1236 from Scriptwonder/fix/issue-1023-session-isolation
fix(server): stop silent cross-instance routing when multiple editors are connected (#1023)
2026-07-06 14:22:18 -07:00
Shutong Wu c344625e4c fix(editor): stop stdio connection UI showing false "Bridge not running"
Two related stdio connection-UI reliability bugs surfaced while testing
with a live domain-reload cycle:

1. Start Session race (severe). StdioTransportClient.StartAsync returned
   true unconditionally, then callers immediately verified — but
   VerifyAsync only reads StdioBridgeHost.IsRunning, which is still false
   while the previous port releases after a reload (Start() defers the
   bind to an editor-idle retry, or falls back to a new port after
   BusyPortFallbackWindowSeconds). Result: "Connection verification
   failed: Bridge not running", and Start Session only connected after
   several clicks. StartAsync now waits (bounded, ReadyWaitTimeoutSeconds)
   for the bridge to actually bind before reporting success.

2. Health-indicator flash. VerifyBridgeConnectionInternalAsync flipped the
   indicator to Unhealthy on a single transient verify miss during a
   reload/port-hop, then recovered — misleading. It now debounces via
   UnhealthyVerificationThreshold (mirrors the #1207 orphan-session
   debounce), resetting on any reachable result.

Both decisions are pure, unit-tested helpers (ShouldKeepWaitingForReady,
ShouldReportUnhealthy). 16 EditMode tests green (4 readiness + 5 debounce
+ 7 existing #1207 orphan, no regression).

Claude-Session: https://claude.ai/code/session_015JRaRFZy4piZzZtW5NabJS
2026-07-06 11:06:01 -07:00
Shutong Wu d72859700f Merge pull request #1227 from GallopingDino/fix/multiline-console-logs
fix: preserve multi-line message body in read_console
2026-07-05 21:37:47 -07:00
Shutong Wu 2c8ae3ec33 fix(editor): reset the orphan down-poll streak when a session starts
Review follow-up: a streak accumulated while no session was running (detector
inert, counter still counting) survived into a freshly started session and
could satisfy orphan detection before the first post-start probe refreshed.
Reset on the not-running -> running transition, which covers every start path
(manual Connect, auto-start, resume) since UpdateConnectionStatus runs on the
UI tick.
2026-07-04 17:49:13 -07:00
Shutong Wu d42c7d352d fix(editor): track compilation via pipeline events; harden StartAsync mode + retry-loop races
Review follow-ups:
- CompilationPipeline.isCompiling does not exist on the supported Unity range
  (reflection probe on 2021.3 and 6000.4: neither public nor non-public), so
  the reflected play-mode double-check never resolved and GetActualIsCompiling
  silently fell back to the raw signal — the #549 false positive was never
  actually mitigated. Track compilationStarted/compilationFinished events
  instead (public across the range); in Play mode trust the event-tracked
  state, outside it the raw signal is reliable.
- TransportManager.StartAsync validates the mode before touching the
  coalescing slots, matching the class's unsupported-mode contract instead of
  routing unknown values to the stdio slot.
- The resume retry loop's transport/IsRunning pre-checks moved inside the
  attempt try: a service read racing the reload boundary now burns a retry
  instead of killing the fire-and-forget task with the flag still set.
2026-07-04 17:48:07 -07:00
Shutong Wu 18158fa353 fix: stop tearing down healthy HTTP sessions on transient probe misses (#1207)
The orphaned-session detector ended an active session on a SINGLE stale
reachability reading, and the reading came from a lone 50ms TCP connect
cached for 0.75s — trivially false-negative on a machine busy with test
runs or domain reloads. Evidence bundles in #1207 show 13 teardowns and
147 socket closures in one session from exactly this loop, wedging the
bridge in no_unity_session churn until manual recovery.

- Require 3 consecutive failed polls (0.75s cadence) before declaring a
  session orphaned; probe readings taken while the editor is compiling or
  importing don't count toward teardown, and detection is skipped entirely
  while busy.
- Raise the probe's connect wait 50ms -> 250ms, as an overall budget shared
  across candidate hosts so the worst-case main-thread wait cannot multiply.
- Honor UNITY_MCP_SESSION_RESOLVE_MAX_WAIT_S above 20s (ceiling now 120s;
  default unchanged): the old ceiling equalled the default, silently
  neutering the documented escape hatch for projects whose reloads or test
  boundaries legitimately exceed 20s. Same treatment for
  UNITY_MCP_SESSION_READY_WAIT_SECONDS, and both now share one bounded
  env-read helper.

The remaining piece of #1207 (keepalive reload-awareness in
WebSocketTransportClient) is untouched here: the resume machinery reworked
in #1234 already covers reload boundaries, and the detector debounce
removes the dominant churn source dsarno identified.
2026-07-04 15:42:52 -07:00
Shutong Wu 7cfb1afc07 fix(editor): skip shutdown cleanup in batch mode so CI instances don't stop the interactive server
OnEditorQuitting stopped the managed HTTP server unconditionally; the pidfile+port handshake is global per-user state, so a -batchmode/CI instance killed the interactive editor's server on quit. Add an early-return guard mirroring HttpAutoStartHandler/StdioBridgeHost (skip in batch unless UNITY_MCP_ALLOW_BATCH), extracted into a testable ShouldRunCleanup predicate + EditMode regression test.

Closes #1196, #1010
2026-07-04 15:33:14 -07:00
Shutong Wu 301511700c fix(editor): survive domain reloads in HTTP auto-start and reload-resume (#1229)
Auto-start died for the whole session whenever startup included a compile:
the ctor latched SessionState before the delayCall ran, and the reload wiped
the delayCall. Reload-resume died at multi-pass compiles: the one-shot flag
was consumed before the deferred (delayCall) resume ran, and the next
boundary deleted it again.

- Replace delayCall with EditorApplication.update ticks that the
  [InitializeOnLoad] ctor re-arms on every domain load; latch only when the
  deferred work actually dispatches, retry (bounded per domain) while editor
  services are still initializing, and skip the subscription entirely in the
  common case where auto-start is off and nothing is pending.
- Move the resume flag from EditorPrefs (per-user machine-global, survives
  crashes, leaks across concurrently open editors) to SessionState; keep it
  until the resume succeeds, is cancelled, or exhausts its retries, instead
  of consuming it at boundaries where the bridge is down. Manual Connect,
  End Session, transport switch, and orphan cleanup cancel a pending resume
  through a named seam (CancelPendingResume), which also aborts an in-flight
  retry loop; exhaustion erases the flag so later reloads don't replay 49s
  failure loops.
- Serialize TransportManager.StartAsync per mode: concurrent starts coalesce
  onto one in-flight attempt, so a manual Connect can no longer race the
  resume/auto-start loops into bouncing a just-established session
  (WebSocketTransportClient.StartAsync tears down a live connection first).
- A SessionState connect-pending marker lets the next domain load finish an
  auto-start whose connect phase a reload killed — connect-only, never
  re-spawning (StartLocalHttpServer stops a still-booting server first).
  Whether a launch-process handle exists is now answered live by
  ServerManagementService.HasManagedServerLaunchHandle; without one (post-
  reload, or an externally started server) the wait polls to the 5-minute
  hard cap instead of fail-fasting.
- Busy gate uses EditorStateCache.GetActualIsCompiling (now internal, with
  the CompilationPipeline reflection bound once as a delegate): raw
  isCompiling stays true all play session under
  Recompile-After-Finished-Playing (#549).
- One-time (per session) migration deletes the legacy EditorPrefs flag.

The stdio sibling has the same defect class (StdioBridgeReloadHandler.cs:65
delete-when-not-running, :133 delayCall) — follow-up, kept out of scope here,
along with the remaining stdio copies of the isCompiling probe.
2026-07-04 10:46:24 -07:00
Shutong Wu 42d9faaaf2 refactor(editor): drop OceanMark test, tuck version-gated members into the #if
- Remove OceanMarkTests (geometry/color mapping + construction) per request;
  the mark is verified visually in-Editor.
- With the test gone, MapSvgPoint/FromHex no longer need to be `internal`,
  and they (plus the SvgOrigin/SvgSize consts) are only used by the Painter2D
  path — move them inside `#if UNITY_2022_1_OR_NEWER` and make them `private`,
  so the 2021.3 fallback carries no unused members.

Result of a /simplify pass; reuse/altitude/efficiency angles came back clean
(logo-injection duplication left at 2 call sites per the repo's 3+-uses rule).
2026-07-03 17:16:46 -07:00
Shutong Wu c46d86dd18 fix(editor): harden uv-install lifecycle + log brand-icon fallback failure
Addresses AI review (Copilot + CodeRabbit) on #1231:
- PollUvInstall: bail out (and detach) if the window/UI was torn down while
  the task ran, guarding against dereferencing UI fields after teardown.
- OnEnable: resume polling if an install was still in flight when the window
  was disabled, so completion is still processed (button reset, deps re-checked).
- OceanMark.LoadBrandTexture: log a warning instead of silently swallowing
  asset-load failures on the 2021.3 raster-fallback path.

Declined CodeRabbit's "use a compat shim" suggestion for OceanMark's
#if UNITY_2022_1_OR_NEWER: per UnityCompatShims.cs policy, shims are for
[Obsolete] APIs, 3+ gated call sites, or announced removals — Painter2D is a
new API used via static dispatch in one self-contained file, which the policy
explicitly says to handle with #if, not a shim.
2026-07-03 15:15:06 -07:00
Shutong Wu 3e36002ded fix(editor): polish setup wizard + main-window narrow-width layout
- Setup wizard: de-box the title beside the logo; footer buttons size to
  content and sit together on one row (Refresh no longer stretches
  full-width and overflows); rename the deps-step "Done" button to "Next".
- Client Configuration dialog: show the count + only failures + next step
  instead of enumerating every successfully configured client.
- Main window header: version pill never shrinks/clips (flex-shrink 0);
  title stays at natural width.
- Tabs: flex-shrink so all six stay reachable when docked narrow.
- Connection status label wraps/shrinks instead of sliding under the
  Disconnect button (scoped to #connection-section).
2026-07-03 11:19:02 -07:00
Shutong Wu e677663497 feat(editor): embed Ocean brand mark in Editor UI + setup polish
Add the Ocean split-cube brand mark to the Editor UI and land three
low-risk installation-flow improvements.

Logo:
- OceanMark: reusable UI Toolkit control drawing the mark in Painter2D
  vector (geometry transcribed from logo-mark.svg, brand colors baked in),
  crisp at any DPI with no new package dependency. Painter2D is 2022.1+,
  so it is guarded by #if UNITY_2022_1_OR_NEWER with a raster fallback
  (package-icon.png) on the 2021.3 floor.
- Embedded in the main window header (#header-left group) and the
  setup-wizard header.
- Ship package-icon.png inside the package (listing icon + 2021.3 fallback).

Installation polish:
- One-click uv install: UvInstaller builds the official installer command
  per platform; setup wizard runs it off-thread with a confirm dialog and
  re-checks dependencies.
- Claude CLI auto-discovery: consolidate the weaker resolver into
  ExecPath.ResolveClaude() and add the ~/.claude/local (migrate-installer)
  location both resolvers missed.
- Setup wizard clarity: success/next-step copy after configuring clients.

Tests: EditMode coverage for the geometry/color mapping, per-platform uv
command, and the Claude override contract.

Verified: compiles clean on 2021.3.45f2 (fallback path) and 6000.4.11f1
(Painter2D path).
2026-07-02 22:09:23 -07:00
Vladimir Kuznetsov 85b693958a fix: preserve multi-line message body in read_console 2026-07-01 23:24:01 +07:00
Shutong Wu 36fcefbcbf Merge remote-tracking branch 'upstream/beta' into revamp/brand-distribution-analytics
# Conflicts:
#	README.md
2026-06-30 11:33:02 -07:00
Shutong Wu a884ada409 Clarify generate image unsupported action docs 2026-06-30 10:37:05 -07:00
Shutong Wu 39eb562d9a Harden asset generation file paths 2026-06-29 09:15:59 -07:00
Shutong Wu 4b4b7bd4dd fix(asset-gen): address image_path code-review findings
Follow-up to 2efb7860, from a focused AI review of the image_path work:

- Validate image input synchronously in the handlers: reject unsupported
  extensions (LocalImage.ResolveExisting) and Tripo local-image up front, so bad
  input fails immediately instead of returning a fake "pending" that only faults
  on the next poll.
- Meshy: the "no task id" submit/refine error again includes the response body
  (the PostTask refactor had dropped it).
- Sketchfab archive allowlist: add .dds/.ktx (real GPU texture formats) so glTF
  textures in those formats aren't silently skipped on extract.
- fal: don't send image_size to the /edit (image->image) endpoint, which derives
  size from the source image; build the request_id fallback poll URL from the base
  model id rather than the /edit sub-path.
- Meshy two-phase preview+refine now runs under a 600s job timeout (was 300s) so a
  default textured text->3D has room for both tasks.

Deferred (perf-only, noted on the PR): the blanket AssetDatabase.Refresh
double-import and the synchronous main-thread base64 of large local images.

Verified: package compiles clean; EditMode 977/1023 pass, 0 failures via
tools/local_harness.py.

Claude-Session: https://claude.ai/code/session_015DAUrMR5UaSEzEn2wNPrEP
2026-06-28 23:38:02 -07:00
Shutong Wu 5ad4e5eb55 feat(asset-gen): Blender -> Unity handoff row in the Asset Gen tab
Informational row (not a keyed provider) with a best-effort "Blender app detected"
status and a pointer to the blender-to-unity workflow / import_model_file. BlenderMCP
runs in the AI client and isn't detectable from Unity, so this reports only the
locally installed Blender app.

- BlenderDetection helper: checks well-known install paths + PATH per platform; the
  pure DetectIn(candidates, exists) core is unit-tested with a fake predicate.
- McpAssetGenSection: AddBlenderHandoffRow() appended after the provider rows.
- README: note the tab's best-effort detection + that BlenderMCP lives in the AI client.

Verified: package compiles clean; EditMode 972/1018 pass, 0 failures (4 new
BlenderDetection tests) via tools/local_harness.py.

Claude-Session: https://claude.ai/code/session_015DAUrMR5UaSEzEn2wNPrEP
2026-06-28 23:19:59 -07:00