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
61 changes: 39 additions & 22 deletions .github/workflows/split-upm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,13 @@ jobs:
contents: read
steps:
- name: Check out source
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Install pinned Rust toolchain
run: rustup toolchain install "$RUST_TOOLCHAIN" --profile minimal

- name: Cache Cargo inputs
uses: Swatinem/rust-cache@aa7c1c80a07a27a84c0aa76d0cef0aad3830e330 # v2.7.8
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: native

Expand Down Expand Up @@ -83,7 +83,7 @@ jobs:
git diff --exit-code -- "Packages/dev.tnayuki.unterm/Third Party Notices.md"

- name: Upload SBOM and notices
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: unterm-supply-chain
path: |
Expand All @@ -94,20 +94,20 @@ jobs:
retention-days: 30

build-macos:
runs-on: macos-latest
runs-on: macos-15
permissions:
contents: read
steps:
- name: Check out source
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Install pinned Rust toolchain
run: |
rustup toolchain install "$RUST_TOOLCHAIN" --profile minimal --component clippy
rustup default "$RUST_TOOLCHAIN"

- name: Cache Cargo inputs
uses: Swatinem/rust-cache@aa7c1c80a07a27a84c0aa76d0cef0aad3830e330 # v2.7.8
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: native

Expand All @@ -117,7 +117,7 @@ jobs:
cargo test -p unterm --lib --locked
# Deny new warnings while preserving the upstream crate's documented
# legacy lint debt until those broad refactors can be reviewed separately.
cargo clippy -p unterm --lib --tests --locked -- \
cargo clippy -p unterm --lib --bins --tests --locked -- \
-D warnings \
-A clippy::too_many_arguments \
-A clippy::type_complexity \
Expand All @@ -135,23 +135,32 @@ jobs:

- name: Write checksum and build provenance
run: |
BINARY="Packages/dev.tnayuki.unterm/Editor/Plugins/macOS/unterm.dylib"
shasum -a 256 "$BINARY" > unterm-macos.sha256
LIBRARY="Packages/dev.tnayuki.unterm/Editor/Plugins/macOS/unterm.dylib"
DEBUGGER="Packages/dev.tnayuki.unterm/Editor/Plugins/macOS/unterm-debugger"
test -x "$DEBUGGER"
library_sha256="$(shasum -a 256 "$LIBRARY" | awk '{print $1}')"
debugger_sha256="$(shasum -a 256 "$DEBUGGER" | awk '{print $1}')"
printf '%s %s\n%s %s\n' \
"$library_sha256" "$(basename "$LIBRARY")" \
"$debugger_sha256" "$(basename "$DEBUGGER")" \
> unterm-macos.sha256
jq -n \
--arg commit "$GITHUB_SHA" \
--arg runner "macos-latest" \
--arg runner "macos-15" \
--arg toolchain "$RUST_TOOLCHAIN" \
--arg sourceDateEpoch "$(git log -1 --pretty=%ct)" \
--arg sha256 "$(awk '{print $1}' unterm-macos.sha256)" \
'{schemaVersion:1, commit:$commit, runner:$runner, rustToolchain:$toolchain, sourceDateEpoch:$sourceDateEpoch, artifact:"unterm.dylib", sha256:$sha256}' \
--arg sha256 "$library_sha256" \
--arg debuggerSha256 "$debugger_sha256" \
'{schemaVersion:1, commit:$commit, runner:$runner, rustToolchain:$toolchain, sourceDateEpoch:$sourceDateEpoch, artifact:"unterm.dylib", sha256:$sha256, debuggerArtifact:"unterm-debugger", debuggerSha256:$debuggerSha256, debuggerSignature:"ad-hoc"}' \
> build-provenance-macos.json

- name: Upload macOS build
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: unterm-macos
path: |
Packages/dev.tnayuki.unterm/Editor/Plugins/macOS/unterm.dylib
Packages/dev.tnayuki.unterm/Editor/Plugins/macOS/unterm-debugger
unterm-macos.sha256
build-provenance-macos.json
if-no-files-found: error
Expand All @@ -163,7 +172,7 @@ jobs:
contents: read
steps:
- name: Check out source
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0

- name: Install pinned Rust toolchain
shell: pwsh
Expand All @@ -172,7 +181,7 @@ jobs:
rustup default $env:RUST_TOOLCHAIN

- name: Cache Cargo inputs
uses: Swatinem/rust-cache@aa7c1c80a07a27a84c0aa76d0cef0aad3830e330 # v2.7.8
uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: native

Expand All @@ -184,8 +193,13 @@ jobs:
shell: pwsh
run: |
$binary = 'Packages/dev.tnayuki.unterm/Editor/Plugins/Windows/x86_64/unterm.dll'
$debugger = 'Packages/dev.tnayuki.unterm/Editor/Plugins/Windows/x86_64/unterm-debugger.exe'
$hash = (Get-FileHash -Algorithm SHA256 -LiteralPath $binary).Hash.ToLowerInvariant()
"$hash unterm.dll" | Set-Content -Encoding ascii unterm-windows.sha256
$debuggerHash = (Get-FileHash -Algorithm SHA256 -LiteralPath $debugger).Hash.ToLowerInvariant()
@(
"$hash unterm.dll"
"$debuggerHash unterm-debugger.exe"
) | Set-Content -Encoding ascii unterm-windows.sha256
$sourceDateEpoch = (git log -1 --pretty=%ct)
[ordered]@{
schemaVersion = 1
Expand All @@ -195,14 +209,17 @@ jobs:
sourceDateEpoch = $sourceDateEpoch
artifact = 'unterm.dll'
sha256 = $hash
debuggerArtifact = 'unterm-debugger.exe'
debuggerSha256 = $debuggerHash
} | ConvertTo-Json | Set-Content -Encoding utf8 build-provenance-windows.json

- name: Upload Windows build
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: unterm-windows
path: |
Packages/dev.tnayuki.unterm/Editor/Plugins/Windows/x86_64/unterm.dll
Packages/dev.tnayuki.unterm/Editor/Plugins/Windows/x86_64/unterm-debugger.exe
unterm-windows.sha256
build-provenance-windows.json
if-no-files-found: error
Expand All @@ -218,24 +235,24 @@ jobs:
attestations: write
steps:
- name: Download macOS build
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: unterm-macos
path: artifacts/macos

- name: Download Windows build
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: unterm-windows
path: artifacts/windows

- name: Download supply-chain evidence
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: unterm-supply-chain
path: artifacts/supply-chain

- name: Attest native binaries and evidence
uses: actions/attest-build-provenance@e8998f949152b193b063cb0ec769d69d929409be # v2.4.0
- name: Attest native libraries, debugger executables, and evidence
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: artifacts/**/*
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,6 @@ InitTestScene*.unity*
# with `native/build-macos.sh` and `cargo about` (see native/about.toml). Their
# `.meta` files stay tracked so GUIDs are stable.
/Packages/dev.tnayuki.unterm/Editor/Plugins/macOS/unterm.dylib
/Packages/dev.tnayuki.unterm/Editor/Plugins/macOS/unterm-debugger
/Packages/dev.tnayuki.unterm/Editor/Plugins/Windows/x86_64/unterm.dll
/Packages/dev.tnayuki.unterm/Editor/Plugins/Windows/x86_64/unterm-debugger.exe
18 changes: 18 additions & 0 deletions Packages/dev.tnayuki.unterm/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
# Changelog

## [Unreleased]

### Added

- Unterm now ships a full Mono debugger for play mode. Enable it under Preferences ▸ Unterm ▸ Debugger and click in the code editor's gutter to set breakpoints (a dedicated dot column appears next to the line numbers); pressing Play then just debugs — the debugger attaches to the editor's Mono runtime over the soft-debugger protocol, arms your breakpoints as their types load (so a breakpoint in `Awake`/`Start` hits from the very first frame), and pops to the front when one hits. It runs as a standalone process with its own window (`Window ▸ Unterm ▸ Debugger (Standalone Process)`, ⇧⌘D), so it stays fully interactive while the editor is frozen at a breakpoint, and it persists across Play/Stop cycles.
- The debugger window gives you the essentials of an IDE debugger, rendered with the same native code editor as Unterm's own: a project file tree, a read-only source view with the executing line highlighted, continue / step over / into / out (also `c`/`n`/`i`/`o`), pause ("break all", `p`), a call stack you can click to inspect any frame, a thread list when a stop involves multiple managed threads, and a variables panel showing locals, `this`, and inherited fields — private members included — with lazy expansion of objects and arrays (reference cycles like `transform ↔ gameObject` are handled). Hovering an identifier in the source shows its current value, and the breakpoint list jumps to source on click.
- Breakpoints live in `Library/Unterm/breakpoints.json`, shared between the editor and the debugger and synced live in both directions: toggling in the code editor arms/disarms in a running debug session immediately (even mid-play), and toggling in the debugger — including while the editor is frozen at a breakpoint — updates the editor's gutter dots as soon as it runs again.
- The debugger can attach to more than the editor: development builds with script debugging enabled are discovered on the network via Unity's player multicast and offered in a target picker (labelled with their project and host), and source locations reported by a player built on another machine are mapped back to this project's files for display and breakpoints.

- The code editor now syntax-highlights Markdown while editing and can render it (toggle Preview with ⇧⌘V, or the tab ⋮ / right-click menu); a `.md` opened from a transcript or preview link opens rendered by default, while a Project double-click opens for editing.

- The set of file extensions the code editor opens is now its own preference (Preferences ▸ Unterm ▸ Code Editor, semicolon-separated, with a reset button) instead of borrowing Unity's C# project-generation extension settings — Unterm never generates `.csproj` files, so tying "what opens" to a generation setting that never runs was misleading. The default covers Unity's code and text formats plus the docs, configs, and native-plugin sources an agent transcript typically links (`md`, `yml`, `toml`, `rs`, `c`/`cpp`, `java`/`kt`, `swift`, shell scripts, …), so those open in-editor out of the box.

### Fixed

- The Unterm code editor stays selected as the External Script Editor across package updates — it's now recognized by package identity rather than the exact cached path (which changes each update), so script and link opens keep landing in Unterm instead of silently falling back to another editor.
- File-path links in the Claude Code transcript now underline every existing file instead of only a hard-coded list of code extensions, and a click always opens the file somewhere: the configured script editor first, then Unity's own asset pipeline on decline (a scene link loads the scene, a prefab opens in prefab mode, an image opens in its associated app), and the OS default app for files outside the asset database — so paths like `native/**/*.rs` or `docs/*.md` no longer show an underline that does nothing when clicked.

## [0.6.0] - 2026-07-08

### Added
Expand Down

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

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

7 changes: 4 additions & 3 deletions Packages/dev.tnayuki.unterm/Editor/UntermAgentWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -923,9 +923,10 @@ private void HandlePanelMouse(Rect rect)

case EventType.MouseUp when _selecting:
_selecting = false;
// A plain click (no drag-selection) on a file path opens it through
// the configured script editor. OpenFromAgent no-ops for non-file /
// non-editable tokens (the underline only marks files that exist).
// A plain click (no drag-selection) on a file path opens it: the
// configured script editor first, then Unity's asset pipeline or
// the OS default app on decline. OpenFromAgent no-ops for tokens
// that aren't existing files (matching the underline).
if (!_native.AgentviewPanelHasSelection(Vid))
{
string tok = _native.AgentviewPanelTokenAt(Vid, lx, ly);
Expand Down
148 changes: 148 additions & 0 deletions Packages/dev.tnayuki.unterm/Editor/UntermBreakpoints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEngine;

namespace Unterm.Editor
{
/// <summary>
/// Project-wide debugger breakpoint store, persisted to
/// <c>Library/Unterm/breakpoints.json</c> (the same convention as the rest of
/// Unterm's editor state). It is the single source of truth shared by BOTH the
/// editor and the standalone debugger process — the debugger writes the same file
/// directly when you toggle a breakpoint while stopped (when the editor is frozen
/// and can't run), so edits from either side persist.
///
/// Lines are 0-based, matching the native code editor's gutter line indices
/// (converted to 1-based when handed to the SDB debugger, whose sequence points
/// are 1-based source lines).
/// </summary>
[InitializeOnLoad]
internal static class UntermBreakpoints
{
[System.Serializable]
private class Entry { public string path; public int[] lines; }
[System.Serializable]
private class Store { public Entry[] files; }

private static Dictionary<string, SortedSet<int>> _map;

private static string Dir =>
Path.Combine(Path.GetDirectoryName(Application.dataPath), "Library", "Unterm");
private static string StorePath => Path.Combine(Dir, "breakpoints.json");

/// Raised when the store file changes on disk — i.e. the debugger toggled a
/// breakpoint while we run — so open code editors can refresh their gutter dots.
public static event System.Action Changed;

// Last write time we've accounted for; poll for anything newer (the debugger's
// edits), and stamp it after our own writes so we don't fire on those.
private static long _seenTicks;

static UntermBreakpoints()
{
_seenTicks = FileTicks();
EditorApplication.update += Poll;
}

private static long FileTicks()
{
try { return File.Exists(StorePath) ? File.GetLastWriteTimeUtc(StorePath).Ticks : 0; }
catch { return 0; }
}

private static void Poll()
{
long ticks = FileTicks();
if (ticks == _seenTicks) return;
_seenTicks = ticks;
_map = null; // drop the cache; next read reloads from disk
Changed?.Invoke();
}

private static Dictionary<string, SortedSet<int>> Map
{
get { if (_map == null) Load(); return _map; }
}

/// Re-read from disk (e.g. to pick up edits the debugger made while stopped).
public static void Reload() => _map = null;

/// 0-based breakpoint lines for a file (ascending), as uint[] for the FFI.
public static uint[] For(string path)
{
if (!string.IsNullOrEmpty(path) && Map.TryGetValue(Norm(path), out var s))
{
var a = new uint[s.Count];
int i = 0;
foreach (var l in s) a[i++] = (uint)l;
return a;
}
return System.Array.Empty<uint>();
}

/// Toggle a 0-based breakpoint line; returns the file's new full set.
public static uint[] Toggle(string path, int line)
{
if (string.IsNullOrEmpty(path)) return System.Array.Empty<uint>();
var key = Norm(path);
if (!Map.TryGetValue(key, out var s)) { s = new SortedSet<int>(); Map[key] = s; }
if (!s.Remove(line)) s.Add(line);
if (s.Count == 0) Map.Remove(key);
Save();
return For(path);
}

/// All (absolute file path, 0-based line) breakpoints across the project.
public static List<KeyValuePair<string, int>> All()
{
var r = new List<KeyValuePair<string, int>>();
foreach (var kv in Map)
foreach (var l in kv.Value)
r.Add(new KeyValuePair<string, int>(kv.Key, l));
return r;
}

private static string Norm(string p)
{
try { return Path.GetFullPath(p); } catch { return p; }
}

private static void Load()
{
_map = new Dictionary<string, SortedSet<int>>();
try
{
if (!File.Exists(StorePath)) return;
var store = JsonUtility.FromJson<Store>(File.ReadAllText(StorePath));
if (store?.files == null) return;
foreach (var e in store.files)
{
if (string.IsNullOrEmpty(e.path) || e.lines == null) continue;
var set = new SortedSet<int>();
foreach (var l in e.lines) set.Add(l);
if (set.Count > 0) _map[Norm(e.path)] = set;
}
}
catch { /* corrupt store: start empty */ }
}

private static void Save()
{
try
{
Directory.CreateDirectory(Dir);
var entries = new List<Entry>(_map.Count);
foreach (var kv in _map)
{
var arr = new int[kv.Value.Count];
kv.Value.CopyTo(arr);
entries.Add(new Entry { path = kv.Key, lines = arr });
}
File.WriteAllText(StorePath, JsonUtility.ToJson(new Store { files = entries.ToArray() }));
_seenTicks = FileTicks(); // our own write; don't re-fire Changed for it
}
catch { /* best effort */ }
}
}
}
2 changes: 2 additions & 0 deletions Packages/dev.tnayuki.unterm/Editor/UntermBreakpoints.cs.meta

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

Loading