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.
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.
#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.
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.
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.
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.
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.
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.
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
- 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
- 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
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
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
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
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
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
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
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
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
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
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
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
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.
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.
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.
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
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.
- 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).
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.
- 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).
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).
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
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