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
2 changes: 1 addition & 1 deletion .agents/skills/uloop-pause-point/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ If a `simulate-*` command instead returns a failure whose message says PlayMode

## Timeout Checks

If this command times out, the patched line was not reached while the command waited. Read `Error.Details.Hint` first: it names the most likely cause when PlayMode is not running, Unity is already paused, or the marker was enabled but never hit. A `PAUSE_POINT_EXPIRED` error means the marker's own `enable-pause-point --timeout-seconds` window (measured from enable, not from wait) ran out first — clear and re-enable the pause point using the returned `Id` and `TimeoutSeconds`. The countdown freezes while a hit holds the Editor paused; a manual pause without a hit does not stop it.
If this command times out, the patched line was not reached while the command waited. Read `Error.Details.Hint` first: it names the most likely cause when PlayMode is not running, Unity is already paused, or the marker was enabled but never hit. A `PAUSE_POINT_EXPIRED` error means the marker's own `enable-pause-point --timeout-seconds` window (measured from enable, not from wait) ran out first — clear and re-enable the pause point using the returned `Id` and `TimeoutSeconds`. When `--trigger` was passed, the expired envelope also carries `Error.Details.TriggerResult` (with `Completed: false` and no `Error` field when the trigger's outcome was still unknown at expiry). The countdown freezes while a hit holds the Editor paused; a manual pause without a hit does not stop it.

Use `uloop pause-point-status --id "Assets/Scripts/Enemy.cs:42"` only when you need to confirm the marker is armed or inspect the current hit state.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Read this before interpreting unexpected, missing, or truncated captured values,

- Nested previews stop at `MaxCollectionPreviewDepth` (2 levels) below each captured variable: past that, an object or collection renders as type-name-only text instead of expanding — a type name where you expected contents means you hit this cap, not a bug. The budget is counted per captured variable, so reaching a value through `this` costs one extra level compared to reading it as a direct local: `this.CurrentPiece.Origin` bottoms out as a type name, while a `dropped` local holding the same piece expands to `{Kind, RotationState, Origin: {X, Y}}`. When the value you need sits too deep, pick a pause point line where it is a direct local or parameter — as its own top-level entry it starts with a fresh full budget. Primitive leaves (numbers, strings, booleans, and any type that overrides `ToString()`) always render regardless of depth; only nested objects and collections get cut off.
- A value's `Value` string is not always its plain `ToString()`. A materialized collection (`List<T>`, arrays, dictionaries, ...) previews as a shallow JSON array/object instead of the default type-name text. A custom struct/class whose declared type does not override `ToString()` previews the same way — a shallow JSON object of its fields — so you do not need to add a temporary `ToString()` override just to see its contents. A type that does override `ToString()` keeps using that result unchanged. Either kind of preview is capped by depth, element count, and length like any other captured value; the element-count cap (default 10) and the preview's character budget both scale with `enable-pause-point --max-preview-elements` (1–1000). Raising it scales the character budget proportionally, so each element keeps the same ~100-character share it has at the default — plenty for numeric or boolean cells, but individually long elements can still be clipped by the scaled budget. The enable response echoes the effective `MaxPreviewElements`.
- A captured `Collision2D` is previewed as `{"Collider":{"Name":...,"UnityObjectPath":...},"OtherCollider":{...},"RelativeVelocity":...,"ContactCount":...}` — read `UnityObjectPath` to identify both colliding objects without an extra `execute-dynamic-code` round-trip. Each of `Collider` / `OtherCollider` is either that object form or the string `"(none)"` when the collider is null or destroyed.
- A multidimensional array (`int[,]`, `int[,,]`, ...) previews as `{"Shape":"Int32[2,3]","TotalElements":6,"Elements":[...]}` instead of a bare JSON array, since `Elements` alone would flatten every rank in row-major order with no way to tell it apart from an empty or 1D collection; a `T[]` or jagged `T[][]` array is unaffected and still previews as a plain JSON array.
- `CapturedVariablesTruncated=true` means at least one value was clipped to the length cap or the variable-count cap stopped enumeration; clipped values are still present up to the cap.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ uloop get-watch-values --id "speed"

Because a watch only re-evaluates on a changed, paused frame, a value that looks stuck across several reads usually means no new paused frame has occurred — most often the linked pause point has not been hit again (a marker on a conditional line freezes after its first hit; see Line Placement in SKILL.md). `get-watch-values` surfaces this as a non-empty `ValueFrozenHint` on the entry once the last few evaluations came back identical; treat it as a prompt to re-trigger the code path, not as proof the value cannot legitimately stay the same.

The expression may use `UloopPausePoint.TryGetCapturedValue("name")` to inspect the latest raw pause-point capture while paused. Each history entry includes the frame and either a stringified value or an explicit error type and message. A throwing expression is recorded as an error and does not stop the Editor update loop. `--max-history` accepts 1 through 100 and drops the oldest entries after the limit.
The expression may use `UloopPausePoint.TryGetCapturedValue("name")` to inspect the latest raw pause-point capture while paused. Each history entry includes the frame and either a stringified value or an explicit error type and message. Watch values are serialized with the same preview rules as pause point `CapturedVariables`: collections become compact JSON previews (e.g. `[0,1,2]`), and types with a custom `ToString()` keep their `ToString()` form. Previews share the capture-side caps (10 elements, 1024 characters); a clipped value sets `Truncated: true` on the history entry, and the freeze hint on truncated previews warns that changes beyond the caps are invisible. A throwing expression is recorded as an error and does not stop the Editor update loop. `--max-history` accepts 1 through 100 and drops the oldest entries after the limit.

## Lifetime

Expand Down
2 changes: 1 addition & 1 deletion .claude/skills/uloop-pause-point/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ If a `simulate-*` command instead returns a failure whose message says PlayMode

## Timeout Checks

If this command times out, the patched line was not reached while the command waited. Read `Error.Details.Hint` first: it names the most likely cause when PlayMode is not running, Unity is already paused, or the marker was enabled but never hit. A `PAUSE_POINT_EXPIRED` error means the marker's own `enable-pause-point --timeout-seconds` window (measured from enable, not from wait) ran out first — clear and re-enable the pause point using the returned `Id` and `TimeoutSeconds`. The countdown freezes while a hit holds the Editor paused; a manual pause without a hit does not stop it.
If this command times out, the patched line was not reached while the command waited. Read `Error.Details.Hint` first: it names the most likely cause when PlayMode is not running, Unity is already paused, or the marker was enabled but never hit. A `PAUSE_POINT_EXPIRED` error means the marker's own `enable-pause-point --timeout-seconds` window (measured from enable, not from wait) ran out first — clear and re-enable the pause point using the returned `Id` and `TimeoutSeconds`. When `--trigger` was passed, the expired envelope also carries `Error.Details.TriggerResult` (with `Completed: false` and no `Error` field when the trigger's outcome was still unknown at expiry). The countdown freezes while a hit holds the Editor paused; a manual pause without a hit does not stop it.

Use `uloop pause-point-status --id "Assets/Scripts/Enemy.cs:42"` only when you need to confirm the marker is armed or inspect the current hit state.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Read this before interpreting unexpected, missing, or truncated captured values,

- Nested previews stop at `MaxCollectionPreviewDepth` (2 levels) below each captured variable: past that, an object or collection renders as type-name-only text instead of expanding — a type name where you expected contents means you hit this cap, not a bug. The budget is counted per captured variable, so reaching a value through `this` costs one extra level compared to reading it as a direct local: `this.CurrentPiece.Origin` bottoms out as a type name, while a `dropped` local holding the same piece expands to `{Kind, RotationState, Origin: {X, Y}}`. When the value you need sits too deep, pick a pause point line where it is a direct local or parameter — as its own top-level entry it starts with a fresh full budget. Primitive leaves (numbers, strings, booleans, and any type that overrides `ToString()`) always render regardless of depth; only nested objects and collections get cut off.
- A value's `Value` string is not always its plain `ToString()`. A materialized collection (`List<T>`, arrays, dictionaries, ...) previews as a shallow JSON array/object instead of the default type-name text. A custom struct/class whose declared type does not override `ToString()` previews the same way — a shallow JSON object of its fields — so you do not need to add a temporary `ToString()` override just to see its contents. A type that does override `ToString()` keeps using that result unchanged. Either kind of preview is capped by depth, element count, and length like any other captured value; the element-count cap (default 10) and the preview's character budget both scale with `enable-pause-point --max-preview-elements` (1–1000). Raising it scales the character budget proportionally, so each element keeps the same ~100-character share it has at the default — plenty for numeric or boolean cells, but individually long elements can still be clipped by the scaled budget. The enable response echoes the effective `MaxPreviewElements`.
- A captured `Collision2D` is previewed as `{"Collider":{"Name":...,"UnityObjectPath":...},"OtherCollider":{...},"RelativeVelocity":...,"ContactCount":...}` — read `UnityObjectPath` to identify both colliding objects without an extra `execute-dynamic-code` round-trip. Each of `Collider` / `OtherCollider` is either that object form or the string `"(none)"` when the collider is null or destroyed.
- A multidimensional array (`int[,]`, `int[,,]`, ...) previews as `{"Shape":"Int32[2,3]","TotalElements":6,"Elements":[...]}` instead of a bare JSON array, since `Elements` alone would flatten every rank in row-major order with no way to tell it apart from an empty or 1D collection; a `T[]` or jagged `T[][]` array is unaffected and still previews as a plain JSON array.
- `CapturedVariablesTruncated=true` means at least one value was clipped to the length cap or the variable-count cap stopped enumeration; clipped values are still present up to the cap.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ uloop get-watch-values --id "speed"

Because a watch only re-evaluates on a changed, paused frame, a value that looks stuck across several reads usually means no new paused frame has occurred — most often the linked pause point has not been hit again (a marker on a conditional line freezes after its first hit; see Line Placement in SKILL.md). `get-watch-values` surfaces this as a non-empty `ValueFrozenHint` on the entry once the last few evaluations came back identical; treat it as a prompt to re-trigger the code path, not as proof the value cannot legitimately stay the same.

The expression may use `UloopPausePoint.TryGetCapturedValue("name")` to inspect the latest raw pause-point capture while paused. Each history entry includes the frame and either a stringified value or an explicit error type and message. A throwing expression is recorded as an error and does not stop the Editor update loop. `--max-history` accepts 1 through 100 and drops the oldest entries after the limit.
The expression may use `UloopPausePoint.TryGetCapturedValue("name")` to inspect the latest raw pause-point capture while paused. Each history entry includes the frame and either a stringified value or an explicit error type and message. Watch values are serialized with the same preview rules as pause point `CapturedVariables`: collections become compact JSON previews (e.g. `[0,1,2]`), and types with a custom `ToString()` keep their `ToString()` form. Previews share the capture-side caps (10 elements, 1024 characters); a clipped value sets `Truncated: true` on the history entry, and the freeze hint on truncated previews warns that changes beyond the caps are invisible. A throwing expression is recorded as an error and does not stop the Editor update loop. `--max-history` accepts 1 through 100 and drops the oldest entries after the limit.

## Lifetime

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
using System.Collections.Generic;

using Newtonsoft.Json.Linq;

using NUnit.Framework;

using UnityEngine;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor
{
/// <summary>
/// Verifies Collision2D capture previews expose collider hierarchy paths instead of raw IDs.
/// </summary>
[TestFixture]
public sealed class SourcePausePointCollision2DPreviewBuilderTests
{
private GameObject _rootGameObject;
private GameObject _childGameObject;
private GameObject _otherGameObject;

[TearDown]
public void TearDown()
{
if (_childGameObject != null)
{
Object.DestroyImmediate(_childGameObject);
_childGameObject = null;
}

if (_rootGameObject != null)
{
Object.DestroyImmediate(_rootGameObject);
_rootGameObject = null;
}

if (_otherGameObject != null)
{
Object.DestroyImmediate(_otherGameObject);
_otherGameObject = null;
}
}

[Test]
public void BuildPreviewToken_WithSceneColliders_IncludesHierarchyPaths()
{
// Verifies Collider/OtherCollider carry Name and UnityObjectPath in {scene}:/parent/child form.
_rootGameObject = new GameObject("Root");
_childGameObject = new GameObject("Enemy");
_childGameObject.transform.SetParent(_rootGameObject.transform);
BoxCollider2D collider = _childGameObject.AddComponent<BoxCollider2D>();

_otherGameObject = new GameObject("Ball");
BoxCollider2D otherCollider = _otherGameObject.AddComponent<BoxCollider2D>();

Vector2 relativeVelocity = new Vector2(1.5f, -2f);
const int contactCount = 2;

JToken token = SourcePausePointCollision2DPreviewBuilder.BuildPreviewToken(
collider, otherCollider, relativeVelocity, contactCount);

Assert.That(token["Collider"]["Name"].Value<string>(), Is.EqualTo("Enemy"));
Assert.That(
token["Collider"]["UnityObjectPath"].Value<string>(),
Is.EqualTo($"{_childGameObject.scene.name}:/Root/Enemy"));
Assert.That(token["OtherCollider"]["Name"].Value<string>(), Is.EqualTo("Ball"));
Assert.That(
token["OtherCollider"]["UnityObjectPath"].Value<string>(),
Is.EqualTo($"{_otherGameObject.scene.name}:/Ball"));
Assert.That(token["RelativeVelocity"].Value<string>(), Is.EqualTo(relativeVelocity.ToString()));
Assert.That(token["ContactCount"].Value<int>(), Is.EqualTo(contactCount));
}

[Test]
public void BuildPreviewToken_WithNullColliders_RendersNone()
{
// Verifies a null (or Unity fake-null) collider previews as "(none)".
JToken token = SourcePausePointCollision2DPreviewBuilder.BuildPreviewToken(
null, null, Vector2.zero, 0);

Assert.That(token["Collider"].Value<string>(), Is.EqualTo("(none)"));
Assert.That(token["OtherCollider"].Value<string>(), Is.EqualTo("(none)"));
Assert.That(token["ContactCount"].Value<int>(), Is.EqualTo(0));
}

[Test]
public void TryBuildToken_WithNonCollision2D_ReturnsFalse()
{
// Verifies non-Collision2D values leave the Collision2D special-case path.
List<int> value = new List<int> { 1, 2, 3 };

bool built = SourcePausePointCollision2DPreviewBuilder.TryBuildToken(value, out JToken token);

Assert.That(built, Is.False);
Assert.That(token, Is.Null);
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading