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
28 changes: 28 additions & 0 deletions .github/workflows/split-upm.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ jobs:
- name: Verify the locked dependency graph
run: cargo metadata --manifest-path native/Cargo.toml --locked --format-version 1 > /dev/null

- name: Verify standalone Linux debugger dependencies
run: |
linux_tree="$RUNNER_TEMP/unterm-linux-features.txt"
cargo tree --manifest-path native/Cargo.toml -p unterm \
--target x86_64-unknown-linux-gnu --edges features --invert winit --locked \
| tee "$linux_tree"
grep -Fq 'winit feature "x11"' "$linux_tree"
if grep -Fq 'winit feature "wayland"' "$linux_tree"; then
echo "Wayland must remain disabled until wayland-scanner uses a patched quick-xml release."
exit 1
fi

- name: Deny vulnerable and unsound Rust advisories
run: cargo audit --file native/Cargo.lock

Expand All @@ -82,6 +94,22 @@ jobs:
native/generate-notices.sh
git diff --exit-code -- "Packages/dev.tnayuki.unterm/Third Party Notices.md"

- name: Verify pinned source provenance
env:
HARDENING_BRANCH: feat/toolkit-icon-performance-mcp
UPSTREAM_BASE_COMMIT: ead1391bd38532eebfb9f13f10064eddc372b769
run: |
jq -e \
--arg hardeningBranch "$HARDENING_BRANCH" \
--arg upstreamBaseCommit "$UPSTREAM_BASE_COMMIT" \
'.schemaVersion == 1 and
.source.repository == "https://github.com/tnayuki/Unity-Unterm" and
.source.commit == $upstreamBaseCommit and
.distribution.repository == "https://github.com/rankupgames/Unity-Unterm" and
.distribution.sourceBranch == "upstream/main" and
.distribution.hardeningBranch == $hardeningBranch' \
provenance/source.json

- name: Upload SBOM and notices
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
Expand Down
19 changes: 6 additions & 13 deletions Packages/dev.tnayuki.unterm/Editor/ClaudeCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,10 @@
namespace Unterm.Editor
{
/// <summary>
/// Gates the "Window/Unterm/Claude Code" entry on Unterm's own managed Claude Code
/// engine binary: the item is enabled only once the binary has been downloaded
/// (see <see cref="UntermClaudeInstaller"/> and the "Preferences &gt; Unterm" page,
/// <see cref="UntermSettingsProvider"/>), and selecting it opens the agent panel
/// (<see cref="UntermAgentWindow"/>).
///
/// Unterm deliberately drives only the engine it manages, not whatever <c>claude</c>
/// the user may have installed, so it always runs a known-good binary. The managed
/// path is handed to the native agent at spawn (see <see cref="ClaudePath"/>), so
/// "available" is exactly "what gets launched".
/// Gates the "Window/Unterm/Claude Code" entry on an existing Claude Code
/// executable discovered from explicit configuration, PATH, common user install
/// locations, or a legacy managed install. Unterm never downloads or updates it.
/// Selecting the enabled item opens the agent panel (<see cref="UntermAgentWindow"/>).
///
/// Unity has no supported API to add/remove a menu item at runtime, so the entry is
/// a static <c>[MenuItem]</c> whose validate callback greys it out until the binary
Expand All @@ -22,9 +16,8 @@ internal static class ClaudeCode
{
private const string MenuPath = "Window/Unterm/Claude Code";

/// The absolute path to the managed `claude` binary, passed to the native agent
/// at spawn (<see cref="UntermNative.AgentviewCreate"/>). "" until it has been
/// downloaded via the Preferences page.
/// The absolute path to the discovered `claude` binary, passed to the native
/// agent at spawn (<see cref="UntermNative.AgentviewCreate"/>), or "".
internal static string ClaudePath => UntermClaudeInstaller.InstalledBinaryPath();

#if UNITY_EDITOR_OSX || UNITY_EDITOR_WIN
Expand Down
9 changes: 8 additions & 1 deletion Packages/dev.tnayuki.unterm/Editor/ToolkitMenuItems.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace Unterm.Editor
internal static class ToolkitMenuItems
{
private const string ClaudeMenuPath = "Tools/Unity Cursor Toolkit/Unterm/Claude Code";
private const string DebuggerMenuPath = "Tools/Unity Cursor Toolkit/Unterm/Debugger (Standalone Process)";

[MenuItem("Tools/Unity Cursor Toolkit/Unterm/New Terminal", priority = 300)]
private static void OpenTerminal() => UntermWindow.OpenNew();
Expand All @@ -19,7 +20,13 @@ internal static class ToolkitMenuItems
[MenuItem("Tools/Unity Cursor Toolkit/Unterm/Code Editor", priority = 302)]
private static void OpenCodeEditor() => UntermCodeEditorWindow.OpenEmpty();

[MenuItem("Tools/Unity Cursor Toolkit/Unterm/Settings", priority = 303)]
[MenuItem(DebuggerMenuPath, priority = 303)]
private static void OpenDebugger() => UntermDebuggerLauncher.Open();

[MenuItem(DebuggerMenuPath, validate = true)]
private static bool OpenDebuggerValidate() => UntermDebuggerLauncher.OpenValidate();

[MenuItem("Tools/Unity Cursor Toolkit/Unterm/Settings", priority = 304)]
private static void OpenSettings() => SettingsService.OpenUserPreferences("Preferences/Unterm");
}
}
125 changes: 78 additions & 47 deletions Packages/dev.tnayuki.unterm/Editor/UntermAgentWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -128,12 +128,16 @@ private HashSet<string> BusyIds()

// Transcript panel texture (zero-copy wrap of the native MTLTexture).
private Texture2D _tex;
private IntPtr _externalTexPtr;
private readonly Dictionary<IntPtr, Texture2D> _panelTextures =
new Dictionary<IntPtr, Texture2D>();
private int _panelTextureW, _panelTextureH;

// Input strip texture (the native input box renders the field AND the
// Send/Stop button into this one surface).
private Texture2D _inputTex;
private IntPtr _inputExternalTexPtr;
private readonly Dictionary<IntPtr, Texture2D> _inputTextures =
new Dictionary<IntPtr, Texture2D>();
private int _inputTextureW, _inputTextureH;
private float _inputHeight = InputHeight; // logical px, grows with content

private float _scroll; // physical px, 0 = latest
Expand Down Expand Up @@ -166,7 +170,7 @@ public static void Open()
// focused one so new windows don't stack exactly on top.
var from = focusedWindow as UntermAgentWindow;
var w = CreateWindow<UntermAgentWindow>();
w.titleContent = new GUIContent("Claude Code");
w.titleContent = UntermWindowTitle.Create("Claude Code", UntermWindowTitle.AgentIcon, w.titleContent);
w.minSize = new Vector2(320, 200);

Vector2 size = from != null ? from.position.size : new Vector2(420f, 520f);
Expand Down Expand Up @@ -371,7 +375,7 @@ private void OnEditorUpdate()
{
string agent = _native.AgentviewTitle(Vid);
if (!string.IsNullOrEmpty(agent) && titleContent.text != agent)
titleContent = new GUIContent(agent);
titleContent = UntermWindowTitle.Create(agent, UntermWindowTitle.AgentIcon, titleContent);
}

// Drain any in-flight recent-sessions listing (for the picker dropdown).
Expand Down Expand Up @@ -542,16 +546,7 @@ private void ApplyFonts()
/// conversation persists; otherwise destroy the view and unload.
private void Teardown(bool keepView)
{
if (_tex != null)
{
DestroyImmediate(_tex);
_tex = null;
}
if (_inputTex != null)
{
DestroyImmediate(_inputTex);
_inputTex = null;
}
ClearRenderTextureCaches();
if (_imeBgTex != null)
{
DestroyImmediate(_imeBgTex);
Expand Down Expand Up @@ -673,65 +668,100 @@ private void RenderView(bool measureInput = true)
}
}

private static System.Reflection.MethodInfo s_bgMethod;
private static bool s_bgLooked;
private static Color s_bgColor;
private static bool s_bgDark, s_bgValid;

private static Color GetEditorBackground()
{
// Prefer Unity's exact themed background; fall back to standard grays.
var m = typeof(EditorGUIUtility).GetMethod(
"GetDefaultBackgroundColor",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
if (m != null && m.ReturnType == typeof(Color))
return (Color)m.Invoke(null, null);

return EditorGUIUtility.isProSkin
? (Color)new Color32(56, 56, 56, 255)
: (Color)new Color32(194, 194, 194, 255);
bool dark = EditorGUIUtility.isProSkin;
if (s_bgValid && s_bgDark == dark) return s_bgColor;
if (!s_bgLooked)
{
s_bgLooked = true;
var m = typeof(EditorGUIUtility).GetMethod(
"GetDefaultBackgroundColor",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
if (m != null && m.ReturnType == typeof(Color)) s_bgMethod = m;
}
s_bgColor = s_bgMethod != null
? (Color)s_bgMethod.Invoke(null, null)
: dark
? (Color)new Color32(56, 56, 56, 255)
: (Color)new Color32(194, 194, 194, 255);
s_bgDark = dark;
s_bgValid = true;
return s_bgColor;
}

// Wrap the native transcript texture directly — no CPU copy (zero-copy
// only, like the terminal). The pointer alternates on Windows' double
// buffer, so re-wrap whenever it changes; null means the frame isn't ready
// yet (e.g. Windows D3D device not captured) — skip this tick.
// buffer. Cache one Unity wrapper per pointer so the alternating buffers do
// not allocate and destroy Texture2D objects every rendered frame.
private void UploadPanel(int iw, int ih)
{
IntPtr texPtr = _native.AgentviewPanelTexture(Vid);
if (texPtr == IntPtr.Zero) return;

if (_tex == null || _tex.width != iw || _tex.height != ih || _externalTexPtr != texPtr)
if (_panelTextureW != iw || _panelTextureH != ih)
{
if (_tex != null) DestroyImmediate(_tex);
_tex = Texture2D.CreateExternalTexture(
iw, ih, TextureFormat.RGBA32, false, false, texPtr);
_tex.filterMode = FilterMode.Bilinear;
_tex.hideFlags = HideFlags.HideAndDontSave;
_externalTexPtr = texPtr;
ClearTextureCache(_panelTextures);
_panelTextureW = iw;
_panelTextureH = ih;
}
else
if (!_panelTextures.TryGetValue(texPtr, out var tex) || tex == null)
{
_tex.UpdateExternalTexture(texPtr);
tex = Texture2D.CreateExternalTexture(
iw, ih, TextureFormat.RGBA32, false, false, texPtr);
tex.filterMode = FilterMode.Bilinear;
tex.hideFlags = HideFlags.HideAndDontSave;
_panelTextures[texPtr] = tex;
}
_tex = tex;
}

// Wrap the native input strip texture (zero-copy only); re-wrap when the
// pointer changes (Windows double buffer), skip a null frame.
// Wrap the native input strip texture (zero-copy only), caching both sides
// of the Windows double buffer just like the transcript panel.
private void UploadInput(int iw, int ih)
{
IntPtr texPtr = _native.AgentviewInputTexture(Vid);
if (texPtr == IntPtr.Zero) return;

if (_inputTex == null || _inputTex.width != iw || _inputTex.height != ih
|| _inputExternalTexPtr != texPtr)
if (_inputTextureW != iw || _inputTextureH != ih)
{
ClearTextureCache(_inputTextures);
_inputTextureW = iw;
_inputTextureH = ih;
}
if (!_inputTextures.TryGetValue(texPtr, out var tex) || tex == null)
{
if (_inputTex != null) DestroyImmediate(_inputTex);
_inputTex = Texture2D.CreateExternalTexture(
tex = Texture2D.CreateExternalTexture(
iw, ih, TextureFormat.RGBA32, false, false, texPtr);
_inputTex.filterMode = FilterMode.Bilinear;
_inputTex.hideFlags = HideFlags.HideAndDontSave;
_inputExternalTexPtr = texPtr;
tex.filterMode = FilterMode.Bilinear;
tex.hideFlags = HideFlags.HideAndDontSave;
_inputTextures[texPtr] = tex;
}
else
_inputTex = tex;
}

private void ClearRenderTextureCaches()
{
ClearTextureCache(_panelTextures);
ClearTextureCache(_inputTextures);
_tex = null;
_inputTex = null;
_panelTextureW = _panelTextureH = 0;
_inputTextureW = _inputTextureH = 0;
}

private void ClearTextureCache(Dictionary<IntPtr, Texture2D> cache)
{
foreach (var texture in cache.Values)
{
_inputTex.UpdateExternalTexture(texPtr);
if (texture != null) DestroyImmediate(texture);
}
cache.Clear();
}

private void OnGUI()
Expand Down Expand Up @@ -1698,7 +1728,8 @@ private void SwitchTo(string id, string title = null)
var (pw, ph) = CurrentPanelSize();
var (iw, ih) = CurrentInputSize();
_viewId = (long)_native.AgentviewLoad(ProjectRoot, id, pw, ph, iw, ih, _effort, ClaudeCode.ClaudePath);
if (!string.IsNullOrEmpty(title)) titleContent = new GUIContent(title);
if (!string.IsNullOrEmpty(title))
titleContent = UntermWindowTitle.Create(title, UntermWindowTitle.AgentIcon, titleContent);
ApplyFonts();
ApplyAgentSettings();
RenderView(); Repaint();
Expand Down
Loading